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/prefs/pref_service.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "chrome/browser/chrome_notification_types.h"
13 #include "chrome/browser/extensions/extension_action.h"
14 #include "chrome/browser/extensions/extension_action_manager.h"
15 #include "chrome/browser/extensions/extension_service.h"
16 #include "chrome/browser/extensions/extension_system.h"
17 #include "chrome/browser/extensions/extension_toolbar_model.h"
18 #include "chrome/browser/extensions/extension_util.h"
19 #include "chrome/browser/profiles/profile.h"
20 #include "chrome/browser/sessions/session_tab_helper.h"
21 #include "chrome/browser/ui/browser.h"
22 #include "chrome/browser/ui/browser_window.h"
23 #import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
24 #import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
25 #import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
26 #import "chrome/browser/ui/cocoa/image_button_cell.h"
27 #import "chrome/browser/ui/cocoa/menu_button.h"
28 #include "chrome/browser/ui/tabs/tab_strip_model.h"
29 #include "chrome/common/extensions/api/extension_action/action_info.h"
30 #include "chrome/common/pref_names.h"
31 #include "content/public/browser/notification_details.h"
32 #include "content/public/browser/notification_observer.h"
33 #include "content/public/browser/notification_registrar.h"
34 #include "content/public/browser/notification_source.h"
35 #include "extensions/browser/pref_names.h"
36 #include "grit/theme_resources.h"
37 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
39 using extensions::Extension;
40 using extensions::ExtensionList;
42 NSString* const kBrowserActionVisibilityChangedNotification =
43 @"BrowserActionVisibilityChangedNotification";
46 const CGFloat kAnimationDuration = 0.2;
48 const CGFloat kChevronWidth = 18;
50 // Since the container is the maximum height of the toolbar, we have
51 // to move the buttons up by this amount in order to have them look
52 // vertically centered within the toolbar.
53 const CGFloat kBrowserActionOriginYOffset = 5.0;
55 // The size of each button on the toolbar.
56 const CGFloat kBrowserActionHeight = 29.0;
57 const CGFloat kBrowserActionWidth = 29.0;
59 // The padding between browser action buttons.
60 const CGFloat kBrowserActionButtonPadding = 2.0;
62 // Padding between Omnibox and first button. Since the buttons have a
63 // pixel of internal padding, this needs an extra pixel.
64 const CGFloat kBrowserActionLeftPadding = kBrowserActionButtonPadding + 1.0;
66 // How far to inset from the bottom of the view to get the top border
67 // of the popup 2px below the bottom of the Omnibox.
68 const CGFloat kBrowserActionBubbleYOffset = 3.0;
72 @interface BrowserActionsController(Private)
73 // Used during initialization to create the BrowserActionButton objects from the
74 // stored toolbar model.
75 - (void)createButtons;
77 // Creates and then adds the given extension's action button to the container
78 // at the given index within the container. It does not affect the toolbar model
79 // object since it is called when the toolbar model changes.
80 - (void)createActionButtonForExtension:(const Extension*)extension
81 withIndex:(NSUInteger)index;
83 // Removes an action button for the given extension from the container. This
84 // method also does not affect the underlying toolbar model since it is called
85 // when the toolbar model changes.
86 - (void)removeActionButtonForExtension:(const Extension*)extension;
88 // Useful in the case of a Browser Action being added/removed from the middle of
89 // the container, this method repositions each button according to the current
91 - (void)positionActionButtonsAndAnimate:(BOOL)animate;
93 // During container resizing, buttons become more transparent as they are pushed
94 // off the screen. This method updates each button's opacity determined by the
95 // position of the button.
96 - (void)updateButtonOpacity;
98 // Returns the existing button with the given extension backing it; nil if it
99 // cannot be found or the extension's ID is invalid.
100 - (BrowserActionButton*)buttonForExtension:(const Extension*)extension;
102 // Returns the preferred width of the container given the number of visible
103 // buttons |buttonCount|.
104 - (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount;
106 // Returns the number of buttons that can fit in the container according to its
108 - (NSUInteger)containerButtonCapacity;
110 // Notification handlers for events registered by the class.
112 // Updates each button's opacity, the cursor rects and chevron position.
113 - (void)containerFrameChanged:(NSNotification*)notification;
115 // Hides the chevron and unhides every hidden button so that dragging the
116 // container out smoothly shows the Browser Action buttons.
117 - (void)containerDragStart:(NSNotification*)notification;
119 // Sends a notification for the toolbar to reposition surrounding UI elements.
120 - (void)containerDragging:(NSNotification*)notification;
122 // Determines which buttons need to be hidden based on the new size, hides them
123 // and updates the chevron overflow menu. Also fires a notification to let the
124 // toolbar know that the drag has finished.
125 - (void)containerDragFinished:(NSNotification*)notification;
127 // Adjusts the position of the surrounding action buttons depending on where the
128 // button is within the container.
129 - (void)actionButtonDragging:(NSNotification*)notification;
131 // Updates the position of the Browser Actions within the container. This fires
132 // when _any_ Browser Action button is done dragging to keep all open windows in
134 - (void)actionButtonDragFinished:(NSNotification*)notification;
136 // Moves the given button both visually and within the toolbar model to the
138 - (void)moveButton:(BrowserActionButton*)button
139 toIndex:(NSUInteger)index
140 animate:(BOOL)animate;
142 // Handles when the given BrowserActionButton object is clicked and whether
143 // it should grant tab permissions. API-simulated clicks should not grant.
144 - (BOOL)browserActionClicked:(BrowserActionButton*)button
145 shouldGrant:(BOOL)shouldGrant;
146 - (BOOL)browserActionClicked:(BrowserActionButton*)button;
148 // Returns whether the given extension should be displayed. Only displays
149 // incognito-enabled extensions in incognito mode. Otherwise returns YES.
150 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension;
152 // The reason |frame| is specified in these chevron functions is because the
153 // container may be animating and the end frame of the animation should be
154 // passed instead of the current frame (which may be off and cause the chevron
155 // to jump at the end of its animation).
157 // Shows the overflow chevron button depending on whether there are any hidden
158 // extensions within the frame given.
159 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate;
161 // Moves the chevron to its correct position within |frame|.
162 - (void)updateChevronPositionInFrame:(NSRect)frame;
164 // Shows or hides the chevron, animating as specified by |animate|.
165 - (void)setChevronHidden:(BOOL)hidden
166 inFrame:(NSRect)frame
167 animate:(BOOL)animate;
169 // Handles when a menu item within the chevron overflow menu is selected.
170 - (void)chevronItemSelected:(id)menuItem;
172 // Updates the container's grippy cursor based on the number of hidden buttons.
173 - (void)updateGrippyCursors;
175 // Returns the ID of the currently selected tab or -1 if none exists.
179 // A helper class to proxy extension notifications to the view controller's
180 // appropriate methods.
181 class ExtensionServiceObserverBridge : public content::NotificationObserver,
182 public ExtensionToolbarModel::Observer {
184 ExtensionServiceObserverBridge(BrowserActionsController* owner,
186 : owner_(owner), browser_(browser) {
187 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE,
188 content::Source<Profile>(browser->profile()));
190 chrome::NOTIFICATION_EXTENSION_COMMAND_BROWSER_ACTION_MAC,
191 content::Source<Profile>(browser->profile()));
194 // Overridden from content::NotificationObserver.
195 virtual void Observe(
197 const content::NotificationSource& source,
198 const content::NotificationDetails& details) OVERRIDE {
200 case chrome::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE: {
201 ExtensionPopupController* popup = [ExtensionPopupController popup];
202 if (popup && ![popup isClosing])
207 case chrome::NOTIFICATION_EXTENSION_COMMAND_BROWSER_ACTION_MAC: {
208 std::pair<const std::string, gfx::NativeWindow>* payload =
209 content::Details<std::pair<const std::string, gfx::NativeWindow> >(
211 std::string extension_id = payload->first;
212 gfx::NativeWindow window = payload->second;
213 if (window != browser_->window()->GetNativeWindow())
215 ExtensionService* service = browser_->profile()->GetExtensionService();
218 const Extension* extension = service->GetExtensionById(extension_id,
222 BrowserActionButton* button = [owner_ buttonForExtension:extension];
223 // |button| can be nil when the browser action has its button hidden.
225 [owner_ browserActionClicked:button];
229 NOTREACHED() << L"Unexpected notification";
233 // ExtensionToolbarModel::Observer implementation.
234 virtual void BrowserActionAdded(
235 const Extension* extension,
236 int index) OVERRIDE {
237 [owner_ createActionButtonForExtension:extension withIndex:index];
238 [owner_ resizeContainerAndAnimate:NO];
241 virtual void BrowserActionRemoved(const Extension* extension) OVERRIDE {
242 [owner_ removeActionButtonForExtension:extension];
243 [owner_ resizeContainerAndAnimate:NO];
246 virtual bool BrowserActionShowPopup(const Extension* extension) 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
258 // The object we need to inform when we get a notification. Weak. Owns us.
259 BrowserActionsController* owner_;
261 // The browser we listen for events from. Weak.
264 // Used for registering to receive notifications and automatic clean up.
265 content::NotificationRegistrar registrar_;
267 DISALLOW_COPY_AND_ASSIGN(ExtensionServiceObserverBridge);
270 @implementation BrowserActionsController
272 @synthesize containerView = containerView_;
275 #pragma mark Public Methods
277 - (id)initWithBrowser:(Browser*)browser
278 containerView:(BrowserActionsContainerView*)container {
279 DCHECK(browser && container);
281 if ((self = [super init])) {
283 profile_ = browser->profile();
285 observer_.reset(new ExtensionServiceObserverBridge(self, browser_));
286 toolbarModel_ = ExtensionToolbarModel::Get(profile_);
288 toolbarModel_->AddObserver(observer_.get());
290 containerView_ = container;
291 [containerView_ setPostsFrameChangedNotifications:YES];
292 [[NSNotificationCenter defaultCenter]
294 selector:@selector(containerFrameChanged:)
295 name:NSViewFrameDidChangeNotification
296 object:containerView_];
297 [[NSNotificationCenter defaultCenter]
299 selector:@selector(containerDragStart:)
300 name:kBrowserActionGrippyDragStartedNotification
301 object:containerView_];
302 [[NSNotificationCenter defaultCenter]
304 selector:@selector(containerDragging:)
305 name:kBrowserActionGrippyDraggingNotification
306 object:containerView_];
307 [[NSNotificationCenter defaultCenter]
309 selector:@selector(containerDragFinished:)
310 name:kBrowserActionGrippyDragFinishedNotification
311 object:containerView_];
312 // Listen for a finished drag from any button to make sure each open window
314 [[NSNotificationCenter defaultCenter]
316 selector:@selector(actionButtonDragFinished:)
317 name:kBrowserActionButtonDragEndNotification
320 chevronAnimation_.reset([[NSViewAnimation alloc] init]);
321 [chevronAnimation_ gtm_setDuration:kAnimationDuration
322 eventMask:NSLeftMouseUpMask];
323 [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
325 hiddenButtons_.reset([[NSMutableArray alloc] init]);
326 buttons_.reset([[NSMutableDictionary alloc] init]);
327 [self createButtons];
328 [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:NO];
329 [self updateGrippyCursors];
330 [container setResizable:!profile_->IsOffTheRecord()];
338 toolbarModel_->RemoveObserver(observer_.get());
340 [[NSNotificationCenter defaultCenter] removeObserver:self];
345 for (BrowserActionButton* button in [buttons_ allValues]) {
346 [button setTabId:[self currentTabId]];
347 [button updateState];
351 - (NSUInteger)buttonCount {
352 return [buttons_ count];
355 - (NSUInteger)visibleButtonCount {
356 return [self buttonCount] - [hiddenButtons_ count];
359 - (void)resizeContainerAndAnimate:(BOOL)animate {
360 int iconCount = toolbarModel_->GetVisibleIconCount();
361 if (iconCount < 0) // If no buttons are hidden.
362 iconCount = [self buttonCount];
364 [containerView_ resizeToWidth:[self containerWidthWithButtonCount:iconCount]
366 NSRect frame = animate ? [containerView_ animationEndFrame] :
367 [containerView_ frame];
369 [self showChevronIfNecessaryInFrame:frame animate:animate];
372 [[NSNotificationCenter defaultCenter]
373 postNotificationName:kBrowserActionVisibilityChangedNotification
378 - (NSView*)browserActionViewForExtension:(const Extension*)extension {
379 for (BrowserActionButton* button in [buttons_ allValues]) {
380 if ([button extension] == extension)
387 - (CGFloat)savedWidth {
390 if (!profile_->GetPrefs()->HasPrefPath(
391 extensions::pref_names::kToolbarSize)) {
392 // Migration code to the new VisibleIconCount pref.
393 // TODO(mpcomplete): remove this at some point.
394 double predefinedWidth = profile_->GetPrefs()->GetDouble(
395 extensions::pref_names::kBrowserActionContainerWidth);
396 if (predefinedWidth != 0) {
397 int iconWidth = kBrowserActionWidth + kBrowserActionButtonPadding;
398 int extraWidth = kChevronWidth;
399 toolbarModel_->SetVisibleIconCount(
400 (predefinedWidth - extraWidth) / iconWidth);
404 int savedButtonCount = toolbarModel_->GetVisibleIconCount();
405 if (savedButtonCount < 0 || // all icons are visible
406 static_cast<NSUInteger>(savedButtonCount) > [self buttonCount])
407 savedButtonCount = [self buttonCount];
408 return [self containerWidthWithButtonCount:savedButtonCount];
411 - (NSPoint)popupPointForBrowserAction:(const Extension*)extension {
412 if (!extensions::ExtensionActionManager::Get(profile_)->
413 GetBrowserAction(*extension)) {
417 NSButton* button = [self buttonForExtension:extension];
421 if ([hiddenButtons_ containsObject:button])
422 button = chevronMenuButton_.get();
424 // Anchor point just above the center of the bottom.
425 const NSRect bounds = [button bounds];
426 DCHECK([button isFlipped]);
427 NSPoint anchor = NSMakePoint(NSMidX(bounds),
428 NSMaxY(bounds) - kBrowserActionBubbleYOffset);
429 return [button convertPoint:anchor toView:nil];
432 - (BOOL)chevronIsHidden {
433 if (!chevronMenuButton_.get())
436 if (![chevronAnimation_ isAnimating])
437 return [chevronMenuButton_ isHidden];
439 DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
441 // The chevron is animating in or out. Determine which one and have the return
442 // value reflect where the animation is headed.
443 NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
444 valueForKey:NSViewAnimationEffectKey];
445 if (effect == NSViewAnimationFadeInEffect) {
447 } else if (effect == NSViewAnimationFadeOutEffect) {
456 #pragma mark NSMenuDelegate
458 - (void)menuNeedsUpdate:(NSMenu*)menu {
459 [menu removeAllItems];
461 // See menu_button.h for documentation on why this is needed.
462 [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
464 for (BrowserActionButton* button in hiddenButtons_.get()) {
465 NSString* name = base::SysUTF8ToNSString([button extension]->name());
467 [menu addItemWithTitle:name
468 action:@selector(chevronItemSelected:)
470 [item setRepresentedObject:button];
471 [item setImage:[button compositedImage]];
472 [item setTarget:self];
473 [item setEnabled:[button isEnabled]];
478 #pragma mark Private Methods
480 - (void)createButtons {
485 for (ExtensionList::const_iterator iter =
486 toolbarModel_->toolbar_items().begin();
487 iter != toolbarModel_->toolbar_items().end(); ++iter) {
488 if (![self shouldDisplayBrowserAction:iter->get()])
491 [self createActionButtonForExtension:iter->get() withIndex:i++];
494 CGFloat width = [self savedWidth];
495 [containerView_ resizeToWidth:width animate:NO];
498 - (void)createActionButtonForExtension:(const Extension*)extension
499 withIndex:(NSUInteger)index {
500 if (!extensions::ExtensionActionManager::Get(profile_)->
501 GetBrowserAction(*extension))
504 if (![self shouldDisplayBrowserAction:extension])
507 if (profile_->IsOffTheRecord())
508 index = toolbarModel_->OriginalIndexToIncognito(index);
510 // Show the container if it's the first button. Otherwise it will be shown
512 if ([self buttonCount] == 0)
513 [containerView_ setHidden:NO];
515 NSRect buttonFrame = NSMakeRect(0.0, kBrowserActionOriginYOffset,
516 kBrowserActionWidth, kBrowserActionHeight);
517 BrowserActionButton* newButton =
518 [[[BrowserActionButton alloc]
519 initWithFrame:buttonFrame
522 tabId:[self currentTabId]] autorelease];
523 [newButton setTarget:self];
524 [newButton setAction:@selector(browserActionClicked:)];
525 NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
528 [buttons_ setObject:newButton forKey:buttonKey];
530 [self positionActionButtonsAndAnimate:NO];
532 [[NSNotificationCenter defaultCenter]
534 selector:@selector(actionButtonDragging:)
535 name:kBrowserActionButtonDraggingNotification
539 [containerView_ setMaxWidth:
540 [self containerWidthWithButtonCount:[self buttonCount]]];
541 [containerView_ setNeedsDisplay:YES];
544 - (void)removeActionButtonForExtension:(const Extension*)extension {
545 if (!extensions::ActionInfo::GetBrowserActionInfo(extension))
548 NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
552 BrowserActionButton* button = [buttons_ objectForKey:buttonKey];
553 // This could be the case in incognito, where only a subset of extensions are
558 [button removeFromSuperview];
559 // It may or may not be hidden, but it won't matter to NSMutableArray either
561 [hiddenButtons_ removeObject:button];
563 [buttons_ removeObjectForKey:buttonKey];
564 if ([self buttonCount] == 0) {
565 // No more buttons? Hide the container.
566 [containerView_ setHidden:YES];
568 [self positionActionButtonsAndAnimate:NO];
570 [containerView_ setMaxWidth:
571 [self containerWidthWithButtonCount:[self buttonCount]]];
572 [containerView_ setNeedsDisplay:YES];
575 - (void)positionActionButtonsAndAnimate:(BOOL)animate {
577 for (ExtensionList::const_iterator iter =
578 toolbarModel_->toolbar_items().begin();
579 iter != toolbarModel_->toolbar_items().end(); ++iter) {
580 if (![self shouldDisplayBrowserAction:iter->get()])
582 BrowserActionButton* button = [self buttonForExtension:(iter->get())];
585 if (![button isBeingDragged])
586 [self moveButton:button toIndex:i animate:animate];
591 - (void)updateButtonOpacity {
592 for (BrowserActionButton* button in [buttons_ allValues]) {
593 NSRect buttonFrame = [button frame];
594 if (NSContainsRect([containerView_ bounds], buttonFrame)) {
595 if ([button alphaValue] != 1.0)
596 [button setAlphaValue:1.0];
600 CGFloat intersectionWidth =
601 NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
602 CGFloat alpha = std::max(static_cast<CGFloat>(0.0),
603 intersectionWidth / NSWidth(buttonFrame));
604 [button setAlphaValue:alpha];
605 [button setNeedsDisplay:YES];
609 - (BrowserActionButton*)buttonForExtension:(const Extension*)extension {
610 NSString* extensionId = base::SysUTF8ToNSString(extension->id());
614 return [buttons_ objectForKey:extensionId];
617 - (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount {
618 // Left-side padding which works regardless of whether a button or
620 CGFloat width = kBrowserActionLeftPadding;
622 // Include the buttons and padding between.
623 if (buttonCount > 0) {
624 width += buttonCount * kBrowserActionWidth;
625 width += (buttonCount - 1) * kBrowserActionButtonPadding;
628 // Make room for the chevron if any buttons are hidden.
629 if ([self buttonCount] != [self visibleButtonCount]) {
630 // Chevron and buttons both include 1px padding w/in their bounds,
631 // so this leaves 2px between the last browser action and chevron,
632 // and also works right if the chevron is the only button.
633 width += kChevronWidth;
639 - (NSUInteger)containerButtonCapacity {
640 // Edge-to-edge span of the browser action buttons.
641 CGFloat actionSpan = [self savedWidth] - kBrowserActionLeftPadding;
643 // Add in some padding for the browser action on the end, then
644 // divide out to get the number of action buttons that fit.
645 return (actionSpan + kBrowserActionButtonPadding) /
646 (kBrowserActionWidth + kBrowserActionButtonPadding);
649 - (void)containerFrameChanged:(NSNotification*)notification {
650 [self updateButtonOpacity];
651 [[containerView_ window] invalidateCursorRectsForView:containerView_];
652 [self updateChevronPositionInFrame:[containerView_ frame]];
655 - (void)containerDragStart:(NSNotification*)notification {
656 [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
657 while([hiddenButtons_ count] > 0) {
658 [containerView_ addSubview:[hiddenButtons_ objectAtIndex:0]];
659 [hiddenButtons_ removeObjectAtIndex:0];
663 - (void)containerDragging:(NSNotification*)notification {
664 [[NSNotificationCenter defaultCenter]
665 postNotificationName:kBrowserActionGrippyDraggingNotification
669 - (void)containerDragFinished:(NSNotification*)notification {
670 for (ExtensionList::const_iterator iter =
671 toolbarModel_->toolbar_items().begin();
672 iter != toolbarModel_->toolbar_items().end(); ++iter) {
673 BrowserActionButton* button = [self buttonForExtension:(iter->get())];
674 NSRect buttonFrame = [button frame];
675 if (NSContainsRect([containerView_ bounds], buttonFrame))
678 CGFloat intersectionWidth =
679 NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
680 // Pad the threshold by 5 pixels in order to have the buttons hide more
682 if (([containerView_ grippyPinned] && intersectionWidth > 0) ||
683 (intersectionWidth <= (NSWidth(buttonFrame) / 2) + 5.0)) {
684 [button setAlphaValue:0.0];
685 [button removeFromSuperview];
686 [hiddenButtons_ addObject:button];
689 [self updateGrippyCursors];
691 if (!profile_->IsOffTheRecord())
692 toolbarModel_->SetVisibleIconCount([self visibleButtonCount]);
694 [[NSNotificationCenter defaultCenter]
695 postNotificationName:kBrowserActionGrippyDragFinishedNotification
699 - (void)actionButtonDragging:(NSNotification*)notification {
700 if (![self chevronIsHidden])
701 [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
703 // Determine what index the dragged button should lie in, alter the model and
704 // reposition the buttons.
705 CGFloat dragThreshold = std::floor(kBrowserActionWidth / 2);
706 BrowserActionButton* draggedButton = [notification object];
707 NSRect draggedButtonFrame = [draggedButton frame];
709 NSUInteger index = 0;
710 for (ExtensionList::const_iterator iter =
711 toolbarModel_->toolbar_items().begin();
712 iter != toolbarModel_->toolbar_items().end(); ++iter) {
713 BrowserActionButton* button = [self buttonForExtension:(iter->get())];
714 CGFloat intersectionWidth =
715 NSWidth(NSIntersectionRect(draggedButtonFrame, [button frame]));
717 if (intersectionWidth > dragThreshold && button != draggedButton &&
718 ![button isAnimating] && index < [self visibleButtonCount]) {
719 toolbarModel_->MoveBrowserAction([draggedButton extension], index);
720 [self positionActionButtonsAndAnimate:YES];
727 - (void)actionButtonDragFinished:(NSNotification*)notification {
728 [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:YES];
729 [self positionActionButtonsAndAnimate:YES];
732 - (void)moveButton:(BrowserActionButton*)button
733 toIndex:(NSUInteger)index
734 animate:(BOOL)animate {
735 CGFloat xOffset = kBrowserActionLeftPadding +
736 (index * (kBrowserActionWidth + kBrowserActionButtonPadding));
737 NSRect buttonFrame = [button frame];
738 buttonFrame.origin.x = xOffset;
739 [button setFrame:buttonFrame animate:animate];
741 if (index < [self containerButtonCapacity]) {
742 // Make sure the button is within the visible container.
743 if ([button superview] != containerView_) {
744 [containerView_ addSubview:button];
745 [button setAlphaValue:1.0];
746 [hiddenButtons_ removeObjectIdenticalTo:button];
748 } else if (![hiddenButtons_ containsObject:button]) {
749 [hiddenButtons_ addObject:button];
750 [button removeFromSuperview];
751 [button setAlphaValue:0.0];
755 - (BOOL)browserActionClicked:(BrowserActionButton*)button
756 shouldGrant:(BOOL)shouldGrant {
757 const Extension* extension = [button extension];
759 switch (toolbarModel_->ExecuteBrowserAction(extension, browser_, &popupUrl,
761 case ExtensionToolbarModel::ACTION_NONE:
763 case ExtensionToolbarModel::ACTION_SHOW_POPUP: {
764 NSPoint arrowPoint = [self popupPointForBrowserAction:extension];
765 [ExtensionPopupController showURL:popupUrl
767 anchoredAt:arrowPoint
768 arrowLocation:info_bubble::kTopRight
776 - (BOOL)browserActionClicked:(BrowserActionButton*)button {
777 return [self browserActionClicked:button
781 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension {
782 // Only display incognito-enabled extensions while in incognito mode.
784 (!profile_->IsOffTheRecord() ||
785 extension_util::IsIncognitoEnabled(
787 extensions::ExtensionSystem::Get(profile_)->extension_service()));
790 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate {
791 [self setChevronHidden:([self buttonCount] == [self visibleButtonCount])
796 - (void)updateChevronPositionInFrame:(NSRect)frame {
797 CGFloat xPos = NSWidth(frame) - kChevronWidth;
798 NSRect buttonFrame = NSMakeRect(xPos,
799 kBrowserActionOriginYOffset,
801 kBrowserActionHeight);
802 [chevronMenuButton_ setFrame:buttonFrame];
805 - (void)setChevronHidden:(BOOL)hidden
806 inFrame:(NSRect)frame
807 animate:(BOOL)animate {
808 if (hidden == [self chevronIsHidden])
811 if (!chevronMenuButton_.get()) {
812 chevronMenuButton_.reset([[MenuButton alloc] init]);
813 [chevronMenuButton_ setOpenMenuOnClick:YES];
814 [chevronMenuButton_ setBordered:NO];
815 [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
817 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW
818 forButtonState:image_button_cell::kDefaultState];
819 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_H
820 forButtonState:image_button_cell::kHoverState];
821 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_P
822 forButtonState:image_button_cell::kPressedState];
824 overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
825 [overflowMenu_ setAutoenablesItems:NO];
826 [overflowMenu_ setDelegate:self];
827 [chevronMenuButton_ setAttachedMenu:overflowMenu_];
829 [containerView_ addSubview:chevronMenuButton_];
832 [self updateChevronPositionInFrame:frame];
834 // Stop any running animation.
835 [chevronAnimation_ stopAnimation];
838 [chevronMenuButton_ setHidden:hidden];
842 NSDictionary* animationDictionary;
844 animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
845 chevronMenuButton_.get(), NSViewAnimationTargetKey,
846 NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey,
849 [chevronMenuButton_ setHidden:NO];
850 animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
851 chevronMenuButton_.get(), NSViewAnimationTargetKey,
852 NSViewAnimationFadeInEffect, NSViewAnimationEffectKey,
855 [chevronAnimation_ setViewAnimations:
856 [NSArray arrayWithObject:animationDictionary]];
857 [chevronAnimation_ startAnimation];
860 - (void)chevronItemSelected:(id)menuItem {
861 [self browserActionClicked:[menuItem representedObject]];
864 - (void)updateGrippyCursors {
865 [containerView_ setCanDragLeft:[hiddenButtons_ count] > 0];
866 [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
867 [[containerView_ window] invalidateCursorRectsForView:containerView_];
870 - (int)currentTabId {
871 content::WebContents* active_tab =
872 browser_->tab_strip_model()->GetActiveWebContents();
876 return SessionTabHelper::FromWebContents(active_tab)->session_id().id();
880 #pragma mark Testing Methods
882 - (NSButton*)buttonWithIndex:(NSUInteger)index {
883 if (profile_->IsOffTheRecord())
884 index = toolbarModel_->IncognitoIndexToOriginal(index);
885 const extensions::ExtensionList& toolbar_items =
886 toolbarModel_->toolbar_items();
887 if (index < toolbar_items.size()) {
888 const Extension* extension = toolbar_items[index].get();
889 return [buttons_ objectForKey:base::SysUTF8ToNSString(extension->id())];