Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / tabs / tab_controller.mm
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.
4
5 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
6
7 #include <algorithm>
8 #include <cmath>
9
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"
22
23 @implementation TabController
24
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_;
32
33 namespace TabControllerInternal {
34
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
37 // tab".
38 class MenuDelegate : public ui::SimpleMenuModel::Delegate {
39  public:
40   explicit MenuDelegate(id<TabControllerTarget> target, TabController* owner)
41       : target_(target),
42         owner_(owner) {}
43
44   // Overridden from ui::SimpleMenuModel::Delegate
45   virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
46     return false;
47   }
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_];
52   }
53   virtual bool GetAcceleratorForCommandId(
54       int command_id,
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_];
60   }
61
62  private:
63   id<TabControllerTarget> target_;  // weak
64   TabController* owner_;  // weak, owns me
65 };
66
67 }  // TabControllerInternal namespace
68
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; }
78
79 - (TabView*)tabView {
80   DCHECK([[self view] isKindOfClass:[TabView class]]);
81   return static_cast<TabView*>([self view]);
82 }
83
84 - (id)init {
85   if ((self = [super init])) {
86     // Icon.
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];
92
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);
100
101     // Close button.
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:)];
107
108     base::scoped_nsobject<TabView> view(
109         [[TabView alloc] initWithFrame:NSMakeRect(0, 0, 160, 25)
110                             controller:self
111                            closeButton:closeButton_]);
112     [view setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
113     [view addSubview:iconView_];
114     [view addSubview:closeButton_];
115     [view setTitleFrame:titleFrame];
116     [super setView:view];
117
118     isIconShowing_ = YES;
119     NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
120     [defaultCenter addObserver:self
121                       selector:@selector(themeChangedNotification:)
122                           name:kBrowserThemeDidChangeNotification
123                         object:nil];
124
125     [self internalSetSelected:selected_];
126   }
127   return self;
128 }
129
130 - (void)dealloc {
131   [mediaIndicatorView_ setAnimationDoneCallbackObject:nil withSelector:nil];
132   [[NSNotificationCenter defaultCenter] removeObserver:self];
133   [[self tabView] setController:nil];
134   [super dealloc];
135 }
136
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
139 // a re-draw.
140 - (void)internalSetSelected:(BOOL)selected {
141   TabView* tabView = [self tabView];
142   if ([self active]) {
143     [tabView setState:NSOnState];
144     [tabView cancelAlert];
145   } else {
146     [tabView setState:selected ? NSMixedState : NSOffState];
147   }
148   [self updateVisibility];
149   [self updateTitleColor];
150 }
151
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.
155 - (NSMenu*)menu {
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];
165 }
166
167 - (void)closeTab:(id)sender {
168   if ([[self target] respondsToSelector:@selector(closeTab:)]) {
169     [[self target] performSelector:@selector(closeTab:)
170                         withObject:[self view]];
171   }
172 }
173
174 - (void)selectTab:(id)sender {
175   if ([[self tabView] isClosing])
176     return;
177   if ([[self target] respondsToSelector:[self action]]) {
178     [[self target] performSelector:[self action]
179                         withObject:[self view]];
180   }
181 }
182
183 - (void)setTitle:(NSString*)title {
184   if ([[self title] isEqualToString:title])
185     return;
186
187   TabView* tabView = [self tabView];
188   [tabView setTitle:title];
189
190   if ([self mini] && ![self active]) {
191     [tabView startAlert];
192   }
193   [super setTitle:title];
194 }
195
196 - (void)setToolTip:(NSString*)toolTip {
197   [[self view] setToolTip:toolTip];
198 }
199
200 - (void)setActive:(BOOL)active {
201   if (active != active_) {
202     active_ = active;
203     [self internalSetSelected:[self selected]];
204   }
205 }
206
207 - (BOOL)active {
208   return active_;
209 }
210
211 - (void)setSelected:(BOOL)selected {
212   if (selected_ != selected) {
213     selected_ = selected;
214     [self internalSetSelected:[self selected]];
215   }
216 }
217
218 - (BOOL)selected {
219   return selected_ || active_;
220 }
221
222 - (SpriteView*)iconView {
223   return iconView_;
224 }
225
226 - (void)setIconView:(SpriteView*)iconView {
227   [iconView_ removeFromSuperview];
228   iconView_.reset([iconView retain]);
229
230   if (iconView_)
231     [[self view] addSubview:iconView_];
232 }
233
234 - (MediaIndicatorView*)mediaIndicatorView {
235   return mediaIndicatorView_;
236 }
237
238 - (void)setMediaIndicatorView:(MediaIndicatorView*)mediaIndicatorView {
239   [mediaIndicatorView_ removeFromSuperview];
240   mediaIndicatorView_.reset([mediaIndicatorView retain]);
241   [self updateVisibility];
242   if (mediaIndicatorView_) {
243     [[self view] addSubview:mediaIndicatorView_];
244     [mediaIndicatorView_
245       setAnimationDoneCallbackObject:self
246                         withSelector:@selector(updateVisibility)];
247
248   }
249 }
250
251 - (HoverCloseButton*)closeButton {
252   return closeButton_;
253 }
254
255 - (NSString*)toolTip {
256   return [[self tabView] toolTipText];
257 }
258
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)) {
269     return 1;
270   }
271   return availableWidth / (widthPerIcon + kPaddingBetweenIcons);
272 }
273
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]);
279 }
280
281 - (BOOL)shouldShowMediaIndicator {
282   if (!mediaIndicatorView_)
283     return NO;
284   return chrome::ShouldTabShowMediaIndicator(
285       [self iconCapacity], [self mini], [self active], iconView_ != nil,
286       [mediaIndicatorView_ animatingMediaState]);
287 }
288
289 - (BOOL)shouldShowCloseButton {
290   return chrome::ShouldTabShowCloseButton(
291       [self iconCapacity], [self mini], [self active]);
292 }
293
294 - (void)setIconImage:(NSImage*)image {
295   [self setIconImage:image withToastAnimation:NO];
296 }
297
298 - (void)setIconImage:(NSImage*)image withToastAnimation:(BOOL)animate {
299   if (image == nil) {
300     [self setIconView:nil];
301   } else {
302     if (iconView_.get() == nil) {
303       base::scoped_nsobject<SpriteView> iconView([[SpriteView alloc] init]);
304       [iconView setAutoresizingMask:NSViewMaxXMargin | NSViewMinYMargin];
305       [self setIconView:iconView];
306     }
307
308     [iconView_ setImage:image withToastAnimation:animate];
309
310     if ([self app] || [self mini]) {
311       NSRect appIconFrame = [iconView_ frame];
312       appIconFrame.origin = originalIconFrame_.origin;
313
314       const CGFloat tabWidth = [self app] ? [TabController appTabWidth]
315                                           : [TabController miniTabWidth];
316
317       // Center the icon.
318       appIconFrame.origin.x =
319           std::floor((tabWidth - NSWidth(appIconFrame)) / 2.0);
320       [iconView_ setFrame:appIconFrame];
321     } else {
322       [iconView_ setFrame:originalIconFrame_];
323     }
324   }
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];
328 }
329
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
333   // isIconShowing_.
334   BOOL newShowIcon = [self shouldShowIcon];
335
336   [iconView_ setHidden:!newShowIcon];
337   isIconShowing_ = newShowIcon;
338
339   // If the tab is a mini-tab, hide the title.
340   TabView* tabView = [self tabView];
341   [tabView setTitleHidden:[self mini]];
342
343   BOOL newShowCloseButton = [self shouldShowCloseButton];
344
345   [closeButton_ setHidden:!newShowCloseButton];
346
347   BOOL newShowMediaIndicator = [self shouldShowMediaIndicator];
348
349   [mediaIndicatorView_ setHidden:!newShowMediaIndicator];
350
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);
360     } else {
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);
371     }
372     [mediaIndicatorView_ setFrame:newFrame];
373   }
374
375   // Adjust the title view based on changes to the icon's and close button's
376   // visibility.
377   NSRect oldTitleFrame = [tabView titleFrame];
378   NSRect newTitleFrame;
379   newTitleFrame.size.height = oldTitleFrame.size.height;
380   newTitleFrame.origin.y = oldTitleFrame.origin.y;
381
382   if (newShowIcon) {
383     newTitleFrame.origin.x = NSMaxX([iconView_ frame]);
384   } else {
385     newTitleFrame.origin.x = originalIconFrame_.origin.x;
386   }
387
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;
394   } else {
395     newTitleFrame.size.width = NSMaxX([closeButton_ frame]) -
396                                newTitleFrame.origin.x;
397   }
398
399   [tabView setTitleFrame:newTitleFrame];
400 }
401
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]];
411 }
412
413 - (void)themeChangedNotification:(NSNotification*)notification {
414   [self updateTitleColor];
415 }
416
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)] ?
421         YES : NO;
422   }
423   return NO;
424 }
425
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];
430 }
431
432 - (void)maybeStartDrag:(NSEvent*)event forTab:(TabController*)tab {
433   [[target_ dragController] maybeStartDrag:event forTab:tab];
434 }
435
436 @end