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