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