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 23 namespace { 24 25 // Width of "a" and most other letters/digits in "small" table views. 26 const 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. 31 const 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 69 class 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 463 TaskManagerMac::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 473 TaskManagerMac* TaskManagerMac::instance_ = NULL; 474 475 TaskManagerMac::~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 487 void TaskManagerMac::OnModelChanged() { 488 icon_cache_.OnModelChanged(); 489 [window_controller_ deselectRows]; 490 [window_controller_ reloadData]; 491 } 492 493 void TaskManagerMac::OnItemsChanged(int start, int length) { 494 icon_cache_.OnItemsChanged(start, length); 495 [window_controller_ reloadData]; 496 } 497 498 void TaskManagerMac::OnItemsAdded(int start, int length) { 499 icon_cache_.OnItemsAdded(start, length); 500 [window_controller_ deselectRows]; 501 [window_controller_ reloadData]; 502 } 503 504 void TaskManagerMac::OnItemsRemoved(int start, int length) { 505 icon_cache_.OnItemsRemoved(start, length); 506 [window_controller_ deselectRows]; 507 [window_controller_ reloadData]; 508 } 509 510 NSImage* TaskManagerMac::GetImageForRow(int row) { 511 return icon_cache_.GetImageForRow(row); 512 } 513 514 //////////////////////////////////////////////////////////////////////////////// 515 // TaskManagerMac, public: 516 517 void TaskManagerMac::WindowWasClosed() { 518 instance_ = NULL; 519 delete this; 520 } 521 522 int TaskManagerMac::RowCount() const { 523 return model_->ResourceCount(); 524 } 525 526 gfx::ImageSkia TaskManagerMac::GetIcon(int r) const { 527 return model_->GetResourceIcon(r); 528 } 529 530 // static 531 void 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 542 namespace chrome { 543 544 // Declared in browser_dialogs.h. 545 void ShowTaskManager(Browser* browser) { 546 TaskManagerMac::Show(); 547 } 548 549 } // namespace chrome 550 551