Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / tabs / tab_controller.mm
blob166339fd019444438970e02ddf8a58cbdb44b266
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"
7 #include <algorithm>
8 #include <cmath>
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
35 // tab".
36 class MenuDelegate : public ui::SimpleMenuModel::Delegate {
37  public:
38   explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner)
39       : target_(target),
40         owner_(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_];
48   }
49   bool GetAcceleratorForCommandId(int command_id,
50                                   ui::Accelerator* accelerator) override {
51     return false;
52   }
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_];
57   }
59  private:
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; }
77 - (TabView*)tabView {
78   DCHECK([[self view] isKindOfClass:[TabView class]]);
79   return static_cast<TabView*>([self view]);
82 - (id)init {
83   if ((self = [super init])) {
84     // Icon.
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);
99     // Close button.
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])
108            controller:self
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
123                         object:nil];
125     [self internalSetSelected:selected_];
126   }
127   return self;
130 - (void)dealloc {
131   [mediaIndicatorButton_ setAnimationDoneTarget:nil withAction:nil];
132   [mediaIndicatorButton_ setClickTarget:nil withAction:nil];
133   [[NSNotificationCenter defaultCenter] removeObserver:self];
134   [[self tabView] setController:nil];
135   [super dealloc];
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
140 // a re-draw.
141 - (void)internalSetSelected:(BOOL)selected {
142   TabView* tabView = [self tabView];
143   if ([self active]) {
144     [tabView setState:NSOnState];
145     [tabView cancelAlert];
146   } else {
147     [tabView setState:selected ? NSMixedState : NSOffState];
148   }
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.
156 - (NSMenu*)menu {
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]];
172   }
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"));
184     } else {
185       content::RecordAction(UserMetricsAction("CloseTab_RecordingIndicator"));
186     }
187   } else {
188     content::RecordAction(UserMetricsAction("CloseTab_NoMediaIndicator"));
189   }
191   if ([[self target] respondsToSelector:@selector(closeTab:)]) {
192     [[self target] performSelector:@selector(closeTab:)
193                         withObject:[self view]];
194   }
197 - (void)selectTab:(id)sender {
198   if ([[self tabView] isClosing])
199     return;
200   if ([[self target] respondsToSelector:[self action]]) {
201     [[self target] performSelector:[self action]
202                         withObject:[self view]];
203   }
206 - (void)setTitle:(NSString*)title {
207   if ([[self title] isEqualToString:title])
208     return;
210   TabView* tabView = [self tabView];
211   [tabView setTitle:title];
213   if ([self pinned] && ![self active]) {
214     [tabView startAlert];
215   }
216   [super setTitle:title];
219 - (void)setActive:(BOOL)active {
220   if (active != active_) {
221     active_ = active;
222     [self internalSetSelected:[self selected]];
223   }
226 - (BOOL)active {
227   return active_;
230 - (void)setSelected:(BOOL)selected {
231   if (selected_ != selected) {
232     selected_ = selected;
233     [self internalSetSelected:[self selected]];
234   }
237 - (BOOL)selected {
238   return selected_ || active_;
241 - (SpriteView*)iconView {
242   return iconView_;
245 - (void)setIconView:(SpriteView*)iconView {
246   [iconView_ removeFromSuperview];
247   iconView_.reset([iconView retain]);
249   if (iconView_)
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:)];
266   }
267   [mediaIndicatorButton_ transitionToMediaState:mediaState];
270 - (HoverCloseButton*)closeButton {
271   return 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)) {
292     return 1;
293   }
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 {
321   if (image == nil) {
322     [self setIconView:nil];
323   } else {
324     if (iconView_.get() == nil) {
325       base::scoped_nsobject<SpriteView> iconView([[SpriteView alloc] init]);
326       [iconView setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
327       [self setIconView:iconView];
328     }
330     [iconView_ setImage:image withToastAnimation:animate];
332     if ([self pinned]) {
333       NSRect appIconFrame = [iconView_ frame];
334       appIconFrame.origin = originalIconFrame_.origin;
336       const CGFloat tabWidth = [TabController pinnedTabWidth];
338       // Center the icon.
339       appIconFrame.origin.x =
340           std::floor((tabWidth - NSWidth(appIconFrame)) / 2.0);
341       [iconView_ setFrame:appIconFrame];
342     } else {
343       [iconView_ setFrame:originalIconFrame_];
344     }
345   }
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
351   // isIconShowing_.
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];
372     if ([self pinned]) {
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);
378     } else {
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);
389     }
390     [mediaIndicatorButton_ setFrame:newFrame];
391     [mediaIndicatorButton_ updateEnabledForMuteToggle];
392   }
394   // Adjust the title view based on changes to the icon's and close button's
395   // visibility.
396   NSRect oldTitleFrame = [tabView titleFrame];
397   NSRect newTitleFrame;
398   newTitleFrame.size.height = oldTitleFrame.size.height;
399   newTitleFrame.origin.y = oldTitleFrame.origin.y;
401   if (newShowIcon) {
402     newTitleFrame.origin.x = NSMaxX([iconView_ frame]);
403   } else {
404     newTitleFrame.origin.x = originalIconFrame_.origin.x;
405   }
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;
413   } else {
414     newTitleFrame.size.width = NSMaxX([closeButton_ frame]) -
415                                newTitleFrame.origin.x;
416   }
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)] ?
440         YES : NO;
441   }
442   return NO;
445 - (void)maybeStartDrag:(NSEvent*)event forTab:(TabController*)tab {
446   [[target_ dragController] maybeStartDrag:event forTab:tab];
449 @end