summaryrefslogtreecommitdiffstats
path: root/chrome/browser/cocoa/extensions/browser_actions_controller.mm
diff options
context:
space:
mode:
Diffstat (limited to 'chrome/browser/cocoa/extensions/browser_actions_controller.mm')
-rw-r--r--chrome/browser/cocoa/extensions/browser_actions_controller.mm838
1 files changed, 838 insertions, 0 deletions
diff --git a/chrome/browser/cocoa/extensions/browser_actions_controller.mm b/chrome/browser/cocoa/extensions/browser_actions_controller.mm
new file mode 100644
index 0000000..19a1d56
--- /dev/null
+++ b/chrome/browser/cocoa/extensions/browser_actions_controller.mm
@@ -0,0 +1,838 @@
+// Copyright (c) 2010 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import "browser_actions_controller.h"
+
+#include <cmath>
+#include <string>
+
+#include "app/resource_bundle.h"
+#include "base/sys_string_conversions.h"
+#include "chrome/browser/browser.h"
+#include "chrome/browser/pref_service.h"
+#import "chrome/browser/cocoa/extensions/browser_action_button.h"
+#import "chrome/browser/cocoa/extensions/browser_actions_container_view.h"
+#import "chrome/browser/cocoa/extensions/extension_popup_controller.h"
+#import "chrome/browser/cocoa/menu_button.h"
+#include "chrome/browser/extensions/extension_browser_event_router.h"
+#include "chrome/browser/extensions/extension_host.h"
+#include "chrome/browser/extensions/extension_toolbar_model.h"
+#include "chrome/browser/extensions/extensions_service.h"
+#include "chrome/browser/profile.h"
+#include "chrome/browser/tab_contents/tab_contents.h"
+#include "chrome/common/extensions/extension_action.h"
+#include "chrome/common/notification_observer.h"
+#include "chrome/common/notification_registrar.h"
+#include "chrome/common/pref_names.h"
+#include "grit/theme_resources.h"
+#import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
+
+const CGFloat kBrowserActionButtonPadding = 3;
+
+NSString* const kBrowserActionVisibilityChangedNotification =
+ @"BrowserActionVisibilityChangedNotification";
+
+namespace {
+const CGFloat kAnimationDuration = 0.2;
+// When determining the opacity during a drag, we artificially reduce the
+// distance to the edge in order to make the fade more apparent.
+const CGFloat kButtonOpacityLeadPadding = 5.0;
+const CGFloat kChevronHeight = 28.0;
+const CGFloat kChevronLowerPadding = 5.0;
+const CGFloat kChevronRightPadding = 5.0;
+const CGFloat kChevronWidth = 14.0;
+const CGFloat kGrippyXOffset = 5.0;
+} // namespace
+
+@interface BrowserActionsController(Private)
+// Used during initialization to create the BrowserActionButton objects from the
+// stored toolbar model.
+- (void)createButtons;
+
+// Creates and then adds the given extension's action button to the container
+// at the given index within the container. It does not affect the toolbar model
+// object since it is called when the toolbar model changes.
+- (void)createActionButtonForExtension:(Extension*)extension
+ withIndex:(NSUInteger)index;
+
+// Removes an action button for the given extension from the container. This
+// method also does not affect the underlying toolbar model since it is called
+// when the toolbar model changes.
+- (void)removeActionButtonForExtension:(Extension*)extension;
+
+// Useful in the case of a Browser Action being added/removed from the middle of
+// the container, this method repositions each button according to the current
+// toolbar model.
+- (void)positionActionButtonsAndAnimate:(BOOL)animate;
+
+// During container resizing, buttons become more transparent as they are pushed
+// off the screen. This method updates each button's opacity determined by the
+// position of the button.
+- (void)updateButtonOpacity;
+
+// Returns the existing button with the given extension backing it; nil if it
+// cannot be found or the extension's ID is invalid.
+- (BrowserActionButton*)buttonForExtension:(Extension*)extension;
+
+// Returns the preferred width of the container given the number of visible
+// buttons |buttonCount|.
+- (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount;
+
+// Returns the number of buttons that can fit in the container according to its
+// current size.
+- (NSUInteger)containerButtonCapacity;
+
+// Notification handlers for events registered by the class.
+
+// Updates each button's opacity, the cursor rects and chevron position.
+- (void)containerFrameChanged:(NSNotification*)notification;
+
+// Hides the chevron and unhides every hidden button so that dragging the
+// container out smoothly shows the Browser Action buttons.
+- (void)containerDragStart:(NSNotification*)notification;
+
+// Sends a notification for the toolbar to reposition surrounding UI elements.
+- (void)containerDragging:(NSNotification*)notification;
+
+// Determines which buttons need to be hidden based on the new size, hides them
+// and updates the chevron overflow menu. Also fires a notification to let the
+// toolbar know that the drag has finished.
+- (void)containerDragFinished:(NSNotification*)notification;
+
+// Updates the image associated with the button should it be within the chevron
+// menu.
+- (void)actionButtonUpdated:(NSNotification*)notification;
+
+// Adjusts the position of the surrounding action buttons depending on where the
+// button is within the container.
+- (void)actionButtonDragging:(NSNotification*)notification;
+
+// Updates the position of the Browser Actions within the container. This fires
+// when _any_ Browser Action button is done dragging to keep all open windows in
+// sync visually.
+- (void)actionButtonDragFinished:(NSNotification*)notification;
+
+// Moves the given button both visually and within the toolbar model to the
+// specified index.
+- (void)moveButton:(BrowserActionButton*)button
+ toIndex:(NSUInteger)index
+ animate:(BOOL)animate;
+
+// Handles when the given BrowserActionButton object is clicked.
+- (void)browserActionClicked:(BrowserActionButton*)button;
+
+// Returns whether the given extension should be displayed. Only displays
+// incognito-enabled extensions in incognito mode. Otherwise returns YES.
+- (BOOL)shouldDisplayBrowserAction:(Extension*)extension;
+
+// The reason |frame| is specified in these chevron functions is because the
+// container may be animating and the end frame of the animation should be
+// passed instead of the current frame (which may be off and cause the chevron
+// to jump at the end of its animation).
+
+// Shows the overflow chevron button depending on whether there are any hidden
+// extensions within the frame given.
+- (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate;
+
+// Moves the chevron to its correct position within |frame|.
+- (void)updateChevronPositionInFrame:(NSRect)frame;
+
+// Shows or hides the chevron, animating as specified by |animate|.
+- (void)setChevronHidden:(BOOL)hidden
+ inFrame:(NSRect)frame
+ animate:(BOOL)animate;
+
+// Handles when a menu item within the chevron overflow menu is selected.
+- (void)chevronItemSelected:(id)menuItem;
+
+// Clears and then populates the overflow menu based on the contents of
+// |hiddenButtons_|.
+- (void)updateOverflowMenu;
+
+// Updates the container's grippy cursor based on the number of hidden buttons.
+- (void)updateGrippyCursors;
+
+// Returns the ID of the currently selected tab or -1 if none exists.
+- (int)currentTabId;
+@end
+
+// A helper class to proxy extension notifications to the view controller's
+// appropriate methods.
+class ExtensionsServiceObserverBridge : public NotificationObserver,
+ public ExtensionToolbarModel::Observer {
+ public:
+ ExtensionsServiceObserverBridge(BrowserActionsController* owner,
+ Profile* profile) : owner_(owner) {
+ registrar_.Add(this, NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE,
+ Source<Profile>(profile));
+ }
+
+ // Overridden from NotificationObserver.
+ void Observe(NotificationType type,
+ const NotificationSource& source,
+ const NotificationDetails& details) {
+ switch (type.value) {
+ case NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE: {
+ ExtensionPopupController* popup = [ExtensionPopupController popup];
+ if (popup && ![popup isClosing])
+ [popup close];
+
+ break;
+ }
+ default:
+ NOTREACHED() << L"Unexpected notification";
+ }
+ }
+
+ // ExtensionToolbarModel::Observer implementation.
+ void BrowserActionAdded(Extension* extension, int index) {
+ [owner_ createActionButtonForExtension:extension withIndex:index];
+ [owner_ resizeContainerAndAnimate:NO];
+ }
+
+ void BrowserActionRemoved(Extension* extension) {
+ [owner_ removeActionButtonForExtension:extension];
+ [owner_ resizeContainerAndAnimate:NO];
+ }
+
+ private:
+ // The object we need to inform when we get a notification. Weak. Owns us.
+ BrowserActionsController* owner_;
+
+ // Used for registering to receive notifications and automatic clean up.
+ NotificationRegistrar registrar_;
+
+ DISALLOW_COPY_AND_ASSIGN(ExtensionsServiceObserverBridge);
+};
+
+@implementation BrowserActionsController
+
+@synthesize containerView = containerView_;
+
+#pragma mark -
+#pragma mark Public Methods
+
+- (id)initWithBrowser:(Browser*)browser
+ containerView:(BrowserActionsContainerView*)container {
+ DCHECK(browser && container);
+
+ if ((self = [super init])) {
+ browser_ = browser;
+ profile_ = browser->profile();
+
+ if (!profile_->GetPrefs()->FindPreference(
+ prefs::kBrowserActionContainerWidth))
+ [BrowserActionsController registerUserPrefs:profile_->GetPrefs()];
+
+ observer_.reset(new ExtensionsServiceObserverBridge(self, profile_));
+ ExtensionsService* extensionsService = profile_->GetExtensionsService();
+ // |extensionsService| can be NULL in Incognito.
+ if (extensionsService) {
+ toolbarModel_ = extensionsService->toolbar_model();
+ toolbarModel_->AddObserver(observer_.get());
+ }
+
+ containerView_ = container;
+ [containerView_ setPostsFrameChangedNotifications:YES];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(containerFrameChanged:)
+ name:NSViewFrameDidChangeNotification
+ object:containerView_];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(containerDragStart:)
+ name:kBrowserActionGrippyDragStartedNotification
+ object:containerView_];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(containerDragging:)
+ name:kBrowserActionGrippyDraggingNotification
+ object:containerView_];
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(containerDragFinished:)
+ name:kBrowserActionGrippyDragFinishedNotification
+ object:containerView_];
+ // Listen for a finished drag from any button to make sure each open window
+ // stays in sync.
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(actionButtonDragFinished:)
+ name:kBrowserActionButtonDragEndNotification
+ object:nil];
+
+ chevronAnimation_.reset([[NSViewAnimation alloc] init]);
+ [chevronAnimation_ gtm_setDuration:kAnimationDuration
+ eventMask:NSLeftMouseUpMask];
+ [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
+
+ hiddenButtons_.reset([[NSMutableArray alloc] init]);
+ buttons_.reset([[NSMutableDictionary alloc] init]);
+ [self createButtons];
+ [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:NO];
+ [self updateGrippyCursors];
+ [container setResizable:!profile_->IsOffTheRecord()];
+ }
+
+ return self;
+}
+
+- (void)dealloc {
+ if (toolbarModel_)
+ toolbarModel_->RemoveObserver(observer_.get());
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+ [super dealloc];
+}
+
+- (void)update {
+ for (BrowserActionButton* button in [buttons_ allValues]) {
+ [button setTabId:[self currentTabId]];
+ [button updateState];
+ }
+}
+
+- (NSUInteger)buttonCount {
+ return [buttons_ count];
+}
+
+- (NSUInteger)visibleButtonCount {
+ return [self buttonCount] - [hiddenButtons_ count];
+}
+
+- (MenuButton*)chevronMenuButton {
+ return chevronMenuButton_.get();
+}
+
+- (void)resizeContainerAndAnimate:(BOOL)animate {
+ int iconCount = toolbarModel_->GetVisibleIconCount();
+ if (iconCount < 0) // If no buttons are hidden.
+ iconCount = [self buttonCount];
+
+ [containerView_ resizeToWidth:[self containerWidthWithButtonCount:iconCount]
+ animate:animate];
+ NSRect frame = animate ? [containerView_ animationEndFrame] :
+ [containerView_ frame];
+
+ [self showChevronIfNecessaryInFrame:frame animate:animate];
+
+ if (!animate) {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionVisibilityChangedNotification
+ object:self];
+ }
+}
+
+- (NSView*)browserActionViewForExtension:(Extension*)extension {
+ for (BrowserActionButton* button in [buttons_ allValues]) {
+ if ([button extension] == extension)
+ return button;
+ }
+ NOTREACHED();
+ return nil;
+}
+
+- (CGFloat)savedWidth {
+ if (!toolbarModel_)
+ return 0;
+ if (!profile_->GetPrefs()->HasPrefPath(prefs::kExtensionToolbarSize)) {
+ // Migration code to the new VisibleIconCount pref.
+ // TODO(mpcomplete): remove this at some point.
+ double predefinedWidth =
+ profile_->GetPrefs()->GetReal(prefs::kBrowserActionContainerWidth);
+ if (predefinedWidth != 0) {
+ int iconWidth = kBrowserActionWidth + kBrowserActionButtonPadding;
+ int extraWidth = kChevronWidth;
+ toolbarModel_->SetVisibleIconCount(
+ (predefinedWidth - extraWidth) / iconWidth);
+ }
+ }
+
+ int savedButtonCount = toolbarModel_->GetVisibleIconCount();
+ if (savedButtonCount < 0 || // all icons are visible
+ static_cast<NSUInteger>(savedButtonCount) > [self buttonCount])
+ savedButtonCount = [self buttonCount];
+ return [self containerWidthWithButtonCount:savedButtonCount];
+}
+
+- (NSPoint)popupPointForBrowserAction:(Extension*)extension {
+ if (!extension->browser_action())
+ return NSZeroPoint;
+ BrowserActionButton* button = [self buttonForExtension:extension];
+ if (!button)
+ return NSZeroPoint;
+
+ NSView* view = button;
+ BOOL isHidden = [hiddenButtons_ containsObject:button];
+ if (isHidden)
+ view = chevronMenuButton_.get();
+
+ NSPoint arrowPoint = [view frame].origin;
+ // Adjust the anchor point to be at the center of the browser action button
+ // or chevron.
+ arrowPoint.x += NSWidth([view frame]) / 2;
+ // Move the arrow up a bit in the case that it's pointing to the chevron.
+ if (isHidden)
+ arrowPoint.y += NSHeight([view frame]) / 4;
+
+ return [[view superview] convertPoint:arrowPoint toView:nil];
+}
+
+- (BOOL)chevronIsHidden {
+ if (!chevronMenuButton_.get())
+ return YES;
+
+ if (![chevronAnimation_ isAnimating])
+ return [chevronMenuButton_ isHidden];
+
+ DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
+
+ // The chevron is animating in or out. Determine which one and have the return
+ // value reflect where the animation is headed.
+ NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
+ valueForKey:NSViewAnimationEffectKey];
+ if (effect == NSViewAnimationFadeInEffect) {
+ return NO;
+ } else if (effect == NSViewAnimationFadeOutEffect) {
+ return YES;
+ }
+
+ NOTREACHED();
+ return YES;
+}
+
++ (void)registerUserPrefs:(PrefService*)prefs {
+ prefs->RegisterRealPref(prefs::kBrowserActionContainerWidth, 0);
+}
+
+#pragma mark -
+#pragma mark Private Methods
+
+- (void)createButtons {
+ if (!toolbarModel_)
+ return;
+
+ NSUInteger i = 0;
+ for (ExtensionList::iterator iter = toolbarModel_->begin();
+ iter != toolbarModel_->end(); ++iter) {
+ if (![self shouldDisplayBrowserAction:*iter])
+ continue;
+
+ [self createActionButtonForExtension:*iter withIndex:i++];
+ }
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(actionButtonUpdated:)
+ name:kBrowserActionButtonUpdatedNotification
+ object:nil];
+
+ CGFloat width = [self savedWidth];
+ [containerView_ resizeToWidth:width animate:NO];
+}
+
+- (void)createActionButtonForExtension:(Extension*)extension
+ withIndex:(NSUInteger)index {
+ if (!extension->browser_action())
+ return;
+
+ if (![self shouldDisplayBrowserAction:extension])
+ return;
+
+ if (profile_->IsOffTheRecord())
+ index = toolbarModel_->OriginalIndexToIncognito(index);
+
+ // Show the container if it's the first button. Otherwise it will be shown
+ // already.
+ if ([self buttonCount] == 0)
+ [containerView_ setHidden:NO];
+
+ BrowserActionButton* newButton = [[[BrowserActionButton alloc]
+ initWithExtension:extension
+ profile:profile_
+ tabId:[self currentTabId]] autorelease];
+ [newButton setTarget:self];
+ [newButton setAction:@selector(browserActionClicked:)];
+ NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
+ if (!buttonKey)
+ return;
+ [buttons_ setObject:newButton forKey:buttonKey];
+
+ [self positionActionButtonsAndAnimate:NO];
+
+ [[NSNotificationCenter defaultCenter]
+ addObserver:self
+ selector:@selector(actionButtonDragging:)
+ name:kBrowserActionButtonDraggingNotification
+ object:newButton];
+
+
+ [containerView_ setMaxWidth:
+ [self containerWidthWithButtonCount:[self buttonCount]]];
+ [containerView_ setNeedsDisplay:YES];
+}
+
+- (void)removeActionButtonForExtension:(Extension*)extension {
+ if (!extension->browser_action())
+ return;
+
+ NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
+ if (!buttonKey)
+ return;
+
+ BrowserActionButton* button = [buttons_ objectForKey:buttonKey];
+ // This could be the case in incognito, where only a subset of extensions are
+ // shown.
+ if (!button)
+ return;
+
+ [button removeFromSuperview];
+ // It may or may not be hidden, but it won't matter to NSMutableArray either
+ // way.
+ [hiddenButtons_ removeObject:button];
+ [self updateOverflowMenu];
+
+ [buttons_ removeObjectForKey:buttonKey];
+ if ([self buttonCount] == 0) {
+ // No more buttons? Hide the container.
+ [containerView_ setHidden:YES];
+ } else {
+ [self positionActionButtonsAndAnimate:NO];
+ }
+ [containerView_ setMaxWidth:
+ [self containerWidthWithButtonCount:[self buttonCount]]];
+ [containerView_ setNeedsDisplay:YES];
+}
+
+- (void)positionActionButtonsAndAnimate:(BOOL)animate {
+ NSUInteger i = 0;
+ for (ExtensionList::iterator iter = toolbarModel_->begin();
+ iter != toolbarModel_->end(); ++iter) {
+ if (![self shouldDisplayBrowserAction:*iter])
+ continue;
+ BrowserActionButton* button = [self buttonForExtension:(*iter)];
+ if (!button)
+ continue;
+ if (![button isBeingDragged])
+ [self moveButton:button toIndex:i animate:animate];
+ ++i;
+ }
+}
+
+- (void)updateButtonOpacity {
+ for (BrowserActionButton* button in [buttons_ allValues]) {
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.x += kButtonOpacityLeadPadding;
+ if (NSContainsRect([containerView_ bounds], buttonFrame)) {
+ if ([button alphaValue] != 1.0)
+ [button setAlphaValue:1.0];
+
+ continue;
+ }
+ CGFloat intersectionWidth =
+ NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
+ CGFloat alpha = std::max(0.0f,
+ (intersectionWidth - kButtonOpacityLeadPadding) / NSWidth(buttonFrame));
+ [button setAlphaValue:alpha];
+ [button setNeedsDisplay:YES];
+ }
+}
+
+- (BrowserActionButton*)buttonForExtension:(Extension*)extension {
+ NSString* extensionId = base::SysUTF8ToNSString(extension->id());
+ DCHECK(extensionId);
+ if (!extensionId)
+ return nil;
+ return [buttons_ objectForKey:extensionId];
+}
+
+- (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount {
+ CGFloat width = 0.0;
+ if (buttonCount > 0) {
+ width = kGrippyXOffset + (2 * kBrowserActionButtonPadding) +
+ (buttonCount * (kBrowserActionWidth + kBrowserActionButtonPadding));
+ }
+ // Make room for the chevron if any buttons are hidden.
+ if ([self buttonCount] != [self visibleButtonCount]) {
+ width += kChevronWidth + kBrowserActionButtonPadding;
+ // Add more space if all buttons are hidden.
+ if ([self visibleButtonCount] == 0)
+ width += 3 * kBrowserActionButtonPadding;
+ }
+
+ return width;
+}
+
+- (NSUInteger)containerButtonCapacity {
+ CGFloat containerWidth = [self savedWidth];
+ return (containerWidth - kGrippyXOffset) /
+ (kBrowserActionWidth + kBrowserActionButtonPadding);
+}
+
+- (void)containerFrameChanged:(NSNotification*)notification {
+ [self updateButtonOpacity];
+ [[containerView_ window] invalidateCursorRectsForView:containerView_];
+ [self updateChevronPositionInFrame:[containerView_ frame]];
+}
+
+- (void)containerDragStart:(NSNotification*)notification {
+ [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
+ while([hiddenButtons_ count] > 0) {
+ [containerView_ addSubview:[hiddenButtons_ objectAtIndex:0]];
+ [hiddenButtons_ removeObjectAtIndex:0];
+ }
+}
+
+- (void)containerDragging:(NSNotification*)notification {
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionGrippyDraggingNotification
+ object:self];
+}
+
+- (void)containerDragFinished:(NSNotification*)notification {
+ for (ExtensionList::iterator iter = toolbarModel_->begin();
+ iter != toolbarModel_->end(); ++iter) {
+ BrowserActionButton* button = [self buttonForExtension:(*iter)];
+ NSRect buttonFrame = [button frame];
+ if (NSContainsRect([containerView_ bounds], buttonFrame))
+ continue;
+
+ CGFloat intersectionWidth =
+ NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
+ // Pad the threshold by 5 pixels in order to have the buttons hide more
+ // easily.
+ if (([containerView_ grippyPinned] && intersectionWidth > 0) ||
+ (intersectionWidth <= (NSWidth(buttonFrame) / 2) + 5.0)) {
+ [button setAlphaValue:0.0];
+ [button removeFromSuperview];
+ [hiddenButtons_ addObject:button];
+ }
+ }
+ [self updateOverflowMenu];
+ [self updateGrippyCursors];
+
+ if (!profile_->IsOffTheRecord())
+ toolbarModel_->SetVisibleIconCount([self visibleButtonCount]);
+
+ [[NSNotificationCenter defaultCenter]
+ postNotificationName:kBrowserActionGrippyDragFinishedNotification
+ object:self];
+}
+
+- (void)actionButtonUpdated:(NSNotification*)notification {
+ BrowserActionButton* button = [notification object];
+ if (![hiddenButtons_ containsObject:button])
+ return;
+
+ // +1 item because of the title placeholder. See |updateOverflowMenu|.
+ NSUInteger menuIndex = [hiddenButtons_ indexOfObject:button] + 1;
+ NSMenuItem* item = [[chevronMenuButton_ attachedMenu] itemAtIndex:menuIndex];
+ DCHECK(button == [item representedObject]);
+ [item setImage:[button compositedImage]];
+}
+
+- (void)actionButtonDragging:(NSNotification*)notification {
+ if (![self chevronIsHidden])
+ [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
+
+ // Determine what index the dragged button should lie in, alter the model and
+ // reposition the buttons.
+ CGFloat dragThreshold = std::floor(kBrowserActionWidth / 2);
+ BrowserActionButton* draggedButton = [notification object];
+ NSRect draggedButtonFrame = [draggedButton frame];
+
+ NSUInteger index = 0;
+ for (ExtensionList::iterator iter = toolbarModel_->begin();
+ iter != toolbarModel_->end(); ++iter) {
+ BrowserActionButton* button = [self buttonForExtension:(*iter)];
+ CGFloat intersectionWidth =
+ NSWidth(NSIntersectionRect(draggedButtonFrame, [button frame]));
+
+ if (intersectionWidth > dragThreshold && button != draggedButton &&
+ ![button isAnimating] && index < [self visibleButtonCount]) {
+ toolbarModel_->MoveBrowserAction([draggedButton extension], index);
+ [self positionActionButtonsAndAnimate:YES];
+ return;
+ }
+ ++index;
+ }
+}
+
+- (void)actionButtonDragFinished:(NSNotification*)notification {
+ [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:YES];
+ [self positionActionButtonsAndAnimate:YES];
+}
+
+- (void)moveButton:(BrowserActionButton*)button
+ toIndex:(NSUInteger)index
+ animate:(BOOL)animate {
+ CGFloat xOffset = kGrippyXOffset +
+ (index * (kBrowserActionWidth + kBrowserActionButtonPadding));
+ NSRect buttonFrame = [button frame];
+ buttonFrame.origin.x = xOffset;
+ [button setFrame:buttonFrame animate:animate];
+
+ if (index < [self containerButtonCapacity]) {
+ // Make sure the button is within the visible container.
+ if ([button superview] != containerView_) {
+ [containerView_ addSubview:button];
+ [button setAlphaValue:1.0];
+ [hiddenButtons_ removeObjectIdenticalTo:button];
+ }
+ } else if (![hiddenButtons_ containsObject:button]) {
+ [hiddenButtons_ addObject:button];
+ [button removeFromSuperview];
+ [button setAlphaValue:0.0];
+ [self updateOverflowMenu];
+ }
+}
+
+- (void)browserActionClicked:(BrowserActionButton*)button {
+ int tabId = [self currentTabId];
+ if (tabId < 0) {
+ NOTREACHED() << "No current tab.";
+ return;
+ }
+
+ ExtensionAction* action = [button extension]->browser_action();
+ if (action->HasPopup(tabId)) {
+ GURL popupUrl = action->GetPopupUrl(tabId);
+ // If a popup is already showing, check if the popup URL is the same. If so,
+ // then close the popup.
+ ExtensionPopupController* popup = [ExtensionPopupController popup];
+ if (popup &&
+ [[popup window] isVisible] &&
+ [popup extensionHost]->GetURL() == popupUrl) {
+ [popup close];
+ return;
+ }
+ NSPoint arrowPoint = [self popupPointForBrowserAction:[button extension]];
+ [ExtensionPopupController showURL:popupUrl
+ inBrowser:browser_
+ anchoredAt:arrowPoint
+ arrowLocation:info_bubble::kTopRight
+ devMode:NO];
+ } else {
+ ExtensionBrowserEventRouter::GetInstance()->BrowserActionExecuted(
+ profile_, action->extension_id(), browser_);
+ }
+}
+
+- (BOOL)shouldDisplayBrowserAction:(Extension*)extension {
+ // Only display incognito-enabled extensions while in incognito mode.
+ return (!profile_->IsOffTheRecord() ||
+ profile_->GetExtensionsService()->IsIncognitoEnabled(extension));
+}
+
+- (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate {
+ [self setChevronHidden:([self buttonCount] == [self visibleButtonCount])
+ inFrame:frame
+ animate:animate];
+}
+
+- (void)updateChevronPositionInFrame:(NSRect)frame {
+ CGFloat xPos = NSWidth(frame) - kChevronWidth - kChevronRightPadding;
+ NSRect buttonFrame = NSMakeRect(xPos,
+ kChevronLowerPadding,
+ kChevronWidth,
+ kChevronHeight);
+ [chevronMenuButton_ setFrame:buttonFrame];
+}
+
+- (void)setChevronHidden:(BOOL)hidden
+ inFrame:(NSRect)frame
+ animate:(BOOL)animate {
+ if (hidden == [self chevronIsHidden])
+ return;
+
+ if (!chevronMenuButton_.get()) {
+ chevronMenuButton_.reset([[MenuButton alloc] init]);
+ [chevronMenuButton_ setBordered:NO];
+ ResourceBundle& rb = ResourceBundle::GetSharedInstance();
+ [chevronMenuButton_ setImage:rb.GetNSImageNamed(IDR_BOOKMARK_BAR_CHEVRONS)];
+ [containerView_ addSubview:chevronMenuButton_];
+ }
+
+ if (!hidden)
+ [self updateOverflowMenu];
+
+ [self updateChevronPositionInFrame:frame];
+
+ // Stop any running animation.
+ [chevronAnimation_ stopAnimation];
+
+ if (!animate) {
+ [chevronMenuButton_ setHidden:hidden];
+ return;
+ }
+
+ NSDictionary* animationDictionary;
+ if (hidden) {
+ animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
+ chevronMenuButton_.get(), NSViewAnimationTargetKey,
+ NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey,
+ nil];
+ } else {
+ [chevronMenuButton_ setHidden:NO];
+ animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
+ chevronMenuButton_.get(), NSViewAnimationTargetKey,
+ NSViewAnimationFadeInEffect, NSViewAnimationEffectKey,
+ nil];
+ }
+ [chevronAnimation_ setViewAnimations:
+ [NSArray arrayWithObject:animationDictionary]];
+ [chevronAnimation_ startAnimation];
+}
+
+- (void)chevronItemSelected:(id)menuItem {
+ [self browserActionClicked:[menuItem representedObject]];
+}
+
+- (void)updateOverflowMenu {
+ overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
+ // See menu_button.h for documentation on why this is needed.
+ [overflowMenu_ addItemWithTitle:@"" action:nil keyEquivalent:@""];
+
+ for (BrowserActionButton* button in hiddenButtons_.get()) {
+ NSString* name = base::SysUTF8ToNSString([button extension]->name());
+ NSMenuItem* item =
+ [overflowMenu_ addItemWithTitle:name
+ action:@selector(chevronItemSelected:)
+ keyEquivalent:@""];
+ [item setRepresentedObject:button];
+ [item setImage:[button compositedImage]];
+ [item setTarget:self];
+ }
+ [chevronMenuButton_ setAttachedMenu:overflowMenu_];
+}
+
+- (void)updateGrippyCursors {
+ [containerView_ setCanDragLeft:[hiddenButtons_ count] > 0];
+ [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
+ [[containerView_ window] invalidateCursorRectsForView:containerView_];
+}
+
+- (int)currentTabId {
+ TabContents* selected_tab = browser_->GetSelectedTabContents();
+ if (!selected_tab)
+ return -1;
+
+ return selected_tab->controller().session_id().id();
+}
+
+#pragma mark -
+#pragma mark Testing Methods
+
+- (NSButton*)buttonWithIndex:(NSUInteger)index {
+ if (profile_->IsOffTheRecord())
+ index = toolbarModel_->IncognitoIndexToOriginal(index);
+ if (index < toolbarModel_->size()) {
+ Extension* extension = toolbarModel_->GetExtensionByIndex(index);
+ return [buttons_ objectForKey:base::SysUTF8ToNSString(extension->id())];
+ }
+ return nil;
+}
+
+@end