1// Copyright (c) 2011 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 "base/mac/mac_util.h" 6#import "chrome/browser/themes/theme_service.h" 7#import "chrome/browser/ui/cocoa/menu_controller.h" 8#import "chrome/browser/ui/cocoa/tabs/tab_controller.h" 9#import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h" 10#import "chrome/browser/ui/cocoa/tabs/tab_view.h" 11#import "chrome/browser/ui/cocoa/themed_window.h" 12#import "chrome/common/extensions/extension.h" 13#include "grit/generated_resources.h" 14#import "third_party/GTM/AppKit/GTMFadeTruncatingTextFieldCell.h" 15#include "ui/base/l10n/l10n_util_mac.h" 16 17@implementation TabController 18 19@synthesize action = action_; 20@synthesize app = app_; 21@synthesize loadingState = loadingState_; 22@synthesize mini = mini_; 23@synthesize pinned = pinned_; 24@synthesize target = target_; 25@synthesize url = url_; 26@synthesize iconView = iconView_; 27@synthesize titleView = titleView_; 28@synthesize closeButton = closeButton_; 29 30namespace TabControllerInternal { 31 32// A C++ delegate that handles enabling/disabling menu items and handling when 33// a menu command is chosen. Also fixes up the menu item label for "pin/unpin 34// tab". 35class MenuDelegate : public ui::SimpleMenuModel::Delegate { 36 public: 37 explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner) 38 : target_(target), 39 owner_(owner) {} 40 41 // Overridden from ui::SimpleMenuModel::Delegate 42 virtual bool IsCommandIdChecked(int command_id) const { return false; } 43 virtual bool IsCommandIdEnabled(int command_id) const { 44 TabStripModel::ContextMenuCommand command = 45 static_cast<TabStripModel::ContextMenuCommand>(command_id); 46 return [target_ isCommandEnabled:command forController:owner_]; 47 } 48 virtual bool GetAcceleratorForCommandId( 49 int command_id, 50 ui::Accelerator* accelerator) { return false; } 51 virtual void ExecuteCommand(int command_id) { 52 TabStripModel::ContextMenuCommand command = 53 static_cast<TabStripModel::ContextMenuCommand>(command_id); 54 [target_ commandDispatch:command forController:owner_]; 55 } 56 57 private: 58 id<TabControllerTarget> target_; // weak 59 TabController* owner_; // weak, owns me 60}; 61 62} // TabControllerInternal namespace 63 64// The min widths match the windows values and are sums of left + right 65// padding, of which we have no comparable constants (we draw using paths, not 66// images). The selected tab width includes the close button width. 67+ (CGFloat)minTabWidth { return 31; } 68+ (CGFloat)minSelectedTabWidth { return 46; } 69+ (CGFloat)maxTabWidth { return 220; } 70+ (CGFloat)miniTabWidth { return 53; } 71+ (CGFloat)appTabWidth { return 66; } 72 73- (TabView*)tabView { 74 return static_cast<TabView*>([self view]); 75} 76 77- (id)init { 78 self = [super initWithNibName:@"TabView" bundle:base::mac::MainAppBundle()]; 79 if (self != nil) { 80 isIconShowing_ = YES; 81 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; 82 [defaultCenter addObserver:self 83 selector:@selector(viewResized:) 84 name:NSViewFrameDidChangeNotification 85 object:[self view]]; 86 [defaultCenter addObserver:self 87 selector:@selector(themeChangedNotification:) 88 name:kBrowserThemeDidChangeNotification 89 object:nil]; 90 } 91 return self; 92} 93 94- (void)dealloc { 95 [[NSNotificationCenter defaultCenter] removeObserver:self]; 96 [[self tabView] setController:nil]; 97 [super dealloc]; 98} 99 100// The internals of |-setSelected:| but doesn't check if we're already set 101// to |selected|. Pass the selection change to the subviews that need it and 102// mark ourselves as needing a redraw. 103- (void)internalSetSelected:(BOOL)selected { 104 selected_ = selected; 105 TabView* tabView = static_cast<TabView*>([self view]); 106 DCHECK([tabView isKindOfClass:[TabView class]]); 107 [tabView setState:selected]; 108 [tabView cancelAlert]; 109 [self updateVisibility]; 110 [self updateTitleColor]; 111} 112 113// Called when the tab's nib is done loading and all outlets are hooked up. 114- (void)awakeFromNib { 115 // Remember the icon's frame, so that if the icon is ever removed, a new 116 // one can later replace it in the proper location. 117 originalIconFrame_ = [iconView_ frame]; 118 119 // When the icon is removed, the title expands to the left to fill the space 120 // left by the icon. When the close button is removed, the title expands to 121 // the right to fill its space. These are the amounts to expand and contract 122 // titleView_ under those conditions. We don't have to explicilty save the 123 // offset between the title and the close button since we can just get that 124 // value for the close button's frame. 125 NSRect titleFrame = [titleView_ frame]; 126 iconTitleXOffset_ = NSMinX(titleFrame) - NSMinX(originalIconFrame_); 127 128 [self internalSetSelected:selected_]; 129} 130 131// Called when Cocoa wants to display the context menu. Lazily instantiate 132// the menu based off of the cross-platform model. Re-create the menu and 133// model every time to get the correct labels and enabling. 134- (NSMenu*)menu { 135 contextMenuDelegate_.reset( 136 new TabControllerInternal::MenuDelegate(target_, self)); 137 contextMenuModel_.reset(new TabMenuModel(contextMenuDelegate_.get(), 138 [self pinned])); 139 contextMenuController_.reset( 140 [[MenuController alloc] initWithModel:contextMenuModel_.get() 141 useWithPopUpButtonCell:NO]); 142 return [contextMenuController_ menu]; 143} 144 145- (IBAction)closeTab:(id)sender { 146 if ([[self target] respondsToSelector:@selector(closeTab:)]) { 147 [[self target] performSelector:@selector(closeTab:) 148 withObject:[self view]]; 149 } 150} 151 152- (void)setTitle:(NSString*)title { 153 [[self view] setToolTip:title]; 154 if ([self mini] && ![self selected]) { 155 TabView* tabView = static_cast<TabView*>([self view]); 156 DCHECK([tabView isKindOfClass:[TabView class]]); 157 [tabView startAlert]; 158 } 159 [super setTitle:title]; 160} 161 162- (void)setSelected:(BOOL)selected { 163 if (selected_ != selected) 164 [self internalSetSelected:selected]; 165} 166 167- (BOOL)selected { 168 return selected_; 169} 170 171- (void)setIconView:(NSView*)iconView { 172 [iconView_ removeFromSuperview]; 173 iconView_ = iconView; 174 if ([self app]) { 175 NSRect appIconFrame = [iconView frame]; 176 appIconFrame.origin = originalIconFrame_.origin; 177 // Center the icon. 178 appIconFrame.origin.x = ([TabController appTabWidth] - 179 NSWidth(appIconFrame)) / 2.0; 180 [iconView setFrame:appIconFrame]; 181 } else { 182 [iconView_ setFrame:originalIconFrame_]; 183 } 184 // Ensure that the icon is suppressed if no icon is set or if the tab is too 185 // narrow to display one. 186 [self updateVisibility]; 187 188 if (iconView_) 189 [[self view] addSubview:iconView_]; 190} 191 192- (NSString*)toolTip { 193 return [[self view] toolTip]; 194} 195 196// Return a rough approximation of the number of icons we could fit in the 197// tab. We never actually do this, but it's a helpful guide for determining 198// how much space we have available. 199- (int)iconCapacity { 200 CGFloat width = NSMaxX([closeButton_ frame]) - NSMinX(originalIconFrame_); 201 CGFloat iconWidth = NSWidth(originalIconFrame_); 202 203 return width / iconWidth; 204} 205 206// Returns YES if we should show the icon. When tabs get too small, we clip 207// the favicon before the close button for selected tabs, and prefer the 208// favicon for unselected tabs. The icon can also be suppressed more directly 209// by clearing iconView_. 210- (BOOL)shouldShowIcon { 211 if (!iconView_) 212 return NO; 213 214 if ([self mini]) 215 return YES; 216 217 int iconCapacity = [self iconCapacity]; 218 if ([self selected]) 219 return iconCapacity >= 2; 220 return iconCapacity >= 1; 221} 222 223// Returns YES if we should be showing the close button. The selected tab 224// always shows the close button. 225- (BOOL)shouldShowCloseButton { 226 if ([self mini]) 227 return NO; 228 return ([self selected] || [self iconCapacity] >= 3); 229} 230 231- (void)updateVisibility { 232 // iconView_ may have been replaced or it may be nil, so [iconView_ isHidden] 233 // won't work. Instead, the state of the icon is tracked separately in 234 // isIconShowing_. 235 BOOL newShowIcon = [self shouldShowIcon]; 236 237 [iconView_ setHidden:!newShowIcon]; 238 isIconShowing_ = newShowIcon; 239 240 // If the tab is a mini-tab, hide the title. 241 [titleView_ setHidden:[self mini]]; 242 243 BOOL newShowCloseButton = [self shouldShowCloseButton]; 244 245 [closeButton_ setHidden:!newShowCloseButton]; 246 247 // Adjust the title view based on changes to the icon's and close button's 248 // visibility. 249 NSRect oldTitleFrame = [titleView_ frame]; 250 NSRect newTitleFrame; 251 newTitleFrame.size.height = oldTitleFrame.size.height; 252 newTitleFrame.origin.y = oldTitleFrame.origin.y; 253 254 if (newShowIcon) { 255 newTitleFrame.origin.x = originalIconFrame_.origin.x + iconTitleXOffset_; 256 } else { 257 newTitleFrame.origin.x = originalIconFrame_.origin.x; 258 } 259 260 if (newShowCloseButton) { 261 newTitleFrame.size.width = NSMinX([closeButton_ frame]) - 262 newTitleFrame.origin.x; 263 } else { 264 newTitleFrame.size.width = NSMaxX([closeButton_ frame]) - 265 newTitleFrame.origin.x; 266 } 267 268 [titleView_ setFrame:newTitleFrame]; 269} 270 271- (void)updateTitleColor { 272 NSColor* titleColor = nil; 273 ui::ThemeProvider* theme = [[[self view] window] themeProvider]; 274 if (theme && ![self selected]) { 275 titleColor = 276 theme->GetNSColor(ThemeService::COLOR_BACKGROUND_TAB_TEXT, 277 true); 278 } 279 // Default to the selected text color unless told otherwise. 280 if (theme && !titleColor) { 281 titleColor = theme->GetNSColor(ThemeService::COLOR_TAB_TEXT, 282 true); 283 } 284 [titleView_ setTextColor:titleColor ? titleColor : [NSColor textColor]]; 285} 286 287// Called when our view is resized. If it gets too small, start by hiding 288// the close button and only show it if tab is selected. Eventually, hide the 289// icon as well. We know that this is for our view because we only registered 290// for notifications from our specific view. 291- (void)viewResized:(NSNotification*)info { 292 [self updateVisibility]; 293} 294 295- (void)themeChangedNotification:(NSNotification*)notification { 296 [self updateTitleColor]; 297} 298 299// Called by the tabs to determine whether we are in rapid (tab) closure mode. 300- (BOOL)inRapidClosureMode { 301 if ([[self target] respondsToSelector:@selector(inRapidClosureMode)]) { 302 return [[self target] performSelector:@selector(inRapidClosureMode)] ? 303 YES : NO; 304 } 305 return NO; 306} 307 308@end 309