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 #import "chrome/browser/themes/theme_properties.h"
13 #import "chrome/browser/themes/theme_service.h"
14 #import "chrome/browser/ui/cocoa/sprite_view.h"
15 #import "chrome/browser/ui/cocoa/tabs/media_indicator_button_cocoa.h"
16 #import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h"
17 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
18 #import "chrome/browser/ui/cocoa/themed_window.h"
19 #include "content/public/browser/user_metrics.h"
20 #import "extensions/common/extension.h"
21 #import "ui/base/cocoa/menu_controller.h"
23 @implementation TabController
25 @synthesize action = action_;
26 @synthesize loadingState = loadingState_;
27 @synthesize pinned = pinned_;
28 @synthesize target = target_;
29 @synthesize url = url_;
31 namespace TabControllerInternal {
33 // A C++ delegate that handles enabling/disabling menu items and handling when
34 // a menu command is chosen. Also fixes up the menu item label for "pin/unpin
36 class MenuDelegate : public ui::SimpleMenuModel::Delegate {
38 explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner)
42 // Overridden from ui::SimpleMenuModel::Delegate
43 bool IsCommandIdChecked(int command_id) const override { return false; }
44 bool IsCommandIdEnabled(int command_id) const override {
45 TabStripModel::ContextMenuCommand command =
46 static_cast<TabStripModel::ContextMenuCommand>(command_id);
47 return [target_ isCommandEnabled:command forController:owner_];
49 bool GetAcceleratorForCommandId(int command_id,
50 ui::Accelerator* accelerator) override {
53 void ExecuteCommand(int command_id, int event_flags) override {
54 TabStripModel::ContextMenuCommand command =
55 static_cast<TabStripModel::ContextMenuCommand>(command_id);
56 [target_ commandDispatch:command forController:owner_];
60 id<TabControllerTarget> target_; // weak
61 TabController* owner_; // weak, owns me
64 } // TabControllerInternal namespace
66 + (CGFloat)defaultTabHeight { return 26; }
68 // The min widths is the smallest number at which the right edge of the right
69 // tab border image is not visibly clipped. It is a bit smaller than the sum
70 // of the two tab edge bitmaps because these bitmaps have a few transparent
71 // pixels on the side. The selected tab width includes the close button width.
72 + (CGFloat)minTabWidth { return 36; }
73 + (CGFloat)minActiveTabWidth { return 52; }
74 + (CGFloat)maxTabWidth { return 214; }
75 + (CGFloat)pinnedTabWidth { return 58; }
78 DCHECK([[self view] isKindOfClass:[TabView class]]);
79 return static_cast<TabView*>([self view]);
83 if ((self = [super init])) {
85 // Remember the icon's frame, so that if the icon is ever removed, a new
86 // one can later replace it in the proper location.
87 originalIconFrame_ = NSMakeRect(19, 5, 16, 16);
88 iconView_.reset([[SpriteView alloc] initWithFrame:originalIconFrame_]);
89 [iconView_ setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
91 // When the icon is removed, the title expands to the left to fill the
92 // space left by the icon. When the close button is removed, the title
93 // expands to the right to fill its space. These are the amounts to expand
94 // and contract the title frame under those conditions. We don't have to
95 // explicilty save the offset between the title and the close button since
96 // we can just get that value for the close button's frame.
97 NSRect titleFrame = NSMakeRect(35, 6, 92, 14);
100 closeButton_.reset([[HoverCloseButton alloc] initWithFrame:
101 NSMakeRect(127, 4, 18, 18)]);
102 [closeButton_ setAutoresizingMask:NSViewMinXMargin];
103 [closeButton_ setTarget:self];
104 [closeButton_ setAction:@selector(closeTab:)];
106 base::scoped_nsobject<TabView> view([[TabView alloc]
107 initWithFrame:NSMakeRect(0, 0, 160, [TabController defaultTabHeight])
109 closeButton:closeButton_]);
110 [view setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
111 [view setPostsFrameChangedNotifications:NO];
112 [view setPostsBoundsChangedNotifications:NO];
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 [mediaIndicatorButton_ setAnimationDoneTarget:nil withAction:nil];
132 [mediaIndicatorButton_ setClickTarget:nil withAction:nil];
133 [[NSNotificationCenter defaultCenter] removeObserver:self];
134 [[self tabView] setController:nil];
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
141 - (void)internalSetSelected:(BOOL)selected {
142 TabView* tabView = [self tabView];
144 [tabView setState:NSOnState];
145 [tabView cancelAlert];
147 [tabView setState:selected ? NSMixedState : NSOffState];
149 [self updateVisibility];
150 [self updateTitleColor];
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.
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];
168 - (void)toggleMute:(id)sender {
169 if ([[self target] respondsToSelector:@selector(toggleMute:)]) {
170 [[self target] performSelector:@selector(toggleMute:)
171 withObject:[self view]];
175 - (void)closeTab:(id)sender {
176 using base::UserMetricsAction;
178 if (mediaIndicatorButton_ && ![mediaIndicatorButton_ isHidden]) {
179 if ([mediaIndicatorButton_ isEnabled]) {
180 content::RecordAction(UserMetricsAction("CloseTab_MuteToggleAvailable"));
181 } else if ([mediaIndicatorButton_ showingMediaState] ==
182 TAB_MEDIA_STATE_AUDIO_PLAYING) {
183 content::RecordAction(UserMetricsAction("CloseTab_AudioIndicator"));
185 content::RecordAction(UserMetricsAction("CloseTab_RecordingIndicator"));
188 content::RecordAction(UserMetricsAction("CloseTab_NoMediaIndicator"));
191 if ([[self target] respondsToSelector:@selector(closeTab:)]) {
192 [[self target] performSelector:@selector(closeTab:)
193 withObject:[self view]];
197 - (void)selectTab:(id)sender {
198 if ([[self tabView] isClosing])
200 if ([[self target] respondsToSelector:[self action]]) {
201 [[self target] performSelector:[self action]
202 withObject:[self view]];
206 - (void)setTitle:(NSString*)title {
207 if ([[self title] isEqualToString:title])
210 TabView* tabView = [self tabView];
211 [tabView setTitle:title];
213 if ([self pinned] && ![self active]) {
214 [tabView startAlert];
216 [super setTitle:title];
219 - (void)setActive:(BOOL)active {
220 if (active != active_) {
222 [self internalSetSelected:[self selected]];
230 - (void)setSelected:(BOOL)selected {
231 if (selected_ != selected) {
232 selected_ = selected;
233 [self internalSetSelected:[self selected]];
238 return selected_ || active_;
241 - (SpriteView*)iconView {
245 - (void)setIconView:(SpriteView*)iconView {
246 [iconView_ removeFromSuperview];
247 iconView_.reset([iconView retain]);
250 [[self view] addSubview:iconView_];
253 - (MediaIndicatorButton*)mediaIndicatorButton {
254 return mediaIndicatorButton_;
257 - (void)setMediaState:(TabMediaState)mediaState {
258 if (!mediaIndicatorButton_ && mediaState != TAB_MEDIA_STATE_NONE) {
259 mediaIndicatorButton_.reset([[MediaIndicatorButton alloc] init]);
260 [self updateVisibility]; // Do layout and visibility before adding subview.
261 [[self view] addSubview:mediaIndicatorButton_];
262 [mediaIndicatorButton_ setAnimationDoneTarget:self
263 withAction:@selector(updateVisibility)];
264 [mediaIndicatorButton_ setClickTarget:self
265 withAction:@selector(toggleMute:)];
267 [mediaIndicatorButton_ transitionToMediaState:mediaState];
270 - (HoverCloseButton*)closeButton {
274 - (NSString*)toolTip {
275 return [[self tabView] toolTipText];
278 - (void)setToolTip:(NSString*)toolTip {
279 [[self tabView] setToolTipText:toolTip];
282 // Return a rough approximation of the number of icons we could fit in the
283 // tab. We never actually do this, but it's a helpful guide for determining
284 // how much space we have available.
285 - (int)iconCapacity {
286 const CGFloat availableWidth = std::max<CGFloat>(
287 0, NSMaxX([closeButton_ frame]) - NSMinX(originalIconFrame_));
288 const CGFloat widthPerIcon = NSWidth(originalIconFrame_);
289 const int kPaddingBetweenIcons = 2;
290 if (availableWidth >= widthPerIcon &&
291 availableWidth < (widthPerIcon + kPaddingBetweenIcons)) {
294 return availableWidth / (widthPerIcon + kPaddingBetweenIcons);
297 - (BOOL)shouldShowIcon {
298 return chrome::ShouldTabShowFavicon(
299 [self iconCapacity], [self pinned], [self active], iconView_ != nil,
300 !mediaIndicatorButton_ ? TAB_MEDIA_STATE_NONE :
301 [mediaIndicatorButton_ showingMediaState]);
304 - (BOOL)shouldShowMediaIndicator {
305 return chrome::ShouldTabShowMediaIndicator(
306 [self iconCapacity], [self pinned], [self active], iconView_ != nil,
307 !mediaIndicatorButton_ ? TAB_MEDIA_STATE_NONE :
308 [mediaIndicatorButton_ showingMediaState]);
311 - (BOOL)shouldShowCloseButton {
312 return chrome::ShouldTabShowCloseButton(
313 [self iconCapacity], [self pinned], [self active]);
316 - (void)setIconImage:(NSImage*)image {
317 [self setIconImage:image withToastAnimation:NO];
320 - (void)setIconImage:(NSImage*)image withToastAnimation:(BOOL)animate {
322 [self setIconView:nil];
324 if (iconView_.get() == nil) {
325 base::scoped_nsobject<SpriteView> iconView([[SpriteView alloc] init]);
326 [iconView setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
327 [self setIconView:iconView];
330 [iconView_ setImage:image withToastAnimation:animate];
333 NSRect appIconFrame = [iconView_ frame];
334 appIconFrame.origin = originalIconFrame_.origin;
336 const CGFloat tabWidth = [TabController pinnedTabWidth];
339 appIconFrame.origin.x =
340 std::floor((tabWidth - NSWidth(appIconFrame)) / 2.0);
341 [iconView_ setFrame:appIconFrame];
343 [iconView_ setFrame:originalIconFrame_];
348 - (void)updateVisibility {
349 // iconView_ may have been replaced or it may be nil, so [iconView_ isHidden]
350 // won't work. Instead, the state of the icon is tracked separately in
352 BOOL newShowIcon = [self shouldShowIcon];
354 [iconView_ setHidden:!newShowIcon];
355 isIconShowing_ = newShowIcon;
357 // If the tab is a pinned-tab, hide the title.
358 TabView* tabView = [self tabView];
359 [tabView setTitleHidden:[self pinned]];
361 BOOL newShowCloseButton = [self shouldShowCloseButton];
363 [closeButton_ setHidden:!newShowCloseButton];
365 BOOL newShowMediaIndicator = [self shouldShowMediaIndicator];
367 [mediaIndicatorButton_ setHidden:!newShowMediaIndicator];
369 if (newShowMediaIndicator) {
370 NSRect newFrame = [mediaIndicatorButton_ frame];
371 newFrame.size = [[mediaIndicatorButton_ image] size];
373 // Tab is pinned: Position the media indicator in the center.
374 const CGFloat tabWidth = [TabController pinnedTabWidth];
375 newFrame.origin.x = std::floor((tabWidth - NSWidth(newFrame)) / 2);
376 newFrame.origin.y = NSMinY(originalIconFrame_) -
377 std::floor((NSHeight(newFrame) - NSHeight(originalIconFrame_)) / 2);
379 // The Frame for the mediaIndicatorButton_ depends on whether iconView_
380 // and/or closeButton_ are visible, and where they have been positioned.
381 const NSRect closeButtonFrame = [closeButton_ frame];
382 newFrame.origin.x = NSMinX(closeButtonFrame);
383 // Position to the left of the close button when it is showing.
384 if (newShowCloseButton)
385 newFrame.origin.x -= NSWidth(newFrame);
386 // Media indicator is centered vertically, with respect to closeButton_.
387 newFrame.origin.y = NSMinY(closeButtonFrame) -
388 std::floor((NSHeight(newFrame) - NSHeight(closeButtonFrame)) / 2);
390 [mediaIndicatorButton_ setFrame:newFrame];
391 [mediaIndicatorButton_ updateEnabledForMuteToggle];
394 // Adjust the title view based on changes to the icon's and close button's
396 NSRect oldTitleFrame = [tabView titleFrame];
397 NSRect newTitleFrame;
398 newTitleFrame.size.height = oldTitleFrame.size.height;
399 newTitleFrame.origin.y = oldTitleFrame.origin.y;
402 newTitleFrame.origin.x = NSMaxX([iconView_ frame]);
404 newTitleFrame.origin.x = originalIconFrame_.origin.x;
407 if (newShowMediaIndicator) {
408 newTitleFrame.size.width = NSMinX([mediaIndicatorButton_ frame]) -
409 newTitleFrame.origin.x;
410 } else if (newShowCloseButton) {
411 newTitleFrame.size.width = NSMinX([closeButton_ frame]) -
412 newTitleFrame.origin.x;
414 newTitleFrame.size.width = NSMaxX([closeButton_ frame]) -
415 newTitleFrame.origin.x;
418 [tabView setTitleFrame:newTitleFrame];
421 - (void)updateTitleColor {
422 NSColor* titleColor = nil;
423 ui::ThemeProvider* theme = [[[self view] window] themeProvider];
424 if (theme && ![self selected])
425 titleColor = theme->GetNSColor(ThemeProperties::COLOR_BACKGROUND_TAB_TEXT);
426 // Default to the selected text color unless told otherwise.
427 if (theme && !titleColor)
428 titleColor = theme->GetNSColor(ThemeProperties::COLOR_TAB_TEXT);
429 [[self tabView] setTitleColor:titleColor ? titleColor : [NSColor textColor]];
432 - (void)themeChangedNotification:(NSNotification*)notification {
433 [self updateTitleColor];
436 // Called by the tabs to determine whether we are in rapid (tab) closure mode.
437 - (BOOL)inRapidClosureMode {
438 if ([[self target] respondsToSelector:@selector(inRapidClosureMode)]) {
439 return [[self target] performSelector:@selector(inRapidClosureMode)] ?
445 - (void)maybeStartDrag:(NSEvent*)event forTab:(TabController*)tab {
446 [[target_ dragController] maybeStartDrag:event forTab:tab];