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