• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5#include "chrome/browser/ui/cocoa/task_manager_mac.h"
6
7#include <algorithm>
8#include <vector>
9
10#include "base/mac/bundle_locations.h"
11#include "base/mac/mac_util.h"
12#include "base/prefs/pref_service.h"
13#include "base/strings/sys_string_conversions.h"
14#include "chrome/browser/browser_process.h"
15#import "chrome/browser/ui/cocoa/window_size_autosaver.h"
16#include "chrome/browser/ui/host_desktop.h"
17#include "chrome/common/pref_names.h"
18#include "chrome/grit/generated_resources.h"
19#include "third_party/skia/include/core/SkBitmap.h"
20#include "ui/base/l10n/l10n_util_mac.h"
21#include "ui/gfx/image/image_skia.h"
22
23namespace {
24
25// Width of "a" and most other letters/digits in "small" table views.
26const int kCharWidth = 6;
27
28// Some of the strings below have spaces at the end or are missing letters, to
29// make the columns look nicer, and to take potentially longer localized strings
30// into account.
31const struct ColumnWidth {
32  int columnId;
33  int minWidth;
34  int maxWidth;  // If this is -1, 1.5*minColumWidth is used as max width.
35} columnWidths[] = {
36  // Note that arraysize includes the trailing \0. That's intended.
37  { IDS_TASK_MANAGER_TASK_COLUMN, 120, 600 },
38  { IDS_TASK_MANAGER_PROFILE_NAME_COLUMN, 60, 200 },
39  { IDS_TASK_MANAGER_PHYSICAL_MEM_COLUMN,
40      arraysize("800 MiB") * kCharWidth, -1 },
41  { IDS_TASK_MANAGER_SHARED_MEM_COLUMN,
42      arraysize("800 MiB") * kCharWidth, -1 },
43  { IDS_TASK_MANAGER_PRIVATE_MEM_COLUMN,
44      arraysize("800 MiB") * kCharWidth, -1 },
45  { IDS_TASK_MANAGER_CPU_COLUMN,
46      arraysize("99.9") * kCharWidth, -1 },
47  { IDS_TASK_MANAGER_NET_COLUMN,
48      arraysize("150 kiB/s") * kCharWidth, -1 },
49  { IDS_TASK_MANAGER_PROCESS_ID_COLUMN,
50      arraysize("73099  ") * kCharWidth, -1 },
51  { IDS_TASK_MANAGER_WEBCORE_IMAGE_CACHE_COLUMN,
52      arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
53  { IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN,
54      arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
55  { IDS_TASK_MANAGER_WEBCORE_CSS_CACHE_COLUMN,
56      arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
57  { IDS_TASK_MANAGER_VIDEO_MEMORY_COLUMN,
58      arraysize("2000.0K") * kCharWidth, -1 },
59  { IDS_TASK_MANAGER_SQLITE_MEMORY_USED_COLUMN,
60      arraysize("800 kB") * kCharWidth, -1 },
61  { IDS_TASK_MANAGER_JAVASCRIPT_MEMORY_ALLOCATED_COLUMN,
62      arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 },
63  { IDS_TASK_MANAGER_NACL_DEBUG_STUB_PORT_COLUMN,
64      arraysize("32767") * kCharWidth, -1 },
65  { IDS_TASK_MANAGER_IDLE_WAKEUPS_COLUMN,
66      arraysize("idlewakeups") * kCharWidth, -1 },
67};
68
69class SortHelper {
70 public:
71  SortHelper(TaskManagerModel* model, NSSortDescriptor* column)
72      : sort_column_([[column key] intValue]),
73        ascending_([column ascending]),
74        model_(model) {}
75
76  bool operator()(int a, int b) {
77    TaskManagerModel::GroupRange group_range1 =
78        model_->GetGroupRangeForResource(a);
79    TaskManagerModel::GroupRange group_range2 =
80        model_->GetGroupRangeForResource(b);
81    if (group_range1 == group_range2) {
82      // The two rows are in the same group, sort so that items in the same
83      // group always appear in the same order. |ascending_| is intentionally
84      // ignored.
85      return a < b;
86    }
87    // Sort by the first entry of each of the groups.
88    int cmp_result = model_->CompareValues(
89        group_range1.first, group_range2.first, sort_column_);
90    if (!ascending_)
91      cmp_result = -cmp_result;
92    return cmp_result < 0;
93  }
94 private:
95  int sort_column_;
96  bool ascending_;
97  TaskManagerModel* model_;  // weak;
98};
99
100}  // namespace
101
102@interface TaskManagerWindowController (Private)
103- (NSTableColumn*)addColumnWithId:(int)columnId visible:(BOOL)isVisible;
104- (void)setUpTableColumns;
105- (void)setUpTableHeaderContextMenu;
106- (void)toggleColumn:(id)sender;
107- (void)adjustSelectionAndEndProcessButton;
108- (void)deselectRows;
109@end
110
111////////////////////////////////////////////////////////////////////////////////
112// TaskManagerWindowController implementation:
113
114@implementation TaskManagerWindowController
115
116- (id)initWithTaskManagerObserver:(TaskManagerMac*)taskManagerObserver {
117  NSString* nibpath = [base::mac::FrameworkBundle()
118                        pathForResource:@"TaskManager"
119                                 ofType:@"nib"];
120  if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
121    taskManagerObserver_ = taskManagerObserver;
122    taskManager_ = taskManagerObserver_->task_manager();
123    model_ = taskManager_->model();
124
125    if (g_browser_process && g_browser_process->local_state()) {
126      size_saver_.reset([[WindowSizeAutosaver alloc]
127          initWithWindow:[self window]
128             prefService:g_browser_process->local_state()
129                    path:prefs::kTaskManagerWindowPlacement]);
130    }
131    [self showWindow:self];
132  }
133  return self;
134}
135
136- (void)sortShuffleArray {
137  viewToModelMap_.resize(model_->ResourceCount());
138  for (size_t i = 0; i < viewToModelMap_.size(); ++i)
139    viewToModelMap_[i] = i;
140
141  std::sort(viewToModelMap_.begin(), viewToModelMap_.end(),
142            SortHelper(model_, currentSortDescriptor_.get()));
143
144  modelToViewMap_.resize(viewToModelMap_.size());
145  for (size_t i = 0; i < viewToModelMap_.size(); ++i)
146    modelToViewMap_[viewToModelMap_[i]] = i;
147}
148
149- (void)reloadData {
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];
154       i != NSNotFound;
155       i = [viewSelection indexLessThanIndex:i]) {
156    modelSelection.push_back(viewToModelMap_[i]);
157  }
158
159  // Sort.
160  [self sortShuffleArray];
161
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];
171
172  [tableView_ reloadData];
173  [self adjustSelectionAndEndProcessButton];
174}
175
176- (IBAction)statsLinkClicked:(id)sender {
177  TaskManager::GetInstance()->OpenAboutMemory(chrome::HOST_DESKTOP_TYPE_NATIVE);
178}
179
180- (IBAction)killSelectedProcesses:(id)sender {
181  NSIndexSet* selection = [tableView_ selectedRowIndexes];
182  for (NSUInteger i = [selection lastIndex];
183       i != NSNotFound;
184       i = [selection indexLessThanIndex:i]) {
185    taskManager_->KillProcess(viewToModelMap_[i]);
186  }
187}
188
189- (void)selectDoubleClickedTab:(id)sender {
190  NSInteger row = [tableView_ clickedRow];
191  if (row < 0)
192    return;  // Happens e.g. if the table header is double-clicked.
193  taskManager_->ActivateProcess(viewToModelMap_[row]);
194}
195
196- (NSTableView*)tableView {
197  return tableView_;
198}
199
200- (void)awakeFromNib {
201  [self setUpTableColumns];
202  [self setUpTableHeaderContextMenu];
203  [self adjustSelectionAndEndProcessButton];
204
205  [tableView_ setDoubleAction:@selector(selectDoubleClickedTab:)];
206  [tableView_ setIntercellSpacing:NSMakeSize(0.0, 0.0)];
207  [tableView_ sizeToFit];
208}
209
210- (void)dealloc {
211  [tableView_ setDelegate:nil];
212  [tableView_ setDataSource:nil];
213  [super dealloc];
214}
215
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]]);
221
222  NSTextAlignment textAlignment =
223      (columnId == IDS_TASK_MANAGER_TASK_COLUMN ||
224       columnId == IDS_TASK_MANAGER_PROFILE_NAME_COLUMN) ?
225          NSLeftTextAlignment : NSRightTextAlignment;
226
227  [[column.get() headerCell]
228      setStringValue:l10n_util::GetNSStringWithFixup(columnId)];
229  [[column.get() headerCell] setAlignment:textAlignment];
230  [[column.get() dataCell] setAlignment:textAlignment];
231
232  NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
233  [[column.get() dataCell] setFont:font];
234
235  [column.get() setHidden:!isVisible];
236  [column.get() setEditable:NO];
237
238  // The page column should by default be sorted ascending.
239  BOOL ascending = columnId == IDS_TASK_MANAGER_TASK_COLUMN;
240
241  base::scoped_nsobject<NSSortDescriptor> sortDescriptor(
242      [[NSSortDescriptor alloc]
243          initWithKey:[NSString stringWithFormat:@"%d", columnId]
244            ascending:ascending]);
245  [column.get() setSortDescriptorPrototype:sortDescriptor.get()];
246
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;
250
251  size_t i;
252  for (i = 0; i < arraysize(columnWidths); ++i) {
253    if (columnWidths[i].columnId == columnId) {
254      minWidth = columnWidths[i].minWidth;
255      maxWidth = columnWidths[i].maxWidth;
256      if (maxWidth < 0)
257        maxWidth = 3 * minWidth / 2;  // *1.5 for ints.
258      break;
259    }
260  }
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];
266
267  [tableView_ addTableColumn:column.get()];
268  return column.get();  // Now retained by |tableView_|.
269}
270
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
276                                            visible:YES];
277  // |nameColumn| displays an icon for every row -- this is done by an
278  // NSButtonCell.
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()];
286
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
298                visible:NO];
299  [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN
300                visible:NO];
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
305                visible:NO];
306  [self addColumnWithId:IDS_TASK_MANAGER_NACL_DEBUG_STUB_PORT_COLUMN
307                visible:NO];
308  [self addColumnWithId:IDS_TASK_MANAGER_IDLE_WAKEUPS_COLUMN
309                visible:NO];
310}
311
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:)
322           keyEquivalent:@""];
323    [item setTarget:self];
324    [item setRepresentedObject:column];
325    [item setState:[column isHidden] ? NSOffState : NSOnState];
326  }
327  [[tableView_ headerView] setMenu:contextMenu.get()];
328}
329
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]])
335    return;
336
337  NSTableColumn* column = [item representedObject];
338  DCHECK(column);
339  NSInteger oldState = [item state];
340  NSInteger newState = oldState == NSOnState ? NSOffState : NSOnState;
341  [column setHidden:newState == NSOffState];
342  [item setState:newState];
343  [tableView_ sizeToFit];
344  [tableView_ setNeedsDisplay];
345}
346
347// This function appropriately sets the enabled states on the table's editing
348// buttons.
349- (void)adjustSelectionAndEndProcessButton {
350  bool selectionContainsBrowserProcess = false;
351
352  // If a row is selected, make sure that all rows belonging to the same process
353  // are selected as well. Also, check if the selection contains the browser
354  // process.
355  NSIndexSet* selection = [tableView_ selectedRowIndexes];
356  for (NSUInteger i = [selection lastIndex];
357       i != NSNotFound;
358       i = [selection indexLessThanIndex:i]) {
359    int modelIndex = viewToModelMap_[i];
360    if (taskManager_->IsBrowserProcess(modelIndex))
361      selectionContainsBrowserProcess = true;
362
363    TaskManagerModel::GroupRange rangePair =
364        model_->GetGroupRangeForResource(modelIndex);
365    NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
366    for (int j = 0; j < rangePair.second; ++j)
367      [indexSet addIndex:modelToViewMap_[rangePair.first + j]];
368    [tableView_ selectRowIndexes:indexSet byExtendingSelection:YES];
369  }
370
371  bool enabled = [selection count] > 0 && !selectionContainsBrowserProcess;
372  [endProcessButton_ setEnabled:enabled];
373}
374
375- (void)deselectRows {
376  [tableView_ deselectAll:self];
377}
378
379// Table view delegate methods.
380
381// The selection is being changed by mouse (drag/click).
382- (void)tableViewSelectionIsChanging:(NSNotification*)aNotification {
383  [self adjustSelectionAndEndProcessButton];
384}
385
386// The selection is being changed by keyboard (arrows).
387- (void)tableViewSelectionDidChange:(NSNotification*)aNotification {
388  [self adjustSelectionAndEndProcessButton];
389}
390
391- (void)windowWillClose:(NSNotification*)notification {
392  if (taskManagerObserver_) {
393    taskManagerObserver_->WindowWasClosed();
394    taskManagerObserver_ = nil;
395  }
396  [self autorelease];
397}
398
399@end
400
401@implementation TaskManagerWindowController (NSTableDataSource)
402
403- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView {
404  DCHECK(tableView == tableView_ || tableView_ == nil);
405  return model_->ResourceCount();
406}
407
408- (NSString*)modelTextForRow:(int)row column:(int)columnId {
409  DCHECK_LT(static_cast<size_t>(row), viewToModelMap_.size());
410  return base::SysUTF16ToNSString(
411      model_->GetResourceById(viewToModelMap_[row], columnId));
412}
413
414- (id)tableView:(NSTableView*)tableView
415    objectValueForTableColumn:(NSTableColumn*)tableColumn
416                          row:(NSInteger)rowIndex {
417  // NSButtonCells expect an on/off state as objectValue. Their title is set
418  // in |tableView:dataCellForTableColumn:row:| below.
419  if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_TASK_COLUMN) {
420    return [NSNumber numberWithInt:NSOffState];
421  }
422
423  return [self modelTextForRow:rowIndex
424                        column:[[tableColumn identifier] intValue]];
425}
426
427- (NSCell*)tableView:(NSTableView*)tableView
428    dataCellForTableColumn:(NSTableColumn*)tableColumn
429                       row:(NSInteger)rowIndex {
430  NSCell* cell = [tableColumn dataCellForRow:rowIndex];
431
432  // Set the favicon and title for the task in the name column.
433  if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_TASK_COLUMN) {
434    DCHECK([cell isKindOfClass:[NSButtonCell class]]);
435    NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell);
436    NSString* title = [self modelTextForRow:rowIndex
437                                    column:[[tableColumn identifier] intValue]];
438    [buttonCell setTitle:title];
439    [buttonCell setImage:
440        taskManagerObserver_->GetImageForRow(viewToModelMap_[rowIndex])];
441    [buttonCell setRefusesFirstResponder:YES];  // Don't push in like a button.
442    [buttonCell setHighlightsBy:NSNoCellMask];
443  }
444
445  return cell;
446}
447
448- (void)           tableView:(NSTableView*)tableView
449    sortDescriptorsDidChange:(NSArray*)oldDescriptors {
450  NSArray* newDescriptors = [tableView sortDescriptors];
451  if ([newDescriptors count] < 1)
452    return;
453
454  currentSortDescriptor_.reset([[newDescriptors objectAtIndex:0] retain]);
455  [self reloadData];  // Sorts.
456}
457
458@end
459
460////////////////////////////////////////////////////////////////////////////////
461// TaskManagerMac implementation:
462
463TaskManagerMac::TaskManagerMac(TaskManager* task_manager)
464  : task_manager_(task_manager),
465    model_(task_manager->model()),
466    icon_cache_(this) {
467  window_controller_ =
468      [[TaskManagerWindowController alloc] initWithTaskManagerObserver:this];
469  model_->AddObserver(this);
470}
471
472// static
473TaskManagerMac* TaskManagerMac::instance_ = NULL;
474
475TaskManagerMac::~TaskManagerMac() {
476  if (this == instance_) {
477    // Do not do this when running in unit tests: |StartUpdating()| never got
478    // called in that case.
479    task_manager_->OnWindowClosed();
480  }
481  model_->RemoveObserver(this);
482}
483
484////////////////////////////////////////////////////////////////////////////////
485// TaskManagerMac, TaskManagerModelObserver implementation:
486
487void TaskManagerMac::OnModelChanged() {
488  icon_cache_.OnModelChanged();
489  [window_controller_ deselectRows];
490  [window_controller_ reloadData];
491}
492
493void TaskManagerMac::OnItemsChanged(int start, int length) {
494  icon_cache_.OnItemsChanged(start, length);
495  [window_controller_ reloadData];
496}
497
498void TaskManagerMac::OnItemsAdded(int start, int length) {
499  icon_cache_.OnItemsAdded(start, length);
500  [window_controller_ deselectRows];
501  [window_controller_ reloadData];
502}
503
504void TaskManagerMac::OnItemsRemoved(int start, int length) {
505  icon_cache_.OnItemsRemoved(start, length);
506  [window_controller_ deselectRows];
507  [window_controller_ reloadData];
508}
509
510NSImage* TaskManagerMac::GetImageForRow(int row) {
511  return icon_cache_.GetImageForRow(row);
512}
513
514////////////////////////////////////////////////////////////////////////////////
515// TaskManagerMac, public:
516
517void TaskManagerMac::WindowWasClosed() {
518  instance_ = NULL;
519  delete this;
520}
521
522int TaskManagerMac::RowCount() const {
523  return model_->ResourceCount();
524}
525
526gfx::ImageSkia TaskManagerMac::GetIcon(int r) const {
527  return model_->GetResourceIcon(r);
528}
529
530// static
531void TaskManagerMac::Show() {
532  if (instance_) {
533    [[instance_->window_controller_ window]
534      makeKeyAndOrderFront:instance_->window_controller_];
535    return;
536  }
537  // Create a new instance.
538  instance_ = new TaskManagerMac(TaskManager::GetInstance());
539  instance_->model_->StartUpdating();
540}
541
542namespace chrome {
543
544// Declared in browser_dialogs.h.
545void ShowTaskManager(Browser* browser) {
546  TaskManagerMac::Show();
547}
548
549}  // namespace chrome
550
551