blob: c533fef0d177a73790bc26124a75f2ff04af44e6 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/cocoa/task_manager_mac.h"
#include <stddef.h>
#include <algorithm>
#include <vector>
#include "base/feature_list.h"
#include "base/mac/bundle_locations.h"
#import "base/mac/sdk_forward_declarations.h"
#include "base/macros.h"
#include "base/strings/sys_string_conversions.h"
#include "build/buildflag.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chrome_notification_types.h"
#include "chrome/browser/task_manager/task_manager_interface.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_dialogs.h"
#import "chrome/browser/ui/cocoa/window_size_autosaver.h"
#include "chrome/browser/ui/task_manager/task_manager_columns.h"
#include "chrome/browser/ui/views_mode_controller.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/notification_service.h"
#include "content/public/browser/notification_source.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util_mac.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_util_mac.h"
namespace {
NSString* ColumnIdentifier(int id) {
return [NSString stringWithFormat:@"%d", id];
}
bool ShouldUseViewsTaskManager() {
return base::FeatureList::IsEnabled(features::kViewsTaskManager);
}
} // namespace
@interface TaskManagerWindowController (Private)
- (NSTableColumn*)addColumnWithData:
(const task_manager::TableColumnData&)columnData;
- (void)setUpTableColumns;
- (void)setUpTableHeaderContextMenu;
- (void)toggleColumn:(id)sender;
- (void)adjustSelectionAndEndProcessButton;
- (void)deselectRows;
@end
////////////////////////////////////////////////////////////////////////////////
// TaskManagerWindowController implementation:
@implementation TaskManagerWindowController
- (id)initWithTaskManagerMac:(task_manager::TaskManagerMac*)taskManagerMac
tableModel:(task_manager::TaskManagerTableModel*)tableModel {
NSString* nibpath = [base::mac::FrameworkBundle()
pathForResource:@"TaskManager"
ofType:@"nib"];
if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
taskManagerMac_ = taskManagerMac;
tableModel_ = tableModel;
if (g_browser_process && g_browser_process->local_state()) {
size_saver_.reset([[WindowSizeAutosaver alloc]
initWithWindow:[self window]
prefService:g_browser_process->local_state()
path:prefs::kTaskManagerWindowPlacement]);
}
[[self window] setExcludedFromWindowsMenu:YES];
[self reloadData];
[self showWindow:self];
}
return self;
}
- (void)sortShuffleArray {
viewToModelMap_.resize(tableModel_->RowCount());
for (size_t i = 0; i < viewToModelMap_.size(); ++i)
viewToModelMap_[i] = i;
if (currentSortDescriptor_.sorted_column_id != -1) {
task_manager::TaskManagerTableModel* tableModel = tableModel_;
task_manager::TableSortDescriptor currentSortDescriptor =
currentSortDescriptor_;
std::stable_sort(viewToModelMap_.begin(), viewToModelMap_.end(),
[tableModel, currentSortDescriptor](int a, int b) {
int aStart, aLength;
tableModel->GetRowsGroupRange(a, &aStart, &aLength);
int bStart, bLength;
tableModel->GetRowsGroupRange(b, &bStart, &bLength);
if (aStart == bStart) {
// The two rows are in the same group, sort so that
// items in the same group always appear in the same
// order. The sort descriptor's ascending value is
// intentionally ignored.
return a < b;
}
// Sort by the first entry of each of the groups.
int cmp_result = tableModel->CompareValues(
aStart, bStart,
currentSortDescriptor.sorted_column_id);
if (!currentSortDescriptor.is_ascending)
cmp_result = -cmp_result;
return cmp_result < 0;
});
}
modelToViewMap_.resize(viewToModelMap_.size());
for (size_t i = 0; i < viewToModelMap_.size(); ++i)
modelToViewMap_[viewToModelMap_[i]] = i;
}
- (void)reloadData {
[self reloadDataWithRows:0 addedAtIndex:0];
}
- (void)reloadDataWithRows:(int)addedRows addedAtIndex:(int)addedRowIndex {
// Store old view indices, and the model indices they map to.
NSIndexSet* viewSelection = [tableView_ selectedRowIndexes];
std::vector<int> modelSelection;
for (NSUInteger i = [viewSelection lastIndex];
i != NSNotFound;
i = [viewSelection indexLessThanIndex:i]) {
modelSelection.push_back(viewToModelMap_[i]);
}
// Adjust for any added or removed rows.
if (addedRows != 0) {
for (int& selectedItem : modelSelection) {
if (addedRowIndex > selectedItem) {
// Nothing to do; added/removed items are beyond the selected item.
continue;
}
if (addedRows > 0) {
selectedItem += addedRows;
} else {
int removedRows = -addedRows;
if (addedRowIndex + removedRows <= selectedItem)
selectedItem -= removedRows;
else
selectedItem = -1; // The item was removed.
}
}
}
// Sort.
[self sortShuffleArray];
// Reload the data.
[tableView_ reloadData];
// Reload the selection.
NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
for (auto selectedItem : modelSelection) {
if (selectedItem != -1)
[indexSet addIndex:modelToViewMap_[selectedItem]];
}
[tableView_ selectRowIndexes:indexSet byExtendingSelection:NO];
[self adjustSelectionAndEndProcessButton];
}
- (task_manager::TableSortDescriptor)sortDescriptor {
return currentSortDescriptor_;
}
- (void)setSortDescriptor:
(const task_manager::TableSortDescriptor&)sortDescriptor {
base::scoped_nsobject<NSSortDescriptor> nsSortDescriptor(
[[NSSortDescriptor alloc]
initWithKey:ColumnIdentifier(sortDescriptor.sorted_column_id)
ascending:sortDescriptor.is_ascending]);
[tableView_ setSortDescriptors:@[ nsSortDescriptor ]];
}
- (BOOL)visibilityOfColumnWithId:(int)columnId {
NSTableColumn* column =
[tableView_ tableColumnWithIdentifier:ColumnIdentifier(columnId)];
return column ? ![column isHidden] : NO;
}
- (void)setColumnWithId:(int)columnId toVisibility:(BOOL)visibility {
NSTableColumn* column =
[tableView_ tableColumnWithIdentifier:ColumnIdentifier(columnId)];
[column setHidden:!visibility];
[tableView_ sizeToFit];
[tableView_ setNeedsDisplay];
}
- (IBAction)killSelectedProcesses:(id)sender {
NSIndexSet* selection = [tableView_ selectedRowIndexes];
for (NSUInteger i = [selection lastIndex];
i != NSNotFound;
i = [selection indexLessThanIndex:i]) {
tableModel_->KillTask(viewToModelMap_[i]);
}
}
- (void)tableWasDoubleClicked:(id)sender {
NSInteger row = [tableView_ clickedRow];
if (row < 0)
return; // Happens e.g. if the table header is double-clicked.
tableModel_->ActivateTask(viewToModelMap_[row]);
}
- (void)awakeFromNib {
[self setUpTableColumns];
[self setUpTableHeaderContextMenu];
[self adjustSelectionAndEndProcessButton];
[tableView_ setDoubleAction:@selector(tableWasDoubleClicked:)];
[tableView_ setIntercellSpacing:NSMakeSize(0.0, 0.0)];
[tableView_ sizeToFit];
}
- (void)dealloc {
// Paranoia. These should have been nilled out in -windowWillClose: but let's
// make sure we have no dangling references.
[tableView_ setDelegate:nil];
[tableView_ setDataSource:nil];
[super dealloc];
}
// Adds a column which has the given string id as title. |isVisible| specifies
// if the column is initially visible.
- (NSTableColumn*)addColumnWithData:
(const task_manager::TableColumnData&)columnData {
base::scoped_nsobject<NSTableColumn> column([[NSTableColumn alloc]
initWithIdentifier:ColumnIdentifier(columnData.id)]);
NSTextAlignment textAlignment = (columnData.align == ui::TableColumn::LEFT)
? NSLeftTextAlignment
: NSRightTextAlignment;
[[column.get() headerCell]
setStringValue:l10n_util::GetNSStringWithFixup(columnData.id)];
[[column.get() headerCell] setAlignment:textAlignment];
[[column.get() dataCell] setAlignment:textAlignment];
const CGFloat smallSystemFontSize = [NSFont smallSystemFontSize];
NSFont* font = nil;
if (@available(macOS 10.11, *)) {
font = [NSFont monospacedDigitSystemFontOfSize:smallSystemFontSize
weight:NSFontWeightRegular];
} else {
font = [NSFont systemFontOfSize:smallSystemFontSize];
}
[[column.get() dataCell] setFont:font];
[column.get() setHidden:!columnData.default_visibility];
[column.get() setEditable:NO];
base::scoped_nsobject<NSSortDescriptor> sortDescriptor(
[[NSSortDescriptor alloc]
initWithKey:ColumnIdentifier(columnData.id)
ascending:columnData.initial_sort_is_ascending]);
[column.get() setSortDescriptorPrototype:sortDescriptor.get()];
[column.get() setMinWidth:columnData.min_width];
int maxWidth = columnData.max_width;
if (maxWidth < 0)
maxWidth = 3 * columnData.min_width / 2; // *1.5 for ints.
[column.get() setMaxWidth:maxWidth];
[column.get() setResizingMask:NSTableColumnAutoresizingMask |
NSTableColumnUserResizingMask];
[tableView_ addTableColumn:column.get()];
return column.get(); // Now retained by |tableView_|.
}
// Adds all the task manager's columns to the table.
- (void)setUpTableColumns {
for (NSTableColumn* column in [tableView_ tableColumns])
[tableView_ removeTableColumn:column];
for (size_t i = 0; i < task_manager::kColumnsSize; ++i) {
const auto& columnData = task_manager::kColumns[i];
NSTableColumn* column = [self addColumnWithData:columnData];
if (columnData.id == IDS_TASK_MANAGER_TASK_COLUMN) {
// The task column displays an icon for every row, done by an
// NSButtonCell.
base::scoped_nsobject<NSButtonCell> nameCell(
[[NSButtonCell alloc] initTextCell:@""]);
[nameCell.get() setImagePosition:NSImageLeft];
[nameCell.get() setButtonType:NSSwitchButton];
[nameCell.get() setAlignment:[[column dataCell] alignment]];
[nameCell.get() setFont:[[column dataCell] font]];
[column setDataCell:nameCell.get()];
}
}
}
// Creates a context menu for the table header that allows the user to toggle
// which columns should be shown and which should be hidden (like the Activity
// Monitor.app's table header context menu).
- (void)setUpTableHeaderContextMenu {
base::scoped_nsobject<NSMenu> contextMenu(
[[NSMenu alloc] initWithTitle:@"Task Manager context menu"]);
[contextMenu setDelegate:self];
[[tableView_ headerView] setMenu:contextMenu.get()];
}
- (void)menuNeedsUpdate:(NSMenu*)menu {
[menu removeAllItems];
for (NSTableColumn* column in [tableView_ tableColumns]) {
NSMenuItem* item = [menu addItemWithTitle:[[column headerCell] stringValue]
action:@selector(toggleColumn:)
keyEquivalent:@""];
[item setTarget:self];
[item setRepresentedObject:column];
[item setState:[column isHidden] ? NSOffState : NSOnState];
}
}
// Callback for the table header context menu. Toggles visibility of the table
// column associated with the clicked menu item.
- (void)toggleColumn:(NSMenuItem*)item {
DCHECK([item isKindOfClass:[NSMenuItem class]]);
if (![item isKindOfClass:[NSMenuItem class]])
return;
NSTableColumn* column = [item representedObject];
int columnId = [[column identifier] intValue];
DCHECK(column);
NSInteger oldState = [item state];
NSInteger newState = oldState == NSOnState ? NSOffState : NSOnState;
// If hiding the column, make sure at least one column will remain visible.
if (newState == NSOffState) {
// Find the first column that will be visible after hiding |column|.
NSTableColumn* firstRemainingVisibleColumn = nil;
for (NSTableColumn* nextColumn in [tableView_ tableColumns]) {
if (nextColumn != column && ![nextColumn isHidden]) {
firstRemainingVisibleColumn = nextColumn;
break;
}
}
// If no column will be visible, abort the toggle. This will basically cause
// the toggle operation to silently fail. The other way to ensure at least
// one visible column is to disable the menu item corresponding to the last
// remaining visible column. That would place the menu in a weird state to
// the user, where there's one item somewhere that's grayed out with no
// clear explanation of why. It will be rare for a user to try hiding all
// columns, but we still want to guard against it. If they are really intent
// on hiding the last visible column (perhaps they plan to choose another
// one after that to be visible), odds are they will try making another
// column visible and then hiding the one column that would not hide.
if (firstRemainingVisibleColumn == nil) {
return;
}
// If |column| is being used to sort the table (i.e. it's the primary sort
// column), make the first remaining visible column the new primary sort
// column.
int primarySortColumnId = currentSortDescriptor_.sorted_column_id;
DCHECK(primarySortColumnId);
if (primarySortColumnId == columnId) {
NSSortDescriptor* newSortDescriptor =
[firstRemainingVisibleColumn sortDescriptorPrototype];
[tableView_ setSortDescriptors:@[ newSortDescriptor ]];
}
}
// Make the change. (This will call back into the SetColumnVisibility()
// function to actually do the visibility change.)
tableModel_->ToggleColumnVisibility(columnId);
}
// This function appropriately sets the enabled states on the table's editing
// buttons.
- (void)adjustSelectionAndEndProcessButton {
bool allSelectionRowsAreKillableTasks = true;
NSMutableIndexSet* groupIndexes = [NSMutableIndexSet indexSet];
NSIndexSet* selection = [tableView_ selectedRowIndexes];
for (NSUInteger i = [selection lastIndex];
i != NSNotFound;
i = [selection indexLessThanIndex:i]) {
int modelIndex = viewToModelMap_[i];
if (!tableModel_->IsTaskKillable(modelIndex))
allSelectionRowsAreKillableTasks = false;
int groupStart, groupLength;
tableModel_->GetRowsGroupRange(modelIndex, &groupStart, &groupLength);
for (int j = 0; j < groupLength; ++j)
[groupIndexes addIndex:modelToViewMap_[groupStart + j]];
}
[tableView_ selectRowIndexes:groupIndexes byExtendingSelection:YES];
bool enabled = [selection count] > 0 && allSelectionRowsAreKillableTasks &&
task_manager::TaskManagerInterface::IsEndProcessEnabled();
[endProcessButton_ setEnabled:enabled];
}
- (void)deselectRows {
[tableView_ deselectAll:self];
}
// Table view delegate methods.
// The selection is being changed by mouse (drag/click).
- (void)tableViewSelectionIsChanging:(NSNotification*)aNotification {
[self adjustSelectionAndEndProcessButton];
}
// The selection is being changed by keyboard (arrows).
- (void)tableViewSelectionDidChange:(NSNotification*)aNotification {
[self adjustSelectionAndEndProcessButton];
}
- (void)windowWillClose:(NSNotification*)notification {
if (taskManagerMac_) {
tableModel_->StoreColumnsSettings();
taskManagerMac_->WindowWasClosed();
taskManagerMac_ = nullptr;
tableModel_ = nullptr;
// Now that there is no model, ensure that this object gets no data requests
// in the window of time between the autorelease and the actual dealloc.
// https://crbug.com/763367
[tableView_ setDelegate:nil];
[tableView_ setDataSource:nil];
}
[self autorelease];
}
@end
@implementation TaskManagerWindowController (NSTableDataSource)
- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView {
DCHECK(tableView == tableView_ || tableView_ == nil);
return tableModel_->RowCount();
}
- (NSString*)modelTextForRow:(int)row column:(int)columnId {
DCHECK_LT(static_cast<size_t>(row), viewToModelMap_.size());
return base::SysUTF16ToNSString(
tableModel_->GetText(viewToModelMap_[row], columnId));
}
- (id)tableView:(NSTableView*)tableView
objectValueForTableColumn:(NSTableColumn*)tableColumn
row:(NSInteger)rowIndex {
// NSButtonCells expect an on/off state as objectValue. Their title is set
// in |tableView:dataCellForTableColumn:row:| below.
if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_TASK_COLUMN) {
return [NSNumber numberWithInt:NSOffState];
}
return [self modelTextForRow:rowIndex
column:[[tableColumn identifier] intValue]];
}
- (NSCell*)tableView:(NSTableView*)tableView
dataCellForTableColumn:(NSTableColumn*)tableColumn
row:(NSInteger)rowIndex {
NSCell* cell = [tableColumn dataCellForRow:rowIndex];
// Set the favicon and title for the task in the name column.
if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_TASK_COLUMN) {
DCHECK([cell isKindOfClass:[NSButtonCell class]]);
NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell);
NSString* title = [self modelTextForRow:rowIndex
column:[[tableColumn identifier] intValue]];
[buttonCell setTitle:title];
[buttonCell
setImage:taskManagerMac_->GetImageForRow(viewToModelMap_[rowIndex])];
[buttonCell setRefusesFirstResponder:YES]; // Don't push in like a button.
[buttonCell setHighlightsBy:NSNoCellMask];
}
return cell;
}
- (void)tableView:(NSTableView*)tableView
sortDescriptorsDidChange:(NSArray*)oldDescriptors {
if (withinSortDescriptorsDidChange_)
return;
NSSortDescriptor* oldDescriptor = [oldDescriptors firstObject];
NSSortDescriptor* newDescriptor = [[tableView sortDescriptors] firstObject];
// Implement three-way sorting, toggling "unsorted" as a third option.
if (oldDescriptor && newDescriptor &&
[[oldDescriptor key] isEqual:[newDescriptor key]]) {
// The user clicked to change the sort on the previously sorted column.
// AppKit toggled the sort order. However, if the sort was toggled to become
// the initial sorting direction, clear it instead.
NSTableColumn* column = [tableView
tableColumnWithIdentifier:ColumnIdentifier(
[[newDescriptor key] intValue])];
NSSortDescriptor* initialDescriptor = [column sortDescriptorPrototype];
if ([newDescriptor ascending] == [initialDescriptor ascending]) {
withinSortDescriptorsDidChange_ = YES;
[tableView_ setSortDescriptors:[NSArray array]];
newDescriptor = nil;
withinSortDescriptorsDidChange_ = NO;
}
}
if (newDescriptor) {
currentSortDescriptor_.sorted_column_id = [[newDescriptor key] intValue];
currentSortDescriptor_.is_ascending = [newDescriptor ascending];
} else {
currentSortDescriptor_.sorted_column_id = -1;
}
[self reloadData]; // Sorts.
}
@end
@implementation TaskManagerWindowController (TestingAPI)
- (NSTableView*)tableViewForTesting {
return tableView_;
}
- (NSButton*)endProcessButtonForTesting {
return endProcessButton_;
}
@end
namespace task_manager {
////////////////////////////////////////////////////////////////////////////////
// TaskManagerMac implementation:
TaskManagerMac::TaskManagerMac()
: table_model_(this),
window_controller_([[TaskManagerWindowController alloc]
initWithTaskManagerMac:this
tableModel:&table_model_]) {
table_model_.SetObserver(this); // Hook up the ui::TableModelObserver.
table_model_.RetrieveSavedColumnsSettingsAndUpdateTable();
registrar_.Add(this, chrome::NOTIFICATION_APP_TERMINATING,
content::NotificationService::AllSources());
}
// static
TaskManagerMac* TaskManagerMac::instance_ = nullptr;
TaskManagerMac::~TaskManagerMac() {
table_model_.SetObserver(nullptr);
}
////////////////////////////////////////////////////////////////////////////////
// ui::TableModelObserver implementation:
void TaskManagerMac::OnModelChanged() {
[window_controller_ deselectRows];
[window_controller_ reloadData];
}
void TaskManagerMac::OnItemsChanged(int start, int length) {
[window_controller_ reloadData];
}
void TaskManagerMac::OnItemsAdded(int start, int length) {
[window_controller_ reloadDataWithRows:length addedAtIndex:start];
}
void TaskManagerMac::OnItemsRemoved(int start, int length) {
[window_controller_ reloadDataWithRows:-length addedAtIndex:start];
}
////////////////////////////////////////////////////////////////////////////////
// TableViewDelegate implementation:
bool TaskManagerMac::IsColumnVisible(int column_id) const {
return [window_controller_ visibilityOfColumnWithId:column_id];
}
void TaskManagerMac::SetColumnVisibility(int column_id, bool new_visibility) {
[window_controller_ setColumnWithId:column_id toVisibility:new_visibility];
}
bool TaskManagerMac::IsTableSorted() const {
return [window_controller_ sortDescriptor].sorted_column_id != -1;
}
TableSortDescriptor TaskManagerMac::GetSortDescriptor() const {
return [window_controller_ sortDescriptor];
}
void TaskManagerMac::SetSortDescriptor(const TableSortDescriptor& descriptor) {
[window_controller_ setSortDescriptor:descriptor];
}
////////////////////////////////////////////////////////////////////////////////
// Called by the TaskManagerWindowController:
void TaskManagerMac::WindowWasClosed() {
delete this;
instance_ = nullptr; // |instance_| is static
}
NSImage* TaskManagerMac::GetImageForRow(int row) {
const NSSize kImageSize = NSMakeSize(16.0, 16.0);
NSImage* image = gfx::NSImageFromImageSkia(table_model_.GetIcon(row));
if (image)
image.size = kImageSize;
else
image = [[[NSImage alloc] initWithSize:kImageSize] autorelease];
return image;
}
void TaskManagerMac::Observe(int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK_EQ(chrome::NOTIFICATION_APP_TERMINATING, type);
Hide();
}
// static
TaskManagerTableModel* TaskManagerMac::Show() {
if (instance_) {
[[instance_->window_controller_ window]
makeKeyAndOrderFront:instance_->window_controller_];
} else {
instance_ = new TaskManagerMac();
}
return &instance_->table_model_;
}
// static
void TaskManagerMac::Hide() {
if (instance_)
[instance_->window_controller_ close];
}
} // namespace task_manager
namespace chrome {
// Declared in browser_dialogs.h.
task_manager::TaskManagerTableModel* ShowTaskManager(Browser* browser) {
if (ShouldUseViewsTaskManager())
return chrome::ShowTaskManagerViews(browser);
return task_manager::TaskManagerMac::Show();
}
void HideTaskManager() {
if (ShouldUseViewsTaskManager())
return chrome::HideTaskManagerViews();
task_manager::TaskManagerMac::Hide();
}
} // namespace chrome