Upstream version 10.39.225.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / extensions / browser_actions_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/extensions/browser_actions_controller.h"
6
7 #include <cmath>
8 #include <string>
9
10 #include "base/strings/sys_string_conversions.h"
11 #include "chrome/browser/chrome_notification_types.h"
12 #include "chrome/browser/extensions/extension_action.h"
13 #include "chrome/browser/extensions/extension_action_manager.h"
14 #include "chrome/browser/extensions/extension_toolbar_model.h"
15 #include "chrome/browser/extensions/extension_util.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/sessions/session_tab_helper.h"
18 #include "chrome/browser/ui/browser.h"
19 #include "chrome/browser/ui/browser_window.h"
20 #import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
21 #import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
22 #import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
23 #import "chrome/browser/ui/cocoa/image_button_cell.h"
24 #import "chrome/browser/ui/cocoa/menu_button.h"
25 #include "chrome/browser/ui/tabs/tab_strip_model.h"
26 #include "chrome/common/extensions/api/extension_action/action_info.h"
27 #include "content/public/browser/notification_details.h"
28 #include "content/public/browser/notification_observer.h"
29 #include "content/public/browser/notification_registrar.h"
30 #include "content/public/browser/notification_source.h"
31 #include "extensions/browser/extension_registry.h"
32 #include "grit/theme_resources.h"
33 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
34
35 using extensions::Extension;
36 using extensions::ExtensionList;
37
38 NSString* const kBrowserActionVisibilityChangedNotification =
39     @"BrowserActionVisibilityChangedNotification";
40
41 namespace {
42 const CGFloat kAnimationDuration = 0.2;
43
44 const CGFloat kChevronWidth = 18;
45
46 // Since the container is the maximum height of the toolbar, we have
47 // to move the buttons up by this amount in order to have them look
48 // vertically centered within the toolbar.
49 const CGFloat kBrowserActionOriginYOffset = 5.0;
50
51 // The size of each button on the toolbar.
52 const CGFloat kBrowserActionHeight = 29.0;
53 const CGFloat kBrowserActionWidth = 29.0;
54
55 // The padding between browser action buttons.
56 const CGFloat kBrowserActionButtonPadding = 2.0;
57
58 // Padding between Omnibox and first button.  Since the buttons have a
59 // pixel of internal padding, this needs an extra pixel.
60 const CGFloat kBrowserActionLeftPadding = kBrowserActionButtonPadding + 1.0;
61
62 // How far to inset from the bottom of the view to get the top border
63 // of the popup 2px below the bottom of the Omnibox.
64 const CGFloat kBrowserActionBubbleYOffset = 3.0;
65
66 }  // namespace
67
68 @interface BrowserActionsController(Private)
69 // Used during initialization to create the BrowserActionButton objects from the
70 // stored toolbar model.
71 - (void)createButtons;
72
73 // Creates and then adds the given extension's action button to the container
74 // at the given index within the container. It does not affect the toolbar model
75 // object since it is called when the toolbar model changes.
76 - (void)createActionButtonForExtension:(const Extension*)extension
77                              withIndex:(NSUInteger)index;
78
79 // Removes an action button for the given extension from the container. This
80 // method also does not affect the underlying toolbar model since it is called
81 // when the toolbar model changes.
82 - (void)removeActionButtonForExtension:(const Extension*)extension;
83
84 // Useful in the case of a Browser Action being added/removed from the middle of
85 // the container, this method repositions each button according to the current
86 // toolbar model.
87 - (void)positionActionButtonsAndAnimate:(BOOL)animate;
88
89 // During container resizing, buttons become more transparent as they are pushed
90 // off the screen. This method updates each button's opacity determined by the
91 // position of the button.
92 - (void)updateButtonOpacity;
93
94 // Returns the existing button with the given extension backing it; nil if it
95 // cannot be found or the extension's ID is invalid.
96 - (BrowserActionButton*)buttonForExtension:(const Extension*)extension;
97
98 // Returns the preferred width of the container given the number of visible
99 // buttons |buttonCount|.
100 - (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount;
101
102 // Returns the number of buttons that can fit in the container according to its
103 // current size.
104 - (NSUInteger)containerButtonCapacity;
105
106 // Notification handlers for events registered by the class.
107
108 // Updates each button's opacity, the cursor rects and chevron position.
109 - (void)containerFrameChanged:(NSNotification*)notification;
110
111 // Hides the chevron and unhides every hidden button so that dragging the
112 // container out smoothly shows the Browser Action buttons.
113 - (void)containerDragStart:(NSNotification*)notification;
114
115 // Sends a notification for the toolbar to reposition surrounding UI elements.
116 - (void)containerDragging:(NSNotification*)notification;
117
118 // Determines which buttons need to be hidden based on the new size, hides them
119 // and updates the chevron overflow menu. Also fires a notification to let the
120 // toolbar know that the drag has finished.
121 - (void)containerDragFinished:(NSNotification*)notification;
122
123 // Adjusts the position of the surrounding action buttons depending on where the
124 // button is within the container.
125 - (void)actionButtonDragging:(NSNotification*)notification;
126
127 // Updates the position of the Browser Actions within the container. This fires
128 // when _any_ Browser Action button is done dragging to keep all open windows in
129 // sync visually.
130 - (void)actionButtonDragFinished:(NSNotification*)notification;
131
132 // Moves the given button both visually and within the toolbar model to the
133 // specified index.
134 - (void)moveButton:(BrowserActionButton*)button
135            toIndex:(NSUInteger)index
136            animate:(BOOL)animate;
137
138 // Handles when the given BrowserActionButton object is clicked and whether
139 // it should grant tab permissions. API-simulated clicks should not grant.
140 - (BOOL)browserActionClicked:(BrowserActionButton*)button
141                  shouldGrant:(BOOL)shouldGrant;
142 - (BOOL)browserActionClicked:(BrowserActionButton*)button;
143
144 // Returns whether the given extension should be displayed. Only displays
145 // incognito-enabled extensions in incognito mode. Otherwise returns YES.
146 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension;
147
148 // The reason |frame| is specified in these chevron functions is because the
149 // container may be animating and the end frame of the animation should be
150 // passed instead of the current frame (which may be off and cause the chevron
151 // to jump at the end of its animation).
152
153 // Shows the overflow chevron button depending on whether there are any hidden
154 // extensions within the frame given.
155 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate;
156
157 // Moves the chevron to its correct position within |frame|.
158 - (void)updateChevronPositionInFrame:(NSRect)frame;
159
160 // Shows or hides the chevron, animating as specified by |animate|.
161 - (void)setChevronHidden:(BOOL)hidden
162                  inFrame:(NSRect)frame
163                  animate:(BOOL)animate;
164
165 // Handles when a menu item within the chevron overflow menu is selected.
166 - (void)chevronItemSelected:(id)menuItem;
167
168 // Updates the container's grippy cursor based on the number of hidden buttons.
169 - (void)updateGrippyCursors;
170
171 // Returns the ID of the currently selected tab or -1 if none exists.
172 - (int)currentTabId;
173 @end
174
175 // A helper class to proxy extension notifications to the view controller's
176 // appropriate methods.
177 class ExtensionServiceObserverBridge
178     : public content::NotificationObserver,
179       public extensions::ExtensionToolbarModel::Observer {
180  public:
181   ExtensionServiceObserverBridge(BrowserActionsController* owner,
182                                  Browser* browser)
183     : owner_(owner), browser_(browser) {
184     registrar_.Add(this,
185                    extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE,
186                    content::Source<Profile>(browser->profile()));
187     registrar_.Add(
188         this,
189         extensions::NOTIFICATION_EXTENSION_COMMAND_BROWSER_ACTION_MAC,
190         content::Source<Profile>(browser->profile()));
191   }
192
193   // Overridden from content::NotificationObserver.
194   virtual void Observe(
195       int type,
196       const content::NotificationSource& source,
197       const content::NotificationDetails& details) OVERRIDE {
198     switch (type) {
199       case extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE: {
200         ExtensionPopupController* popup = [ExtensionPopupController popup];
201         if (popup && ![popup isClosing])
202           [popup close];
203
204         break;
205       }
206       case extensions::NOTIFICATION_EXTENSION_COMMAND_BROWSER_ACTION_MAC: {
207         std::pair<const std::string, gfx::NativeWindow>* payload =
208             content::Details<std::pair<const std::string, gfx::NativeWindow> >(
209                 details).ptr();
210         std::string extension_id = payload->first;
211         gfx::NativeWindow window = payload->second;
212         if (window != browser_->window()->GetNativeWindow())
213           break;
214         [owner_ activateBrowserAction:extension_id];
215         break;
216       }
217       default:
218         NOTREACHED() << L"Unexpected notification";
219     }
220   }
221
222   // extensions::ExtensionToolbarModel::Observer implementation.
223   virtual void ToolbarExtensionAdded(
224       const Extension* extension,
225       int index) OVERRIDE {
226     [owner_ createActionButtonForExtension:extension withIndex:index];
227     [owner_ resizeContainerAndAnimate:NO];
228   }
229
230   virtual void ToolbarExtensionRemoved(const Extension* extension) OVERRIDE {
231     [owner_ removeActionButtonForExtension:extension];
232     [owner_ resizeContainerAndAnimate:NO];
233   }
234
235   virtual void ToolbarExtensionMoved(const Extension* extension,
236                                      int index) OVERRIDE {
237   }
238
239   virtual void ToolbarExtensionUpdated(const Extension* extension) OVERRIDE {
240     BrowserActionButton* button = [owner_ buttonForExtension:extension];
241     if (button)
242       [button updateState];
243   }
244
245   virtual bool ShowExtensionActionPopup(const Extension* extension,
246                                         bool grant_active_tab) OVERRIDE {
247     // Do not override other popups and only show in active window.
248     ExtensionPopupController* popup = [ExtensionPopupController popup];
249     if (popup || !browser_->window()->IsActive())
250       return false;
251
252     BrowserActionButton* button = [owner_ buttonForExtension:extension];
253     return button && [owner_ browserActionClicked:button
254                                       shouldGrant:grant_active_tab];
255   }
256
257   virtual void ToolbarVisibleCountChanged() OVERRIDE {
258   }
259
260   virtual void ToolbarHighlightModeChanged(bool is_highlighting) OVERRIDE {
261   }
262
263   virtual Browser* GetBrowser() OVERRIDE {
264     return browser_;
265   }
266
267  private:
268   // The object we need to inform when we get a notification. Weak. Owns us.
269   BrowserActionsController* owner_;
270
271   // The browser we listen for events from. Weak.
272   Browser* browser_;
273
274   // Used for registering to receive notifications and automatic clean up.
275   content::NotificationRegistrar registrar_;
276
277   DISALLOW_COPY_AND_ASSIGN(ExtensionServiceObserverBridge);
278 };
279
280 @implementation BrowserActionsController
281
282 @synthesize containerView = containerView_;
283
284 #pragma mark -
285 #pragma mark Public Methods
286
287 - (id)initWithBrowser:(Browser*)browser
288         containerView:(BrowserActionsContainerView*)container {
289   DCHECK(browser && container);
290
291   if ((self = [super init])) {
292     browser_ = browser;
293     profile_ = browser->profile();
294
295     observer_.reset(new ExtensionServiceObserverBridge(self, browser_));
296     toolbarModel_ = extensions::ExtensionToolbarModel::Get(profile_);
297     if (toolbarModel_)
298       toolbarModel_->AddObserver(observer_.get());
299
300     containerView_ = container;
301     [containerView_ setPostsFrameChangedNotifications:YES];
302     [[NSNotificationCenter defaultCenter]
303         addObserver:self
304            selector:@selector(containerFrameChanged:)
305                name:NSViewFrameDidChangeNotification
306              object:containerView_];
307     [[NSNotificationCenter defaultCenter]
308         addObserver:self
309            selector:@selector(containerDragStart:)
310                name:kBrowserActionGrippyDragStartedNotification
311              object:containerView_];
312     [[NSNotificationCenter defaultCenter]
313         addObserver:self
314            selector:@selector(containerDragging:)
315                name:kBrowserActionGrippyDraggingNotification
316              object:containerView_];
317     [[NSNotificationCenter defaultCenter]
318         addObserver:self
319            selector:@selector(containerDragFinished:)
320                name:kBrowserActionGrippyDragFinishedNotification
321              object:containerView_];
322     // Listen for a finished drag from any button to make sure each open window
323     // stays in sync.
324     [[NSNotificationCenter defaultCenter]
325       addObserver:self
326          selector:@selector(actionButtonDragFinished:)
327              name:kBrowserActionButtonDragEndNotification
328            object:nil];
329
330     chevronAnimation_.reset([[NSViewAnimation alloc] init]);
331     [chevronAnimation_ gtm_setDuration:kAnimationDuration
332                              eventMask:NSLeftMouseUpMask];
333     [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
334
335     hiddenButtons_.reset([[NSMutableArray alloc] init]);
336     buttons_.reset([[NSMutableDictionary alloc] init]);
337     [self createButtons];
338     [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:NO];
339     [self updateGrippyCursors];
340     [container setResizable:!profile_->IsOffTheRecord()];
341   }
342
343   return self;
344 }
345
346 - (void)dealloc {
347   if (toolbarModel_)
348     toolbarModel_->RemoveObserver(observer_.get());
349
350   [[NSNotificationCenter defaultCenter] removeObserver:self];
351   [super dealloc];
352 }
353
354 - (void)update {
355   for (BrowserActionButton* button in [buttons_ allValues]) {
356     [button setTabId:[self currentTabId]];
357     [button updateState];
358   }
359 }
360
361 - (NSUInteger)buttonCount {
362   return [buttons_ count];
363 }
364
365 - (NSUInteger)visibleButtonCount {
366   return [self buttonCount] - [hiddenButtons_ count];
367 }
368
369 - (void)resizeContainerAndAnimate:(BOOL)animate {
370   int iconCount = toolbarModel_->GetVisibleIconCount();
371   if (iconCount < 0)  // If no buttons are hidden.
372     iconCount = [self buttonCount];
373
374   [containerView_ resizeToWidth:[self containerWidthWithButtonCount:iconCount]
375                         animate:animate];
376   NSRect frame = animate ? [containerView_ animationEndFrame] :
377                            [containerView_ frame];
378
379   [self showChevronIfNecessaryInFrame:frame animate:animate];
380
381   if (!animate) {
382     [[NSNotificationCenter defaultCenter]
383         postNotificationName:kBrowserActionVisibilityChangedNotification
384                       object:self];
385   }
386 }
387
388 - (NSView*)browserActionViewForExtension:(const Extension*)extension {
389   for (BrowserActionButton* button in [buttons_ allValues]) {
390     if ([button extension] == extension)
391       return button;
392   }
393   NOTREACHED();
394   return nil;
395 }
396
397 - (CGFloat)savedWidth {
398   if (!toolbarModel_)
399     return 0;
400
401   int savedButtonCount = toolbarModel_->GetVisibleIconCount();
402   if (savedButtonCount < 0 ||  // all icons are visible
403       static_cast<NSUInteger>(savedButtonCount) > [self buttonCount])
404     savedButtonCount = [self buttonCount];
405   return [self containerWidthWithButtonCount:savedButtonCount];
406 }
407
408 - (NSPoint)popupPointForBrowserAction:(const Extension*)extension {
409   if (!extensions::ExtensionActionManager::Get(profile_)->
410       GetBrowserAction(*extension)) {
411     return NSZeroPoint;
412   }
413
414   NSButton* button = [self buttonForExtension:extension];
415   if (!button)
416     return NSZeroPoint;
417
418   if ([hiddenButtons_ containsObject:button])
419     button = chevronMenuButton_.get();
420
421   // Anchor point just above the center of the bottom.
422   const NSRect bounds = [button bounds];
423   DCHECK([button isFlipped]);
424   NSPoint anchor = NSMakePoint(NSMidX(bounds),
425                                NSMaxY(bounds) - kBrowserActionBubbleYOffset);
426   return [button convertPoint:anchor toView:nil];
427 }
428
429 - (BOOL)chevronIsHidden {
430   if (!chevronMenuButton_.get())
431     return YES;
432
433   if (![chevronAnimation_ isAnimating])
434     return [chevronMenuButton_ isHidden];
435
436   DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
437
438   // The chevron is animating in or out. Determine which one and have the return
439   // value reflect where the animation is headed.
440   NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
441       valueForKey:NSViewAnimationEffectKey];
442   if (effect == NSViewAnimationFadeInEffect) {
443     return NO;
444   } else if (effect == NSViewAnimationFadeOutEffect) {
445     return YES;
446   }
447
448   NOTREACHED();
449   return YES;
450 }
451
452 - (void)activateBrowserAction:(const std::string&)extension_id {
453   const Extension* extension = extensions::ExtensionRegistry::Get(
454       browser_->profile())->enabled_extensions().GetByID(extension_id);
455   if (!extension)
456     return;
457
458   BrowserActionButton* button = [self buttonForExtension:extension];
459   // |button| can be nil when the browser action has its button hidden.
460   if (button)
461     [self browserActionClicked:button];
462 }
463
464 #pragma mark -
465 #pragma mark NSMenuDelegate
466
467 - (void)menuNeedsUpdate:(NSMenu*)menu {
468   [menu removeAllItems];
469
470   // See menu_button.h for documentation on why this is needed.
471   [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
472
473   for (BrowserActionButton* button in hiddenButtons_.get()) {
474     NSString* name = base::SysUTF8ToNSString([button extension]->name());
475     NSMenuItem* item =
476         [menu addItemWithTitle:name
477                         action:@selector(chevronItemSelected:)
478                  keyEquivalent:@""];
479     [item setRepresentedObject:button];
480     [item setImage:[button compositedImage]];
481     [item setTarget:self];
482     [item setEnabled:[button isEnabled]];
483   }
484 }
485
486 #pragma mark -
487 #pragma mark Private Methods
488
489 - (void)createButtons {
490   if (!toolbarModel_)
491     return;
492
493   NSUInteger i = 0;
494   for (ExtensionList::const_iterator iter =
495            toolbarModel_->toolbar_items().begin();
496        iter != toolbarModel_->toolbar_items().end(); ++iter) {
497     if (![self shouldDisplayBrowserAction:iter->get()])
498       continue;
499
500     [self createActionButtonForExtension:iter->get() withIndex:i++];
501   }
502
503   CGFloat width = [self savedWidth];
504   [containerView_ resizeToWidth:width animate:NO];
505 }
506
507 - (void)createActionButtonForExtension:(const Extension*)extension
508                              withIndex:(NSUInteger)index {
509   if (!extensions::ExtensionActionManager::Get(profile_)->
510       GetBrowserAction(*extension))
511     return;
512
513   if (![self shouldDisplayBrowserAction:extension])
514     return;
515
516   if (profile_->IsOffTheRecord())
517     index = toolbarModel_->OriginalIndexToIncognito(index);
518
519   // Show the container if it's the first button. Otherwise it will be shown
520   // already.
521   if ([self buttonCount] == 0)
522     [containerView_ setHidden:NO];
523
524   NSRect buttonFrame = NSMakeRect(0.0, kBrowserActionOriginYOffset,
525                                   kBrowserActionWidth, kBrowserActionHeight);
526   BrowserActionButton* newButton =
527       [[[BrowserActionButton alloc]
528          initWithFrame:buttonFrame
529              extension:extension
530                browser:browser_
531                  tabId:[self currentTabId]] autorelease];
532   [newButton setTarget:self];
533   [newButton setAction:@selector(browserActionClicked:)];
534   NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
535   if (!buttonKey)
536     return;
537   [buttons_ setObject:newButton forKey:buttonKey];
538
539   [self positionActionButtonsAndAnimate:NO];
540
541   [[NSNotificationCenter defaultCenter]
542       addObserver:self
543          selector:@selector(actionButtonDragging:)
544              name:kBrowserActionButtonDraggingNotification
545            object:newButton];
546
547
548   [containerView_ setMaxWidth:
549       [self containerWidthWithButtonCount:[self buttonCount]]];
550   [containerView_ setNeedsDisplay:YES];
551 }
552
553 - (void)removeActionButtonForExtension:(const Extension*)extension {
554   if (!extensions::ActionInfo::GetBrowserActionInfo(extension))
555     return;
556
557   NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
558   if (!buttonKey)
559     return;
560
561   BrowserActionButton* button = [buttons_ objectForKey:buttonKey];
562   // This could be the case in incognito, where only a subset of extensions are
563   // shown.
564   if (!button)
565     return;
566
567   [button removeFromSuperview];
568   // It may or may not be hidden, but it won't matter to NSMutableArray either
569   // way.
570   [hiddenButtons_ removeObject:button];
571
572   [buttons_ removeObjectForKey:buttonKey];
573   if ([self buttonCount] == 0) {
574     // No more buttons? Hide the container.
575     [containerView_ setHidden:YES];
576   } else {
577     [self positionActionButtonsAndAnimate:NO];
578   }
579   [containerView_ setMaxWidth:
580       [self containerWidthWithButtonCount:[self buttonCount]]];
581   [containerView_ setNeedsDisplay:YES];
582 }
583
584 - (void)positionActionButtonsAndAnimate:(BOOL)animate {
585   NSUInteger i = 0;
586   for (ExtensionList::const_iterator iter =
587            toolbarModel_->toolbar_items().begin();
588        iter != toolbarModel_->toolbar_items().end(); ++iter) {
589     if (![self shouldDisplayBrowserAction:iter->get()])
590       continue;
591     BrowserActionButton* button = [self buttonForExtension:(iter->get())];
592     if (!button)
593       continue;
594     if (![button isBeingDragged])
595       [self moveButton:button toIndex:i animate:animate];
596     ++i;
597   }
598 }
599
600 - (void)updateButtonOpacity {
601   for (BrowserActionButton* button in [buttons_ allValues]) {
602     NSRect buttonFrame = [button frame];
603     if (NSContainsRect([containerView_ bounds], buttonFrame)) {
604       if ([button alphaValue] != 1.0)
605         [button setAlphaValue:1.0];
606
607       continue;
608     }
609     CGFloat intersectionWidth =
610         NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
611     CGFloat alpha = std::max(static_cast<CGFloat>(0.0),
612                              intersectionWidth / NSWidth(buttonFrame));
613     [button setAlphaValue:alpha];
614     [button setNeedsDisplay:YES];
615   }
616 }
617
618 - (BrowserActionButton*)buttonForExtension:(const Extension*)extension {
619   NSString* extensionId = base::SysUTF8ToNSString(extension->id());
620   DCHECK(extensionId);
621   if (!extensionId)
622     return nil;
623   return [buttons_ objectForKey:extensionId];
624 }
625
626 - (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount {
627   // Left-side padding which works regardless of whether a button or
628   // chevron leads.
629   CGFloat width = kBrowserActionLeftPadding;
630
631   // Include the buttons and padding between.
632   if (buttonCount > 0) {
633     width += buttonCount * kBrowserActionWidth;
634     width += (buttonCount - 1) * kBrowserActionButtonPadding;
635   }
636
637   // Make room for the chevron if any buttons are hidden.
638   if ([self buttonCount] != [self visibleButtonCount]) {
639     // Chevron and buttons both include 1px padding w/in their bounds,
640     // so this leaves 2px between the last browser action and chevron,
641     // and also works right if the chevron is the only button.
642     width += kChevronWidth;
643   }
644
645   return width;
646 }
647
648 - (NSUInteger)containerButtonCapacity {
649   // Edge-to-edge span of the browser action buttons.
650   CGFloat actionSpan = [self savedWidth] - kBrowserActionLeftPadding;
651
652   // Add in some padding for the browser action on the end, then
653   // divide out to get the number of action buttons that fit.
654   return (actionSpan + kBrowserActionButtonPadding) /
655       (kBrowserActionWidth + kBrowserActionButtonPadding);
656 }
657
658 - (void)containerFrameChanged:(NSNotification*)notification {
659   [self updateButtonOpacity];
660   [[containerView_ window] invalidateCursorRectsForView:containerView_];
661   [self updateChevronPositionInFrame:[containerView_ frame]];
662 }
663
664 - (void)containerDragStart:(NSNotification*)notification {
665   [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
666   while([hiddenButtons_ count] > 0) {
667     [containerView_ addSubview:[hiddenButtons_ objectAtIndex:0]];
668     [hiddenButtons_ removeObjectAtIndex:0];
669   }
670 }
671
672 - (void)containerDragging:(NSNotification*)notification {
673   [[NSNotificationCenter defaultCenter]
674       postNotificationName:kBrowserActionGrippyDraggingNotification
675                     object:self];
676 }
677
678 - (void)containerDragFinished:(NSNotification*)notification {
679   for (ExtensionList::const_iterator iter =
680            toolbarModel_->toolbar_items().begin();
681        iter != toolbarModel_->toolbar_items().end(); ++iter) {
682     BrowserActionButton* button = [self buttonForExtension:(iter->get())];
683     NSRect buttonFrame = [button frame];
684     if (NSContainsRect([containerView_ bounds], buttonFrame))
685       continue;
686
687     CGFloat intersectionWidth =
688         NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
689     // Pad the threshold by 5 pixels in order to have the buttons hide more
690     // easily.
691     if (([containerView_ grippyPinned] && intersectionWidth > 0) ||
692         (intersectionWidth <= (NSWidth(buttonFrame) / 2) + 5.0)) {
693       [button setAlphaValue:0.0];
694       [button removeFromSuperview];
695       [hiddenButtons_ addObject:button];
696     }
697   }
698   [self updateGrippyCursors];
699
700   if (!profile_->IsOffTheRecord())
701     toolbarModel_->SetVisibleIconCount([self visibleButtonCount]);
702
703   [[NSNotificationCenter defaultCenter]
704       postNotificationName:kBrowserActionGrippyDragFinishedNotification
705                     object:self];
706 }
707
708 - (void)actionButtonDragging:(NSNotification*)notification {
709   if (![self chevronIsHidden])
710     [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
711
712   // Determine what index the dragged button should lie in, alter the model and
713   // reposition the buttons.
714   CGFloat dragThreshold = std::floor(kBrowserActionWidth / 2);
715   BrowserActionButton* draggedButton = [notification object];
716   NSRect draggedButtonFrame = [draggedButton frame];
717
718   NSUInteger index = 0;
719   for (ExtensionList::const_iterator iter =
720            toolbarModel_->toolbar_items().begin();
721        iter != toolbarModel_->toolbar_items().end(); ++iter) {
722     BrowserActionButton* button = [self buttonForExtension:(iter->get())];
723     CGFloat intersectionWidth =
724         NSWidth(NSIntersectionRect(draggedButtonFrame, [button frame]));
725
726     if (intersectionWidth > dragThreshold && button != draggedButton &&
727         ![button isAnimating] && index < [self visibleButtonCount]) {
728       toolbarModel_->MoveExtensionIcon([draggedButton extension], index);
729       [self positionActionButtonsAndAnimate:YES];
730       return;
731     }
732     ++index;
733   }
734 }
735
736 - (void)actionButtonDragFinished:(NSNotification*)notification {
737   [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:YES];
738   [self positionActionButtonsAndAnimate:YES];
739 }
740
741 - (void)moveButton:(BrowserActionButton*)button
742            toIndex:(NSUInteger)index
743            animate:(BOOL)animate {
744   CGFloat xOffset = kBrowserActionLeftPadding +
745       (index * (kBrowserActionWidth + kBrowserActionButtonPadding));
746   NSRect buttonFrame = [button frame];
747   buttonFrame.origin.x = xOffset;
748   [button setFrame:buttonFrame animate:animate];
749
750   if (index < [self containerButtonCapacity]) {
751     // Make sure the button is within the visible container.
752     if ([button superview] != containerView_) {
753       [containerView_ addSubview:button];
754       [button setAlphaValue:1.0];
755       [hiddenButtons_ removeObjectIdenticalTo:button];
756     }
757   } else if (![hiddenButtons_ containsObject:button]) {
758     [hiddenButtons_ addObject:button];
759     [button removeFromSuperview];
760     [button setAlphaValue:0.0];
761   }
762 }
763
764 - (BOOL)browserActionClicked:(BrowserActionButton*)button
765                  shouldGrant:(BOOL)shouldGrant {
766   const Extension* extension = [button extension];
767   switch (extensions::ExtensionActionAPI::Get(profile_)->ExecuteExtensionAction(
768               extension, browser_, shouldGrant)) {
769     case ExtensionAction::ACTION_NONE:
770       break;
771     case ExtensionAction::ACTION_SHOW_POPUP: {
772       GURL popupUrl = extensions::ExtensionActionManager::Get(profile_)->
773           GetBrowserAction(*extension)->GetPopupUrl([self currentTabId]);
774       NSPoint arrowPoint = [self popupPointForBrowserAction:extension];
775       [ExtensionPopupController showURL:popupUrl
776                               inBrowser:browser_
777                              anchoredAt:arrowPoint
778                           arrowLocation:info_bubble::kTopRight
779                                 devMode:NO];
780       return YES;
781     }
782   }
783   return NO;
784 }
785
786 - (BOOL)browserActionClicked:(BrowserActionButton*)button {
787   return [self browserActionClicked:button
788                         shouldGrant:YES];
789 }
790
791 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension {
792   // Only display incognito-enabled extensions while in incognito mode.
793   return !profile_->IsOffTheRecord() ||
794       extensions::util::IsIncognitoEnabled(extension->id(), profile_);
795 }
796
797 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate {
798   [self setChevronHidden:([self buttonCount] == [self visibleButtonCount])
799                  inFrame:frame
800                  animate:animate];
801 }
802
803 - (void)updateChevronPositionInFrame:(NSRect)frame {
804   CGFloat xPos = NSWidth(frame) - kChevronWidth;
805   NSRect buttonFrame = NSMakeRect(xPos,
806                                   kBrowserActionOriginYOffset,
807                                   kChevronWidth,
808                                   kBrowserActionHeight);
809   [chevronMenuButton_ setFrame:buttonFrame];
810 }
811
812 - (void)setChevronHidden:(BOOL)hidden
813                  inFrame:(NSRect)frame
814                  animate:(BOOL)animate {
815   if (hidden == [self chevronIsHidden])
816     return;
817
818   if (!chevronMenuButton_.get()) {
819     chevronMenuButton_.reset([[MenuButton alloc] init]);
820     [chevronMenuButton_ setOpenMenuOnClick:YES];
821     [chevronMenuButton_ setBordered:NO];
822     [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
823
824     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW
825                            forButtonState:image_button_cell::kDefaultState];
826     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_H
827                            forButtonState:image_button_cell::kHoverState];
828     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_P
829                            forButtonState:image_button_cell::kPressedState];
830
831     overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
832     [overflowMenu_ setAutoenablesItems:NO];
833     [overflowMenu_ setDelegate:self];
834     [chevronMenuButton_ setAttachedMenu:overflowMenu_];
835
836     [containerView_ addSubview:chevronMenuButton_];
837   }
838
839   [self updateChevronPositionInFrame:frame];
840
841   // Stop any running animation.
842   [chevronAnimation_ stopAnimation];
843
844   if (!animate) {
845     [chevronMenuButton_ setHidden:hidden];
846     return;
847   }
848
849   NSDictionary* animationDictionary;
850   if (hidden) {
851     animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
852         chevronMenuButton_.get(), NSViewAnimationTargetKey,
853         NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey,
854         nil];
855   } else {
856     [chevronMenuButton_ setHidden:NO];
857     animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
858         chevronMenuButton_.get(), NSViewAnimationTargetKey,
859         NSViewAnimationFadeInEffect, NSViewAnimationEffectKey,
860         nil];
861   }
862   [chevronAnimation_ setViewAnimations:
863       [NSArray arrayWithObject:animationDictionary]];
864   [chevronAnimation_ startAnimation];
865 }
866
867 - (void)chevronItemSelected:(id)menuItem {
868   [self browserActionClicked:[menuItem representedObject]];
869 }
870
871 - (void)updateGrippyCursors {
872   [containerView_ setCanDragLeft:[hiddenButtons_ count] > 0];
873   [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
874   [[containerView_ window] invalidateCursorRectsForView:containerView_];
875 }
876
877 - (int)currentTabId {
878   content::WebContents* active_tab =
879       browser_->tab_strip_model()->GetActiveWebContents();
880   if (!active_tab)
881     return -1;
882
883   return SessionTabHelper::FromWebContents(active_tab)->session_id().id();
884 }
885
886 #pragma mark -
887 #pragma mark Testing Methods
888
889 - (NSButton*)buttonWithIndex:(NSUInteger)index {
890   if (profile_->IsOffTheRecord())
891     index = toolbarModel_->IncognitoIndexToOriginal(index);
892   const extensions::ExtensionList& toolbar_items =
893       toolbarModel_->toolbar_items();
894   if (index < toolbar_items.size()) {
895     const Extension* extension = toolbar_items[index].get();
896     return [buttons_ objectForKey:base::SysUTF8ToNSString(extension->id())];
897   }
898   return nil;
899 }
900
901 @end