• 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 "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  { IDS_TASK_MANAGER_GOATS_TELEPORTED_COLUMN,
68      arraysize("15 ") * kCharWidth, -1 },
69};
70
71class SortHelper {
72 public:
73  SortHelper(TaskManagerModel* model, NSSortDescriptor* column)
74      : sort_column_([[column key] intValue]),
75        ascending_([column ascending]),
76        model_(model) {}
77
78  bool operator()(int a, int b) {
79    TaskManagerModel::GroupRange group_range1 =
80        model_->GetGroupRangeForResource(a);
81    TaskManagerModel::GroupRange group_range2 =
82        model_->GetGroupRangeForResource(b);
83    if (group_range1 == group_range2) {
84      // The two rows are in the same group, sort so that items in the same
85      // group always appear in the same order. |ascending_| is intentionally
86      // ignored.
87      return a < b;
88    }
89    // Sort by the first entry of each of the groups.
90    int cmp_result = model_->CompareValues(
91        group_range1.first, group_range2.first, sort_column_);
92    if (!ascending_)
93      cmp_result = -cmp_result;
94    return cmp_result < 0;
95  }
96 private:
97  int sort_column_;
98  bool ascending_;
99  TaskManagerModel* model_;  // weak;
100};
101
102}  // namespace
103
104@interface TaskManagerWindowController (Private)
105- (NSTableColumn*)addColumnWithId:(int)columnId visible:(BOOL)isVisible;
106- (void)setUpTableColumns;
107- (void)setUpTableHeaderContextMenu;
108- (void)toggleColumn:(id)sender;
109- (void)adjustSelectionAndEndProcessButton;
110- (void)deselectRows;
111@end
112
113////////////////////////////////////////////////////////////////////////////////
114// TaskManagerWindowController implementation:
115
116@implementation TaskManagerWindowController
117
118- (id)initWithTaskManagerObserver:(TaskManagerMac*)taskManagerObserver {
119  NSString* nibpath = [base::mac::FrameworkBundle()
120                        pathForResource:@"TaskManager"
121                                 ofType:@"nib"];
122  if ((self = [super initWithWindowNibPath:nibpath owner:self])) {
123    taskManagerObserver_ = taskManagerObserver;
124    taskManager_ = taskManagerObserver_->task_manager();
125    model_ = taskManager_->model();
126
127    if (g_browser_process && g_browser_process->local_state()) {
128      size_saver_.reset([[WindowSizeAutosaver alloc]
129          initWithWindow:[self window]
130             prefService:g_browser_process->local_state()
131                    path:prefs::kTaskManagerWindowPlacement]);
132    }
133    [self showWindow:self];
134  }
135  return self;
136}
137
138- (void)sortShuffleArray {
139  viewToModelMap_.resize(model_->ResourceCount());
140  for (size_t i = 0; i < viewToModelMap_.size(); ++i)
141    viewToModelMap_[i] = i;
142
143  std::sort(viewToModelMap_.begin(), viewToModelMap_.end(),
144            SortHelper(model_, currentSortDescriptor_.get()));
145
146  modelToViewMap_.resize(viewToModelMap_.size());
147  for (size_t i = 0; i < viewToModelMap_.size(); ++i)
148    modelToViewMap_[viewToModelMap_[i]] = i;
149}
150
151- (void)reloadData {
152  // Store old view indices, and the model indices they map to.
153  NSIndexSet* viewSelection = [tableView_ selectedRowIndexes];
154  std::vector<int> modelSelection;
155  for (NSUInteger i = [viewSelection lastIndex];
156       i != NSNotFound;
157       i = [viewSelection indexLessThanIndex:i]) {
158    modelSelection.push_back(viewToModelMap_[i]);
159  }
160
161  // Sort.
162  [self sortShuffleArray];
163
164  // Use the model indices to get the new view indices of the selection, and
165  // set selection to that. This assumes that no rows were added or removed
166  // (in that case, the selection is cleared before -reloadData is called).
167  if (!modelSelection.empty())
168    DCHECK_EQ([tableView_ numberOfRows], model_->ResourceCount());
169  NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
170  for (size_t i = 0; i < modelSelection.size(); ++i)
171    [indexSet addIndex:modelToViewMap_[modelSelection[i]]];
172  [tableView_ selectRowIndexes:indexSet byExtendingSelection:NO];
173
174  [tableView_ reloadData];
175  [self adjustSelectionAndEndProcessButton];
176}
177
178- (IBAction)statsLinkClicked:(id)sender {
179  TaskManager::GetInstance()->OpenAboutMemory(chrome::HOST_DESKTOP_TYPE_NATIVE);
180}
181
182- (IBAction)killSelectedProcesses:(id)sender {
183  NSIndexSet* selection = [tableView_ selectedRowIndexes];
184  for (NSUInteger i = [selection lastIndex];
185       i != NSNotFound;
186       i = [selection indexLessThanIndex:i]) {
187    taskManager_->KillProcess(viewToModelMap_[i]);
188  }
189}
190
191- (void)selectDoubleClickedTab:(id)sender {
192  NSInteger row = [tableView_ clickedRow];
193  if (row < 0)
194    return;  // Happens e.g. if the table header is double-clicked.
195  taskManager_->ActivateProcess(viewToModelMap_[row]);
196}
197
198- (NSTableView*)tableView {
199  return tableView_;
200}
201
202- (void)awakeFromNib {
203  [self setUpTableColumns];
204  [self setUpTableHeaderContextMenu];
205  [self adjustSelectionAndEndProcessButton];
206
207  [tableView_ setDoubleAction:@selector(selectDoubleClickedTab:)];
208  [tableView_ setIntercellSpacing:NSMakeSize(0.0, 0.0)];
209  [tableView_ sizeToFit];
210}
211
212- (void)dealloc {
213  [tableView_ setDelegate:nil];
214  [tableView_ setDataSource:nil];
215  [super dealloc];
216}
217
218// Adds a column which has the given string id as title. |isVisible| specifies
219// if the column is initially visible.
220- (NSTableColumn*)addColumnWithId:(int)columnId visible:(BOOL)isVisible {
221  base::scoped_nsobject<NSTableColumn> column([[NSTableColumn alloc]
222      initWithIdentifier:[NSString stringWithFormat:@"%d", columnId]]);
223
224  NSTextAlignment textAlignment =
225      (columnId == IDS_TASK_MANAGER_TASK_COLUMN ||
226       columnId == IDS_TASK_MANAGER_PROFILE_NAME_COLUMN) ?
227          NSLeftTextAlignment : NSRightTextAlignment;
228
229  [[column.get() headerCell]
230      setStringValue:l10n_util::GetNSStringWithFixup(columnId)];
231  [[column.get() headerCell] setAlignment:textAlignment];
232  [[column.get() dataCell] setAlignment:textAlignment];
233
234  NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]];
235  [[column.get() dataCell] setFont:font];
236
237  [column.get() setHidden:!isVisible];
238  [column.get() setEditable:NO];
239
240  // The page column should by default be sorted ascending.
241  BOOL ascending = columnId == IDS_TASK_MANAGER_TASK_COLUMN;
242
243  base::scoped_nsobject<NSSortDescriptor> sortDescriptor(
244      [[NSSortDescriptor alloc]
245          initWithKey:[NSString stringWithFormat:@"%d", columnId]
246            ascending:ascending]);
247  [column.get() setSortDescriptorPrototype:sortDescriptor.get()];
248
249  // Default values, only used in release builds if nobody notices the DCHECK
250  // during development when adding new columns.
251  int minWidth = 200, maxWidth = 400;
252
253  size_t i;
254  for (i = 0; i < arraysize(columnWidths); ++i) {
255    if (columnWidths[i].columnId == columnId) {
256      minWidth = columnWidths[i].minWidth;
257      maxWidth = columnWidths[i].maxWidth;
258      if (maxWidth < 0)
259        maxWidth = 3 * minWidth / 2;  // *1.5 for ints.
260      break;
261    }
262  }
263  DCHECK(i < arraysize(columnWidths)) << "Could not find " << columnId;
264  [column.get() setMinWidth:minWidth];
265  [column.get() setMaxWidth:maxWidth];
266  [column.get() setResizingMask:NSTableColumnAutoresizingMask |
267                                NSTableColumnUserResizingMask];
268
269  [tableView_ addTableColumn:column.get()];
270  return column.get();  // Now retained by |tableView_|.
271}
272
273// Adds all the task manager's columns to the table.
274- (void)setUpTableColumns {
275  for (NSTableColumn* column in [tableView_ tableColumns])
276    [tableView_ removeTableColumn:column];
277  NSTableColumn* nameColumn = [self addColumnWithId:IDS_TASK_MANAGER_TASK_COLUMN
278                                            visible:YES];
279  // |nameColumn| displays an icon for every row -- this is done by an
280  // NSButtonCell.
281  base::scoped_nsobject<NSButtonCell> nameCell(
282      [[NSButtonCell alloc] initTextCell:@""]);
283  [nameCell.get() setImagePosition:NSImageLeft];
284  [nameCell.get() setButtonType:NSSwitchButton];
285  [nameCell.get() setAlignment:[[nameColumn dataCell] alignment]];
286  [nameCell.get() setFont:[[nameColumn dataCell] font]];
287  [nameColumn setDataCell:nameCell.get()];
288
289  // Initially, sort on the tab name.
290  [tableView_ setSortDescriptors:
291      [NSArray arrayWithObject:[nameColumn sortDescriptorPrototype]]];
292  [self addColumnWithId:IDS_TASK_MANAGER_PROFILE_NAME_COLUMN visible:NO];
293  [self addColumnWithId:IDS_TASK_MANAGER_PHYSICAL_MEM_COLUMN visible:YES];
294  [self addColumnWithId:IDS_TASK_MANAGER_SHARED_MEM_COLUMN visible:NO];
295  [self addColumnWithId:IDS_TASK_MANAGER_PRIVATE_MEM_COLUMN visible:NO];
296  [self addColumnWithId:IDS_TASK_MANAGER_CPU_COLUMN visible:YES];
297  [self addColumnWithId:IDS_TASK_MANAGER_NET_COLUMN visible:YES];
298  [self addColumnWithId:IDS_TASK_MANAGER_PROCESS_ID_COLUMN visible:YES];
299  [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_IMAGE_CACHE_COLUMN
300                visible:NO];
301  [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN
302                visible:NO];
303  [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_CSS_CACHE_COLUMN visible:NO];
304  [self addColumnWithId:IDS_TASK_MANAGER_VIDEO_MEMORY_COLUMN visible:NO];
305  [self addColumnWithId:IDS_TASK_MANAGER_SQLITE_MEMORY_USED_COLUMN visible:NO];
306  [self addColumnWithId:IDS_TASK_MANAGER_JAVASCRIPT_MEMORY_ALLOCATED_COLUMN
307                visible:NO];
308  [self addColumnWithId:IDS_TASK_MANAGER_NACL_DEBUG_STUB_PORT_COLUMN
309                visible:NO];
310  [self addColumnWithId:IDS_TASK_MANAGER_IDLE_WAKEUPS_COLUMN
311                visible:NO];
312  [self addColumnWithId:IDS_TASK_MANAGER_GOATS_TELEPORTED_COLUMN visible:NO];
313}
314
315// Creates a context menu for the table header that allows the user to toggle
316// which columns should be shown and which should be hidden (like e.g.
317// Task Manager.app's table header context menu).
318- (void)setUpTableHeaderContextMenu {
319  base::scoped_nsobject<NSMenu> contextMenu(
320      [[NSMenu alloc] initWithTitle:@"Task Manager context menu"]);
321  for (NSTableColumn* column in [tableView_ tableColumns]) {
322    NSMenuItem* item = [contextMenu.get()
323        addItemWithTitle:[[column headerCell] stringValue]
324                  action:@selector(toggleColumn:)
325           keyEquivalent:@""];
326    [item setTarget:self];
327    [item setRepresentedObject:column];
328    [item setState:[column isHidden] ? NSOffState : NSOnState];
329  }
330  [[tableView_ headerView] setMenu:contextMenu.get()];
331}
332
333// Callback for the table header context menu. Toggles visibility of the table
334// column associated with the clicked menu item.
335- (void)toggleColumn:(id)item {
336  DCHECK([item isKindOfClass:[NSMenuItem class]]);
337  if (![item isKindOfClass:[NSMenuItem class]])
338    return;
339
340  NSTableColumn* column = [item representedObject];
341  DCHECK(column);
342  NSInteger oldState = [item state];
343  NSInteger newState = oldState == NSOnState ? NSOffState : NSOnState;
344  [column setHidden:newState == NSOffState];
345  [item setState:newState];
346  [tableView_ sizeToFit];
347  [tableView_ setNeedsDisplay];
348}
349
350// This function appropriately sets the enabled states on the table's editing
351// buttons.
352- (void)adjustSelectionAndEndProcessButton {
353  bool selectionContainsBrowserProcess = false;
354
355  // If a row is selected, make sure that all rows belonging to the same process
356  // are selected as well. Also, check if the selection contains the browser
357  // process.
358  NSIndexSet* selection = [tableView_ selectedRowIndexes];
359  for (NSUInteger i = [selection lastIndex];
360       i != NSNotFound;
361       i = [selection indexLessThanIndex:i]) {
362    int modelIndex = viewToModelMap_[i];
363    if (taskManager_->IsBrowserProcess(modelIndex))
364      selectionContainsBrowserProcess = true;
365
366    TaskManagerModel::GroupRange rangePair =
367        model_->GetGroupRangeForResource(modelIndex);
368    NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet];
369    for (int j = 0; j < rangePair.second; ++j)
370      [indexSet addIndex:modelToViewMap_[rangePair.first + j]];
371    [tableView_ selectRowIndexes:indexSet byExtendingSelection:YES];
372  }
373
374  bool enabled = [selection count] > 0 && !selectionContainsBrowserProcess;
375  [endProcessButton_ setEnabled:enabled];
376}
377
378- (void)deselectRows {
379  [tableView_ deselectAll:self];
380}
381
382// Table view delegate methods.
383
384// The selection is being changed by mouse (drag/click).
385- (void)tableViewSelectionIsChanging:(NSNotification*)aNotification {
386  [self adjustSelectionAndEndProcessButton];
387}
388
389// The selection is being changed by keyboard (arrows).
390- (void)tableViewSelectionDidChange:(NSNotification*)aNotification {
391  [self adjustSelectionAndEndProcessButton];
392}
393
394- (void)windowWillClose:(NSNotification*)notification {
395  if (taskManagerObserver_) {
396    taskManagerObserver_->WindowWasClosed();
397    taskManagerObserver_ = nil;
398  }
399  [self autorelease];
400}
401
402@end
403
404@implementation TaskManagerWindowController (NSTableDataSource)
405
406- (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView {
407  DCHECK(tableView == tableView_ || tableView_ == nil);
408  return model_->ResourceCount();
409}
410
411- (NSString*)modelTextForRow:(int)row column:(int)columnId {
412  DCHECK_LT(static_cast<size_t>(row), viewToModelMap_.size());
413  return base::SysUTF16ToNSString(
414      model_->GetResourceById(viewToModelMap_[row], columnId));
415}
416
417- (id)tableView:(NSTableView*)tableView
418    objectValueForTableColumn:(NSTableColumn*)tableColumn
419                          row:(NSInteger)rowIndex {
420  // NSButtonCells expect an on/off state as objectValue. Their title is set
421  // in |tableView:dataCellForTableColumn:row:| below.
422  if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_TASK_COLUMN) {
423    return [NSNumber numberWithInt:NSOffState];
424  }
425
426  return [self modelTextForRow:rowIndex
427                        column:[[tableColumn identifier] intValue]];
428}
429
430- (NSCell*)tableView:(NSTableView*)tableView
431    dataCellForTableColumn:(NSTableColumn*)tableColumn
432                       row:(NSInteger)rowIndex {
433  NSCell* cell = [tableColumn dataCellForRow:rowIndex];
434
435  // Set the favicon and title for the task in the name column.
436  if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_TASK_COLUMN) {
437    DCHECK([cell isKindOfClass:[NSButtonCell class]]);
438    NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell);
439    NSString* title = [self modelTextForRow:rowIndex
440                                    column:[[tableColumn identifier] intValue]];
441    [buttonCell setTitle:title];
442    [buttonCell setImage:
443        taskManagerObserver_->GetImageForRow(viewToModelMap_[rowIndex])];
444    [buttonCell setRefusesFirstResponder:YES];  // Don't push in like a button.
445    [buttonCell setHighlightsBy:NSNoCellMask];
446  }
447
448  return cell;
449}
450
451- (void)           tableView:(NSTableView*)tableView
452    sortDescriptorsDidChange:(NSArray*)oldDescriptors {
453  NSArray* newDescriptors = [tableView sortDescriptors];
454  if ([newDescriptors count] < 1)
455    return;
456
457  currentSortDescriptor_.reset([[newDescriptors objectAtIndex:0] retain]);
458  [self reloadData];  // Sorts.
459}
460
461@end
462
463////////////////////////////////////////////////////////////////////////////////
464// TaskManagerMac implementation:
465
466TaskManagerMac::TaskManagerMac(TaskManager* task_manager)
467  : task_manager_(task_manager),
468    model_(task_manager->model()),
469    icon_cache_(this) {
470  window_controller_ =
471      [[TaskManagerWindowController alloc] initWithTaskManagerObserver:this];
472  model_->AddObserver(this);
473}
474
475// static
476TaskManagerMac* TaskManagerMac::instance_ = NULL;
477
478TaskManagerMac::~TaskManagerMac() {
479  if (this == instance_) {
480    // Do not do this when running in unit tests: |StartUpdating()| never got
481    // called in that case.
482    task_manager_->OnWindowClosed();
483  }
484  model_->RemoveObserver(this);
485}
486
487////////////////////////////////////////////////////////////////////////////////
488// TaskManagerMac, TaskManagerModelObserver implementation:
489
490void TaskManagerMac::OnModelChanged() {
491  icon_cache_.OnModelChanged();
492  [window_controller_ deselectRows];
493  [window_controller_ reloadData];
494}
495
496void TaskManagerMac::OnItemsChanged(int start, int length) {
497  icon_cache_.OnItemsChanged(start, length);
498  [window_controller_ reloadData];
499}
500
501void TaskManagerMac::OnItemsAdded(int start, int length) {
502  icon_cache_.OnItemsAdded(start, length);
503  [window_controller_ deselectRows];
504  [window_controller_ reloadData];
505}
506
507void TaskManagerMac::OnItemsRemoved(int start, int length) {
508  icon_cache_.OnItemsRemoved(start, length);
509  [window_controller_ deselectRows];
510  [window_controller_ reloadData];
511}
512
513NSImage* TaskManagerMac::GetImageForRow(int row) {
514  return icon_cache_.GetImageForRow(row);
515}
516
517////////////////////////////////////////////////////////////////////////////////
518// TaskManagerMac, public:
519
520void TaskManagerMac::WindowWasClosed() {
521  instance_ = NULL;
522  delete this;
523}
524
525int TaskManagerMac::RowCount() const {
526  return model_->ResourceCount();
527}
528
529gfx::ImageSkia TaskManagerMac::GetIcon(int r) const {
530  return model_->GetResourceIcon(r);
531}
532
533// static
534void TaskManagerMac::Show() {
535  if (instance_) {
536    [[instance_->window_controller_ window]
537      makeKeyAndOrderFront:instance_->window_controller_];
538    return;
539  }
540  // Create a new instance.
541  instance_ = new TaskManagerMac(TaskManager::GetInstance());
542  instance_->model_->StartUpdating();
543}
544
545namespace chrome {
546
547// Declared in browser_dialogs.h.
548void ShowTaskManager(Browser* browser) {
549  TaskManagerMac::Show();
550}
551
552}  // namespace chrome
553
554