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 #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