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 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h" 6 7 #include <algorithm> 8 #include <cmath> 9 10 #include "base/i18n/rtl.h" 11 #include "base/mac/bundle_locations.h" 12 #include "base/mac/mac_util.h" 13 #import "chrome/browser/themes/theme_properties.h" 14 #import "chrome/browser/themes/theme_service.h" 15 #import "chrome/browser/ui/cocoa/sprite_view.h" 16 #import "chrome/browser/ui/cocoa/tabs/media_indicator_view.h" 17 #import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h" 18 #import "chrome/browser/ui/cocoa/tabs/tab_view.h" 19 #import "chrome/browser/ui/cocoa/themed_window.h" 20 #import "extensions/common/extension.h" 21 #import "ui/base/cocoa/menu_controller.h" 22 23 @implementation TabController 24 25 @synthesize action = action_; 26 @synthesize app = app_; 27 @synthesize loadingState = loadingState_; 28 @synthesize mini = mini_; 29 @synthesize pinned = pinned_; 30 @synthesize target = target_; 31 @synthesize url = url_; 32 33 namespace TabControllerInternal { 34 35 // A C++ delegate that handles enabling/disabling menu items and handling when 36 // a menu command is chosen. Also fixes up the menu item label for "pin/unpin 37 // tab". 38 class MenuDelegate : public ui::SimpleMenuModel::Delegate { 39 public: 40 explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner) 41 : target_(target), 42 owner_(owner) {} 43 44 // Overridden from ui::SimpleMenuModel::Delegate 45 virtual bool IsCommandIdChecked(int command_id) const OVERRIDE { 46 return false; 47 } 48 virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE { 49 TabStripModel::ContextMenuCommand command = 50 static_cast<TabStripModel::ContextMenuCommand>(command_id); 51 return [target_ isCommandEnabled:command forController:owner_]; 52 } 53 virtual bool GetAcceleratorForCommandId( 54 int command_id, 55 ui::Accelerator* accelerator) OVERRIDE { return false; } 56 virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE { 57 TabStripModel::ContextMenuCommand command = 58 static_cast<TabStripModel::ContextMenuCommand>(command_id); 59 [target_ commandDispatch:command forController:owner_]; 60 } 61 62 private: 63 id<TabControllerTarget> target_; // weak 64 TabController* owner_; // weak, owns me 65 }; 66 67 } // TabControllerInternal namespace 68 69 // The min widths is the smallest number at which the right edge of the right 70 // tab border image is not visibly clipped. It is a bit smaller than the sum 71 // of the two tab edge bitmaps because these bitmaps have a few transparent 72 // pixels on the side. The selected tab width includes the close button width. 73 + (CGFloat)minTabWidth { return 36; } 74 + (CGFloat)minActiveTabWidth { return 52; } 75 + (CGFloat)maxTabWidth { return 214; } 76 + (CGFloat)miniTabWidth { return 58; } 77 + (CGFloat)appTabWidth { return 66; } 78 79 - (TabView*)tabView { 80 DCHECK([[self view] isKindOfClass:[TabView class]]); 81 return static_cast<TabView*>([self view]); 82 } 83 84 - (id)init { 85 if ((self = [super init])) { 86 // Icon. 87 // Remember the icon's frame, so that if the icon is ever removed, a new 88 // one can later replace it in the proper location. 89 originalIconFrame_ = NSMakeRect(19, 5, 16, 16); 90 iconView_.reset([[SpriteView alloc] initWithFrame:originalIconFrame_]); 91 [iconView_ setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin]; 92 93 // When the icon is removed, the title expands to the left to fill the 94 // space left by the icon. When the close button is removed, the title 95 // expands to the right to fill its space. These are the amounts to expand 96 // and contract the title frame under those conditions. We don't have to 97 // explicilty save the offset between the title and the close button since 98 // we can just get that value for the close button's frame. 99 NSRect titleFrame = NSMakeRect(35, 6, 92, 14); 100 101 // Close button. 102 closeButton_.reset([[HoverCloseButton alloc] initWithFrame: 103 NSMakeRect(127, 4, 18, 18)]); 104 [closeButton_ setAutoresizingMask:NSViewMinXMargin]; 105 [closeButton_ setTarget:self]; 106 [closeButton_ setAction:@selector(closeTab:)]; 107 108 base::scoped_nsobject<TabView> view( 109 [[TabView alloc] initWithFrame:NSMakeRect(0, 0, 160, 25) 110 controller:self 111 closeButton:closeButton_]); 112 [view setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin]; 113 [view addSubview:iconView_]; 114 [view addSubview:closeButton_]; 115 [view setTitleFrame:titleFrame]; 116 [super setView:view]; 117 118 isIconShowing_ = YES; 119 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; 120 [defaultCenter addObserver:self 121 selector:@selector(themeChangedNotification:) 122 name:kBrowserThemeDidChangeNotification 123 object:nil]; 124 125 [self internalSetSelected:selected_]; 126 } 127 return self; 128 } 129 130 - (void)dealloc { 131 [mediaIndicatorView_ setAnimationDoneCallbackObject:nil withSelector:nil]; 132 [[NSNotificationCenter defaultCenter] removeObserver:self]; 133 [[self tabView] setController:nil]; 134 [super dealloc]; 135 } 136 137 // The internals of |-setSelected:| and |-setActive:| but doesn't set the 138 // backing variables. This updates the drawing state and marks self as needing 139 // a re-draw. 140 - (void)internalSetSelected:(BOOL)selected { 141 TabView* tabView = [self tabView]; 142 if ([self active]) { 143 [tabView setState:NSOnState]; 144 [tabView cancelAlert]; 145 } else { 146 [tabView setState:selected ? NSMixedState : NSOffState]; 147 } 148 [self updateVisibility]; 149 [self updateTitleColor]; 150 } 151 152 // Called when Cocoa wants to display the context menu. Lazily instantiate 153 // the menu based off of the cross-platform model. Re-create the menu and 154 // model every time to get the correct labels and enabling. 155 - (NSMenu*)menu { 156 contextMenuDelegate_.reset( 157 new TabControllerInternal::MenuDelegate(target_, self)); 158 contextMenuModel_.reset( 159 [target_ contextMenuModelForController:self 160 menuDelegate:contextMenuDelegate_.get()]); 161 contextMenuController_.reset( 162 [[MenuController alloc] initWithModel:contextMenuModel_.get() 163 useWithPopUpButtonCell:NO]); 164 return [contextMenuController_ menu]; 165 } 166 167 - (void)closeTab:(id)sender { 168 if ([[self target] respondsToSelector:@selector(closeTab:)]) { 169 [[self target] performSelector:@selector(closeTab:) 170 withObject:[self view]]; 171 } 172 } 173 174 - (void)selectTab:(id)sender { 175 if ([[self tabView] isClosing]) 176 return; 177 if ([[self target] respondsToSelector:[self action]]) { 178 [[self target] performSelector:[self action] 179 withObject:[self view]]; 180 } 181 } 182 183 - (void)setTitle:(NSString*)title { 184 if ([[self title] isEqualToString:title]) 185 return; 186 187 TabView* tabView = [self tabView]; 188 [tabView setTitle:title]; 189 190 if ([self mini] && ![self active]) { 191 [tabView startAlert]; 192 } 193 [super setTitle:title]; 194 } 195 196 - (void)setToolTip:(NSString*)toolTip { 197 [[self view] setToolTip:toolTip]; 198 } 199 200 - (void)setActive:(BOOL)active { 201 if (active != active_) { 202 active_ = active; 203 [self internalSetSelected:[self selected]]; 204 } 205 } 206 207 - (BOOL)active { 208 return active_; 209 } 210 211 - (void)setSelected:(BOOL)selected { 212 if (selected_ != selected) { 213 selected_ = selected; 214 [self internalSetSelected:[self selected]]; 215 } 216 } 217 218 - (BOOL)selected { 219 return selected_ || active_; 220 } 221 222 - (SpriteView*)iconView { 223 return iconView_; 224 } 225 226 - (void)setIconView:(SpriteView*)iconView { 227 [iconView_ removeFromSuperview]; 228 iconView_.reset([iconView retain]); 229 230 if (iconView_) 231 [[self view] addSubview:iconView_]; 232 } 233 234 - (MediaIndicatorView*)mediaIndicatorView { 235 return mediaIndicatorView_; 236 } 237 238 - (void)setMediaIndicatorView:(MediaIndicatorView*)mediaIndicatorView { 239 [mediaIndicatorView_ removeFromSuperview]; 240 mediaIndicatorView_.reset([mediaIndicatorView retain]); 241 [self updateVisibility]; 242 if (mediaIndicatorView_) { 243 [[self view] addSubview:mediaIndicatorView_]; 244 [mediaIndicatorView_ 245 setAnimationDoneCallbackObject:self 246 withSelector:@selector(updateVisibility)]; 247 248 } 249 } 250 251 - (HoverCloseButton*)closeButton { 252 return closeButton_; 253 } 254 255 - (NSString*)toolTip { 256 return [[self tabView] toolTipText]; 257 } 258 259 // Return a rough approximation of the number of icons we could fit in the 260 // tab. We never actually do this, but it's a helpful guide for determining 261 // how much space we have available. 262 - (int)iconCapacity { 263 const CGFloat availableWidth = std::max<CGFloat>( 264 0, NSMaxX([closeButton_ frame]) - NSMinX(originalIconFrame_)); 265 const CGFloat widthPerIcon = NSWidth(originalIconFrame_); 266 const int kPaddingBetweenIcons = 2; 267 if (availableWidth >= widthPerIcon && 268 availableWidth < (widthPerIcon + kPaddingBetweenIcons)) { 269 return 1; 270 } 271 return availableWidth / (widthPerIcon + kPaddingBetweenIcons); 272 } 273 274 - (BOOL)shouldShowIcon { 275 return chrome::ShouldTabShowFavicon( 276 [self iconCapacity], [self mini], [self active], iconView_ != nil, 277 !mediaIndicatorView_ ? TAB_MEDIA_STATE_NONE : 278 [mediaIndicatorView_ animatingMediaState]); 279 } 280 281 - (BOOL)shouldShowMediaIndicator { 282 if (!mediaIndicatorView_) 283 return NO; 284 return chrome::ShouldTabShowMediaIndicator( 285 [self iconCapacity], [self mini], [self active], iconView_ != nil, 286 [mediaIndicatorView_ animatingMediaState]); 287 } 288 289 - (BOOL)shouldShowCloseButton { 290 return chrome::ShouldTabShowCloseButton( 291 [self iconCapacity], [self mini], [self active]); 292 } 293 294 - (void)setIconImage:(NSImage*)image { 295 [self setIconImage:image withToastAnimation:NO]; 296 } 297 298 - (void)setIconImage:(NSImage*)image withToastAnimation:(BOOL)animate { 299 if (image == nil) { 300 [self setIconView:nil]; 301 } else { 302 if (iconView_.get() == nil) { 303 base::scoped_nsobject<SpriteView> iconView([[SpriteView alloc] init]); 304 [iconView setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin]; 305 [self setIconView:iconView]; 306 } 307 308 [iconView_ setImage:image withToastAnimation:animate]; 309 310 if ([self app] || [self mini]) { 311 NSRect appIconFrame = [iconView_ frame]; 312 appIconFrame.origin = originalIconFrame_.origin; 313 314 const CGFloat tabWidth = [self app] ? [TabController appTabWidth] 315 : [TabController miniTabWidth]; 316 317 // Center the icon. 318 appIconFrame.origin.x = 319 std::floor((tabWidth - NSWidth(appIconFrame)) / 2.0); 320 [iconView_ setFrame:appIconFrame]; 321 } else { 322 [iconView_ setFrame:originalIconFrame_]; 323 } 324 } 325 // Ensure that the icon is suppressed if no icon is set or if the tab is too 326 // narrow to display one. 327 [self updateVisibility]; 328 } 329 330 - (void)updateVisibility { 331 // iconView_ may have been replaced or it may be nil, so [iconView_ isHidden] 332 // won't work. Instead, the state of the icon is tracked separately in 333 // isIconShowing_. 334 BOOL newShowIcon = [self shouldShowIcon]; 335 336 [iconView_ setHidden:!newShowIcon]; 337 isIconShowing_ = newShowIcon; 338 339 // If the tab is a mini-tab, hide the title. 340 TabView* tabView = [self tabView]; 341 [tabView setTitleHidden:[self mini]]; 342 343 BOOL newShowCloseButton = [self shouldShowCloseButton]; 344 345 [closeButton_ setHidden:!newShowCloseButton]; 346 347 BOOL newShowMediaIndicator = [self shouldShowMediaIndicator]; 348 349 [mediaIndicatorView_ setHidden:!newShowMediaIndicator]; 350 351 if (newShowMediaIndicator) { 352 NSRect newFrame = [mediaIndicatorView_ frame]; 353 if ([self app] || [self mini]) { 354 // Tab is pinned: Position the media indicator in the center. 355 const CGFloat tabWidth = [self app] ? 356 [TabController appTabWidth] : [TabController miniTabWidth]; 357 newFrame.origin.x = std::floor((tabWidth - NSWidth(newFrame)) / 2); 358 newFrame.origin.y = NSMinY(originalIconFrame_) - 359 std::floor((NSHeight(newFrame) - NSHeight(originalIconFrame_)) / 2); 360 } else { 361 // The Frame for the mediaIndicatorView_ depends on whether iconView_ 362 // and/or closeButton_ are visible, and where they have been positioned. 363 const NSRect closeButtonFrame = [closeButton_ frame]; 364 newFrame.origin.x = NSMinX(closeButtonFrame); 365 // Position to the left of the close button when it is showing. 366 if (newShowCloseButton) 367 newFrame.origin.x -= NSWidth(newFrame); 368 // Media indicator is centered vertically, with respect to closeButton_. 369 newFrame.origin.y = NSMinY(closeButtonFrame) - 370 std::floor((NSHeight(newFrame) - NSHeight(closeButtonFrame)) / 2); 371 } 372 [mediaIndicatorView_ setFrame:newFrame]; 373 } 374 375 // Adjust the title view based on changes to the icon's and close button's 376 // visibility. 377 NSRect oldTitleFrame = [tabView titleFrame]; 378 NSRect newTitleFrame; 379 newTitleFrame.size.height = oldTitleFrame.size.height; 380 newTitleFrame.origin.y = oldTitleFrame.origin.y; 381 382 if (newShowIcon) { 383 newTitleFrame.origin.x = NSMaxX([iconView_ frame]); 384 } else { 385 newTitleFrame.origin.x = originalIconFrame_.origin.x; 386 } 387 388 if (newShowMediaIndicator) { 389 newTitleFrame.size.width = NSMinX([mediaIndicatorView_ frame]) - 390 newTitleFrame.origin.x; 391 } else if (newShowCloseButton) { 392 newTitleFrame.size.width = NSMinX([closeButton_ frame]) - 393 newTitleFrame.origin.x; 394 } else { 395 newTitleFrame.size.width = NSMaxX([closeButton_ frame]) - 396 newTitleFrame.origin.x; 397 } 398 399 [tabView setTitleFrame:newTitleFrame]; 400 } 401 402 - (void)updateTitleColor { 403 NSColor* titleColor = nil; 404 ui::ThemeProvider* theme = [[[self view] window] themeProvider]; 405 if (theme && ![self selected]) 406 titleColor = theme->GetNSColor(ThemeProperties::COLOR_BACKGROUND_TAB_TEXT); 407 // Default to the selected text color unless told otherwise. 408 if (theme && !titleColor) 409 titleColor = theme->GetNSColor(ThemeProperties::COLOR_TAB_TEXT); 410 [[self tabView] setTitleColor:titleColor ? titleColor : [NSColor textColor]]; 411 } 412 413 - (void)themeChangedNotification:(NSNotification*)notification { 414 [self updateTitleColor]; 415 } 416 417 // Called by the tabs to determine whether we are in rapid (tab) closure mode. 418 - (BOOL)inRapidClosureMode { 419 if ([[self target] respondsToSelector:@selector(inRapidClosureMode)]) { 420 return [[self target] performSelector:@selector(inRapidClosureMode)] ? 421 YES : NO; 422 } 423 return NO; 424 } 425 426 // The following methods are invoked from the TabView and are forwarded to the 427 // TabStripDragController. 428 - (BOOL)tabCanBeDragged:(TabController*)controller { 429 return [[target_ dragController] tabCanBeDragged:controller]; 430 } 431 432 - (void)maybeStartDrag:(NSEvent*)event forTab:(TabController*)tab { 433 [[target_ dragController] maybeStartDrag:event forTab:tab]; 434 } 435 436 @end 437