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.
5 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
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"
23 @implementation TabController
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_;
33 namespace TabControllerInternal {
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
38 class MenuDelegate : public ui::SimpleMenuModel::Delegate {
40 explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner)
44 // Overridden from ui::SimpleMenuModel::Delegate
45 virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
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_];
53 virtual bool GetAcceleratorForCommandId(
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_];
63 id<TabControllerTarget> target_; // weak
64 TabController* owner_; // weak, owns me
67 } // TabControllerInternal namespace
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; }
80 DCHECK([[self view] isKindOfClass:[TabView class]]);
81 return static_cast<TabView*>([self view]);
85 if ((self = [super init])) {
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];
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);
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:)];
108 base::scoped_nsobject<TabView> view(
109 [[TabView alloc] initWithFrame:NSMakeRect(0, 0, 160, 25)
111 closeButton:closeButton_]);
112 [view setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
113 [view addSubview:iconView_];
114 [view addSubview:closeButton_];
115 [view setTitleFrame:titleFrame];
116 [super setView:view];
118 isIconShowing_ = YES;
119 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
120 [defaultCenter addObserver:self
121 selector:@selector(themeChangedNotification:)
122 name:kBrowserThemeDidChangeNotification
125 [self internalSetSelected:selected_];
131 [mediaIndicatorView_ setAnimationDoneCallbackObject:nil withSelector:nil];
132 [[NSNotificationCenter defaultCenter] removeObserver:self];
133 [[self tabView] setController:nil];
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
140 - (void)internalSetSelected:(BOOL)selected {
141 TabView* tabView = [self tabView];
143 [tabView setState:NSOnState];
144 [tabView cancelAlert];
146 [tabView setState:selected ? NSMixedState : NSOffState];
148 [self updateVisibility];
149 [self updateTitleColor];
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.
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];
167 - (void)closeTab:(id)sender {
168 if ([[self target] respondsToSelector:@selector(closeTab:)]) {
169 [[self target] performSelector:@selector(closeTab:)
170 withObject:[self view]];
174 - (void)selectTab:(id)sender {
175 if ([[self tabView] isClosing])
177 if ([[self target] respondsToSelector:[self action]]) {
178 [[self target] performSelector:[self action]
179 withObject:[self view]];
183 - (void)setTitle:(NSString*)title {
184 if ([[self title] isEqualToString:title])
187 TabView* tabView = [self tabView];
188 [tabView setTitle:title];
190 if ([self mini] && ![self active]) {
191 [tabView startAlert];
193 [super setTitle:title];
196 - (void)setToolTip:(NSString*)toolTip {
197 [[self view] setToolTip:toolTip];
200 - (void)setActive:(BOOL)active {
201 if (active != active_) {
203 [self internalSetSelected:[self selected]];
211 - (void)setSelected:(BOOL)selected {
212 if (selected_ != selected) {
213 selected_ = selected;
214 [self internalSetSelected:[self selected]];
219 return selected_ || active_;
222 - (SpriteView*)iconView {
226 - (void)setIconView:(SpriteView*)iconView {
227 [iconView_ removeFromSuperview];
228 iconView_.reset([iconView retain]);
231 [[self view] addSubview:iconView_];
234 - (MediaIndicatorView*)mediaIndicatorView {
235 return mediaIndicatorView_;
238 - (void)setMediaIndicatorView:(MediaIndicatorView*)mediaIndicatorView {
239 [mediaIndicatorView_ removeFromSuperview];
240 mediaIndicatorView_.reset([mediaIndicatorView retain]);
241 [self updateVisibility];
242 if (mediaIndicatorView_) {
243 [[self view] addSubview:mediaIndicatorView_];
245 setAnimationDoneCallbackObject:self
246 withSelector:@selector(updateVisibility)];
251 - (HoverCloseButton*)closeButton {
255 - (NSString*)toolTip {
256 return [[self tabView] toolTipText];
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)) {
271 return availableWidth / (widthPerIcon + kPaddingBetweenIcons);
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]);
281 - (BOOL)shouldShowMediaIndicator {
282 if (!mediaIndicatorView_)
284 return chrome::ShouldTabShowMediaIndicator(
285 [self iconCapacity], [self mini], [self active], iconView_ != nil,
286 [mediaIndicatorView_ animatingMediaState]);
289 - (BOOL)shouldShowCloseButton {
290 return chrome::ShouldTabShowCloseButton(
291 [self iconCapacity], [self mini], [self active]);
294 - (void)setIconImage:(NSImage*)image {
295 [self setIconImage:image withToastAnimation:NO];
298 - (void)setIconImage:(NSImage*)image withToastAnimation:(BOOL)animate {
300 [self setIconView:nil];
302 if (iconView_.get() == nil) {
303 base::scoped_nsobject<SpriteView> iconView([[SpriteView alloc] init]);
304 [iconView setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
305 [self setIconView:iconView];
308 [iconView_ setImage:image withToastAnimation:animate];
310 if ([self app] || [self mini]) {
311 NSRect appIconFrame = [iconView_ frame];
312 appIconFrame.origin = originalIconFrame_.origin;
314 const CGFloat tabWidth = [self app] ? [TabController appTabWidth]
315 : [TabController miniTabWidth];
318 appIconFrame.origin.x =
319 std::floor((tabWidth - NSWidth(appIconFrame)) / 2.0);
320 [iconView_ setFrame:appIconFrame];
322 [iconView_ setFrame:originalIconFrame_];
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];
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
334 BOOL newShowIcon = [self shouldShowIcon];
336 [iconView_ setHidden:!newShowIcon];
337 isIconShowing_ = newShowIcon;
339 // If the tab is a mini-tab, hide the title.
340 TabView* tabView = [self tabView];
341 [tabView setTitleHidden:[self mini]];
343 BOOL newShowCloseButton = [self shouldShowCloseButton];
345 [closeButton_ setHidden:!newShowCloseButton];
347 BOOL newShowMediaIndicator = [self shouldShowMediaIndicator];
349 [mediaIndicatorView_ setHidden:!newShowMediaIndicator];
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);
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);
372 [mediaIndicatorView_ setFrame:newFrame];
375 // Adjust the title view based on changes to the icon's and close button's
377 NSRect oldTitleFrame = [tabView titleFrame];
378 NSRect newTitleFrame;
379 newTitleFrame.size.height = oldTitleFrame.size.height;
380 newTitleFrame.origin.y = oldTitleFrame.origin.y;
383 newTitleFrame.origin.x = NSMaxX([iconView_ frame]);
385 newTitleFrame.origin.x = originalIconFrame_.origin.x;
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;
395 newTitleFrame.size.width = NSMaxX([closeButton_ frame]) -
396 newTitleFrame.origin.x;
399 [tabView setTitleFrame:newTitleFrame];
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]];
413 - (void)themeChangedNotification:(NSNotification*)notification {
414 [self updateTitleColor];
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)] ?
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];
432 - (void)maybeStartDrag:(NSEvent*)event forTab:(TabController*)tab {
433 [[target_ dragController] maybeStartDrag:event forTab:tab];