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