Home | History | Annotate | Download | only in tabs
      1 // Copyright (c) 2011 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 #include "base/mac/mac_util.h"
      6 #import "chrome/browser/themes/theme_service.h"
      7 #import "chrome/browser/ui/cocoa/menu_controller.h"
      8 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
      9 #import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h"
     10 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
     11 #import "chrome/browser/ui/cocoa/themed_window.h"
     12 #import "chrome/common/extensions/extension.h"
     13 #include "grit/generated_resources.h"
     14 #import "third_party/GTM/AppKit/GTMFadeTruncatingTextFieldCell.h"
     15 #include "ui/base/l10n/l10n_util_mac.h"
     16 
     17 @implementation TabController
     18 
     19 @synthesize action = action_;
     20 @synthesize app = app_;
     21 @synthesize loadingState = loadingState_;
     22 @synthesize mini = mini_;
     23 @synthesize pinned = pinned_;
     24 @synthesize target = target_;
     25 @synthesize url = url_;
     26 @synthesize iconView = iconView_;
     27 @synthesize titleView = titleView_;
     28 @synthesize closeButton = closeButton_;
     29 
     30 namespace TabControllerInternal {
     31 
     32 // A C++ delegate that handles enabling/disabling menu items and handling when
     33 // a menu command is chosen. Also fixes up the menu item label for "pin/unpin
     34 // tab".
     35 class MenuDelegate : public ui::SimpleMenuModel::Delegate {
     36  public:
     37   explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner)
     38       : target_(target),
     39         owner_(owner) {}
     40 
     41   // Overridden from ui::SimpleMenuModel::Delegate
     42   virtual bool IsCommandIdChecked(int command_id) const { return false; }
     43   virtual bool IsCommandIdEnabled(int command_id) const {
     44     TabStripModel::ContextMenuCommand command =
     45         static_cast<TabStripModel::ContextMenuCommand>(command_id);
     46     return [target_ isCommandEnabled:command forController:owner_];
     47   }
     48   virtual bool GetAcceleratorForCommandId(
     49       int command_id,
     50       ui::Accelerator* accelerator) { return false; }
     51   virtual void ExecuteCommand(int command_id) {
     52     TabStripModel::ContextMenuCommand command =
     53         static_cast<TabStripModel::ContextMenuCommand>(command_id);
     54     [target_ commandDispatch:command forController:owner_];
     55   }
     56 
     57  private:
     58   id<TabControllerTarget> target_;  // weak
     59   TabController* owner_;  // weak, owns me
     60 };
     61 
     62 }  // TabControllerInternal namespace
     63 
     64 // The min widths match the windows values and are sums of left + right
     65 // padding, of which we have no comparable constants (we draw using paths, not
     66 // images). The selected tab width includes the close button width.
     67 + (CGFloat)minTabWidth { return 31; }
     68 + (CGFloat)minSelectedTabWidth { return 46; }
     69 + (CGFloat)maxTabWidth { return 220; }
     70 + (CGFloat)miniTabWidth { return 53; }
     71 + (CGFloat)appTabWidth { return 66; }
     72 
     73 - (TabView*)tabView {
     74   return static_cast<TabView*>([self view]);
     75 }
     76 
     77 - (id)init {
     78   self = [super initWithNibName:@"TabView" bundle:base::mac::MainAppBundle()];
     79   if (self != nil) {
     80     isIconShowing_ = YES;
     81     NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
     82     [defaultCenter addObserver:self
     83                       selector:@selector(viewResized:)
     84                           name:NSViewFrameDidChangeNotification
     85                         object:[self view]];
     86     [defaultCenter addObserver:self
     87                       selector:@selector(themeChangedNotification:)
     88                           name:kBrowserThemeDidChangeNotification
     89                         object:nil];
     90   }
     91   return self;
     92 }
     93 
     94 - (void)dealloc {
     95   [[NSNotificationCenter defaultCenter] removeObserver:self];
     96   [[self tabView] setController:nil];
     97   [super dealloc];
     98 }
     99 
    100 // The internals of |-setSelected:| but doesn't check if we're already set
    101 // to |selected|. Pass the selection change to the subviews that need it and
    102 // mark ourselves as needing a redraw.
    103 - (void)internalSetSelected:(BOOL)selected {
    104   selected_ = selected;
    105   TabView* tabView = static_cast<TabView*>([self view]);
    106   DCHECK([tabView isKindOfClass:[TabView class]]);
    107   [tabView setState:selected];
    108   [tabView cancelAlert];
    109   [self updateVisibility];
    110   [self updateTitleColor];
    111 }
    112 
    113 // Called when the tab's nib is done loading and all outlets are hooked up.
    114 - (void)awakeFromNib {
    115   // Remember the icon's frame, so that if the icon is ever removed, a new
    116   // one can later replace it in the proper location.
    117   originalIconFrame_ = [iconView_ frame];
    118 
    119   // When the icon is removed, the title expands to the left to fill the space
    120   // left by the icon.  When the close button is removed, the title expands to
    121   // the right to fill its space.  These are the amounts to expand and contract
    122   // titleView_ under those conditions. We don't have to explicilty save the
    123   // offset between the title and the close button since we can just get that
    124   // value for the close button's frame.
    125   NSRect titleFrame = [titleView_ frame];
    126   iconTitleXOffset_ = NSMinX(titleFrame) - NSMinX(originalIconFrame_);
    127 
    128   [self internalSetSelected:selected_];
    129 }
    130 
    131 // Called when Cocoa wants to display the context menu. Lazily instantiate
    132 // the menu based off of the cross-platform model. Re-create the menu and
    133 // model every time to get the correct labels and enabling.
    134 - (NSMenu*)menu {
    135   contextMenuDelegate_.reset(
    136       new TabControllerInternal::MenuDelegate(target_, self));
    137   contextMenuModel_.reset(new TabMenuModel(contextMenuDelegate_.get(),
    138                                            [self pinned]));
    139   contextMenuController_.reset(
    140       [[MenuController alloc] initWithModel:contextMenuModel_.get()
    141                      useWithPopUpButtonCell:NO]);
    142   return [contextMenuController_ menu];
    143 }
    144 
    145 - (IBAction)closeTab:(id)sender {
    146   if ([[self target] respondsToSelector:@selector(closeTab:)]) {
    147     [[self target] performSelector:@selector(closeTab:)
    148                         withObject:[self view]];
    149   }
    150 }
    151 
    152 - (void)setTitle:(NSString*)title {
    153   [[self view] setToolTip:title];
    154   if ([self mini] && ![self selected]) {
    155     TabView* tabView = static_cast<TabView*>([self view]);
    156     DCHECK([tabView isKindOfClass:[TabView class]]);
    157     [tabView startAlert];
    158   }
    159   [super setTitle:title];
    160 }
    161 
    162 - (void)setSelected:(BOOL)selected {
    163   if (selected_ != selected)
    164     [self internalSetSelected:selected];
    165 }
    166 
    167 - (BOOL)selected {
    168   return selected_;
    169 }
    170 
    171 - (void)setIconView:(NSView*)iconView {
    172   [iconView_ removeFromSuperview];
    173   iconView_ = iconView;
    174   if ([self app]) {
    175     NSRect appIconFrame = [iconView frame];
    176     appIconFrame.origin = originalIconFrame_.origin;
    177     // Center the icon.
    178     appIconFrame.origin.x = ([TabController appTabWidth] -
    179         NSWidth(appIconFrame)) / 2.0;
    180     [iconView setFrame:appIconFrame];
    181   } else {
    182     [iconView_ setFrame:originalIconFrame_];
    183   }
    184   // Ensure that the icon is suppressed if no icon is set or if the tab is too
    185   // narrow to display one.
    186   [self updateVisibility];
    187 
    188   if (iconView_)
    189     [[self view] addSubview:iconView_];
    190 }
    191 
    192 - (NSString*)toolTip {
    193   return [[self view] toolTip];
    194 }
    195 
    196 // Return a rough approximation of the number of icons we could fit in the
    197 // tab. We never actually do this, but it's a helpful guide for determining
    198 // how much space we have available.
    199 - (int)iconCapacity {
    200   CGFloat width = NSMaxX([closeButton_ frame]) - NSMinX(originalIconFrame_);
    201   CGFloat iconWidth = NSWidth(originalIconFrame_);
    202 
    203   return width / iconWidth;
    204 }
    205 
    206 // Returns YES if we should show the icon. When tabs get too small, we clip
    207 // the favicon before the close button for selected tabs, and prefer the
    208 // favicon for unselected tabs.  The icon can also be suppressed more directly
    209 // by clearing iconView_.
    210 - (BOOL)shouldShowIcon {
    211   if (!iconView_)
    212     return NO;
    213 
    214   if ([self mini])
    215     return YES;
    216 
    217   int iconCapacity = [self iconCapacity];
    218   if ([self selected])
    219     return iconCapacity >= 2;
    220   return iconCapacity >= 1;
    221 }
    222 
    223 // Returns YES if we should be showing the close button. The selected tab
    224 // always shows the close button.
    225 - (BOOL)shouldShowCloseButton {
    226   if ([self mini])
    227     return NO;
    228   return ([self selected] || [self iconCapacity] >= 3);
    229 }
    230 
    231 - (void)updateVisibility {
    232   // iconView_ may have been replaced or it may be nil, so [iconView_ isHidden]
    233   // won't work.  Instead, the state of the icon is tracked separately in
    234   // isIconShowing_.
    235   BOOL newShowIcon = [self shouldShowIcon];
    236 
    237   [iconView_ setHidden:!newShowIcon];
    238   isIconShowing_ = newShowIcon;
    239 
    240   // If the tab is a mini-tab, hide the title.
    241   [titleView_ setHidden:[self mini]];
    242 
    243   BOOL newShowCloseButton = [self shouldShowCloseButton];
    244 
    245   [closeButton_ setHidden:!newShowCloseButton];
    246 
    247   // Adjust the title view based on changes to the icon's and close button's
    248   // visibility.
    249   NSRect oldTitleFrame = [titleView_ frame];
    250   NSRect newTitleFrame;
    251   newTitleFrame.size.height = oldTitleFrame.size.height;
    252   newTitleFrame.origin.y = oldTitleFrame.origin.y;
    253 
    254   if (newShowIcon) {
    255     newTitleFrame.origin.x = originalIconFrame_.origin.x + iconTitleXOffset_;
    256   } else {
    257     newTitleFrame.origin.x = originalIconFrame_.origin.x;
    258   }
    259 
    260   if (newShowCloseButton) {
    261     newTitleFrame.size.width = NSMinX([closeButton_ frame]) -
    262                                newTitleFrame.origin.x;
    263   } else {
    264     newTitleFrame.size.width = NSMaxX([closeButton_ frame]) -
    265                                newTitleFrame.origin.x;
    266   }
    267 
    268   [titleView_ setFrame:newTitleFrame];
    269 }
    270 
    271 - (void)updateTitleColor {
    272   NSColor* titleColor = nil;
    273   ui::ThemeProvider* theme = [[[self view] window] themeProvider];
    274   if (theme && ![self selected]) {
    275     titleColor =
    276         theme->GetNSColor(ThemeService::COLOR_BACKGROUND_TAB_TEXT,
    277                           true);
    278   }
    279   // Default to the selected text color unless told otherwise.
    280   if (theme && !titleColor) {
    281     titleColor = theme->GetNSColor(ThemeService::COLOR_TAB_TEXT,
    282                                    true);
    283   }
    284   [titleView_ setTextColor:titleColor ? titleColor : [NSColor textColor]];
    285 }
    286 
    287 // Called when our view is resized. If it gets too small, start by hiding
    288 // the close button and only show it if tab is selected. Eventually, hide the
    289 // icon as well. We know that this is for our view because we only registered
    290 // for notifications from our specific view.
    291 - (void)viewResized:(NSNotification*)info {
    292   [self updateVisibility];
    293 }
    294 
    295 - (void)themeChangedNotification:(NSNotification*)notification {
    296   [self updateTitleColor];
    297 }
    298 
    299 // Called by the tabs to determine whether we are in rapid (tab) closure mode.
    300 - (BOOL)inRapidClosureMode {
    301   if ([[self target] respondsToSelector:@selector(inRapidClosureMode)]) {
    302     return [[self target] performSelector:@selector(inRapidClosureMode)] ?
    303         YES : NO;
    304   }
    305   return NO;
    306 }
    307 
    308 @end
    309