Home | History | Annotate | Download | only in tabs
      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