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"
10 #include "base/strings/sys_string_conversions.h"
11 #include "chrome/browser/chrome_notification_types.h"
12 #include "chrome/browser/extensions/extension_action.h"
13 #include "chrome/browser/extensions/extension_action_manager.h"
14 #include "chrome/browser/extensions/extension_toolbar_model.h"
15 #include "chrome/browser/extensions/extension_util.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/sessions/session_tab_helper.h"
18 #include "chrome/browser/ui/browser.h"
19 #include "chrome/browser/ui/browser_window.h"
20 #import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
21 #import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
22 #import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
23 #import "chrome/browser/ui/cocoa/image_button_cell.h"
24 #import "chrome/browser/ui/cocoa/menu_button.h"
25 #include "chrome/browser/ui/tabs/tab_strip_model.h"
26 #include "chrome/common/extensions/api/extension_action/action_info.h"
27 #include "content/public/browser/notification_details.h"
28 #include "content/public/browser/notification_observer.h"
29 #include "content/public/browser/notification_registrar.h"
30 #include "content/public/browser/notification_source.h"
31 #include "extensions/browser/extension_registry.h"
32 #include "grit/theme_resources.h"
33 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
35 using extensions::Extension;
36 using extensions::ExtensionList;
38 NSString* const kBrowserActionVisibilityChangedNotification =
39 @"BrowserActionVisibilityChangedNotification";
42 const CGFloat kAnimationDuration = 0.2;
44 const CGFloat kChevronWidth = 18;
46 // Since the container is the maximum height of the toolbar, we have
47 // to move the buttons up by this amount in order to have them look
48 // vertically centered within the toolbar.
49 const CGFloat kBrowserActionOriginYOffset = 5.0;
51 // The size of each button on the toolbar.
52 const CGFloat kBrowserActionHeight = 29.0;
53 const CGFloat kBrowserActionWidth = 29.0;
55 // The padding between browser action buttons.
56 const CGFloat kBrowserActionButtonPadding = 2.0;
58 // Padding between Omnibox and first button. Since the buttons have a
59 // pixel of internal padding, this needs an extra pixel.
60 const CGFloat kBrowserActionLeftPadding = kBrowserActionButtonPadding + 1.0;
62 // How far to inset from the bottom of the view to get the top border
63 // of the popup 2px below the bottom of the Omnibox.
64 const CGFloat kBrowserActionBubbleYOffset = 3.0;
68 @interface BrowserActionsController(Private)
69 // Used during initialization to create the BrowserActionButton objects from the
70 // stored toolbar model.
71 - (void)createButtons;
73 // Creates and then adds the given extension's action button to the container
74 // at the given index within the container. It does not affect the toolbar model
75 // object since it is called when the toolbar model changes.
76 - (void)createActionButtonForExtension:(const Extension*)extension
77 withIndex:(NSUInteger)index;
79 // Removes an action button for the given extension from the container. This
80 // method also does not affect the underlying toolbar model since it is called
81 // when the toolbar model changes.
82 - (void)removeActionButtonForExtension:(const Extension*)extension;
84 // Useful in the case of a Browser Action being added/removed from the middle of
85 // the container, this method repositions each button according to the current
87 - (void)positionActionButtonsAndAnimate:(BOOL)animate;
89 // During container resizing, buttons become more transparent as they are pushed
90 // off the screen. This method updates each button's opacity determined by the
91 // position of the button.
92 - (void)updateButtonOpacity;
94 // Returns the existing button with the given extension backing it; nil if it
95 // cannot be found or the extension's ID is invalid.
96 - (BrowserActionButton*)buttonForExtension:(const Extension*)extension;
98 // Returns the preferred width of the container given the number of visible
99 // buttons |buttonCount|.
100 - (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount;
102 // Returns the number of buttons that can fit in the container according to its
104 - (NSUInteger)containerButtonCapacity;
106 // Notification handlers for events registered by the class.
108 // Updates each button's opacity, the cursor rects and chevron position.
109 - (void)containerFrameChanged:(NSNotification*)notification;
111 // Hides the chevron and unhides every hidden button so that dragging the
112 // container out smoothly shows the Browser Action buttons.
113 - (void)containerDragStart:(NSNotification*)notification;
115 // Sends a notification for the toolbar to reposition surrounding UI elements.
116 - (void)containerDragging:(NSNotification*)notification;
118 // Determines which buttons need to be hidden based on the new size, hides them
119 // and updates the chevron overflow menu. Also fires a notification to let the
120 // toolbar know that the drag has finished.
121 - (void)containerDragFinished:(NSNotification*)notification;
123 // Adjusts the position of the surrounding action buttons depending on where the
124 // button is within the container.
125 - (void)actionButtonDragging:(NSNotification*)notification;
127 // Updates the position of the Browser Actions within the container. This fires
128 // when _any_ Browser Action button is done dragging to keep all open windows in
130 - (void)actionButtonDragFinished:(NSNotification*)notification;
132 // Moves the given button both visually and within the toolbar model to the
134 - (void)moveButton:(BrowserActionButton*)button
135 toIndex:(NSUInteger)index
136 animate:(BOOL)animate;
138 // Handles when the given BrowserActionButton object is clicked and whether
139 // it should grant tab permissions. API-simulated clicks should not grant.
140 - (BOOL)browserActionClicked:(BrowserActionButton*)button
141 shouldGrant:(BOOL)shouldGrant;
142 - (BOOL)browserActionClicked:(BrowserActionButton*)button;
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;
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).
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;
157 // Moves the chevron to its correct position within |frame|.
158 - (void)updateChevronPositionInFrame:(NSRect)frame;
160 // Shows or hides the chevron, animating as specified by |animate|.
161 - (void)setChevronHidden:(BOOL)hidden
162 inFrame:(NSRect)frame
163 animate:(BOOL)animate;
165 // Handles when a menu item within the chevron overflow menu is selected.
166 - (void)chevronItemSelected:(id)menuItem;
168 // Updates the container's grippy cursor based on the number of hidden buttons.
169 - (void)updateGrippyCursors;
171 // Returns the ID of the currently selected tab or -1 if none exists.
175 // A helper class to proxy extension notifications to the view controller's
176 // appropriate methods.
177 class ExtensionServiceObserverBridge
178 : public content::NotificationObserver,
179 public extensions::ExtensionToolbarModel::Observer {
181 ExtensionServiceObserverBridge(BrowserActionsController* owner,
183 : owner_(owner), browser_(browser) {
185 extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE,
186 content::Source<Profile>(browser->profile()));
189 extensions::NOTIFICATION_EXTENSION_COMMAND_BROWSER_ACTION_MAC,
190 content::Source<Profile>(browser->profile()));
193 // Overridden from content::NotificationObserver.
194 virtual void Observe(
196 const content::NotificationSource& source,
197 const content::NotificationDetails& details) OVERRIDE {
199 case extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE: {
200 ExtensionPopupController* popup = [ExtensionPopupController popup];
201 if (popup && ![popup isClosing])
206 case extensions::NOTIFICATION_EXTENSION_COMMAND_BROWSER_ACTION_MAC: {
207 std::pair<const std::string, gfx::NativeWindow>* payload =
208 content::Details<std::pair<const std::string, gfx::NativeWindow> >(
210 std::string extension_id = payload->first;
211 gfx::NativeWindow window = payload->second;
212 if (window != browser_->window()->GetNativeWindow())
214 [owner_ activateBrowserAction:extension_id];
218 NOTREACHED() << L"Unexpected notification";
222 // extensions::ExtensionToolbarModel::Observer implementation.
223 virtual void ToolbarExtensionAdded(
224 const Extension* extension,
225 int index) OVERRIDE {
226 [owner_ createActionButtonForExtension:extension withIndex:index];
227 [owner_ resizeContainerAndAnimate:NO];
230 virtual void ToolbarExtensionRemoved(const Extension* extension) OVERRIDE {
231 [owner_ removeActionButtonForExtension:extension];
232 [owner_ resizeContainerAndAnimate:NO];
235 virtual void ToolbarExtensionMoved(const Extension* extension,
236 int index) OVERRIDE {
239 virtual void ToolbarExtensionUpdated(const Extension* extension) OVERRIDE {
240 BrowserActionButton* button = [owner_ buttonForExtension:extension];
242 [button updateState];
245 virtual bool ShowExtensionActionPopup(const Extension* extension,
246 bool grant_active_tab) OVERRIDE {
247 // Do not override other popups and only show in active window.
248 ExtensionPopupController* popup = [ExtensionPopupController popup];
249 if (popup || !browser_->window()->IsActive())
252 BrowserActionButton* button = [owner_ buttonForExtension:extension];
253 return button && [owner_ browserActionClicked:button
254 shouldGrant:grant_active_tab];
257 virtual void ToolbarVisibleCountChanged() OVERRIDE {
260 virtual void ToolbarHighlightModeChanged(bool is_highlighting) OVERRIDE {
263 virtual Browser* GetBrowser() OVERRIDE {
268 // The object we need to inform when we get a notification. Weak. Owns us.
269 BrowserActionsController* owner_;
271 // The browser we listen for events from. Weak.
274 // Used for registering to receive notifications and automatic clean up.
275 content::NotificationRegistrar registrar_;
277 DISALLOW_COPY_AND_ASSIGN(ExtensionServiceObserverBridge);
280 @implementation BrowserActionsController
282 @synthesize containerView = containerView_;
285 #pragma mark Public Methods
287 - (id)initWithBrowser:(Browser*)browser
288 containerView:(BrowserActionsContainerView*)container {
289 DCHECK(browser && container);
291 if ((self = [super init])) {
293 profile_ = browser->profile();
295 observer_.reset(new ExtensionServiceObserverBridge(self, browser_));
296 toolbarModel_ = extensions::ExtensionToolbarModel::Get(profile_);
298 toolbarModel_->AddObserver(observer_.get());
300 containerView_ = container;
301 [containerView_ setPostsFrameChangedNotifications:YES];
302 [[NSNotificationCenter defaultCenter]
304 selector:@selector(containerFrameChanged:)
305 name:NSViewFrameDidChangeNotification
306 object:containerView_];
307 [[NSNotificationCenter defaultCenter]
309 selector:@selector(containerDragStart:)
310 name:kBrowserActionGrippyDragStartedNotification
311 object:containerView_];
312 [[NSNotificationCenter defaultCenter]
314 selector:@selector(containerDragging:)
315 name:kBrowserActionGrippyDraggingNotification
316 object:containerView_];
317 [[NSNotificationCenter defaultCenter]
319 selector:@selector(containerDragFinished:)
320 name:kBrowserActionGrippyDragFinishedNotification
321 object:containerView_];
322 // Listen for a finished drag from any button to make sure each open window
324 [[NSNotificationCenter defaultCenter]
326 selector:@selector(actionButtonDragFinished:)
327 name:kBrowserActionButtonDragEndNotification
330 chevronAnimation_.reset([[NSViewAnimation alloc] init]);
331 [chevronAnimation_ gtm_setDuration:kAnimationDuration
332 eventMask:NSLeftMouseUpMask];
333 [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
335 hiddenButtons_.reset([[NSMutableArray alloc] init]);
336 buttons_.reset([[NSMutableDictionary alloc] init]);
337 [self createButtons];
338 [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:NO];
339 [self updateGrippyCursors];
340 [container setResizable:!profile_->IsOffTheRecord()];
348 toolbarModel_->RemoveObserver(observer_.get());
350 [[NSNotificationCenter defaultCenter] removeObserver:self];
355 for (BrowserActionButton* button in [buttons_ allValues]) {
356 [button setTabId:[self currentTabId]];
357 [button updateState];
361 - (NSUInteger)buttonCount {
362 return [buttons_ count];
365 - (NSUInteger)visibleButtonCount {
366 return [self buttonCount] - [hiddenButtons_ count];
369 - (void)resizeContainerAndAnimate:(BOOL)animate {
370 int iconCount = toolbarModel_->GetVisibleIconCount();
371 if (iconCount < 0) // If no buttons are hidden.
372 iconCount = [self buttonCount];
374 [containerView_ resizeToWidth:[self containerWidthWithButtonCount:iconCount]
376 NSRect frame = animate ? [containerView_ animationEndFrame] :
377 [containerView_ frame];
379 [self showChevronIfNecessaryInFrame:frame animate:animate];
382 [[NSNotificationCenter defaultCenter]
383 postNotificationName:kBrowserActionVisibilityChangedNotification
388 - (NSView*)browserActionViewForExtension:(const Extension*)extension {
389 for (BrowserActionButton* button in [buttons_ allValues]) {
390 if ([button extension] == extension)
397 - (CGFloat)savedWidth {
401 int savedButtonCount = toolbarModel_->GetVisibleIconCount();
402 if (savedButtonCount < 0 || // all icons are visible
403 static_cast<NSUInteger>(savedButtonCount) > [self buttonCount])
404 savedButtonCount = [self buttonCount];
405 return [self containerWidthWithButtonCount:savedButtonCount];
408 - (NSPoint)popupPointForBrowserAction:(const Extension*)extension {
409 if (!extensions::ExtensionActionManager::Get(profile_)->
410 GetBrowserAction(*extension)) {
414 NSButton* button = [self buttonForExtension:extension];
418 if ([hiddenButtons_ containsObject:button])
419 button = chevronMenuButton_.get();
421 // Anchor point just above the center of the bottom.
422 const NSRect bounds = [button bounds];
423 DCHECK([button isFlipped]);
424 NSPoint anchor = NSMakePoint(NSMidX(bounds),
425 NSMaxY(bounds) - kBrowserActionBubbleYOffset);
426 return [button convertPoint:anchor toView:nil];
429 - (BOOL)chevronIsHidden {
430 if (!chevronMenuButton_.get())
433 if (![chevronAnimation_ isAnimating])
434 return [chevronMenuButton_ isHidden];
436 DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
438 // The chevron is animating in or out. Determine which one and have the return
439 // value reflect where the animation is headed.
440 NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
441 valueForKey:NSViewAnimationEffectKey];
442 if (effect == NSViewAnimationFadeInEffect) {
444 } else if (effect == NSViewAnimationFadeOutEffect) {
452 - (void)activateBrowserAction:(const std::string&)extension_id {
453 const Extension* extension = extensions::ExtensionRegistry::Get(
454 browser_->profile())->enabled_extensions().GetByID(extension_id);
458 BrowserActionButton* button = [self buttonForExtension:extension];
459 // |button| can be nil when the browser action has its button hidden.
461 [self browserActionClicked:button];
465 #pragma mark NSMenuDelegate
467 - (void)menuNeedsUpdate:(NSMenu*)menu {
468 [menu removeAllItems];
470 // See menu_button.h for documentation on why this is needed.
471 [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
473 for (BrowserActionButton* button in hiddenButtons_.get()) {
474 NSString* name = base::SysUTF8ToNSString([button extension]->name());
476 [menu addItemWithTitle:name
477 action:@selector(chevronItemSelected:)
479 [item setRepresentedObject:button];
480 [item setImage:[button compositedImage]];
481 [item setTarget:self];
482 [item setEnabled:[button isEnabled]];
487 #pragma mark Private Methods
489 - (void)createButtons {
494 for (ExtensionList::const_iterator iter =
495 toolbarModel_->toolbar_items().begin();
496 iter != toolbarModel_->toolbar_items().end(); ++iter) {
497 if (![self shouldDisplayBrowserAction:iter->get()])
500 [self createActionButtonForExtension:iter->get() withIndex:i++];
503 CGFloat width = [self savedWidth];
504 [containerView_ resizeToWidth:width animate:NO];
507 - (void)createActionButtonForExtension:(const Extension*)extension
508 withIndex:(NSUInteger)index {
509 if (!extensions::ExtensionActionManager::Get(profile_)->
510 GetBrowserAction(*extension))
513 if (![self shouldDisplayBrowserAction:extension])
516 if (profile_->IsOffTheRecord())
517 index = toolbarModel_->OriginalIndexToIncognito(index);
519 // Show the container if it's the first button. Otherwise it will be shown
521 if ([self buttonCount] == 0)
522 [containerView_ setHidden:NO];
524 NSRect buttonFrame = NSMakeRect(0.0, kBrowserActionOriginYOffset,
525 kBrowserActionWidth, kBrowserActionHeight);
526 BrowserActionButton* newButton =
527 [[[BrowserActionButton alloc]
528 initWithFrame:buttonFrame
531 tabId:[self currentTabId]] autorelease];
532 [newButton setTarget:self];
533 [newButton setAction:@selector(browserActionClicked:)];
534 NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
537 [buttons_ setObject:newButton forKey:buttonKey];
539 [self positionActionButtonsAndAnimate:NO];
541 [[NSNotificationCenter defaultCenter]
543 selector:@selector(actionButtonDragging:)
544 name:kBrowserActionButtonDraggingNotification
548 [containerView_ setMaxWidth:
549 [self containerWidthWithButtonCount:[self buttonCount]]];
550 [containerView_ setNeedsDisplay:YES];
553 - (void)removeActionButtonForExtension:(const Extension*)extension {
554 if (!extensions::ActionInfo::GetBrowserActionInfo(extension))
557 NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
561 BrowserActionButton* button = [buttons_ objectForKey:buttonKey];
562 // This could be the case in incognito, where only a subset of extensions are
567 [button removeFromSuperview];
568 // It may or may not be hidden, but it won't matter to NSMutableArray either
570 [hiddenButtons_ removeObject:button];
572 [buttons_ removeObjectForKey:buttonKey];
573 if ([self buttonCount] == 0) {
574 // No more buttons? Hide the container.
575 [containerView_ setHidden:YES];
577 [self positionActionButtonsAndAnimate:NO];
579 [containerView_ setMaxWidth:
580 [self containerWidthWithButtonCount:[self buttonCount]]];
581 [containerView_ setNeedsDisplay:YES];
584 - (void)positionActionButtonsAndAnimate:(BOOL)animate {
586 for (ExtensionList::const_iterator iter =
587 toolbarModel_->toolbar_items().begin();
588 iter != toolbarModel_->toolbar_items().end(); ++iter) {
589 if (![self shouldDisplayBrowserAction:iter->get()])
591 BrowserActionButton* button = [self buttonForExtension:(iter->get())];
594 if (![button isBeingDragged])
595 [self moveButton:button toIndex:i animate:animate];
600 - (void)updateButtonOpacity {
601 for (BrowserActionButton* button in [buttons_ allValues]) {
602 NSRect buttonFrame = [button frame];
603 if (NSContainsRect([containerView_ bounds], buttonFrame)) {
604 if ([button alphaValue] != 1.0)
605 [button setAlphaValue:1.0];
609 CGFloat intersectionWidth =
610 NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
611 CGFloat alpha = std::max(static_cast<CGFloat>(0.0),
612 intersectionWidth / NSWidth(buttonFrame));
613 [button setAlphaValue:alpha];
614 [button setNeedsDisplay:YES];
618 - (BrowserActionButton*)buttonForExtension:(const Extension*)extension {
619 NSString* extensionId = base::SysUTF8ToNSString(extension->id());
623 return [buttons_ objectForKey:extensionId];
626 - (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount {
627 // Left-side padding which works regardless of whether a button or
629 CGFloat width = kBrowserActionLeftPadding;
631 // Include the buttons and padding between.
632 if (buttonCount > 0) {
633 width += buttonCount * kBrowserActionWidth;
634 width += (buttonCount - 1) * kBrowserActionButtonPadding;
637 // Make room for the chevron if any buttons are hidden.
638 if ([self buttonCount] != [self visibleButtonCount]) {
639 // Chevron and buttons both include 1px padding w/in their bounds,
640 // so this leaves 2px between the last browser action and chevron,
641 // and also works right if the chevron is the only button.
642 width += kChevronWidth;
648 - (NSUInteger)containerButtonCapacity {
649 // Edge-to-edge span of the browser action buttons.
650 CGFloat actionSpan = [self savedWidth] - kBrowserActionLeftPadding;
652 // Add in some padding for the browser action on the end, then
653 // divide out to get the number of action buttons that fit.
654 return (actionSpan + kBrowserActionButtonPadding) /
655 (kBrowserActionWidth + kBrowserActionButtonPadding);
658 - (void)containerFrameChanged:(NSNotification*)notification {
659 [self updateButtonOpacity];
660 [[containerView_ window] invalidateCursorRectsForView:containerView_];
661 [self updateChevronPositionInFrame:[containerView_ frame]];
664 - (void)containerDragStart:(NSNotification*)notification {
665 [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
666 while([hiddenButtons_ count] > 0) {
667 [containerView_ addSubview:[hiddenButtons_ objectAtIndex:0]];
668 [hiddenButtons_ removeObjectAtIndex:0];
672 - (void)containerDragging:(NSNotification*)notification {
673 [[NSNotificationCenter defaultCenter]
674 postNotificationName:kBrowserActionGrippyDraggingNotification
678 - (void)containerDragFinished:(NSNotification*)notification {
679 for (ExtensionList::const_iterator iter =
680 toolbarModel_->toolbar_items().begin();
681 iter != toolbarModel_->toolbar_items().end(); ++iter) {
682 BrowserActionButton* button = [self buttonForExtension:(iter->get())];
683 NSRect buttonFrame = [button frame];
684 if (NSContainsRect([containerView_ bounds], buttonFrame))
687 CGFloat intersectionWidth =
688 NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
689 // Pad the threshold by 5 pixels in order to have the buttons hide more
691 if (([containerView_ grippyPinned] && intersectionWidth > 0) ||
692 (intersectionWidth <= (NSWidth(buttonFrame) / 2) + 5.0)) {
693 [button setAlphaValue:0.0];
694 [button removeFromSuperview];
695 [hiddenButtons_ addObject:button];
698 [self updateGrippyCursors];
700 if (!profile_->IsOffTheRecord())
701 toolbarModel_->SetVisibleIconCount([self visibleButtonCount]);
703 [[NSNotificationCenter defaultCenter]
704 postNotificationName:kBrowserActionGrippyDragFinishedNotification
708 - (void)actionButtonDragging:(NSNotification*)notification {
709 if (![self chevronIsHidden])
710 [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
712 // Determine what index the dragged button should lie in, alter the model and
713 // reposition the buttons.
714 CGFloat dragThreshold = std::floor(kBrowserActionWidth / 2);
715 BrowserActionButton* draggedButton = [notification object];
716 NSRect draggedButtonFrame = [draggedButton frame];
718 NSUInteger index = 0;
719 for (ExtensionList::const_iterator iter =
720 toolbarModel_->toolbar_items().begin();
721 iter != toolbarModel_->toolbar_items().end(); ++iter) {
722 BrowserActionButton* button = [self buttonForExtension:(iter->get())];
723 CGFloat intersectionWidth =
724 NSWidth(NSIntersectionRect(draggedButtonFrame, [button frame]));
726 if (intersectionWidth > dragThreshold && button != draggedButton &&
727 ![button isAnimating] && index < [self visibleButtonCount]) {
728 toolbarModel_->MoveExtensionIcon([draggedButton extension], index);
729 [self positionActionButtonsAndAnimate:YES];
736 - (void)actionButtonDragFinished:(NSNotification*)notification {
737 [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:YES];
738 [self positionActionButtonsAndAnimate:YES];
741 - (void)moveButton:(BrowserActionButton*)button
742 toIndex:(NSUInteger)index
743 animate:(BOOL)animate {
744 CGFloat xOffset = kBrowserActionLeftPadding +
745 (index * (kBrowserActionWidth + kBrowserActionButtonPadding));
746 NSRect buttonFrame = [button frame];
747 buttonFrame.origin.x = xOffset;
748 [button setFrame:buttonFrame animate:animate];
750 if (index < [self containerButtonCapacity]) {
751 // Make sure the button is within the visible container.
752 if ([button superview] != containerView_) {
753 [containerView_ addSubview:button];
754 [button setAlphaValue:1.0];
755 [hiddenButtons_ removeObjectIdenticalTo:button];
757 } else if (![hiddenButtons_ containsObject:button]) {
758 [hiddenButtons_ addObject:button];
759 [button removeFromSuperview];
760 [button setAlphaValue:0.0];
764 - (BOOL)browserActionClicked:(BrowserActionButton*)button
765 shouldGrant:(BOOL)shouldGrant {
766 const Extension* extension = [button extension];
767 switch (extensions::ExtensionActionAPI::Get(profile_)->ExecuteExtensionAction(
768 extension, browser_, shouldGrant)) {
769 case ExtensionAction::ACTION_NONE:
771 case ExtensionAction::ACTION_SHOW_POPUP: {
772 GURL popupUrl = extensions::ExtensionActionManager::Get(profile_)->
773 GetBrowserAction(*extension)->GetPopupUrl([self currentTabId]);
774 NSPoint arrowPoint = [self popupPointForBrowserAction:extension];
775 [ExtensionPopupController showURL:popupUrl
777 anchoredAt:arrowPoint
778 arrowLocation:info_bubble::kTopRight
786 - (BOOL)browserActionClicked:(BrowserActionButton*)button {
787 return [self browserActionClicked:button
791 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension {
792 // Only display incognito-enabled extensions while in incognito mode.
793 return !profile_->IsOffTheRecord() ||
794 extensions::util::IsIncognitoEnabled(extension->id(), profile_);
797 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate {
798 [self setChevronHidden:([self buttonCount] == [self visibleButtonCount])
803 - (void)updateChevronPositionInFrame:(NSRect)frame {
804 CGFloat xPos = NSWidth(frame) - kChevronWidth;
805 NSRect buttonFrame = NSMakeRect(xPos,
806 kBrowserActionOriginYOffset,
808 kBrowserActionHeight);
809 [chevronMenuButton_ setFrame:buttonFrame];
812 - (void)setChevronHidden:(BOOL)hidden
813 inFrame:(NSRect)frame
814 animate:(BOOL)animate {
815 if (hidden == [self chevronIsHidden])
818 if (!chevronMenuButton_.get()) {
819 chevronMenuButton_.reset([[MenuButton alloc] init]);
820 [chevronMenuButton_ setOpenMenuOnClick:YES];
821 [chevronMenuButton_ setBordered:NO];
822 [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
824 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW
825 forButtonState:image_button_cell::kDefaultState];
826 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_H
827 forButtonState:image_button_cell::kHoverState];
828 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_P
829 forButtonState:image_button_cell::kPressedState];
831 overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
832 [overflowMenu_ setAutoenablesItems:NO];
833 [overflowMenu_ setDelegate:self];
834 [chevronMenuButton_ setAttachedMenu:overflowMenu_];
836 [containerView_ addSubview:chevronMenuButton_];
839 [self updateChevronPositionInFrame:frame];
841 // Stop any running animation.
842 [chevronAnimation_ stopAnimation];
845 [chevronMenuButton_ setHidden:hidden];
849 NSDictionary* animationDictionary;
851 animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
852 chevronMenuButton_.get(), NSViewAnimationTargetKey,
853 NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey,
856 [chevronMenuButton_ setHidden:NO];
857 animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
858 chevronMenuButton_.get(), NSViewAnimationTargetKey,
859 NSViewAnimationFadeInEffect, NSViewAnimationEffectKey,
862 [chevronAnimation_ setViewAnimations:
863 [NSArray arrayWithObject:animationDictionary]];
864 [chevronAnimation_ startAnimation];
867 - (void)chevronItemSelected:(id)menuItem {
868 [self browserActionClicked:[menuItem representedObject]];
871 - (void)updateGrippyCursors {
872 [containerView_ setCanDragLeft:[hiddenButtons_ count] > 0];
873 [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
874 [[containerView_ window] invalidateCursorRectsForView:containerView_];
877 - (int)currentTabId {
878 content::WebContents* active_tab =
879 browser_->tab_strip_model()->GetActiveWebContents();
883 return SessionTabHelper::FromWebContents(active_tab)->session_id().id();
887 #pragma mark Testing Methods
889 - (NSButton*)buttonWithIndex:(NSUInteger)index {
890 if (profile_->IsOffTheRecord())
891 index = toolbarModel_->IncognitoIndexToOriginal(index);
892 const extensions::ExtensionList& toolbar_items =
893 toolbarModel_->toolbar_items();
894 if (index < toolbar_items.size()) {
895 const Extension* extension = toolbar_items[index].get();
896 return [buttons_ objectForKey:base::SysUTF8ToNSString(extension->id())];