1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/cocoa/task_manager_mac.h"
10 #include "base/mac/bundle_locations.h"
11 #include "base/prefs/pref_service.h"
12 #include "base/strings/sys_string_conversions.h"
13 #include "chrome/browser/browser_process.h"
14 #import "chrome/browser/ui/cocoa/window_size_autosaver.h"
15 #include "chrome/browser/ui/host_desktop.h"
16 #include "chrome/common/pref_names.h"
17 #include "chrome/grit/generated_resources.h"
18 #include "third_party/skia/include/core/SkBitmap.h"
19 #include "ui/base/l10n/l10n_util_mac.h"
20 #include "ui/gfx/image/image_skia.h"
24 // Width of "a" and most other letters/digits in "small" table views.
25 const int kCharWidth = 6;
27 // Some of the strings below have spaces at the end or are missing letters, to
28 // make the columns look nicer, and to take potentially longer localized strings
30 const struct ColumnWidth {
33 int maxWidth; // If this is -1, 1.5*minColumWidth is used as max width.
35 // Note that arraysize includes the trailing \0. That's intended.
36 { IDS_TASK_MANAGER_TASK_COLUMN, 120, 600 },
37 { IDS_TASK_MANAGER_PROFILE_NAME_COLUMN, 60, 200 },
38 { IDS_TASK_MANAGER_PHYSICAL_MEM_COLUMN,
39 arraysize("800 MiB") * kCharWidth, -1 },
40 { IDS_TASK_MANAGER_SHARED_MEM_COLUMN,
41 arraysize("800 MiB") * kCharWidth, -1 },
42 { IDS_TASK_MANAGER_PRIVATE_MEM_COLUMN,
43 arraysize("800 MiB") * kCharWidth, -1 },
44 { IDS_TASK_MANAGER_CPU_COLUMN,
45 arraysize("99.9") * kCharWidth, -1 },
46 { IDS_TASK_MANAGER_NET_COLUMN,
47 arraysize("150 kiB/s") * kCharWidth, -1 },
48 { IDS_TASK_MANAGER_PROCESS_ID_COLUMN,
49 arraysize("73099 ") * kCharWidth, -1 },
50 { IDS_TASK_MANAGER_WEBCORE_IMAGE_CACHE_COLUMN,
51 arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
52 { IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN,
53 arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
54 { IDS_TASK_MANAGER_WEBCORE_CSS_CACHE_COLUMN,
55 arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
56 { IDS_TASK_MANAGER_VIDEO_MEMORY_COLUMN,
57 arraysize("2000.0K") * kCharWidth, -1 },
58 { IDS_TASK_MANAGER_SQLITE_MEMORY_USED_COLUMN,
59 arraysize("800 kB") * kCharWidth, -1 },
60 { IDS_TASK_MANAGER_JAVASCRIPT_MEMORY_ALLOCATED_COLUMN,
61 arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
62 { IDS_TASK_MANAGER_NACL_DEBUG_STUB_PORT_COLUMN,
63 arraysize("32767") * kCharWidth, -1 },
64 { IDS_TASK_MANAGER_IDLE_WAKEUPS_COLUMN,
65 arraysize("idlewakeups") * kCharWidth, -1 },
70 SortHelper(TaskManagerModel* model, NSSortDescriptor* column)
71 : sort_column_([[column key] intValue]),
72 ascending_([column ascending]),
75 bool operator()(int a, int b) {
76 TaskManagerModel::GroupRange group_range1 =
77 model_->GetGroupRangeForResource(a);
78 TaskManagerModel::GroupRange group_range2 =
79 model_->GetGroupRangeForResource(b);
80 if (group_range1 == group_range2) {
81 // The two rows are in the same group, sort so that items in the same
82 // group always appear in the same order. |ascending_| is intentionally
86 // Sort by the first entry of each of the groups.
87 int cmp_result = model_->CompareValues(
88 group_range1.first, group_range2.first, sort_column_);
90 cmp_result = -cmp_result;
91 return cmp_result < 0;
96 TaskManagerModel* model_; // weak;
101 @interface TaskManagerWindowController (Private)
102 - (NSTableColumn*)addColumnWithId:(int)columnId visible:(BOOL)isVisible;
103 - (void)setUpTableColumns;
104 - (void)setUpTableHeaderContextMenu;
105 - (void)toggleColumn:(id)sender;
106 - (void)adjustSelectionAndEndProcessButton;
107 - (void)deselectRows;
110 ////////////////////////////////////////////////////////////////////////////////
111 // TaskManagerWindowController implementation:
113 @implementation TaskManagerWindowController
115 - (id)initWithTaskManagerObserver:(TaskManagerMac*)taskManagerObserver {
116 NSString* nibpath = [base::mac::FrameworkBundle()
117 pathForResource:@"TaskManager"
119 if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
120 taskManagerObserver_ = taskManagerObserver;
121 taskManager_ = taskManagerObserver_->task_manager();
122 model_ = taskManager_->model();
124 if (g_browser_process && g_browser_process->local_state()) {
125 size_saver_.reset([[WindowSizeAutosaver alloc]
126 initWithWindow:[self window]
127 prefService:g_browser_process->local_state()
128 path:prefs::kTaskManagerWindowPlacement]);
130 [[self window] setExcludedFromWindowsMenu:YES];
131 [self showWindow:self];
136 - (void)sortShuffleArray {
137 viewToModelMap_.resize(model_->ResourceCount());
138 for (size_t i = 0; i < viewToModelMap_.size(); ++i)
139 viewToModelMap_[i] = i;
141 std::sort(viewToModelMap_.begin(), viewToModelMap_.end(),
142 SortHelper(model_, currentSortDescriptor_.get()));
144 modelToViewMap_.resize(viewToModelMap_.size());
145 for (size_t i = 0; i < viewToModelMap_.size(); ++i)
146 modelToViewMap_[viewToModelMap_[i]] = i;
150 // Store old view indices, and the model indices they map to.
151 NSIndexSet* viewSelection = [tableView_ selectedRowIndexes];
152 std::vector<int> modelSelection;
153 for (NSUInteger i = [viewSelection lastIndex];
155 i = [viewSelection indexLessThanIndex:i]) {
156 modelSelection.push_back(viewToModelMap_[i]);
160 [self sortShuffleArray];
162 // Use the model indices to get the new view indices of the selection, and
163 // set selection to that. This assumes that no rows were added or removed
164 // (in that case, the selection is cleared before -reloadData is called).
165 if (!modelSelection.empty())
166 DCHECK_EQ([tableView_ numberOfRows], model_->ResourceCount());
167 NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
168 for (size_t i = 0; i < modelSelection.size(); ++i)
169 [indexSet addIndex:modelToViewMap_[modelSelection[i]]];
170 [tableView_ selectRowIndexes:indexSet byExtendingSelection:NO];
172 [tableView_ reloadData];
173 [self adjustSelectionAndEndProcessButton];
176 - (IBAction)statsLinkClicked:(id)sender {
177 TaskManager::GetInstance()->OpenAboutMemory(chrome::HOST_DESKTOP_TYPE_NATIVE);
180 - (IBAction)killSelectedProcesses:(id)sender {
181 NSIndexSet* selection = [tableView_ selectedRowIndexes];
182 for (NSUInteger i = [selection lastIndex];
184 i = [selection indexLessThanIndex:i]) {
185 taskManager_->KillProcess(viewToModelMap_[i]);
189 - (void)selectDoubleClickedTab:(id)sender {
190 NSInteger row = [tableView_ clickedRow];
192 return; // Happens e.g. if the table header is double-clicked.
193 taskManager_->ActivateProcess(viewToModelMap_[row]);
196 - (NSTableView*)tableView {
200 - (void)awakeFromNib {
201 [self setUpTableColumns];
202 [self setUpTableHeaderContextMenu];
203 [self adjustSelectionAndEndProcessButton];
205 [tableView_ setDoubleAction:@selector(selectDoubleClickedTab:)];
206 [tableView_ setIntercellSpacing:NSMakeSize(0.0, 0.0)];
207 [tableView_ sizeToFit];
211 [tableView_ setDelegate:nil];
212 [tableView_ setDataSource:nil];
216 // Adds a column which has the given string id as title. |isVisible| specifies
217 // if the column is initially visible.
218 - (NSTableColumn*)addColumnWithId:(int)columnId visible:(BOOL)isVisible {
219 base::scoped_nsobject<NSTableColumn> column([[NSTableColumn alloc]
220 initWithIdentifier:[NSString stringWithFormat:@"%d", columnId]]);
222 NSTextAlignment textAlignment =
223 (columnId == IDS_TASK_MANAGER_TASK_COLUMN ||
224 columnId == IDS_TASK_MANAGER_PROFILE_NAME_COLUMN) ?
225 NSLeftTextAlignment : NSRightTextAlignment;
227 [[column.get() headerCell]
228 setStringValue:l10n_util::GetNSStringWithFixup(columnId)];
229 [[column.get() headerCell] setAlignment:textAlignment];
230 [[column.get() dataCell] setAlignment:textAlignment];
232 NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
233 [[column.get() dataCell] setFont:font];
235 [column.get() setHidden:!isVisible];
236 [column.get() setEditable:NO];
238 // The page column should by default be sorted ascending.
239 BOOL ascending = columnId == IDS_TASK_MANAGER_TASK_COLUMN;
241 base::scoped_nsobject<NSSortDescriptor> sortDescriptor(
242 [[NSSortDescriptor alloc]
243 initWithKey:[NSString stringWithFormat:@"%d", columnId]
244 ascending:ascending]);
245 [column.get() setSortDescriptorPrototype:sortDescriptor.get()];
247 // Default values, only used in release builds if nobody notices the DCHECK
248 // during development when adding new columns.
249 int minWidth = 200, maxWidth = 400;
252 for (i = 0; i < arraysize(columnWidths); ++i) {
253 if (columnWidths[i].columnId == columnId) {
254 minWidth = columnWidths[i].minWidth;
255 maxWidth = columnWidths[i].maxWidth;
257 maxWidth = 3 * minWidth / 2; // *1.5 for ints.
261 DCHECK(i < arraysize(columnWidths)) << "Could not find " << columnId;
262 [column.get() setMinWidth:minWidth];
263 [column.get() setMaxWidth:maxWidth];
264 [column.get() setResizingMask:NSTableColumnAutoresizingMask |
265 NSTableColumnUserResizingMask];
267 [tableView_ addTableColumn:column.get()];
268 return column.get(); // Now retained by |tableView_|.
271 // Adds all the task manager's columns to the table.
272 - (void)setUpTableColumns {
273 for (NSTableColumn* column in [tableView_ tableColumns])
274 [tableView_ removeTableColumn:column];
275 NSTableColumn* nameColumn = [self addColumnWithId:IDS_TASK_MANAGER_TASK_COLUMN
277 // |nameColumn| displays an icon for every row -- this is done by an
279 base::scoped_nsobject<NSButtonCell> nameCell(
280 [[NSButtonCell alloc] initTextCell:@""]);
281 [nameCell.get() setImagePosition:NSImageLeft];
282 [nameCell.get() setButtonType:NSSwitchButton];
283 [nameCell.get() setAlignment:[[nameColumn dataCell] alignment]];
284 [nameCell.get() setFont:[[nameColumn dataCell] font]];
285 [nameColumn setDataCell:nameCell.get()];
287 // Initially, sort on the tab name.
288 [tableView_ setSortDescriptors:
289 [NSArray arrayWithObject:[nameColumn sortDescriptorPrototype]]];
290 [self addColumnWithId:IDS_TASK_MANAGER_PROFILE_NAME_COLUMN visible:NO];
291 [self addColumnWithId:IDS_TASK_MANAGER_PHYSICAL_MEM_COLUMN visible:YES];
292 [self addColumnWithId:IDS_TASK_MANAGER_SHARED_MEM_COLUMN visible:NO];
293 [self addColumnWithId:IDS_TASK_MANAGER_PRIVATE_MEM_COLUMN visible:NO];
294 [self addColumnWithId:IDS_TASK_MANAGER_CPU_COLUMN visible:YES];
295 [self addColumnWithId:IDS_TASK_MANAGER_NET_COLUMN visible:YES];
296 [self addColumnWithId:IDS_TASK_MANAGER_PROCESS_ID_COLUMN visible:YES];
297 [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_IMAGE_CACHE_COLUMN
299 [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN
301 [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_CSS_CACHE_COLUMN visible:NO];
302 [self addColumnWithId:IDS_TASK_MANAGER_VIDEO_MEMORY_COLUMN visible:NO];
303 [self addColumnWithId:IDS_TASK_MANAGER_SQLITE_MEMORY_USED_COLUMN visible:NO];
304 [self addColumnWithId:IDS_TASK_MANAGER_JAVASCRIPT_MEMORY_ALLOCATED_COLUMN
306 [self addColumnWithId:IDS_TASK_MANAGER_NACL_DEBUG_STUB_PORT_COLUMN
308 [self addColumnWithId:IDS_TASK_MANAGER_IDLE_WAKEUPS_COLUMN
312 // Creates a context menu for the table header that allows the user to toggle
313 // which columns should be shown and which should be hidden (like e.g.
314 // Task Manager.app's table header context menu).
315 - (void)setUpTableHeaderContextMenu {
316 base::scoped_nsobject<NSMenu> contextMenu(
317 [[NSMenu alloc] initWithTitle:@"Task Manager context menu"]);
318 for (NSTableColumn* column in [tableView_ tableColumns]) {
319 NSMenuItem* item = [contextMenu.get()
320 addItemWithTitle:[[column headerCell] stringValue]
321 action:@selector(toggleColumn:)
323 [item setTarget:self];
324 [item setRepresentedObject:column];
325 [item setState:[column isHidden] ? NSOffState : NSOnState];
327 [[tableView_ headerView] setMenu:contextMenu.get()];
330 // Callback for the table header context menu. Toggles visibility of the table
331 // column associated with the clicked menu item.
332 - (void)toggleColumn:(id)item {
333 DCHECK([item isKindOfClass:[NSMenuItem class]]);
334 if (![item isKindOfClass:[NSMenuItem class]])
337 NSTableColumn* column = [item representedObject];
339 NSInteger oldState = [item state];
340 NSInteger newState = oldState == NSOnState ? NSOffState : NSOnState;
342 // If hiding the column, make sure at least one column will remain visible.
343 if (newState == NSOffState) {
344 // Find the first column that will be visible after hiding |column|.
345 NSTableColumn* firstRemainingVisibleColumn = nil;
347 for (NSTableColumn* nextColumn in [tableView_ tableColumns]) {
348 if (nextColumn != column && ![nextColumn isHidden]) {
349 firstRemainingVisibleColumn = nextColumn;
354 // If no column will be visible, abort the toggle. This will basically cause
355 // the toggle operation to silently fail. The other way to ensure at least
356 // one visible column is to disable the menu item corresponding to the last
357 // remaining visible column. That would place the menu in a weird state to
358 // the user, where there's one item somewhere that's grayed out with no
359 // clear explanation of why. It will be rare for a user to try hiding all
360 // columns, but we still want to guard against it. If they are really intent
361 // on hiding the last visible column (perhaps they plan to choose another
362 // one after that to be visible), odds are they will try making another
363 // column visible and then hiding the one column that would not hide.
364 if (firstRemainingVisibleColumn == nil) {
368 // If |column| is being used to sort the table (i.e. it's the primary sort
369 // column), make the first remaining visible column the new primary sort
371 int primarySortColumnId = [[currentSortDescriptor_.get() key] intValue];
372 DCHECK(primarySortColumnId);
373 int columnId = [[column identifier] intValue];
375 if (primarySortColumnId == columnId) {
376 NSSortDescriptor* newSortDescriptor =
377 [firstRemainingVisibleColumn sortDescriptorPrototype];
378 [tableView_ setSortDescriptors:
379 [NSArray arrayWithObject:newSortDescriptor]];
384 [column setHidden:newState == NSOffState];
385 [item setState:newState];
387 [tableView_ sizeToFit];
388 [tableView_ setNeedsDisplay];
391 // This function appropriately sets the enabled states on the table's editing
393 - (void)adjustSelectionAndEndProcessButton {
394 bool selectionContainsBrowserProcess = false;
396 // If a row is selected, make sure that all rows belonging to the same process
397 // are selected as well. Also, check if the selection contains the browser
399 NSIndexSet* selection = [tableView_ selectedRowIndexes];
400 for (NSUInteger i = [selection lastIndex];
402 i = [selection indexLessThanIndex:i]) {
403 int modelIndex = viewToModelMap_[i];
404 if (taskManager_->IsBrowserProcess(modelIndex))
405 selectionContainsBrowserProcess = true;
407 TaskManagerModel::GroupRange rangePair =
408 model_->GetGroupRangeForResource(modelIndex);
409 NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
410 for (int j = 0; j < rangePair.second; ++j)
411 [indexSet addIndex:modelToViewMap_[rangePair.first + j]];
412 [tableView_ selectRowIndexes:indexSet byExtendingSelection:YES];
415 bool enabled = [selection count] > 0 && !selectionContainsBrowserProcess;
416 [endProcessButton_ setEnabled:enabled];
419 - (void)deselectRows {
420 [tableView_ deselectAll:self];
423 // Table view delegate methods.
425 // The selection is being changed by mouse (drag/click).
426 - (void)tableViewSelectionIsChanging:(NSNotification*)aNotification {
427 [self adjustSelectionAndEndProcessButton];
430 // The selection is being changed by keyboard (arrows).
431 - (void)tableViewSelectionDidChange:(NSNotification*)aNotification {
432 [self adjustSelectionAndEndProcessButton];
435 - (void)windowWillClose:(NSNotification*)notification {
436 if (taskManagerObserver_) {
437 taskManagerObserver_->WindowWasClosed();
438 taskManagerObserver_ = nil;
445 @implementation TaskManagerWindowController (NSTableDataSource)
447 - (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView {
448 DCHECK(tableView == tableView_ || tableView_ == nil);
449 return model_->ResourceCount();
452 - (NSString*)modelTextForRow:(int)row column:(int)columnId {
453 DCHECK_LT(static_cast<size_t>(row), viewToModelMap_.size());
454 return base::SysUTF16ToNSString(
455 model_->GetResourceById(viewToModelMap_[row], columnId));
458 - (id)tableView:(NSTableView*)tableView
459 objectValueForTableColumn:(NSTableColumn*)tableColumn
460 row:(NSInteger)rowIndex {
461 // NSButtonCells expect an on/off state as objectValue. Their title is set
462 // in |tableView:dataCellForTableColumn:row:| below.
463 if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_TASK_COLUMN) {
464 return [NSNumber numberWithInt:NSOffState];
467 return [self modelTextForRow:rowIndex
468 column:[[tableColumn identifier] intValue]];
471 - (NSCell*)tableView:(NSTableView*)tableView
472 dataCellForTableColumn:(NSTableColumn*)tableColumn
473 row:(NSInteger)rowIndex {
474 NSCell* cell = [tableColumn dataCellForRow:rowIndex];
476 // Set the favicon and title for the task in the name column.
477 if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_TASK_COLUMN) {
478 DCHECK([cell isKindOfClass:[NSButtonCell class]]);
479 NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell);
480 NSString* title = [self modelTextForRow:rowIndex
481 column:[[tableColumn identifier] intValue]];
482 [buttonCell setTitle:title];
483 [buttonCell setImage:
484 taskManagerObserver_->GetImageForRow(viewToModelMap_[rowIndex])];
485 [buttonCell setRefusesFirstResponder:YES]; // Don't push in like a button.
486 [buttonCell setHighlightsBy:NSNoCellMask];
492 - (void) tableView:(NSTableView*)tableView
493 sortDescriptorsDidChange:(NSArray*)oldDescriptors {
494 NSArray* newDescriptors = [tableView sortDescriptors];
495 if ([newDescriptors count] < 1) {
496 currentSortDescriptor_.reset(nil);
498 currentSortDescriptor_.reset([[newDescriptors objectAtIndex:0] retain]);
501 [self reloadData]; // Sorts.
506 ////////////////////////////////////////////////////////////////////////////////
507 // TaskManagerMac implementation:
509 TaskManagerMac::TaskManagerMac(TaskManager* task_manager)
510 : task_manager_(task_manager),
511 model_(task_manager->model()),
514 [[TaskManagerWindowController alloc] initWithTaskManagerObserver:this];
515 model_->AddObserver(this);
519 TaskManagerMac* TaskManagerMac::instance_ = NULL;
521 TaskManagerMac::~TaskManagerMac() {
522 if (this == instance_) {
523 // Do not do this when running in unit tests: |StartUpdating()| never got
524 // called in that case.
525 task_manager_->OnWindowClosed();
527 model_->RemoveObserver(this);
530 ////////////////////////////////////////////////////////////////////////////////
531 // TaskManagerMac, TaskManagerModelObserver implementation:
533 void TaskManagerMac::OnModelChanged() {
534 icon_cache_.OnModelChanged();
535 [window_controller_ deselectRows];
536 [window_controller_ reloadData];
539 void TaskManagerMac::OnItemsChanged(int start, int length) {
540 icon_cache_.OnItemsChanged(start, length);
541 [window_controller_ reloadData];
544 void TaskManagerMac::OnItemsAdded(int start, int length) {
545 icon_cache_.OnItemsAdded(start, length);
546 [window_controller_ deselectRows];
547 [window_controller_ reloadData];
550 void TaskManagerMac::OnItemsRemoved(int start, int length) {
551 icon_cache_.OnItemsRemoved(start, length);
552 [window_controller_ deselectRows];
553 [window_controller_ reloadData];
556 NSImage* TaskManagerMac::GetImageForRow(int row) {
557 return icon_cache_.GetImageForRow(row);
560 ////////////////////////////////////////////////////////////////////////////////
561 // TaskManagerMac, public:
563 void TaskManagerMac::WindowWasClosed() {
565 instance_ = NULL; // |instance_| is static
568 int TaskManagerMac::RowCount() const {
569 return model_->ResourceCount();
572 gfx::ImageSkia TaskManagerMac::GetIcon(int r) const {
573 return model_->GetResourceIcon(r);
577 void TaskManagerMac::Show() {
579 [[instance_->window_controller_ window]
580 makeKeyAndOrderFront:instance_->window_controller_];
583 // Create a new instance.
584 instance_ = new TaskManagerMac(TaskManager::GetInstance());
585 instance_->model_->StartUpdating();
589 void TaskManagerMac::Hide() {
591 [instance_->window_controller_ close];
596 // Declared in browser_dialogs.h.
597 void ShowTaskManager(Browser* browser) {
598 TaskManagerMac::Show();
601 void HideTaskManager() {
602 TaskManagerMac::Hide();
605 } // namespace chrome