[Extensions Toolbar] Add a bubble to educate users about the new toolbar
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / browser_actions_controller.mm
blobf55d9fd548d978811a87840ee70dbb93d9c19f7a
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"
7 #include <string>
9 #include "base/strings/sys_string_conversions.h"
10 #include "chrome/browser/ui/browser.h"
11 #include "chrome/browser/ui/browser_window.h"
12 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
13 #import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
14 #import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
15 #import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
16 #import "chrome/browser/ui/cocoa/extensions/extension_toolbar_icon_surfacing_bubble_mac.h"
17 #import "chrome/browser/ui/cocoa/image_button_cell.h"
18 #import "chrome/browser/ui/cocoa/menu_button.h"
19 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
20 #include "chrome/browser/ui/tabs/tab_strip_model.h"
21 #include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
22 #include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
23 #include "chrome/browser/ui/toolbar/toolbar_actions_bar_delegate.h"
24 #include "grit/theme_resources.h"
25 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
27 NSString* const kBrowserActionVisibilityChangedNotification =
28     @"BrowserActionVisibilityChangedNotification";
30 namespace {
32 const CGFloat kAnimationDuration = 0.2;
34 const CGFloat kChevronWidth = 18;
36 // How far to inset from the bottom of the view to get the top border
37 // of the popup 2px below the bottom of the Omnibox.
38 const CGFloat kBrowserActionBubbleYOffset = 3.0;
40 }  // namespace
42 @interface BrowserActionsController(Private)
44 // Creates and adds a view for the given |action| at |index|.
45 - (void)addViewForAction:(ToolbarActionViewController*)action
46                withIndex:(NSUInteger)index;
48 // Removes the view for the given |action| from the ccontainer.
49 - (void)removeViewForAction:(ToolbarActionViewController*)action;
51 // Removes views for all actions.
52 - (void)removeAllViews;
54 // Redraws the BrowserActionsContainerView and updates the button order to match
55 // the order in the ToolbarActionsBar.
56 - (void)redraw;
58 // Resizes the container to the specified |width|, and animates according to
59 // the ToolbarActionsBar.
60 - (void)resizeContainerToWidth:(CGFloat)width;
62 // Sets the container to be either hidden or visible based on whether there are
63 // any actions to show.
64 // Returns whether the container is visible.
65 - (BOOL)updateContainerVisibility;
67 // During container resizing, buttons become more transparent as they are pushed
68 // off the screen. This method updates each button's opacity determined by the
69 // position of the button.
70 - (void)updateButtonOpacity;
72 // When the container is resizing, there's a chance that the buttons' frames
73 // need to be adjusted (for instance, if an action is added to the left, the
74 // frames of the actions to the right should gradually move right in the
75 // container). Adjust the frames accordingly.
76 - (void)updateButtonPositions;
78 // Returns the existing button associated with the given id; nil if it cannot be
79 // found.
80 - (BrowserActionButton*)buttonForId:(const std::string&)id;
82 // Notification handlers for events registered by the class.
84 // Updates each button's opacity, the cursor rects and chevron position.
85 - (void)containerFrameChanged:(NSNotification*)notification;
87 // Hides the chevron and unhides every hidden button so that dragging the
88 // container out smoothly shows the Browser Action buttons.
89 - (void)containerDragStart:(NSNotification*)notification;
91 // Determines which buttons need to be hidden based on the new size, hides them
92 // and updates the chevron overflow menu. Also fires a notification to let the
93 // toolbar know that the drag has finished.
94 - (void)containerDragFinished:(NSNotification*)notification;
96 // Shows the toolbar info bubble, if it should be displayed.
97 - (void)containerMouseEntered:(NSNotification*)notification;
99 // Adjusts the position of the surrounding action buttons depending on where the
100 // button is within the container.
101 - (void)actionButtonDragging:(NSNotification*)notification;
103 // Updates the position of the Browser Actions within the container. This fires
104 // when _any_ Browser Action button is done dragging to keep all open windows in
105 // sync visually.
106 - (void)actionButtonDragFinished:(NSNotification*)notification;
108 // Returns the frame that the button with the given |index| should have.
109 - (NSRect)frameForIndex:(NSUInteger)index;
111 // Moves the given button both visually and within the toolbar model to the
112 // specified index.
113 - (void)moveButton:(BrowserActionButton*)button
114            toIndex:(NSUInteger)index;
116 // Handles clicks for BrowserActionButtons.
117 - (BOOL)browserActionClicked:(BrowserActionButton*)button;
119 // The reason |frame| is specified in these chevron functions is because the
120 // container may be animating and the end frame of the animation should be
121 // passed instead of the current frame (which may be off and cause the chevron
122 // to jump at the end of its animation).
124 // Shows the overflow chevron button depending on whether there are any hidden
125 // extensions within the frame given.
126 - (void)showChevronIfNecessaryInFrame:(NSRect)frame;
128 // Moves the chevron to its correct position within |frame|.
129 - (void)updateChevronPositionInFrame:(NSRect)frame;
131 // Shows or hides the chevron in the given |frame|.
132 - (void)setChevronHidden:(BOOL)hidden
133                  inFrame:(NSRect)frame;
135 // Handles when a menu item within the chevron overflow menu is selected.
136 - (void)chevronItemSelected:(id)menuItem;
138 // Updates the container's grippy cursor based on the number of hidden buttons.
139 - (void)updateGrippyCursors;
141 // Returns the associated ToolbarController.
142 - (ToolbarController*)toolbarController;
144 @end
146 namespace {
148 // A bridge between the ToolbarActionsBar and the BrowserActionsController.
149 class ToolbarActionsBarBridge : public ToolbarActionsBarDelegate {
150  public:
151   explicit ToolbarActionsBarBridge(BrowserActionsController* controller);
152   ~ToolbarActionsBarBridge() override;
154   BrowserActionsController* controller_for_test() { return controller_; }
156  private:
157   // ToolbarActionsBarDelegate:
158   void AddViewForAction(ToolbarActionViewController* action,
159                         size_t index) override;
160   void RemoveViewForAction(ToolbarActionViewController* action) override;
161   void RemoveAllViews() override;
162   void Redraw(bool order_changed) override;
163   void ResizeAndAnimate(gfx::Tween::Type tween_type,
164                         int target_width,
165                         bool suppress_chevron) override;
166   void SetChevronVisibility(bool chevron_visible) override;
167   int GetWidth() const override;
168   bool IsAnimating() const override;
169   void StopAnimating() override;
170   int GetChevronWidth() const override;
171   bool IsPopupRunning() const override;
172   void OnOverflowedActionWantsToRunChanged(bool overflowed_action_wants_to_run)
173       override;
175   // The owning BrowserActionsController; weak.
176   BrowserActionsController* controller_;
178   DISALLOW_COPY_AND_ASSIGN(ToolbarActionsBarBridge);
181 ToolbarActionsBarBridge::ToolbarActionsBarBridge(
182     BrowserActionsController* controller)
183     : controller_(controller) {
186 ToolbarActionsBarBridge::~ToolbarActionsBarBridge() {
189 void ToolbarActionsBarBridge::AddViewForAction(
190     ToolbarActionViewController* action,
191     size_t index) {
192   [controller_ addViewForAction:action
193                       withIndex:index];
196 void ToolbarActionsBarBridge::RemoveViewForAction(
197     ToolbarActionViewController* action) {
198   [controller_ removeViewForAction:action];
201 void ToolbarActionsBarBridge::RemoveAllViews() {
202   [controller_ removeAllViews];
205 void ToolbarActionsBarBridge::Redraw(bool order_changed) {
206   [controller_ redraw];
209 void ToolbarActionsBarBridge::ResizeAndAnimate(gfx::Tween::Type tween_type,
210                                                int target_width,
211                                                bool suppress_chevron) {
212   [controller_ resizeContainerToWidth:target_width];
215 void ToolbarActionsBarBridge::SetChevronVisibility(bool chevron_visible) {
216   [controller_ setChevronHidden:!chevron_visible
217                         inFrame:[[controller_ containerView] frame]];
220 int ToolbarActionsBarBridge::GetWidth() const {
221   return NSWidth([[controller_ containerView] frame]);
224 bool ToolbarActionsBarBridge::IsAnimating() const {
225   return [[controller_ containerView] isAnimating];
228 void ToolbarActionsBarBridge::StopAnimating() {
229   // Unfortunately, animating the browser actions container affects neighboring
230   // views (like the omnibox), which could also be animating. Because of this,
231   // instead of just ending the animation, the cleanest way to terminate is to
232   // "animate" to the current frame.
233   [controller_ resizeContainerToWidth:
234       NSWidth([[controller_ containerView] frame])];
237 int ToolbarActionsBarBridge::GetChevronWidth() const {
238   return kChevronWidth;
241 bool ToolbarActionsBarBridge::IsPopupRunning() const {
242   return [ExtensionPopupController popup] != nil;
245 void ToolbarActionsBarBridge::OnOverflowedActionWantsToRunChanged(
246     bool overflowed_action_wants_to_run) {
247   [[controller_ toolbarController]
248       setOverflowedToolbarActionWantsToRun:overflowed_action_wants_to_run];
251 }  // namespace
253 @implementation BrowserActionsController
255 @synthesize containerView = containerView_;
256 @synthesize browser = browser_;
257 @synthesize isOverflow = isOverflow_;
259 #pragma mark -
260 #pragma mark Public Methods
262 - (id)initWithBrowser:(Browser*)browser
263         containerView:(BrowserActionsContainerView*)container
264        mainController:(BrowserActionsController*)mainController {
265   DCHECK(browser && container);
267   if ((self = [super init])) {
268     browser_ = browser;
269     isOverflow_ = mainController != nil;
271     toolbarActionsBarBridge_.reset(new ToolbarActionsBarBridge(self));
272     ToolbarActionsBar* mainBar =
273         mainController ? [mainController toolbarActionsBar] : nullptr;
274     toolbarActionsBar_.reset(
275         new ToolbarActionsBar(toolbarActionsBarBridge_.get(),
276                               browser_,
277                               mainBar));
279     containerView_ = container;
280     [containerView_ setPostsFrameChangedNotifications:YES];
281     [[NSNotificationCenter defaultCenter]
282         addObserver:self
283            selector:@selector(containerFrameChanged:)
284                name:NSViewFrameDidChangeNotification
285              object:containerView_];
286     [[NSNotificationCenter defaultCenter]
287         addObserver:self
288            selector:@selector(containerDragStart:)
289                name:kBrowserActionGrippyDragStartedNotification
290              object:containerView_];
291     [[NSNotificationCenter defaultCenter]
292         addObserver:self
293            selector:@selector(containerDragFinished:)
294                name:kBrowserActionGrippyDragFinishedNotification
295              object:containerView_];
296     // Listen for a finished drag from any button to make sure each open window
297     // stays in sync.
298     [[NSNotificationCenter defaultCenter]
299       addObserver:self
300          selector:@selector(actionButtonDragFinished:)
301              name:kBrowserActionButtonDragEndNotification
302            object:nil];
304     suppressChevron_ = NO;
305     if (toolbarActionsBar_->platform_settings().chevron_enabled) {
306       chevronAnimation_.reset([[NSViewAnimation alloc] init]);
307       [chevronAnimation_ gtm_setDuration:kAnimationDuration
308                                eventMask:NSLeftMouseUpMask];
309       [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
310     }
312     if (isOverflow_)
313       toolbarActionsBar_->SetOverflowRowWidth(NSWidth([containerView_ frame]));
315     buttons_.reset([[NSMutableArray alloc] init]);
316     toolbarActionsBar_->CreateActions();
317     [self showChevronIfNecessaryInFrame:[containerView_ frame]];
318     [self updateGrippyCursors];
319     [container setResizable:!isOverflow_];
320     if (toolbarActionsBar_->ShouldShowInfoBubble()) {
321       [containerView_ setTrackingEnabled:YES];
322       [[NSNotificationCenter defaultCenter]
323           addObserver:self
324              selector:@selector(containerMouseEntered:)
325                  name:kBrowserActionsContainerMouseEntered
326                object:containerView_];
327     }
328   }
330   return self;
333 - (void)dealloc {
334   // Explicitly destroy the ToolbarActionsBar so all buttons get removed with a
335   // valid BrowserActionsController, and so we can verify state before
336   // destruction.
337   toolbarActionsBar_->DeleteActions();
338   toolbarActionsBar_.reset();
339   DCHECK_EQ(0u, [buttons_ count]);
340   [[NSNotificationCenter defaultCenter] removeObserver:self];
341   [super dealloc];
344 - (void)update {
345   toolbarActionsBar_->Update();
348 - (NSUInteger)buttonCount {
349   return [buttons_ count];
352 - (NSUInteger)visibleButtonCount {
353   NSUInteger visibleCount = 0;
354   for (BrowserActionButton* button in buttons_.get())
355     visibleCount += [button superview] == containerView_;
356   return visibleCount;
359 - (gfx::Size)preferredSize {
360   return toolbarActionsBar_->GetPreferredSize();
363 - (NSPoint)popupPointForId:(const std::string&)id {
364   BrowserActionButton* button = [self buttonForId:id];
365   if (!button)
366     return NSZeroPoint;
368   NSRect bounds;
369   NSView* referenceButton = button;
370   if ([button superview] != containerView_ || isOverflow_) {
371     referenceButton = toolbarActionsBar_->platform_settings().chevron_enabled ?
372          chevronMenuButton_.get() : [[self toolbarController] wrenchButton];
373     bounds = [referenceButton bounds];
374   } else {
375     bounds = [button convertRect:[button frameAfterAnimation]
376                         fromView:[button superview]];
377   }
379   // Anchor point just above the center of the bottom.
380   DCHECK([referenceButton isFlipped]);
381   NSPoint anchor = NSMakePoint(NSMidX(bounds),
382                                NSMaxY(bounds) - kBrowserActionBubbleYOffset);
383   // Convert the point to the container view's frame, and adjust for animation.
384   NSPoint anchorInContainer =
385       [containerView_ convertPoint:anchor fromView:referenceButton];
386   anchorInContainer.x -= NSMinX([containerView_ frame]) -
387       NSMinX([containerView_ animationEndFrame]);
389   return [containerView_ convertPoint:anchorInContainer toView:nil];
392 - (BOOL)chevronIsHidden {
393   if (!chevronMenuButton_.get())
394     return YES;
396   if (![chevronAnimation_ isAnimating])
397     return [chevronMenuButton_ isHidden];
399   DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
401   // The chevron is animating in or out. Determine which one and have the return
402   // value reflect where the animation is headed.
403   NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
404       valueForKey:NSViewAnimationEffectKey];
405   if (effect == NSViewAnimationFadeInEffect) {
406     return NO;
407   } else if (effect == NSViewAnimationFadeOutEffect) {
408     return YES;
409   }
411   NOTREACHED();
412   return YES;
415 - (content::WebContents*)currentWebContents {
416   return browser_->tab_strip_model()->GetActiveWebContents();
419 - (BrowserActionButton*)mainButtonForId:(const std::string&)id {
420   BrowserActionsController* mainController = isOverflow_ ?
421       [[self toolbarController] browserActionsController] : self;
422   return [mainController buttonForId:id];
425 #pragma mark -
426 #pragma mark NSMenuDelegate
428 - (void)menuNeedsUpdate:(NSMenu*)menu {
429   [menu removeAllItems];
431   // See menu_button.h for documentation on why this is needed.
432   [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
434   NSUInteger iconCount = toolbarActionsBar_->GetIconCount();
435   NSRange hiddenButtonRange =
436       NSMakeRange(iconCount, [buttons_ count] - iconCount);
437   for (BrowserActionButton* button in
438            [buttons_ subarrayWithRange:hiddenButtonRange]) {
439     NSString* name =
440         base::SysUTF16ToNSString([button viewController]->GetActionName());
441     NSMenuItem* item =
442         [menu addItemWithTitle:name
443                         action:@selector(chevronItemSelected:)
444                  keyEquivalent:@""];
445     [item setRepresentedObject:button];
446     [item setImage:[button compositedImage]];
447     [item setTarget:self];
448     [item setEnabled:[button isEnabled]];
449   }
452 #pragma mark -
453 #pragma mark Private Methods
455 - (void)addViewForAction:(ToolbarActionViewController*)action
456                withIndex:(NSUInteger)index {
457   NSRect buttonFrame = NSMakeRect(NSMaxX([containerView_ bounds]),
458                                   0,
459                                   ToolbarActionsBar::IconWidth(false),
460                                   ToolbarActionsBar::IconHeight());
461   BrowserActionButton* newButton =
462       [[[BrowserActionButton alloc]
463          initWithFrame:buttonFrame
464         viewController:action
465             controller:self] autorelease];
466   [newButton setTarget:self];
467   [newButton setAction:@selector(browserActionClicked:)];
468   [buttons_ insertObject:newButton atIndex:index];
470   [[NSNotificationCenter defaultCenter]
471       addObserver:self
472          selector:@selector(actionButtonDragging:)
473              name:kBrowserActionButtonDraggingNotification
474            object:newButton];
476   [containerView_ setMaxWidth:toolbarActionsBar_->GetMaximumWidth()];
479 - (void)redraw {
480   if (![self updateContainerVisibility])
481     return;  // Container is hidden; no need to update.
483   std::vector<ToolbarActionViewController*> toolbar_actions =
484       toolbarActionsBar_->toolbar_actions();
485   for (NSUInteger i = 0; i < [buttons_ count]; ++i) {
486     if ([[buttons_ objectAtIndex:i] viewController] != toolbar_actions[i]) {
487       size_t j = i + 1;
488       while (toolbar_actions[i] != [[buttons_ objectAtIndex:j] viewController])
489         ++j;
490       [buttons_ exchangeObjectAtIndex:i withObjectAtIndex: j];
491     }
492   }
494   [self showChevronIfNecessaryInFrame:[containerView_ frame]];
495   NSUInteger minIndex = isOverflow_ ?
496       [buttons_ count] - toolbarActionsBar_->GetIconCount() : 0;
497   NSUInteger maxIndex = isOverflow_ ?
498       [buttons_ count] : toolbarActionsBar_->GetIconCount();
499   for (NSUInteger i = 0; i < [buttons_ count]; ++i) {
500     BrowserActionButton* button = [buttons_ objectAtIndex:i];
501     if ([button isBeingDragged])
502       continue;
504     [self moveButton:[buttons_ objectAtIndex:i] toIndex:i - minIndex];
506     if (i >= minIndex && i < maxIndex) {
507       // Make sure the button is within the visible container.
508       if ([button superview] != containerView_) {
509         // We add the subview under the sibling views so that when it
510         // "slides in", it does so under its neighbors.
511         [containerView_ addSubview:button
512                         positioned:NSWindowBelow
513                         relativeTo:nil];
514       }
515       // We need to set the alpha value in case the container has resized.
516       [button setAlphaValue:1.0];
517     } else if ([button superview] == containerView_) {
518       [button removeFromSuperview];
519       [button setAlphaValue:0.0];
520     }
521   }
524 - (void)removeViewForAction:(ToolbarActionViewController*)action {
525   BrowserActionButton* button = [self buttonForId:action->GetId()];
527   [button removeFromSuperview];
528   [button onRemoved];
529   [buttons_ removeObject:button];
531   [containerView_ setMaxWidth:toolbarActionsBar_->GetMaximumWidth()];
534 - (void)removeAllViews {
535   for (BrowserActionButton* button in buttons_.get()) {
536     [button removeFromSuperview];
537     [button onRemoved];
538   }
539   [buttons_ removeAllObjects];
542 - (void)resizeContainerToWidth:(CGFloat)width {
543   // Cocoa goes a little crazy if we try and change animations while adjusting
544   // child frames (i.e., the buttons). If the toolbar is already animating,
545   // just jump to the new frame. (This typically only happens if someone is
546   // "spamming" a button to add/remove an action.)
547   BOOL animate = !toolbarActionsBar_->suppress_animation() &&
548       ![containerView_ isAnimating];
549   [self updateContainerVisibility];
550   [containerView_ resizeToWidth:width
551                         animate:animate];
552   NSRect frame = animate ? [containerView_ animationEndFrame] :
553                            [containerView_ frame];
555   [self showChevronIfNecessaryInFrame:frame];
557   [containerView_ setNeedsDisplay:YES];
559   if (!animate) {
560     [[NSNotificationCenter defaultCenter]
561         postNotificationName:kBrowserActionVisibilityChangedNotification
562                       object:self];
563   }
564   [self redraw];
565   [self updateGrippyCursors];
568 - (BOOL)updateContainerVisibility {
569   BOOL hidden = [buttons_ count] == 0;
570   if ([containerView_ isHidden] != hidden)
571     [containerView_ setHidden:hidden];
572   return !hidden;
575 - (void)updateButtonOpacity {
576   for (BrowserActionButton* button in buttons_.get()) {
577     NSRect buttonFrame = [button frame];
578     if (NSContainsRect([containerView_ bounds], buttonFrame)) {
579       if ([button alphaValue] != 1.0)
580         [button setAlphaValue:1.0];
582       continue;
583     }
584     CGFloat intersectionWidth =
585         NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
586     CGFloat alpha = std::max(static_cast<CGFloat>(0.0),
587                              intersectionWidth / NSWidth(buttonFrame));
588     [button setAlphaValue:alpha];
589     [button setNeedsDisplay:YES];
590   }
593 - (void)updateButtonPositions {
594   for (NSUInteger index = 0; index < [buttons_ count]; ++index) {
595     BrowserActionButton* button = [buttons_ objectAtIndex:index];
596     NSRect buttonFrame = [self frameForIndex:index];
598     // If the button is at the proper position (or animating to it), then we
599     // don't need to update its position.
600     if (NSMinX([button frameAfterAnimation]) == NSMinX(buttonFrame))
601       continue;
603     // We set the x-origin by calculating the proper distance from the right
604     // edge in the container so that, if the container is animating, the
605     // button appears stationary.
606     buttonFrame.origin.x = NSWidth([containerView_ frame]) -
607         (toolbarActionsBar_->GetPreferredSize().width() - NSMinX(buttonFrame));
608     [button setFrame:buttonFrame animate:NO];
609   }
612 - (BrowserActionButton*)buttonForId:(const std::string&)id {
613   for (BrowserActionButton* button in buttons_.get()) {
614     if ([button viewController]->GetId() == id)
615       return button;
616   }
617   return nil;
620 - (void)containerFrameChanged:(NSNotification*)notification {
621   [self updateButtonPositions];
622   [self updateButtonOpacity];
623   [[containerView_ window] invalidateCursorRectsForView:containerView_];
624   [self updateChevronPositionInFrame:[containerView_ frame]];
627 - (void)containerDragStart:(NSNotification*)notification {
628   [self setChevronHidden:YES inFrame:[containerView_ frame]];
629   for (BrowserActionButton* button in buttons_.get()) {
630     if ([button superview] != containerView_) {
631       [button setAlphaValue:1.0];
632       [containerView_ addSubview:button];
633     }
634   }
637 - (void)containerDragFinished:(NSNotification*)notification {
638   for (BrowserActionButton* button in buttons_.get()) {
639     NSRect buttonFrame = [button frame];
640     if (NSContainsRect([containerView_ bounds], buttonFrame))
641       continue;
643     CGFloat intersectionWidth =
644         NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
645     // Hide the button if it's not "mostly" visible. "Mostly" here equates to
646     // having three or fewer pixels hidden.
647     if (([containerView_ grippyPinned] && intersectionWidth > 0) ||
648         (intersectionWidth <= NSWidth(buttonFrame) - 3.0)) {
649       [button setAlphaValue:0.0];
650       [button removeFromSuperview];
651     }
652   }
654   toolbarActionsBar_->OnResizeComplete(
655       toolbarActionsBar_->IconCountToWidth([self visibleButtonCount]));
657   [self updateGrippyCursors];
658   [self resizeContainerToWidth:toolbarActionsBar_->GetPreferredSize().width()];
661 - (void)containerMouseEntered:(NSNotification*)notification {
662   if (toolbarActionsBar_->ShouldShowInfoBubble()) {
663     NSPoint anchor = [self popupPointForId:[[buttons_ objectAtIndex:0]
664                                                viewController]->GetId()];
665     anchor = [[containerView_ window] convertBaseToScreen:anchor];
666     ExtensionToolbarIconSurfacingBubbleMac* bubble =
667         [[ExtensionToolbarIconSurfacingBubbleMac alloc]
668             initWithParentWindow:[containerView_ window]
669                      anchorPoint:anchor
670                         delegate:toolbarActionsBar_.get()];
671     [bubble showWindow:nil];
672   }
673   [containerView_ setTrackingEnabled:NO];
674   [[NSNotificationCenter defaultCenter]
675       removeObserver:self
676                 name:kBrowserActionsContainerMouseEntered
677               object:containerView_];
680 - (void)actionButtonDragging:(NSNotification*)notification {
681   suppressChevron_ = YES;
682   if (![self chevronIsHidden])
683     [self setChevronHidden:YES inFrame:[containerView_ frame]];
685   // Determine what index the dragged button should lie in, alter the model and
686   // reposition the buttons.
687   BrowserActionButton* draggedButton = [notification object];
688   NSRect draggedButtonFrame = [draggedButton frame];
689   // Find the mid-point. We flip the y-coordinates so that y = 0 is at the
690   // top of the container to make row calculation more logical.
691   NSPoint midPoint =
692       NSMakePoint(NSMidX(draggedButtonFrame),
693                   NSMaxY([containerView_ bounds]) - NSMidY(draggedButtonFrame));
695   // Calculate the row index and the index in the row. We bound the latter
696   // because the view can go farther right than the right-most icon in the last
697   // row of the overflow menu.
698   NSInteger rowIndex = midPoint.y / ToolbarActionsBar::IconHeight();
699   int icons_per_row = isOverflow_ ?
700       toolbarActionsBar_->platform_settings().icons_per_overflow_menu_row :
701       toolbarActionsBar_->GetIconCount();
702   NSInteger indexInRow = std::min(icons_per_row - 1,
703       static_cast<int>(midPoint.x / ToolbarActionsBar::IconWidth(true)));
705   // Find the desired index for the button.
706   NSInteger maxIndex = [buttons_ count] - 1;
707   NSInteger offset = isOverflow_ ?
708       [buttons_ count] - toolbarActionsBar_->GetIconCount() : 0;
709   NSInteger index =
710       std::min(maxIndex, offset + rowIndex * icons_per_row + indexInRow);
712   toolbarActionsBar_->OnDragDrop([buttons_ indexOfObject:draggedButton],
713                                  index,
714                                  ToolbarActionsBar::DRAG_TO_SAME);
717 - (void)actionButtonDragFinished:(NSNotification*)notification {
718   suppressChevron_ = NO;
719   [self redraw];
722 - (NSRect)frameForIndex:(NSUInteger)index {
723   const ToolbarActionsBar::PlatformSettings& platformSettings =
724       toolbarActionsBar_->platform_settings();
725   int icons_per_overflow_row = platformSettings.icons_per_overflow_menu_row;
726   NSUInteger rowIndex = isOverflow_ ? index / icons_per_overflow_row : 0;
727   NSUInteger indexInRow = isOverflow_ ? index % icons_per_overflow_row : index;
729   CGFloat xOffset = platformSettings.left_padding +
730       (indexInRow * ToolbarActionsBar::IconWidth(true));
731   CGFloat yOffset = NSHeight([containerView_ frame]) -
732        (ToolbarActionsBar::IconHeight() * (rowIndex + 1));
734   return NSMakeRect(xOffset,
735                     yOffset,
736                     ToolbarActionsBar::IconWidth(false),
737                     ToolbarActionsBar::IconHeight());
740 - (void)moveButton:(BrowserActionButton*)button
741            toIndex:(NSUInteger)index {
742   NSRect buttonFrame = [self frameForIndex:index];
744   CGFloat currentX = NSMinX([button frame]);
745   CGFloat xLeft = toolbarActionsBar_->GetPreferredSize().width() -
746       NSMinX(buttonFrame);
747   // We check if the button is already in the correct place for the toolbar's
748   // current size. This could mean that the button could be the correct distance
749   // from the left or from the right edge. If it has the correct distance, we
750   // don't move it, and it will be updated when the container frame changes.
751   // This way, if the user has extensions A and C installed, and installs
752   // extension B between them, extension C appears to stay stationary on the
753   // screen while the toolbar expands to the left (even though C's bounds within
754   // the container change).
755   if ((currentX == NSMinX(buttonFrame) ||
756        currentX == NSWidth([containerView_ frame]) - xLeft) &&
757       NSMinY([button frame]) == NSMinY(buttonFrame))
758     return;
760   // It's possible the button is already animating to the right place. Don't
761   // call move again, because it will stop the current animation.
762   if (!NSEqualRects(buttonFrame, [button frameAfterAnimation])) {
763     [button setFrame:buttonFrame
764              animate:!toolbarActionsBar_->suppress_animation() && !isOverflow_];
765   }
768 - (BOOL)browserActionClicked:(BrowserActionButton*)button {
769   return [button viewController]->ExecuteAction(true);
772 - (void)showChevronIfNecessaryInFrame:(NSRect)frame {
773   if (!toolbarActionsBar_->platform_settings().chevron_enabled)
774     return;
775   bool hidden = suppressChevron_ ||
776       toolbarActionsBar_->GetIconCount() == [self buttonCount];
777   [self setChevronHidden:hidden inFrame:frame];
780 - (void)updateChevronPositionInFrame:(NSRect)frame {
781   CGFloat xPos = NSWidth(frame) - kChevronWidth -
782       toolbarActionsBar_->platform_settings().right_padding;
783   NSRect buttonFrame = NSMakeRect(xPos,
784                                   0,
785                                   kChevronWidth,
786                                   ToolbarActionsBar::IconHeight());
787   [chevronAnimation_ stopAnimation];
788   [chevronMenuButton_ setFrame:buttonFrame];
791 - (void)setChevronHidden:(BOOL)hidden
792                  inFrame:(NSRect)frame {
793   if (!toolbarActionsBar_->platform_settings().chevron_enabled ||
794       hidden == [self chevronIsHidden])
795     return;
797   if (!chevronMenuButton_.get()) {
798     chevronMenuButton_.reset([[MenuButton alloc] init]);
799     [chevronMenuButton_ setOpenMenuOnClick:YES];
800     [chevronMenuButton_ setBordered:NO];
801     [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
803     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW
804                            forButtonState:image_button_cell::kDefaultState];
805     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_H
806                            forButtonState:image_button_cell::kHoverState];
807     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_P
808                            forButtonState:image_button_cell::kPressedState];
810     overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
811     [overflowMenu_ setAutoenablesItems:NO];
812     [overflowMenu_ setDelegate:self];
813     [chevronMenuButton_ setAttachedMenu:overflowMenu_];
815     [containerView_ addSubview:chevronMenuButton_];
816   }
818   [self updateChevronPositionInFrame:frame];
820   // Stop any running animation.
821   [chevronAnimation_ stopAnimation];
823   if (toolbarActionsBar_->suppress_animation()) {
824     [chevronMenuButton_ setHidden:hidden];
825     return;
826   }
828   NSString* animationEffect;
829   if (hidden) {
830     animationEffect = NSViewAnimationFadeOutEffect;
831   } else {
832     [chevronMenuButton_ setHidden:NO];
833     animationEffect = NSViewAnimationFadeInEffect;
834   }
835   NSDictionary* animationDictionary = @{
836       NSViewAnimationTargetKey : chevronMenuButton_.get(),
837       NSViewAnimationEffectKey : animationEffect
838   };
839   [chevronAnimation_ setViewAnimations:
840       [NSArray arrayWithObject:animationDictionary]];
841   [chevronAnimation_ startAnimation];
844 - (void)chevronItemSelected:(id)menuItem {
845   [self browserActionClicked:[menuItem representedObject]];
848 - (void)updateGrippyCursors {
849   [containerView_
850       setCanDragLeft:toolbarActionsBar_->GetIconCount() != [buttons_ count]];
851   [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
852   [[containerView_ window] invalidateCursorRectsForView:containerView_];
855 - (ToolbarController*)toolbarController {
856   return [[BrowserWindowController browserWindowControllerForWindow:
857              browser_->window()->GetNativeWindow()] toolbarController];
861 #pragma mark -
862 #pragma mark Testing Methods
864 - (BrowserActionButton*)buttonWithIndex:(NSUInteger)index {
865   return index < [buttons_ count] ? [buttons_ objectAtIndex:index] : nil;
868 - (ToolbarActionsBar*)toolbarActionsBar {
869   return toolbarActionsBar_.get();
872 + (BrowserActionsController*)fromToolbarActionsBarDelegate:
873     (ToolbarActionsBarDelegate*)delegate {
874   return static_cast<ToolbarActionsBarBridge*>(delegate)->controller_for_test();
877 @end