Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / tabs / tab_controller.mm
blobb5c6835283f2b5310257f0ed859d705797346d40
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 #include "base/strings/sys_string_conversions.h"
14 #import "chrome/browser/themes/theme_properties.h"
15 #import "chrome/browser/themes/theme_service.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 "third_party/google_toolbox_for_mac/src/AppKit/GTMFadeTruncatingTextFieldCell.h"
23 #import "ui/base/cocoa/menu_controller.h"
24 #include "ui/base/l10n/l10n_util_mac.h"
26 @implementation TabController
28 @synthesize action = action_;
29 @synthesize app = app_;
30 @synthesize loadingState = loadingState_;
31 @synthesize mini = mini_;
32 @synthesize pinned = pinned_;
33 @synthesize target = target_;
34 @synthesize url = url_;
36 namespace TabControllerInternal {
38 // A C++ delegate that handles enabling/disabling menu items and handling when
39 // a menu command is chosen. Also fixes up the menu item label for "pin/unpin
40 // tab".
41 class MenuDelegate : public ui::SimpleMenuModel::Delegate {
42  public:
43   explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner)
44       : target_(target),
45         owner_(owner) {}
47   // Overridden from ui::SimpleMenuModel::Delegate
48   virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
49     return false;
50   }
51   virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE {
52     TabStripModel::ContextMenuCommand command =
53         static_cast<TabStripModel::ContextMenuCommand>(command_id);
54     return [target_ isCommandEnabled:command forController:owner_];
55   }
56   virtual bool GetAcceleratorForCommandId(
57       int command_id,
58       ui::Accelerator* accelerator) OVERRIDE { return false; }
59   virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE {
60     TabStripModel::ContextMenuCommand command =
61         static_cast<TabStripModel::ContextMenuCommand>(command_id);
62     [target_ commandDispatch:command forController:owner_];
63   }
65  private:
66   id<TabControllerTarget> target_;  // weak
67   TabController* owner_;  // weak, owns me
70 }  // TabControllerInternal namespace
72 // The min widths is the smallest number at which the right edge of the right
73 // tab border image is not visibly clipped.  It is a bit smaller than the sum
74 // of the two tab edge bitmaps because these bitmaps have a few transparent
75 // pixels on the side.  The selected tab width includes the close button width.
76 + (CGFloat)minTabWidth { return 36; }
77 + (CGFloat)minSelectedTabWidth { return 52; }
78 + (CGFloat)maxTabWidth { return 214; }
79 + (CGFloat)miniTabWidth { return 58; }
80 + (CGFloat)appTabWidth { return 66; }
82 - (TabView*)tabView {
83   DCHECK([[self view] isKindOfClass:[TabView class]]);
84   return static_cast<TabView*>([self view]);
87 - (id)init {
88   if ((self = [super init])) {
89     // Icon.
90     // Remember the icon's frame, so that if the icon is ever removed, a new
91     // one can later replace it in the proper location.
92     originalIconFrame_ = NSMakeRect(19, 5, 16, 16);
93     iconView_.reset([[NSImageView alloc] initWithFrame:originalIconFrame_]);
94     [iconView_ setAutoresizingMask:NSViewMaxXMargin];
96     // When the icon is removed, the title expands to the left to fill the
97     // space left by the icon.  When the close button is removed, the title
98     // expands to the right to fill its space.  These are the amounts to expand
99     // and contract titleView_ under those conditions. We don't have to
100     // explicilty save the offset between the title and the close button since
101     // we can just get that value for the close button's frame.
102     NSRect titleFrame = NSMakeRect(35, 6, 92, 14);
104     // Label.
105     titleView_.reset([[NSTextField alloc] initWithFrame:titleFrame]);
106     [titleView_ setAutoresizingMask:NSViewWidthSizable];
107     base::scoped_nsobject<GTMFadeTruncatingTextFieldCell> labelCell(
108         [[GTMFadeTruncatingTextFieldCell alloc] initTextCell:@"Label"]);
109     [labelCell setControlSize:NSSmallControlSize];
110     CGFloat fontSize = [NSFont systemFontSizeForControlSize:NSSmallControlSize];
111     NSFont* font = [NSFont fontWithName:
112         [[labelCell font] fontName] size:fontSize];
113     [labelCell setFont:font];
114     [titleView_ setCell:labelCell];
115     titleViewCell_ = labelCell;
117     // Close button.
118     closeButton_.reset([[HoverCloseButton alloc] initWithFrame:
119         NSMakeRect(127, 4, 18, 18)]);
120     [closeButton_ setAutoresizingMask:NSViewMinXMargin];
121     [closeButton_ setTarget:self];
122     [closeButton_ setAction:@selector(closeTab:)];
124     base::scoped_nsobject<TabView> view(
125         [[TabView alloc] initWithFrame:NSMakeRect(0, 0, 160, 25)
126                             controller:self
127                            closeButton:closeButton_]);
128     [view setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
129     [view addSubview:iconView_];
130     [view addSubview:titleView_];
131     [view addSubview:closeButton_];
132     [super setView:view];
134     isIconShowing_ = YES;
135     NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
136     [defaultCenter addObserver:self
137                       selector:@selector(themeChangedNotification:)
138                           name:kBrowserThemeDidChangeNotification
139                         object:nil];
141     [self internalSetSelected:selected_];
142   }
143   return self;
146 - (void)dealloc {
147   [mediaIndicatorView_ setAnimationDoneCallbackObject:nil withSelector:nil];
148   [[NSNotificationCenter defaultCenter] removeObserver:self];
149   [[self tabView] setController:nil];
150   [super dealloc];
153 // The internals of |-setSelected:| and |-setActive:| but doesn't set the
154 // backing variables. This updates the drawing state and marks self as needing
155 // a re-draw.
156 - (void)internalSetSelected:(BOOL)selected {
157   TabView* tabView = [self tabView];
158   DCHECK([tabView isKindOfClass:[TabView class]]);
159   [tabView setState:selected];
160   if ([self active])
161     [tabView cancelAlert];
162   [self updateVisibility];
163   [self updateTitleColor];
166 // Called when Cocoa wants to display the context menu. Lazily instantiate
167 // the menu based off of the cross-platform model. Re-create the menu and
168 // model every time to get the correct labels and enabling.
169 - (NSMenu*)menu {
170   contextMenuDelegate_.reset(
171       new TabControllerInternal::MenuDelegate(target_, self));
172   contextMenuModel_.reset(
173       [target_ contextMenuModelForController:self
174                                 menuDelegate:contextMenuDelegate_.get()]);
175   contextMenuController_.reset(
176       [[MenuController alloc] initWithModel:contextMenuModel_.get()
177                      useWithPopUpButtonCell:NO]);
178   return [contextMenuController_ menu];
181 - (void)closeTab:(id)sender {
182   if ([[self target] respondsToSelector:@selector(closeTab:)]) {
183     [[self target] performSelector:@selector(closeTab:)
184                         withObject:[self view]];
185   }
188 - (void)selectTab:(id)sender {
189   if ([[self tabView] isClosing])
190     return;
191   if ([[self target] respondsToSelector:[self action]]) {
192     [[self target] performSelector:[self action]
193                         withObject:[self view]];
194   }
197 - (void)setTitle:(NSString*)title {
198   if ([[self title] isEqualToString:title])
199     return;
201   [titleView_ setStringValue:title];
202   base::string16 title16 = base::SysNSStringToUTF16(title);
203   bool isRTL = base::i18n::GetFirstStrongCharacterDirection(title16) ==
204                base::i18n::RIGHT_TO_LEFT;
205   titleViewCell_.truncateMode = isRTL ? GTMFadeTruncatingHead
206                                       : GTMFadeTruncatingTail;
208   if ([self mini] && ![self selected]) {
209     TabView* tabView = static_cast<TabView*>([self view]);
210     DCHECK([tabView isKindOfClass:[TabView class]]);
211     [tabView startAlert];
212   }
213   [super setTitle:title];
216 - (void)setToolTip:(NSString*)toolTip {
217   [[self view] setToolTip:toolTip];
220 - (void)setActive:(BOOL)active {
221   if (active != active_) {
222     active_ = active;
223     [self internalSetSelected:[self selected]];
224   }
227 - (BOOL)active {
228   return active_;
231 - (void)setSelected:(BOOL)selected {
232   if (selected_ != selected) {
233     selected_ = selected;
234     [self internalSetSelected:[self selected]];
235   }
238 - (BOOL)selected {
239   return selected_ || active_;
242 - (NSView*)iconView {
243   return iconView_;
246 - (void)setIconView:(NSView*)iconView {
247   [iconView_ removeFromSuperview];
248   iconView_.reset([iconView retain]);
250   if ([self app] || [self mini]) {
251     NSRect appIconFrame = [iconView frame];
252     appIconFrame.origin = originalIconFrame_.origin;
254     const CGFloat tabWidth = [self app] ? [TabController appTabWidth]
255                                         : [TabController miniTabWidth];
257     // Center the icon.
258     appIconFrame.origin.x =
259         std::floor((tabWidth - NSWidth(appIconFrame)) / 2.0);
260     [iconView_ setFrame:appIconFrame];
261   } else {
262     [iconView_ setFrame:originalIconFrame_];
263   }
264   // Ensure that the icon is suppressed if no icon is set or if the tab is too
265   // narrow to display one.
266   [self updateVisibility];
268   if (iconView_)
269     [[self view] addSubview:iconView_];
272 - (NSTextField*)titleView {
273   return titleView_;
276 - (MediaIndicatorView*)mediaIndicatorView {
277   return mediaIndicatorView_;
280 - (void)setMediaIndicatorView:(MediaIndicatorView*)mediaIndicatorView {
281   [mediaIndicatorView_ removeFromSuperview];
282   mediaIndicatorView_.reset([mediaIndicatorView retain]);
283   [self updateVisibility];
284   if (mediaIndicatorView_) {
285     [[self view] addSubview:mediaIndicatorView_];
286     [mediaIndicatorView_
287       setAnimationDoneCallbackObject:self
288                         withSelector:@selector(updateVisibility)];
290   }
293 - (HoverCloseButton*)closeButton {
294   return closeButton_;
297 - (NSString*)toolTip {
298   return [[self tabView] toolTipText];
301 // Return a rough approximation of the number of icons we could fit in the
302 // tab. We never actually do this, but it's a helpful guide for determining
303 // how much space we have available.
304 - (int)iconCapacity {
305   const CGFloat availableWidth = std::max<CGFloat>(
306       0, NSMaxX([closeButton_ frame]) - NSMinX(originalIconFrame_));
307   const CGFloat widthPerIcon = NSWidth(originalIconFrame_);
308   const int kPaddingBetweenIcons = 2;
309   if (availableWidth >= widthPerIcon &&
310       availableWidth < (widthPerIcon + kPaddingBetweenIcons)) {
311     return 1;
312   }
313   return availableWidth / (widthPerIcon + kPaddingBetweenIcons);
316 - (BOOL)shouldShowIcon {
317   return chrome::ShouldTabShowFavicon(
318       [self iconCapacity], [self mini], [self selected], iconView_ != nil,
319       !mediaIndicatorView_ ? TAB_MEDIA_STATE_NONE :
320           [mediaIndicatorView_ animatingMediaState]);
323 - (BOOL)shouldShowMediaIndicator {
324   if (!mediaIndicatorView_)
325     return NO;
326   return chrome::ShouldTabShowMediaIndicator(
327       [self iconCapacity], [self mini], [self selected], iconView_ != nil,
328       [mediaIndicatorView_ animatingMediaState]);
331 - (BOOL)shouldShowCloseButton {
332   return chrome::ShouldTabShowCloseButton(
333       [self iconCapacity], [self mini], [self selected]);
336 - (void)updateVisibility {
337   // iconView_ may have been replaced or it may be nil, so [iconView_ isHidden]
338   // won't work.  Instead, the state of the icon is tracked separately in
339   // isIconShowing_.
340   BOOL newShowIcon = [self shouldShowIcon];
342   [iconView_ setHidden:!newShowIcon];
343   isIconShowing_ = newShowIcon;
345   // If the tab is a mini-tab, hide the title.
346   [titleView_ setHidden:[self mini]];
348   BOOL newShowCloseButton = [self shouldShowCloseButton];
350   [closeButton_ setHidden:!newShowCloseButton];
352   BOOL newShowMediaIndicator = [self shouldShowMediaIndicator];
354   [mediaIndicatorView_ setHidden:!newShowMediaIndicator];
356   if (newShowMediaIndicator) {
357     NSRect newFrame = [mediaIndicatorView_ frame];
358     if ([self app] || [self mini]) {
359       // Tab is pinned: Position the media indicator in the center.
360       const CGFloat tabWidth = [self app] ?
361           [TabController appTabWidth] : [TabController miniTabWidth];
362       newFrame.origin.x = std::floor((tabWidth - NSWidth(newFrame)) / 2);
363       newFrame.origin.y = NSMinY(originalIconFrame_) -
364           std::floor((NSHeight(newFrame) - NSHeight(originalIconFrame_)) / 2);
365     } else {
366       // The Frame for the mediaIndicatorView_ depends on whether iconView_
367       // and/or closeButton_ are visible, and where they have been positioned.
368       const NSRect closeButtonFrame = [closeButton_ frame];
369       newFrame.origin.x = NSMinX(closeButtonFrame);
370       // Position to the left of the close button when it is showing.
371       if (newShowCloseButton)
372         newFrame.origin.x -= NSWidth(newFrame);
373       // Media indicator is centered vertically, with respect to closeButton_.
374       newFrame.origin.y = NSMinY(closeButtonFrame) -
375           std::floor((NSHeight(newFrame) - NSHeight(closeButtonFrame)) / 2);
376     }
377     [mediaIndicatorView_ setFrame:newFrame];
378   }
380   // Adjust the title view based on changes to the icon's and close button's
381   // visibility.
382   NSRect oldTitleFrame = [titleView_ frame];
383   NSRect newTitleFrame;
384   newTitleFrame.size.height = oldTitleFrame.size.height;
385   newTitleFrame.origin.y = oldTitleFrame.origin.y;
387   if (newShowIcon) {
388     newTitleFrame.origin.x = NSMaxX([iconView_ frame]);
389   } else {
390     newTitleFrame.origin.x = originalIconFrame_.origin.x;
391   }
393   if (newShowMediaIndicator) {
394     newTitleFrame.size.width = NSMinX([mediaIndicatorView_ frame]) -
395                                newTitleFrame.origin.x;
396   } else if (newShowCloseButton) {
397     newTitleFrame.size.width = NSMinX([closeButton_ frame]) -
398                                newTitleFrame.origin.x;
399   } else {
400     newTitleFrame.size.width = NSMaxX([closeButton_ frame]) -
401                                newTitleFrame.origin.x;
402   }
404   [titleView_ setFrame:newTitleFrame];
407 - (void)updateTitleColor {
408   NSColor* titleColor = nil;
409   ui::ThemeProvider* theme = [[[self view] window] themeProvider];
410   if (theme && ![self selected])
411     titleColor = theme->GetNSColor(ThemeProperties::COLOR_BACKGROUND_TAB_TEXT);
412   // Default to the selected text color unless told otherwise.
413   if (theme && !titleColor)
414     titleColor = theme->GetNSColor(ThemeProperties::COLOR_TAB_TEXT);
415   [titleView_ setTextColor:titleColor ? titleColor : [NSColor textColor]];
418 - (void)themeChangedNotification:(NSNotification*)notification {
419   [self updateTitleColor];
422 // Called by the tabs to determine whether we are in rapid (tab) closure mode.
423 - (BOOL)inRapidClosureMode {
424   if ([[self target] respondsToSelector:@selector(inRapidClosureMode)]) {
425     return [[self target] performSelector:@selector(inRapidClosureMode)] ?
426         YES : NO;
427   }
428   return NO;
431 // The following methods are invoked from the TabView and are forwarded to the
432 // TabStripDragController.
433 - (BOOL)tabCanBeDragged:(TabController*)controller {
434   return [[target_ dragController] tabCanBeDragged:controller];
437 - (void)maybeStartDrag:(NSEvent*)event forTab:(TabController*)tab {
438   [[target_ dragController] maybeStartDrag:event forTab:tab];
441 @end