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