[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / tabs / tab_controller.mm
blob390d7f893d307adc5e1395caa1a8e09426a9016d
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 #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 #include "grit/generated_resources.h"
22 #import "ui/base/cocoa/menu_controller.h"
24 @implementation TabController
26 @synthesize action = action_;
27 @synthesize app = app_;
28 @synthesize loadingState = loadingState_;
29 @synthesize mini = mini_;
30 @synthesize pinned = pinned_;
31 @synthesize target = target_;
32 @synthesize url = url_;
34 namespace TabControllerInternal {
36 // A C++ delegate that handles enabling/disabling menu items and handling when
37 // a menu command is chosen. Also fixes up the menu item label for "pin/unpin
38 // tab".
39 class MenuDelegate : public ui::SimpleMenuModel::Delegate {
40  public:
41   explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner)
42       : target_(target),
43         owner_(owner) {}
45   // Overridden from ui::SimpleMenuModel::Delegate
46   virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
47     return false;
48   }
49   virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE {
50     TabStripModel::ContextMenuCommand command =
51         static_cast<TabStripModel::ContextMenuCommand>(command_id);
52     return [target_ isCommandEnabled:command forController:owner_];
53   }
54   virtual bool GetAcceleratorForCommandId(
55       int command_id,
56       ui::Accelerator* accelerator) OVERRIDE { return false; }
57   virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE {
58     TabStripModel::ContextMenuCommand command =
59         static_cast<TabStripModel::ContextMenuCommand>(command_id);
60     [target_ commandDispatch:command forController:owner_];
61   }
63  private:
64   id<TabControllerTarget> target_;  // weak
65   TabController* owner_;  // weak, owns me
68 }  // TabControllerInternal namespace
70 // The min widths is the smallest number at which the right edge of the right
71 // tab border image is not visibly clipped.  It is a bit smaller than the sum
72 // of the two tab edge bitmaps because these bitmaps have a few transparent
73 // pixels on the side.  The selected tab width includes the close button width.
74 + (CGFloat)minTabWidth { return 36; }
75 + (CGFloat)minSelectedTabWidth { return 52; }
76 + (CGFloat)maxTabWidth { return 214; }
77 + (CGFloat)miniTabWidth { return 58; }
78 + (CGFloat)appTabWidth { return 66; }
80 - (TabView*)tabView {
81   DCHECK([[self view] isKindOfClass:[TabView class]]);
82   return static_cast<TabView*>([self view]);
85 - (id)init {
86   if ((self = [super init])) {
87     // Icon.
88     // Remember the icon's frame, so that if the icon is ever removed, a new
89     // one can later replace it in the proper location.
90     originalIconFrame_ = NSMakeRect(19, 5, 16, 16);
91     iconView_.reset([[SpriteView alloc] initWithFrame:originalIconFrame_]);
92     [iconView_ setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
94     // When the icon is removed, the title expands to the left to fill the
95     // space left by the icon.  When the close button is removed, the title
96     // expands to the right to fill its space.  These are the amounts to expand
97     // and contract the title frame under those conditions. We don't have to
98     // explicilty save the offset between the title and the close button since
99     // we can just get that value for the close button's frame.
100     NSRect titleFrame = NSMakeRect(35, 6, 92, 14);
102     // Close button.
103     closeButton_.reset([[HoverCloseButton alloc] initWithFrame:
104         NSMakeRect(127, 4, 18, 18)]);
105     [closeButton_ setAutoresizingMask:NSViewMinXMargin];
106     [closeButton_ setTarget:self];
107     [closeButton_ setAction:@selector(closeTab:)];
109     base::scoped_nsobject<TabView> view(
110         [[TabView alloc] initWithFrame:NSMakeRect(0, 0, 160, 25)
111                             controller:self
112                            closeButton:closeButton_]);
113     [view setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
114     [view addSubview:iconView_];
115     [view addSubview:closeButton_];
116     [view setTitleFrame:titleFrame];
117     [super setView:view];
119     isIconShowing_ = YES;
120     NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
121     [defaultCenter addObserver:self
122                       selector:@selector(themeChangedNotification:)
123                           name:kBrowserThemeDidChangeNotification
124                         object:nil];
126     [self internalSetSelected:selected_];
127   }
128   return self;
131 - (void)dealloc {
132   [mediaIndicatorView_ setAnimationDoneCallbackObject:nil withSelector: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)closeTab:(id)sender {
169   if ([[self target] respondsToSelector:@selector(closeTab:)]) {
170     [[self target] performSelector:@selector(closeTab:)
171                         withObject:[self view]];
172   }
175 - (void)selectTab:(id)sender {
176   if ([[self tabView] isClosing])
177     return;
178   if ([[self target] respondsToSelector:[self action]]) {
179     [[self target] performSelector:[self action]
180                         withObject:[self view]];
181   }
184 - (void)setTitle:(NSString*)title {
185   if ([[self title] isEqualToString:title])
186     return;
188   TabView* tabView = [self tabView];
189   [tabView setTitle:title];
191   if ([self mini] && ![self selected]) {
192     [tabView startAlert];
193   }
194   [super setTitle:title];
197 - (void)setToolTip:(NSString*)toolTip {
198   [[self view] setToolTip:toolTip];
201 - (void)setActive:(BOOL)active {
202   if (active != active_) {
203     active_ = active;
204     [self internalSetSelected:[self selected]];
205   }
208 - (BOOL)active {
209   return active_;
212 - (void)setSelected:(BOOL)selected {
213   if (selected_ != selected) {
214     selected_ = selected;
215     [self internalSetSelected:[self selected]];
216   }
219 - (BOOL)selected {
220   return selected_ || active_;
223 - (SpriteView*)iconView {
224   return iconView_;
227 - (void)setIconView:(SpriteView*)iconView {
228   [iconView_ removeFromSuperview];
229   iconView_.reset([iconView retain]);
231   if (iconView_)
232     [[self view] addSubview:iconView_];
235 - (MediaIndicatorView*)mediaIndicatorView {
236   return mediaIndicatorView_;
239 - (void)setMediaIndicatorView:(MediaIndicatorView*)mediaIndicatorView {
240   [mediaIndicatorView_ removeFromSuperview];
241   mediaIndicatorView_.reset([mediaIndicatorView retain]);
242   [self updateVisibility];
243   if (mediaIndicatorView_) {
244     [[self view] addSubview:mediaIndicatorView_];
245     [mediaIndicatorView_
246       setAnimationDoneCallbackObject:self
247                         withSelector:@selector(updateVisibility)];
249   }
252 - (HoverCloseButton*)closeButton {
253   return closeButton_;
256 - (NSString*)toolTip {
257   return [[self tabView] toolTipText];
260 // Return a rough approximation of the number of icons we could fit in the
261 // tab. We never actually do this, but it's a helpful guide for determining
262 // how much space we have available.
263 - (int)iconCapacity {
264   const CGFloat availableWidth = std::max<CGFloat>(
265       0, NSMaxX([closeButton_ frame]) - NSMinX(originalIconFrame_));
266   const CGFloat widthPerIcon = NSWidth(originalIconFrame_);
267   const int kPaddingBetweenIcons = 2;
268   if (availableWidth >= widthPerIcon &&
269       availableWidth < (widthPerIcon + kPaddingBetweenIcons)) {
270     return 1;
271   }
272   return availableWidth / (widthPerIcon + kPaddingBetweenIcons);
275 - (BOOL)shouldShowIcon {
276   return chrome::ShouldTabShowFavicon(
277       [self iconCapacity], [self mini], [self selected], iconView_ != nil,
278       !mediaIndicatorView_ ? TAB_MEDIA_STATE_NONE :
279           [mediaIndicatorView_ animatingMediaState]);
282 - (BOOL)shouldShowMediaIndicator {
283   if (!mediaIndicatorView_)
284     return NO;
285   return chrome::ShouldTabShowMediaIndicator(
286       [self iconCapacity], [self mini], [self selected], iconView_ != nil,
287       [mediaIndicatorView_ animatingMediaState]);
290 - (BOOL)shouldShowCloseButton {
291   return chrome::ShouldTabShowCloseButton(
292       [self iconCapacity], [self mini], [self selected]);
295 - (void)setIconImage:(NSImage*)image {
296   [self setIconImage:image withToastAnimation:NO];
299 - (void)setIconImage:(NSImage*)image withToastAnimation:(BOOL)animate {
300   if (image == nil) {
301     [self setIconView:nil];
302   } else {
303     if (iconView_.get() == nil) {
304       base::scoped_nsobject<SpriteView> iconView([[SpriteView alloc] init]);
305       [iconView setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
306       [self setIconView:iconView];
307     }
309     [iconView_ setImage:image withToastAnimation:animate];
311     if ([self app] || [self mini]) {
312       NSRect appIconFrame = [iconView_ frame];
313       appIconFrame.origin = originalIconFrame_.origin;
315       const CGFloat tabWidth = [self app] ? [TabController appTabWidth]
316                                           : [TabController miniTabWidth];
318       // Center the icon.
319       appIconFrame.origin.x =
320           std::floor((tabWidth - NSWidth(appIconFrame)) / 2.0);
321       [iconView_ setFrame:appIconFrame];
322     } else {
323       [iconView_ setFrame:originalIconFrame_];
324     }
325   }
326   // Ensure that the icon is suppressed if no icon is set or if the tab is too
327   // narrow to display one.
328   [self updateVisibility];
331 - (void)updateVisibility {
332   // iconView_ may have been replaced or it may be nil, so [iconView_ isHidden]
333   // won't work.  Instead, the state of the icon is tracked separately in
334   // isIconShowing_.
335   BOOL newShowIcon = [self shouldShowIcon];
337   [iconView_ setHidden:!newShowIcon];
338   isIconShowing_ = newShowIcon;
340   // If the tab is a mini-tab, hide the title.
341   TabView* tabView = [self tabView];
342   [tabView setTitleHidden:[self mini]];
344   BOOL newShowCloseButton = [self shouldShowCloseButton];
346   [closeButton_ setHidden:!newShowCloseButton];
348   BOOL newShowMediaIndicator = [self shouldShowMediaIndicator];
350   [mediaIndicatorView_ setHidden:!newShowMediaIndicator];
352   if (newShowMediaIndicator) {
353     NSRect newFrame = [mediaIndicatorView_ frame];
354     if ([self app] || [self mini]) {
355       // Tab is pinned: Position the media indicator in the center.
356       const CGFloat tabWidth = [self app] ?
357           [TabController appTabWidth] : [TabController miniTabWidth];
358       newFrame.origin.x = std::floor((tabWidth - NSWidth(newFrame)) / 2);
359       newFrame.origin.y = NSMinY(originalIconFrame_) -
360           std::floor((NSHeight(newFrame) - NSHeight(originalIconFrame_)) / 2);
361     } else {
362       // The Frame for the mediaIndicatorView_ depends on whether iconView_
363       // and/or closeButton_ are visible, and where they have been positioned.
364       const NSRect closeButtonFrame = [closeButton_ frame];
365       newFrame.origin.x = NSMinX(closeButtonFrame);
366       // Position to the left of the close button when it is showing.
367       if (newShowCloseButton)
368         newFrame.origin.x -= NSWidth(newFrame);
369       // Media indicator is centered vertically, with respect to closeButton_.
370       newFrame.origin.y = NSMinY(closeButtonFrame) -
371           std::floor((NSHeight(newFrame) - NSHeight(closeButtonFrame)) / 2);
372     }
373     [mediaIndicatorView_ setFrame:newFrame];
374   }
376   // Adjust the title view based on changes to the icon's and close button's
377   // visibility.
378   NSRect oldTitleFrame = [tabView titleFrame];
379   NSRect newTitleFrame;
380   newTitleFrame.size.height = oldTitleFrame.size.height;
381   newTitleFrame.origin.y = oldTitleFrame.origin.y;
383   if (newShowIcon) {
384     newTitleFrame.origin.x = NSMaxX([iconView_ frame]);
385   } else {
386     newTitleFrame.origin.x = originalIconFrame_.origin.x;
387   }
389   if (newShowMediaIndicator) {
390     newTitleFrame.size.width = NSMinX([mediaIndicatorView_ frame]) -
391                                newTitleFrame.origin.x;
392   } else if (newShowCloseButton) {
393     newTitleFrame.size.width = NSMinX([closeButton_ frame]) -
394                                newTitleFrame.origin.x;
395   } else {
396     newTitleFrame.size.width = NSMaxX([closeButton_ frame]) -
397                                newTitleFrame.origin.x;
398   }
400   [tabView setTitleFrame:newTitleFrame];
403 - (void)updateTitleColor {
404   NSColor* titleColor = nil;
405   ui::ThemeProvider* theme = [[[self view] window] themeProvider];
406   if (theme && ![self selected])
407     titleColor = theme->GetNSColor(ThemeProperties::COLOR_BACKGROUND_TAB_TEXT);
408   // Default to the selected text color unless told otherwise.
409   if (theme && !titleColor)
410     titleColor = theme->GetNSColor(ThemeProperties::COLOR_TAB_TEXT);
411   [[self tabView] setTitleColor:titleColor ? titleColor : [NSColor textColor]];
414 - (void)themeChangedNotification:(NSNotification*)notification {
415   [self updateTitleColor];
418 // Called by the tabs to determine whether we are in rapid (tab) closure mode.
419 - (BOOL)inRapidClosureMode {
420   if ([[self target] respondsToSelector:@selector(inRapidClosureMode)]) {
421     return [[self target] performSelector:@selector(inRapidClosureMode)] ?
422         YES : NO;
423   }
424   return NO;
427 // The following methods are invoked from the TabView and are forwarded to the
428 // TabStripDragController.
429 - (BOOL)tabCanBeDragged:(TabController*)controller {
430   return [[target_ dragController] tabCanBeDragged:controller];
433 - (void)maybeStartDrag:(NSEvent*)event forTab:(TabController*)tab {
434   [[target_ dragController] maybeStartDrag:event forTab:tab];
437 @end