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