// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "chrome/browser/ui/cocoa/task_manager_mac.h" #include <algorithm> #include <vector> #include "base/mac/mac_util.h" #include "base/sys_string_conversions.h" #include "chrome/browser/browser_process.h" #import "chrome/browser/ui/cocoa/window_size_autosaver.h" #include "chrome/common/pref_names.h" #include "grit/generated_resources.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/base/l10n/l10n_util_mac.h" namespace { // Width of "a" and most other letters/digits in "small" table views. const int kCharWidth = 6; // Some of the strings below have spaces at the end or are missing letters, to // make the columns look nicer, and to take potentially longer localized strings // into account. const struct ColumnWidth { int columnId; int minWidth; int maxWidth; // If this is -1, 1.5*minColumWidth is used as max width. } columnWidths[] = { // Note that arraysize includes the trailing \0. That's intended. { IDS_TASK_MANAGER_PAGE_COLUMN, 120, 600 }, { IDS_TASK_MANAGER_PHYSICAL_MEM_COLUMN, arraysize("800 MiB") * kCharWidth, -1 }, { IDS_TASK_MANAGER_SHARED_MEM_COLUMN, arraysize("800 MiB") * kCharWidth, -1 }, { IDS_TASK_MANAGER_PRIVATE_MEM_COLUMN, arraysize("800 MiB") * kCharWidth, -1 }, { IDS_TASK_MANAGER_CPU_COLUMN, arraysize("99.9") * kCharWidth, -1 }, { IDS_TASK_MANAGER_NET_COLUMN, arraysize("150 kiB/s") * kCharWidth, -1 }, { IDS_TASK_MANAGER_PROCESS_ID_COLUMN, arraysize("73099 ") * kCharWidth, -1 }, { IDS_TASK_MANAGER_WEBCORE_IMAGE_CACHE_COLUMN, arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 }, { IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN, arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 }, { IDS_TASK_MANAGER_WEBCORE_CSS_CACHE_COLUMN, arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 }, { IDS_TASK_MANAGER_SQLITE_MEMORY_USED_COLUMN, arraysize("800 kB") * kCharWidth, -1 }, { IDS_TASK_MANAGER_JAVASCRIPT_MEMORY_ALLOCATED_COLUMN, arraysize("2000.0K (2000.0 live)") * kCharWidth, -1 }, { IDS_TASK_MANAGER_GOATS_TELEPORTED_COLUMN, arraysize("15 ") * kCharWidth, -1 }, }; class SortHelper { public: SortHelper(TaskManagerModel* model, NSSortDescriptor* column) : sort_column_([[column key] intValue]), ascending_([column ascending]), model_(model) {} bool operator()(int a, int b) { std::pair<int, int> group_range1 = model_->GetGroupRangeForResource(a); std::pair<int, int> group_range2 = model_->GetGroupRangeForResource(b); if (group_range1 == group_range2) { // The two rows are in the same group, sort so that items in the same // group always appear in the same order. |ascending_| is intentionally // ignored. return a < b; } // Sort by the first entry of each of the groups. int cmp_result = model_->CompareValues( group_range1.first, group_range2.first, sort_column_); if (!ascending_) cmp_result = -cmp_result; return cmp_result < 0; } private: int sort_column_; bool ascending_; TaskManagerModel* model_; // weak; }; } // namespace @interface TaskManagerWindowController (Private) - (NSTableColumn*)addColumnWithId:(int)columnId visible:(BOOL)isVisible; - (void)setUpTableColumns; - (void)setUpTableHeaderContextMenu; - (void)toggleColumn:(id)sender; - (void)adjustSelectionAndEndProcessButton; - (void)deselectRows; @end //////////////////////////////////////////////////////////////////////////////// // TaskManagerWindowController implementation: @implementation TaskManagerWindowController - (id)initWithTaskManagerObserver:(TaskManagerMac*)taskManagerObserver highlightBackgroundResources:(bool)highlightBackgroundResources { NSString* nibpath = [base::mac::MainAppBundle() pathForResource:@"TaskManager" ofType:@"nib"]; if ((self = [super initWithWindowNibPath:nibpath owner:self])) { taskManagerObserver_ = taskManagerObserver; taskManager_ = taskManagerObserver_->task_manager(); model_ = taskManager_->model(); highlightBackgroundResources_ = highlightBackgroundResources; if (highlightBackgroundResources_) { // Highlight background resources with a yellow background. backgroundResourceColor_.reset( [[NSColor colorWithDeviceRed:0xff/255.0 green:0xfa/255.0 blue:0xcd/255.0 alpha:1.0] retain]); } if (g_browser_process && g_browser_process->local_state()) { size_saver_.reset([[WindowSizeAutosaver alloc] initWithWindow:[self window] prefService:g_browser_process->local_state() path:prefs::kTaskManagerWindowPlacement]); } [self showWindow:self]; } return self; } - (void)sortShuffleArray { viewToModelMap_.resize(model_->ResourceCount()); for (size_t i = 0; i < viewToModelMap_.size(); ++i) viewToModelMap_[i] = i; std::sort(viewToModelMap_.begin(), viewToModelMap_.end(), SortHelper(model_, currentSortDescriptor_.get())); modelToViewMap_.resize(viewToModelMap_.size()); for (size_t i = 0; i < viewToModelMap_.size(); ++i) modelToViewMap_[viewToModelMap_[i]] = i; } - (void)reloadData { // Store old view indices, and the model indices they map to. NSIndexSet* viewSelection = [tableView_ selectedRowIndexes]; std::vector<int> modelSelection; for (NSUInteger i = [viewSelection lastIndex]; i != NSNotFound; i = [viewSelection indexLessThanIndex:i]) { modelSelection.push_back(viewToModelMap_[i]); } // Sort. [self sortShuffleArray]; // Use the model indices to get the new view indices of the selection, and // set selection to that. This assumes that no rows were added or removed // (in that case, the selection is cleared before -reloadData is called). if (!modelSelection.empty()) DCHECK_EQ([tableView_ numberOfRows], model_->ResourceCount()); NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet]; for (size_t i = 0; i < modelSelection.size(); ++i) [indexSet addIndex:modelToViewMap_[modelSelection[i]]]; [tableView_ selectRowIndexes:indexSet byExtendingSelection:NO]; [tableView_ reloadData]; [self adjustSelectionAndEndProcessButton]; } - (IBAction)statsLinkClicked:(id)sender { TaskManager::GetInstance()->OpenAboutMemory(); } - (IBAction)killSelectedProcesses:(id)sender { NSIndexSet* selection = [tableView_ selectedRowIndexes]; for (NSUInteger i = [selection lastIndex]; i != NSNotFound; i = [selection indexLessThanIndex:i]) { taskManager_->KillProcess(viewToModelMap_[i]); } } - (void)selectDoubleClickedTab:(id)sender { NSInteger row = [tableView_ clickedRow]; if (row < 0) return; // Happens e.g. if the table header is double-clicked. taskManager_->ActivateProcess(viewToModelMap_[row]); } - (NSTableView*)tableView { return tableView_; } - (void)awakeFromNib { [self setUpTableColumns]; [self setUpTableHeaderContextMenu]; [self adjustSelectionAndEndProcessButton]; [tableView_ setDoubleAction:@selector(selectDoubleClickedTab:)]; [tableView_ setIntercellSpacing:NSMakeSize(0.0, 0.0)]; [tableView_ sizeToFit]; } - (void)dealloc { [tableView_ setDelegate:nil]; [tableView_ setDataSource:nil]; [super dealloc]; } // Adds a column which has the given string id as title. |isVisible| specifies // if the column is initially visible. - (NSTableColumn*)addColumnWithId:(int)columnId visible:(BOOL)isVisible { scoped_nsobject<NSTableColumn> column([[NSTableColumn alloc] initWithIdentifier:[NSNumber numberWithInt:columnId]]); NSTextAlignment textAlignment = columnId == IDS_TASK_MANAGER_PAGE_COLUMN ? NSLeftTextAlignment : NSRightTextAlignment; [[column.get() headerCell] setStringValue:l10n_util::GetNSStringWithFixup(columnId)]; [[column.get() headerCell] setAlignment:textAlignment]; [[column.get() dataCell] setAlignment:textAlignment]; NSFont* font = [NSFont systemFontOfSize:[NSFont smallSystemFontSize]]; [[column.get() dataCell] setFont:font]; [column.get() setHidden:!isVisible]; [column.get() setEditable:NO]; // The page column should by default be sorted ascending. BOOL ascending = columnId == IDS_TASK_MANAGER_PAGE_COLUMN; scoped_nsobject<NSSortDescriptor> sortDescriptor([[NSSortDescriptor alloc] initWithKey:[NSString stringWithFormat:@"%d", columnId] ascending:ascending]); [column.get() setSortDescriptorPrototype:sortDescriptor.get()]; // Default values, only used in release builds if nobody notices the DCHECK // during development when adding new columns. int minWidth = 200, maxWidth = 400; size_t i; for (i = 0; i < arraysize(columnWidths); ++i) { if (columnWidths[i].columnId == columnId) { minWidth = columnWidths[i].minWidth; maxWidth = columnWidths[i].maxWidth; if (maxWidth < 0) maxWidth = 3 * minWidth / 2; // *1.5 for ints. break; } } DCHECK(i < arraysize(columnWidths)) << "Could not find " << columnId; [column.get() setMinWidth:minWidth]; [column.get() setMaxWidth:maxWidth]; [column.get() setResizingMask:NSTableColumnAutoresizingMask | NSTableColumnUserResizingMask]; [tableView_ addTableColumn:column.get()]; return column.get(); // Now retained by |tableView_|. } // Adds all the task manager's columns to the table. - (void)setUpTableColumns { for (NSTableColumn* column in [tableView_ tableColumns]) [tableView_ removeTableColumn:column]; NSTableColumn* nameColumn = [self addColumnWithId:IDS_TASK_MANAGER_PAGE_COLUMN visible:YES]; // |nameColumn| displays an icon for every row -- this is done by an // NSButtonCell. scoped_nsobject<NSButtonCell> nameCell( [[NSButtonCell alloc] initTextCell:@""]); [nameCell.get() setImagePosition:NSImageLeft]; [nameCell.get() setButtonType:NSSwitchButton]; [nameCell.get() setAlignment:[[nameColumn dataCell] alignment]]; [nameCell.get() setFont:[[nameColumn dataCell] font]]; [nameColumn setDataCell:nameCell.get()]; // Initially, sort on the tab name. [tableView_ setSortDescriptors: [NSArray arrayWithObject:[nameColumn sortDescriptorPrototype]]]; [self addColumnWithId:IDS_TASK_MANAGER_PHYSICAL_MEM_COLUMN visible:YES]; [self addColumnWithId:IDS_TASK_MANAGER_SHARED_MEM_COLUMN visible:NO]; [self addColumnWithId:IDS_TASK_MANAGER_PRIVATE_MEM_COLUMN visible:NO]; [self addColumnWithId:IDS_TASK_MANAGER_CPU_COLUMN visible:YES]; [self addColumnWithId:IDS_TASK_MANAGER_NET_COLUMN visible:YES]; [self addColumnWithId:IDS_TASK_MANAGER_PROCESS_ID_COLUMN visible:NO]; [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_IMAGE_CACHE_COLUMN visible:NO]; [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN visible:NO]; [self addColumnWithId:IDS_TASK_MANAGER_WEBCORE_CSS_CACHE_COLUMN visible:NO]; [self addColumnWithId:IDS_TASK_MANAGER_SQLITE_MEMORY_USED_COLUMN visible:NO]; [self addColumnWithId:IDS_TASK_MANAGER_JAVASCRIPT_MEMORY_ALLOCATED_COLUMN visible:NO]; [self addColumnWithId:IDS_TASK_MANAGER_GOATS_TELEPORTED_COLUMN visible:NO]; } // Creates a context menu for the table header that allows the user to toggle // which columns should be shown and which should be hidden (like e.g. // Task Manager.app's table header context menu). - (void)setUpTableHeaderContextMenu { scoped_nsobject<NSMenu> contextMenu( [[NSMenu alloc] initWithTitle:@"Task Manager context menu"]); for (NSTableColumn* column in [tableView_ tableColumns]) { NSMenuItem* item = [contextMenu.get() addItemWithTitle:[[column headerCell] stringValue] action:@selector(toggleColumn:) keyEquivalent:@""]; [item setTarget:self]; [item setRepresentedObject:column]; [item setState:[column isHidden] ? NSOffState : NSOnState]; } [[tableView_ headerView] setMenu:contextMenu.get()]; } // Callback for the table header context menu. Toggles visibility of the table // column associated with the clicked menu item. - (void)toggleColumn:(id)item { DCHECK([item isKindOfClass:[NSMenuItem class]]); if (![item isKindOfClass:[NSMenuItem class]]) return; NSTableColumn* column = [item representedObject]; DCHECK(column); NSInteger oldState = [item state]; NSInteger newState = oldState == NSOnState ? NSOffState : NSOnState; [column setHidden:newState == NSOffState]; [item setState:newState]; [tableView_ sizeToFit]; [tableView_ setNeedsDisplay]; } // This function appropriately sets the enabled states on the table's editing // buttons. - (void)adjustSelectionAndEndProcessButton { bool selectionContainsBrowserProcess = false; // If a row is selected, make sure that all rows belonging to the same process // are selected as well. Also, check if the selection contains the browser // process. NSIndexSet* selection = [tableView_ selectedRowIndexes]; for (NSUInteger i = [selection lastIndex]; i != NSNotFound; i = [selection indexLessThanIndex:i]) { int modelIndex = viewToModelMap_[i]; if (taskManager_->IsBrowserProcess(modelIndex)) selectionContainsBrowserProcess = true; std::pair<int, int> rangePair = model_->GetGroupRangeForResource(modelIndex); NSMutableIndexSet* indexSet = [NSMutableIndexSet indexSet]; for (int j = 0; j < rangePair.second; ++j) [indexSet addIndex:modelToViewMap_[rangePair.first + j]]; [tableView_ selectRowIndexes:indexSet byExtendingSelection:YES]; } bool enabled = [selection count] > 0 && !selectionContainsBrowserProcess; [endProcessButton_ setEnabled:enabled]; } - (void)deselectRows { [tableView_ deselectAll:self]; } // Table view delegate method. - (void)tableViewSelectionIsChanging:(NSNotification*)aNotification { [self adjustSelectionAndEndProcessButton]; } - (void)windowWillClose:(NSNotification*)notification { if (taskManagerObserver_) { taskManagerObserver_->WindowWasClosed(); taskManagerObserver_ = nil; } [self autorelease]; } // Delegate method invoked before each cell in the table is displayed. We // override this to provide highlighting of background resources. - (void) tableView:(NSTableView*)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)row { if (!highlightBackgroundResources_) return; DCHECK([cell respondsToSelector:@selector(setBackgroundColor:)]); if ([cell respondsToSelector:@selector(setBackgroundColor:)]) { NSColor* color = nil; if (taskManagerObserver_->IsBackgroundRow(viewToModelMap_[row]) && ![tableView isRowSelected:row]) { color = backgroundResourceColor_.get(); if ((row % 2) == 1 && [tableView usesAlternatingRowBackgroundColors]) { color = [color blendedColorWithFraction:0.05 ofColor:[NSColor blackColor]]; } } [cell setBackgroundColor:color]; // The icon at the left is an |NSButtonCell|, which does not // implement this method on 10.5. if ([cell respondsToSelector:@selector(setDrawsBackground:)]) [cell setDrawsBackground:(color != nil)]; } } @end @implementation TaskManagerWindowController (NSTableDataSource) - (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView { DCHECK(tableView == tableView_ || tableView_ == nil); return model_->ResourceCount(); } - (NSString*)modelTextForRow:(int)row column:(int)columnId { DCHECK_LT(static_cast<size_t>(row), viewToModelMap_.size()); row = viewToModelMap_[row]; switch (columnId) { case IDS_TASK_MANAGER_PAGE_COLUMN: // Process return base::SysUTF16ToNSString(model_->GetResourceTitle(row)); case IDS_TASK_MANAGER_PRIVATE_MEM_COLUMN: // Memory if (!model_->IsResourceFirstInGroup(row)) return @""; return base::SysUTF16ToNSString(model_->GetResourcePrivateMemory(row)); case IDS_TASK_MANAGER_SHARED_MEM_COLUMN: // Memory if (!model_->IsResourceFirstInGroup(row)) return @""; return base::SysUTF16ToNSString(model_->GetResourceSharedMemory(row)); case IDS_TASK_MANAGER_PHYSICAL_MEM_COLUMN: // Memory if (!model_->IsResourceFirstInGroup(row)) return @""; return base::SysUTF16ToNSString(model_->GetResourcePhysicalMemory(row)); case IDS_TASK_MANAGER_CPU_COLUMN: // CPU if (!model_->IsResourceFirstInGroup(row)) return @""; return base::SysUTF16ToNSString(model_->GetResourceCPUUsage(row)); case IDS_TASK_MANAGER_NET_COLUMN: // Net return base::SysUTF16ToNSString(model_->GetResourceNetworkUsage(row)); case IDS_TASK_MANAGER_PROCESS_ID_COLUMN: // Process ID if (!model_->IsResourceFirstInGroup(row)) return @""; return base::SysUTF16ToNSString(model_->GetResourceProcessId(row)); case IDS_TASK_MANAGER_WEBCORE_IMAGE_CACHE_COLUMN: // WebCore image cache if (!model_->IsResourceFirstInGroup(row)) return @""; return base::SysUTF16ToNSString( model_->GetResourceWebCoreImageCacheSize(row)); case IDS_TASK_MANAGER_WEBCORE_SCRIPTS_CACHE_COLUMN: // WebCore script cache if (!model_->IsResourceFirstInGroup(row)) return @""; return base::SysUTF16ToNSString( model_->GetResourceWebCoreScriptsCacheSize(row)); case IDS_TASK_MANAGER_WEBCORE_CSS_CACHE_COLUMN: // WebCore CSS cache if (!model_->IsResourceFirstInGroup(row)) return @""; return base::SysUTF16ToNSString( model_->GetResourceWebCoreCSSCacheSize(row)); case IDS_TASK_MANAGER_SQLITE_MEMORY_USED_COLUMN: if (!model_->IsResourceFirstInGroup(row)) return @""; return base::SysUTF16ToNSString( model_->GetResourceSqliteMemoryUsed(row)); case IDS_TASK_MANAGER_JAVASCRIPT_MEMORY_ALLOCATED_COLUMN: if (!model_->IsResourceFirstInGroup(row)) return @""; return base::SysUTF16ToNSString( model_->GetResourceV8MemoryAllocatedSize(row)); case IDS_TASK_MANAGER_GOATS_TELEPORTED_COLUMN: // Goats Teleported! return base::SysUTF16ToNSString(model_->GetResourceGoatsTeleported(row)); default: NOTREACHED(); return @""; } } - (id)tableView:(NSTableView*)tableView objectValueForTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)rowIndex { // NSButtonCells expect an on/off state as objectValue. Their title is set // in |tableView:dataCellForTableColumn:row:| below. if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_PAGE_COLUMN) { return [NSNumber numberWithInt:NSOffState]; } return [self modelTextForRow:rowIndex column:[[tableColumn identifier] intValue]]; } - (NSCell*)tableView:(NSTableView*)tableView dataCellForTableColumn:(NSTableColumn*)tableColumn row:(NSInteger)rowIndex { NSCell* cell = [tableColumn dataCellForRow:rowIndex]; // Set the favicon and title for the task in the name column. if ([[tableColumn identifier] intValue] == IDS_TASK_MANAGER_PAGE_COLUMN) { DCHECK([cell isKindOfClass:[NSButtonCell class]]); NSButtonCell* buttonCell = static_cast<NSButtonCell*>(cell); NSString* title = [self modelTextForRow:rowIndex column:[[tableColumn identifier] intValue]]; [buttonCell setTitle:title]; [buttonCell setImage: taskManagerObserver_->GetImageForRow(viewToModelMap_[rowIndex])]; [buttonCell setRefusesFirstResponder:YES]; // Don't push in like a button. [buttonCell setHighlightsBy:NSNoCellMask]; } return cell; } - (void) tableView:(NSTableView*)tableView sortDescriptorsDidChange:(NSArray*)oldDescriptors { NSArray* newDescriptors = [tableView sortDescriptors]; if ([newDescriptors count] < 1) return; currentSortDescriptor_.reset([[newDescriptors objectAtIndex:0] retain]); [self reloadData]; // Sorts. } @end //////////////////////////////////////////////////////////////////////////////// // TaskManagerMac implementation: TaskManagerMac::TaskManagerMac(TaskManager* task_manager, bool highlight_background_resources) : task_manager_(task_manager), model_(task_manager->model()), icon_cache_(this), highlight_background_resources_(highlight_background_resources) { window_controller_ = [[TaskManagerWindowController alloc] initWithTaskManagerObserver:this highlightBackgroundResources:highlight_background_resources]; model_->AddObserver(this); } // static TaskManagerMac* TaskManagerMac::instance_ = NULL; TaskManagerMac::~TaskManagerMac() { if (this == instance_) { // Do not do this when running in unit tests: |StartUpdating()| never got // called in that case. task_manager_->OnWindowClosed(); } model_->RemoveObserver(this); } //////////////////////////////////////////////////////////////////////////////// // TaskManagerMac, TaskManagerModelObserver implementation: void TaskManagerMac::OnModelChanged() { icon_cache_.OnModelChanged(); [window_controller_ deselectRows]; [window_controller_ reloadData]; } void TaskManagerMac::OnItemsChanged(int start, int length) { icon_cache_.OnItemsChanged(start, length); [window_controller_ reloadData]; } void TaskManagerMac::OnItemsAdded(int start, int length) { icon_cache_.OnItemsAdded(start, length); [window_controller_ deselectRows]; [window_controller_ reloadData]; } void TaskManagerMac::OnItemsRemoved(int start, int length) { icon_cache_.OnItemsRemoved(start, length); [window_controller_ deselectRows]; [window_controller_ reloadData]; } NSImage* TaskManagerMac::GetImageForRow(int row) { return icon_cache_.GetImageForRow(row); } //////////////////////////////////////////////////////////////////////////////// // TaskManagerMac, public: void TaskManagerMac::WindowWasClosed() { delete this; instance_ = NULL; } int TaskManagerMac::RowCount() const { return model_->ResourceCount(); } SkBitmap TaskManagerMac::GetIcon(int r) const { return model_->GetResourceIcon(r); } bool TaskManagerMac::IsBackgroundRow(int row) const { return model_->IsBackgroundResource(row); } // static void TaskManagerMac::Show(bool highlight_background_resources) { if (instance_) { if (instance_->highlight_background_resources_ == highlight_background_resources) { // There's a Task manager window open already, so just activate it. [[instance_->window_controller_ window] makeKeyAndOrderFront:instance_->window_controller_]; return; } else { // The user is switching between "View Background Pages" and // "Task Manager" so close the existing window and fall through to // open a new one. [[instance_->window_controller_ window] close]; } } // Create a new instance. instance_ = new TaskManagerMac(TaskManager::GetInstance(), highlight_background_resources); instance_->model_->StartUpdating(); }