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 "grit/theme_resources.h"
36 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h"
38 using extensions::Extension;
39 using extensions::ExtensionList;
41 NSString* const kBrowserActionVisibilityChangedNotification =
42 @"BrowserActionVisibilityChangedNotification";
45 const CGFloat kAnimationDuration = 0.2;
47 const CGFloat kChevronWidth = 18;
49 // Since the container is the maximum height of the toolbar, we have
50 // to move the buttons up by this amount in order to have them look
51 // vertically centered within the toolbar.
52 const CGFloat kBrowserActionOriginYOffset = 5.0;
54 // The size of each button on the toolbar.
55 const CGFloat kBrowserActionHeight = 29.0;
56 const CGFloat kBrowserActionWidth = 29.0;
58 // The padding between browser action buttons.
59 const CGFloat kBrowserActionButtonPadding = 2.0;
61 // Padding between Omnibox and first button. Since the buttons have a
62 // pixel of internal padding, this needs an extra pixel.
63 const CGFloat kBrowserActionLeftPadding = kBrowserActionButtonPadding + 1.0;
65 // How far to inset from the bottom of the view to get the top border
66 // of the popup 2px below the bottom of the Omnibox.
67 const CGFloat kBrowserActionBubbleYOffset = 3.0;
71 @interface BrowserActionsController(Private)
72 // Used during initialization to create the BrowserActionButton objects from the
73 // stored toolbar model.
74 - (void)createButtons;
76 // Creates and then adds the given extension's action button to the container
77 // at the given index within the container. It does not affect the toolbar model
78 // object since it is called when the toolbar model changes.
79 - (void)createActionButtonForExtension:(const Extension*)extension
80 withIndex:(NSUInteger)index;
82 // Removes an action button for the given extension from the container. This
83 // method also does not affect the underlying toolbar model since it is called
84 // when the toolbar model changes.
85 - (void)removeActionButtonForExtension:(const Extension*)extension;
87 // Useful in the case of a Browser Action being added/removed from the middle of
88 // the container, this method repositions each button according to the current
90 - (void)positionActionButtonsAndAnimate:(BOOL)animate;
92 // During container resizing, buttons become more transparent as they are pushed
93 // off the screen. This method updates each button's opacity determined by the
94 // position of the button.
95 - (void)updateButtonOpacity;
97 // Returns the existing button with the given extension backing it; nil if it
98 // cannot be found or the extension's ID is invalid.
99 - (BrowserActionButton*)buttonForExtension:(const Extension*)extension;
101 // Returns the preferred width of the container given the number of visible
102 // buttons |buttonCount|.
103 - (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount;
105 // Returns the number of buttons that can fit in the container according to its
107 - (NSUInteger)containerButtonCapacity;
109 // Notification handlers for events registered by the class.
111 // Updates each button's opacity, the cursor rects and chevron position.
112 - (void)containerFrameChanged:(NSNotification*)notification;
114 // Hides the chevron and unhides every hidden button so that dragging the
115 // container out smoothly shows the Browser Action buttons.
116 - (void)containerDragStart:(NSNotification*)notification;
118 // Sends a notification for the toolbar to reposition surrounding UI elements.
119 - (void)containerDragging:(NSNotification*)notification;
121 // Determines which buttons need to be hidden based on the new size, hides them
122 // and updates the chevron overflow menu. Also fires a notification to let the
123 // toolbar know that the drag has finished.
124 - (void)containerDragFinished:(NSNotification*)notification;
126 // Adjusts the position of the surrounding action buttons depending on where the
127 // button is within the container.
128 - (void)actionButtonDragging:(NSNotification*)notification;
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
133 - (void)actionButtonDragFinished:(NSNotification*)notification;
135 // Moves the given button both visually and within the toolbar model to the
137 - (void)moveButton:(BrowserActionButton*)button
138 toIndex:(NSUInteger)index
139 animate:(BOOL)animate;
141 // Handles when the given BrowserActionButton object is clicked and whether
142 // it should grant tab permissions. API-simulated clicks should not grant.
143 - (BOOL)browserActionClicked:(BrowserActionButton*)button
144 shouldGrant:(BOOL)shouldGrant;
145 - (BOOL)browserActionClicked:(BrowserActionButton*)button;
147 // Returns whether the given extension should be displayed. Only displays
148 // incognito-enabled extensions in incognito mode. Otherwise returns YES.
149 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension;
151 // The reason |frame| is specified in these chevron functions is because the
152 // container may be animating and the end frame of the animation should be
153 // passed instead of the current frame (which may be off and cause the chevron
154 // to jump at the end of its animation).
156 // Shows the overflow chevron button depending on whether there are any hidden
157 // extensions within the frame given.
158 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate;
160 // Moves the chevron to its correct position within |frame|.
161 - (void)updateChevronPositionInFrame:(NSRect)frame;
163 // Shows or hides the chevron, animating as specified by |animate|.
164 - (void)setChevronHidden:(BOOL)hidden
165 inFrame:(NSRect)frame
166 animate:(BOOL)animate;
168 // Handles when a menu item within the chevron overflow menu is selected.
169 - (void)chevronItemSelected:(id)menuItem;
171 // Updates the container's grippy cursor based on the number of hidden buttons.
172 - (void)updateGrippyCursors;
174 // Returns the ID of the currently selected tab or -1 if none exists.
178 // A helper class to proxy extension notifications to the view controller's
179 // appropriate methods.
180 class ExtensionServiceObserverBridge : public content::NotificationObserver,
181 public ExtensionToolbarModel::Observer {
183 ExtensionServiceObserverBridge(BrowserActionsController* owner,
185 : owner_(owner), browser_(browser) {
186 registrar_.Add(this, chrome::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE,
187 content::Source<Profile>(browser->profile()));
189 chrome::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 chrome::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE: {
200 ExtensionPopupController* popup = [ExtensionPopupController popup];
201 if (popup && ![popup isClosing])
206 case chrome::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 ExtensionService* service = browser_->profile()->GetExtensionService();
217 const Extension* extension = service->GetExtensionById(extension_id,
221 BrowserActionButton* button = [owner_ buttonForExtension:extension];
222 // |button| can be nil when the browser action has its button hidden.
224 [owner_ browserActionClicked:button];
228 NOTREACHED() << L"Unexpected notification";
232 // ExtensionToolbarModel::Observer implementation.
233 virtual void BrowserActionAdded(
234 const Extension* extension,
235 int index) OVERRIDE {
236 [owner_ createActionButtonForExtension:extension withIndex:index];
237 [owner_ resizeContainerAndAnimate:NO];
240 virtual void BrowserActionRemoved(const Extension* extension) OVERRIDE {
241 [owner_ removeActionButtonForExtension:extension];
242 [owner_ resizeContainerAndAnimate:NO];
245 virtual bool BrowserActionShowPopup(const Extension* extension) OVERRIDE {
246 // Do not override other popups and only show in active window.
247 ExtensionPopupController* popup = [ExtensionPopupController popup];
248 if (popup || !browser_->window()->IsActive())
251 BrowserActionButton* button = [owner_ buttonForExtension:extension];
252 return button && [owner_ browserActionClicked:button
257 // The object we need to inform when we get a notification. Weak. Owns us.
258 BrowserActionsController* owner_;
260 // The browser we listen for events from. Weak.
263 // Used for registering to receive notifications and automatic clean up.
264 content::NotificationRegistrar registrar_;
266 DISALLOW_COPY_AND_ASSIGN(ExtensionServiceObserverBridge);
269 @implementation BrowserActionsController
271 @synthesize containerView = containerView_;
274 #pragma mark Public Methods
276 - (id)initWithBrowser:(Browser*)browser
277 containerView:(BrowserActionsContainerView*)container {
278 DCHECK(browser && container);
280 if ((self = [super init])) {
282 profile_ = browser->profile();
284 observer_.reset(new ExtensionServiceObserverBridge(self, browser_));
285 ExtensionService* extensionService =
286 extensions::ExtensionSystem::Get(profile_)->extension_service();
287 // |extensionService| can be NULL in Incognito.
288 if (extensionService) {
289 toolbarModel_ = extensionService->toolbar_model();
290 toolbarModel_->AddObserver(observer_.get());
293 containerView_ = container;
294 [containerView_ setPostsFrameChangedNotifications:YES];
295 [[NSNotificationCenter defaultCenter]
297 selector:@selector(containerFrameChanged:)
298 name:NSViewFrameDidChangeNotification
299 object:containerView_];
300 [[NSNotificationCenter defaultCenter]
302 selector:@selector(containerDragStart:)
303 name:kBrowserActionGrippyDragStartedNotification
304 object:containerView_];
305 [[NSNotificationCenter defaultCenter]
307 selector:@selector(containerDragging:)
308 name:kBrowserActionGrippyDraggingNotification
309 object:containerView_];
310 [[NSNotificationCenter defaultCenter]
312 selector:@selector(containerDragFinished:)
313 name:kBrowserActionGrippyDragFinishedNotification
314 object:containerView_];
315 // Listen for a finished drag from any button to make sure each open window
317 [[NSNotificationCenter defaultCenter]
319 selector:@selector(actionButtonDragFinished:)
320 name:kBrowserActionButtonDragEndNotification
323 chevronAnimation_.reset([[NSViewAnimation alloc] init]);
324 [chevronAnimation_ gtm_setDuration:kAnimationDuration
325 eventMask:NSLeftMouseUpMask];
326 [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
328 hiddenButtons_.reset([[NSMutableArray alloc] init]);
329 buttons_.reset([[NSMutableDictionary alloc] init]);
330 [self createButtons];
331 [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:NO];
332 [self updateGrippyCursors];
333 [container setResizable:!profile_->IsOffTheRecord()];
341 toolbarModel_->RemoveObserver(observer_.get());
343 [[NSNotificationCenter defaultCenter] removeObserver:self];
348 for (BrowserActionButton* button in [buttons_ allValues]) {
349 [button setTabId:[self currentTabId]];
350 [button updateState];
354 - (NSUInteger)buttonCount {
355 return [buttons_ count];
358 - (NSUInteger)visibleButtonCount {
359 return [self buttonCount] - [hiddenButtons_ count];
362 - (void)resizeContainerAndAnimate:(BOOL)animate {
363 int iconCount = toolbarModel_->GetVisibleIconCount();
364 if (iconCount < 0) // If no buttons are hidden.
365 iconCount = [self buttonCount];
367 [containerView_ resizeToWidth:[self containerWidthWithButtonCount:iconCount]
369 NSRect frame = animate ? [containerView_ animationEndFrame] :
370 [containerView_ frame];
372 [self showChevronIfNecessaryInFrame:frame animate:animate];
375 [[NSNotificationCenter defaultCenter]
376 postNotificationName:kBrowserActionVisibilityChangedNotification
381 - (NSView*)browserActionViewForExtension:(const Extension*)extension {
382 for (BrowserActionButton* button in [buttons_ allValues]) {
383 if ([button extension] == extension)
390 - (CGFloat)savedWidth {
393 if (!profile_->GetPrefs()->HasPrefPath(prefs::kExtensionToolbarSize)) {
394 // Migration code to the new VisibleIconCount pref.
395 // TODO(mpcomplete): remove this at some point.
396 double predefinedWidth =
397 profile_->GetPrefs()->GetDouble(prefs::kBrowserActionContainerWidth);
398 if (predefinedWidth != 0) {
399 int iconWidth = kBrowserActionWidth + kBrowserActionButtonPadding;
400 int extraWidth = kChevronWidth;
401 toolbarModel_->SetVisibleIconCount(
402 (predefinedWidth - extraWidth) / iconWidth);
406 int savedButtonCount = toolbarModel_->GetVisibleIconCount();
407 if (savedButtonCount < 0 || // all icons are visible
408 static_cast<NSUInteger>(savedButtonCount) > [self buttonCount])
409 savedButtonCount = [self buttonCount];
410 return [self containerWidthWithButtonCount:savedButtonCount];
413 - (NSPoint)popupPointForBrowserAction:(const Extension*)extension {
414 if (!extensions::ExtensionActionManager::Get(profile_)->
415 GetBrowserAction(*extension)) {
419 NSButton* button = [self buttonForExtension:extension];
423 if ([hiddenButtons_ containsObject:button])
424 button = chevronMenuButton_.get();
426 // Anchor point just above the center of the bottom.
427 const NSRect bounds = [button bounds];
428 DCHECK([button isFlipped]);
429 NSPoint anchor = NSMakePoint(NSMidX(bounds),
430 NSMaxY(bounds) - kBrowserActionBubbleYOffset);
431 return [button convertPoint:anchor toView:nil];
434 - (BOOL)chevronIsHidden {
435 if (!chevronMenuButton_.get())
438 if (![chevronAnimation_ isAnimating])
439 return [chevronMenuButton_ isHidden];
441 DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
443 // The chevron is animating in or out. Determine which one and have the return
444 // value reflect where the animation is headed.
445 NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
446 valueForKey:NSViewAnimationEffectKey];
447 if (effect == NSViewAnimationFadeInEffect) {
449 } else if (effect == NSViewAnimationFadeOutEffect) {
458 #pragma mark NSMenuDelegate
460 - (void)menuNeedsUpdate:(NSMenu*)menu {
461 [menu removeAllItems];
463 // See menu_button.h for documentation on why this is needed.
464 [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
466 for (BrowserActionButton* button in hiddenButtons_.get()) {
467 NSString* name = base::SysUTF8ToNSString([button extension]->name());
469 [menu addItemWithTitle:name
470 action:@selector(chevronItemSelected:)
472 [item setRepresentedObject:button];
473 [item setImage:[button compositedImage]];
474 [item setTarget:self];
475 [item setEnabled:[button isEnabled]];
480 #pragma mark Private Methods
482 - (void)createButtons {
487 for (ExtensionList::const_iterator iter =
488 toolbarModel_->toolbar_items().begin();
489 iter != toolbarModel_->toolbar_items().end(); ++iter) {
490 if (![self shouldDisplayBrowserAction:iter->get()])
493 [self createActionButtonForExtension:iter->get() withIndex:i++];
496 CGFloat width = [self savedWidth];
497 [containerView_ resizeToWidth:width animate:NO];
500 - (void)createActionButtonForExtension:(const Extension*)extension
501 withIndex:(NSUInteger)index {
502 if (!extensions::ExtensionActionManager::Get(profile_)->
503 GetBrowserAction(*extension))
506 if (![self shouldDisplayBrowserAction:extension])
509 if (profile_->IsOffTheRecord())
510 index = toolbarModel_->OriginalIndexToIncognito(index);
512 // Show the container if it's the first button. Otherwise it will be shown
514 if ([self buttonCount] == 0)
515 [containerView_ setHidden:NO];
517 NSRect buttonFrame = NSMakeRect(0.0, kBrowserActionOriginYOffset,
518 kBrowserActionWidth, kBrowserActionHeight);
519 BrowserActionButton* newButton =
520 [[[BrowserActionButton alloc]
521 initWithFrame:buttonFrame
524 tabId:[self currentTabId]] autorelease];
525 [newButton setTarget:self];
526 [newButton setAction:@selector(browserActionClicked:)];
527 NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
530 [buttons_ setObject:newButton forKey:buttonKey];
532 [self positionActionButtonsAndAnimate:NO];
534 [[NSNotificationCenter defaultCenter]
536 selector:@selector(actionButtonDragging:)
537 name:kBrowserActionButtonDraggingNotification
541 [containerView_ setMaxWidth:
542 [self containerWidthWithButtonCount:[self buttonCount]]];
543 [containerView_ setNeedsDisplay:YES];
546 - (void)removeActionButtonForExtension:(const Extension*)extension {
547 if (!extensions::ActionInfo::GetBrowserActionInfo(extension))
550 NSString* buttonKey = base::SysUTF8ToNSString(extension->id());
554 BrowserActionButton* button = [buttons_ objectForKey:buttonKey];
555 // This could be the case in incognito, where only a subset of extensions are
560 [button removeFromSuperview];
561 // It may or may not be hidden, but it won't matter to NSMutableArray either
563 [hiddenButtons_ removeObject:button];
565 [buttons_ removeObjectForKey:buttonKey];
566 if ([self buttonCount] == 0) {
567 // No more buttons? Hide the container.
568 [containerView_ setHidden:YES];
570 [self positionActionButtonsAndAnimate:NO];
572 [containerView_ setMaxWidth:
573 [self containerWidthWithButtonCount:[self buttonCount]]];
574 [containerView_ setNeedsDisplay:YES];
577 - (void)positionActionButtonsAndAnimate:(BOOL)animate {
579 for (ExtensionList::const_iterator iter =
580 toolbarModel_->toolbar_items().begin();
581 iter != toolbarModel_->toolbar_items().end(); ++iter) {
582 if (![self shouldDisplayBrowserAction:iter->get()])
584 BrowserActionButton* button = [self buttonForExtension:(iter->get())];
587 if (![button isBeingDragged])
588 [self moveButton:button toIndex:i animate:animate];
593 - (void)updateButtonOpacity {
594 for (BrowserActionButton* button in [buttons_ allValues]) {
595 NSRect buttonFrame = [button frame];
596 if (NSContainsRect([containerView_ bounds], buttonFrame)) {
597 if ([button alphaValue] != 1.0)
598 [button setAlphaValue:1.0];
602 CGFloat intersectionWidth =
603 NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
604 CGFloat alpha = std::max(static_cast<CGFloat>(0.0),
605 intersectionWidth / NSWidth(buttonFrame));
606 [button setAlphaValue:alpha];
607 [button setNeedsDisplay:YES];
611 - (BrowserActionButton*)buttonForExtension:(const Extension*)extension {
612 NSString* extensionId = base::SysUTF8ToNSString(extension->id());
616 return [buttons_ objectForKey:extensionId];
619 - (CGFloat)containerWidthWithButtonCount:(NSUInteger)buttonCount {
620 // Left-side padding which works regardless of whether a button or
622 CGFloat width = kBrowserActionLeftPadding;
624 // Include the buttons and padding between.
625 if (buttonCount > 0) {
626 width += buttonCount * kBrowserActionWidth;
627 width += (buttonCount - 1) * kBrowserActionButtonPadding;
630 // Make room for the chevron if any buttons are hidden.
631 if ([self buttonCount] != [self visibleButtonCount]) {
632 // Chevron and buttons both include 1px padding w/in their bounds,
633 // so this leaves 2px between the last browser action and chevron,
634 // and also works right if the chevron is the only button.
635 width += kChevronWidth;
641 - (NSUInteger)containerButtonCapacity {
642 // Edge-to-edge span of the browser action buttons.
643 CGFloat actionSpan = [self savedWidth] - kBrowserActionLeftPadding;
645 // Add in some padding for the browser action on the end, then
646 // divide out to get the number of action buttons that fit.
647 return (actionSpan + kBrowserActionButtonPadding) /
648 (kBrowserActionWidth + kBrowserActionButtonPadding);
651 - (void)containerFrameChanged:(NSNotification*)notification {
652 [self updateButtonOpacity];
653 [[containerView_ window] invalidateCursorRectsForView:containerView_];
654 [self updateChevronPositionInFrame:[containerView_ frame]];
657 - (void)containerDragStart:(NSNotification*)notification {
658 [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
659 while([hiddenButtons_ count] > 0) {
660 [containerView_ addSubview:[hiddenButtons_ objectAtIndex:0]];
661 [hiddenButtons_ removeObjectAtIndex:0];
665 - (void)containerDragging:(NSNotification*)notification {
666 [[NSNotificationCenter defaultCenter]
667 postNotificationName:kBrowserActionGrippyDraggingNotification
671 - (void)containerDragFinished:(NSNotification*)notification {
672 for (ExtensionList::const_iterator iter =
673 toolbarModel_->toolbar_items().begin();
674 iter != toolbarModel_->toolbar_items().end(); ++iter) {
675 BrowserActionButton* button = [self buttonForExtension:(iter->get())];
676 NSRect buttonFrame = [button frame];
677 if (NSContainsRect([containerView_ bounds], buttonFrame))
680 CGFloat intersectionWidth =
681 NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
682 // Pad the threshold by 5 pixels in order to have the buttons hide more
684 if (([containerView_ grippyPinned] && intersectionWidth > 0) ||
685 (intersectionWidth <= (NSWidth(buttonFrame) / 2) + 5.0)) {
686 [button setAlphaValue:0.0];
687 [button removeFromSuperview];
688 [hiddenButtons_ addObject:button];
691 [self updateGrippyCursors];
693 if (!profile_->IsOffTheRecord())
694 toolbarModel_->SetVisibleIconCount([self visibleButtonCount]);
696 [[NSNotificationCenter defaultCenter]
697 postNotificationName:kBrowserActionGrippyDragFinishedNotification
701 - (void)actionButtonDragging:(NSNotification*)notification {
702 if (![self chevronIsHidden])
703 [self setChevronHidden:YES inFrame:[containerView_ frame] animate:YES];
705 // Determine what index the dragged button should lie in, alter the model and
706 // reposition the buttons.
707 CGFloat dragThreshold = std::floor(kBrowserActionWidth / 2);
708 BrowserActionButton* draggedButton = [notification object];
709 NSRect draggedButtonFrame = [draggedButton frame];
711 NSUInteger index = 0;
712 for (ExtensionList::const_iterator iter =
713 toolbarModel_->toolbar_items().begin();
714 iter != toolbarModel_->toolbar_items().end(); ++iter) {
715 BrowserActionButton* button = [self buttonForExtension:(iter->get())];
716 CGFloat intersectionWidth =
717 NSWidth(NSIntersectionRect(draggedButtonFrame, [button frame]));
719 if (intersectionWidth > dragThreshold && button != draggedButton &&
720 ![button isAnimating] && index < [self visibleButtonCount]) {
721 toolbarModel_->MoveBrowserAction([draggedButton extension], index);
722 [self positionActionButtonsAndAnimate:YES];
729 - (void)actionButtonDragFinished:(NSNotification*)notification {
730 [self showChevronIfNecessaryInFrame:[containerView_ frame] animate:YES];
731 [self positionActionButtonsAndAnimate:YES];
734 - (void)moveButton:(BrowserActionButton*)button
735 toIndex:(NSUInteger)index
736 animate:(BOOL)animate {
737 CGFloat xOffset = kBrowserActionLeftPadding +
738 (index * (kBrowserActionWidth + kBrowserActionButtonPadding));
739 NSRect buttonFrame = [button frame];
740 buttonFrame.origin.x = xOffset;
741 [button setFrame:buttonFrame animate:animate];
743 if (index < [self containerButtonCapacity]) {
744 // Make sure the button is within the visible container.
745 if ([button superview] != containerView_) {
746 [containerView_ addSubview:button];
747 [button setAlphaValue:1.0];
748 [hiddenButtons_ removeObjectIdenticalTo:button];
750 } else if (![hiddenButtons_ containsObject:button]) {
751 [hiddenButtons_ addObject:button];
752 [button removeFromSuperview];
753 [button setAlphaValue:0.0];
757 - (BOOL)browserActionClicked:(BrowserActionButton*)button
758 shouldGrant:(BOOL)shouldGrant {
759 const Extension* extension = [button extension];
761 switch (toolbarModel_->ExecuteBrowserAction(extension, browser_, &popupUrl,
763 case ExtensionToolbarModel::ACTION_NONE:
765 case ExtensionToolbarModel::ACTION_SHOW_POPUP: {
766 NSPoint arrowPoint = [self popupPointForBrowserAction:extension];
767 [ExtensionPopupController showURL:popupUrl
769 anchoredAt:arrowPoint
770 arrowLocation:info_bubble::kTopRight
778 - (BOOL)browserActionClicked:(BrowserActionButton*)button {
779 return [self browserActionClicked:button
783 - (BOOL)shouldDisplayBrowserAction:(const Extension*)extension {
784 // Only display incognito-enabled extensions while in incognito mode.
786 (!profile_->IsOffTheRecord() ||
787 extension_util::IsIncognitoEnabled(
789 extensions::ExtensionSystem::Get(profile_)->extension_service()));
792 - (void)showChevronIfNecessaryInFrame:(NSRect)frame animate:(BOOL)animate {
793 [self setChevronHidden:([self buttonCount] == [self visibleButtonCount])
798 - (void)updateChevronPositionInFrame:(NSRect)frame {
799 CGFloat xPos = NSWidth(frame) - kChevronWidth;
800 NSRect buttonFrame = NSMakeRect(xPos,
801 kBrowserActionOriginYOffset,
803 kBrowserActionHeight);
804 [chevronMenuButton_ setFrame:buttonFrame];
807 - (void)setChevronHidden:(BOOL)hidden
808 inFrame:(NSRect)frame
809 animate:(BOOL)animate {
810 if (hidden == [self chevronIsHidden])
813 if (!chevronMenuButton_.get()) {
814 chevronMenuButton_.reset([[MenuButton alloc] init]);
815 [chevronMenuButton_ setOpenMenuOnClick:YES];
816 [chevronMenuButton_ setBordered:NO];
817 [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
819 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW
820 forButtonState:image_button_cell::kDefaultState];
821 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_H
822 forButtonState:image_button_cell::kHoverState];
823 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_P
824 forButtonState:image_button_cell::kPressedState];
826 overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
827 [overflowMenu_ setAutoenablesItems:NO];
828 [overflowMenu_ setDelegate:self];
829 [chevronMenuButton_ setAttachedMenu:overflowMenu_];
831 [containerView_ addSubview:chevronMenuButton_];
834 [self updateChevronPositionInFrame:frame];
836 // Stop any running animation.
837 [chevronAnimation_ stopAnimation];
840 [chevronMenuButton_ setHidden:hidden];
844 NSDictionary* animationDictionary;
846 animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
847 chevronMenuButton_.get(), NSViewAnimationTargetKey,
848 NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey,
851 [chevronMenuButton_ setHidden:NO];
852 animationDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
853 chevronMenuButton_.get(), NSViewAnimationTargetKey,
854 NSViewAnimationFadeInEffect, NSViewAnimationEffectKey,
857 [chevronAnimation_ setViewAnimations:
858 [NSArray arrayWithObject:animationDictionary]];
859 [chevronAnimation_ startAnimation];
862 - (void)chevronItemSelected:(id)menuItem {
863 [self browserActionClicked:[menuItem representedObject]];
866 - (void)updateGrippyCursors {
867 [containerView_ setCanDragLeft:[hiddenButtons_ count] > 0];
868 [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
869 [[containerView_ window] invalidateCursorRectsForView:containerView_];
872 - (int)currentTabId {
873 content::WebContents* active_tab =
874 browser_->tab_strip_model()->GetActiveWebContents();
878 return SessionTabHelper::FromWebContents(active_tab)->session_id().id();
882 #pragma mark Testing Methods
884 - (NSButton*)buttonWithIndex:(NSUInteger)index {
885 if (profile_->IsOffTheRecord())
886 index = toolbarModel_->IncognitoIndexToOriginal(index);
887 const extensions::ExtensionList& toolbar_items =
888 toolbarModel_->toolbar_items();
889 if (index < toolbar_items.size()) {
890 const Extension* extension = toolbar_items[index].get();
891 return [buttons_ objectForKey:base::SysUTF8ToNSString(extension->id())];