• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
30namespace 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".
35class 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