Elim cr-checkbox
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / browser_actions_controller.mm
blob377b2105fef2c567334af87c458f67f6800ca294
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/extensions/extension_message_bubble_controller.h"
11 #include "chrome/browser/ui/browser.h"
12 #include "chrome/browser/ui/browser_window.h"
13 #import "chrome/browser/ui/cocoa/browser_window_controller.h"
14 #import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
15 #import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
16 #import "chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.h"
17 #import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
18 #import "chrome/browser/ui/cocoa/extensions/toolbar_actions_bar_bubble_mac.h"
19 #import "chrome/browser/ui/cocoa/image_button_cell.h"
20 #import "chrome/browser/ui/cocoa/menu_button.h"
21 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
22 #include "chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.h"
23 #include "chrome/browser/ui/tabs/tab_strip_model.h"
24 #include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
25 #include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
26 #include "chrome/browser/ui/toolbar/toolbar_actions_bar_delegate.h"
27 #include "grit/theme_resources.h"
28 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
29 #include "ui/base/cocoa/appkit_utils.h"
31 NSString* const kBrowserActionVisibilityChangedNotification =
32     @"BrowserActionVisibilityChangedNotification";
34 namespace {
36 const CGFloat kAnimationDuration = 0.2;
38 const CGFloat kChevronWidth = 18;
40 // How far to inset from the bottom of the view to get the top border
41 // of the popup 2px below the bottom of the Omnibox.
42 const CGFloat kBrowserActionBubbleYOffset = 3.0;
44 }  // namespace
46 @interface BrowserActionsController(Private)
48 // Creates and adds a view for the given |action| at |index|.
49 - (void)addViewForAction:(ToolbarActionViewController*)action
50                withIndex:(NSUInteger)index;
52 // Removes the view for the given |action| from the ccontainer.
53 - (void)removeViewForAction:(ToolbarActionViewController*)action;
55 // Removes views for all actions.
56 - (void)removeAllViews;
58 // Redraws the BrowserActionsContainerView and updates the button order to match
59 // the order in the ToolbarActionsBar.
60 - (void)redraw;
62 // Resizes the container to the specified |width|, and animates according to
63 // the ToolbarActionsBar.
64 - (void)resizeContainerToWidth:(CGFloat)width;
66 // Sets the container to be either hidden or visible based on whether there are
67 // any actions to show.
68 // Returns whether the container is visible.
69 - (BOOL)updateContainerVisibility;
71 // During container resizing, buttons become more transparent as they are pushed
72 // off the screen. This method updates each button's opacity determined by the
73 // position of the button.
74 - (void)updateButtonOpacity;
76 // When the container is resizing, there's a chance that the buttons' frames
77 // need to be adjusted (for instance, if an action is added to the left, the
78 // frames of the actions to the right should gradually move right in the
79 // container). Adjust the frames accordingly.
80 - (void)updateButtonPositions;
82 // Returns the existing button associated with the given id; nil if it cannot be
83 // found.
84 - (BrowserActionButton*)buttonForId:(const std::string&)id;
86 // Returns the button at the given index. This is just a wrapper around
87 // [NSArray objectAtIndex:], since that technically defaults to returning ids
88 // (and can cause compile errors).
89 - (BrowserActionButton*)buttonAtIndex:(NSUInteger)index;
91 // Notification handlers for events registered by the class.
93 // Updates each button's opacity, the cursor rects and chevron position.
94 - (void)containerFrameChanged:(NSNotification*)notification;
96 // Hides the chevron and unhides every hidden button so that dragging the
97 // container out smoothly shows the Browser Action buttons.
98 - (void)containerDragStart:(NSNotification*)notification;
100 // Determines which buttons need to be hidden based on the new size, hides them
101 // and updates the chevron overflow menu. Also fires a notification to let the
102 // toolbar know that the drag has finished.
103 - (void)containerDragFinished:(NSNotification*)notification;
105 // Shows the toolbar info bubble, if it should be displayed.
106 - (void)containerMouseEntered:(NSNotification*)notification;
108 // Notifies the controlling ToolbarActionsBar that any running animation has
109 // ended.
110 - (void)containerAnimationEnded:(NSNotification*)notification;
112 // Processes a key event from the container.
113 - (void)containerKeyEvent:(NSNotification*)notification;
115 // Adjusts the position of the surrounding action buttons depending on where the
116 // button is within the container.
117 - (void)actionButtonDragging:(NSNotification*)notification;
119 // Updates the position of the Browser Actions within the container. This fires
120 // when _any_ Browser Action button is done dragging to keep all open windows in
121 // sync visually.
122 - (void)actionButtonDragFinished:(NSNotification*)notification;
124 // Returns the frame that the button with the given |index| should have.
125 - (NSRect)frameForIndex:(NSUInteger)index;
127 // Returns the popup point for the given |view| with |bounds|.
128 - (NSPoint)popupPointForView:(NSView*)view
129                   withBounds:(NSRect)bounds;
131 // Moves the given button both visually and within the toolbar model to the
132 // specified index.
133 - (void)moveButton:(BrowserActionButton*)button
134            toIndex:(NSUInteger)index;
136 // Handles clicks for BrowserActionButtons.
137 - (BOOL)browserActionClicked:(BrowserActionButton*)button;
139 // The reason |frame| is specified in these chevron functions is because the
140 // container may be animating and the end frame of the animation should be
141 // passed instead of the current frame (which may be off and cause the chevron
142 // to jump at the end of its animation).
144 // Shows the overflow chevron button depending on whether there are any hidden
145 // extensions within the frame given.
146 - (void)showChevronIfNecessaryInFrame:(NSRect)frame;
148 // Moves the chevron to its correct position within |frame|.
149 - (void)updateChevronPositionInFrame:(NSRect)frame;
151 // Shows or hides the chevron in the given |frame|.
152 - (void)setChevronHidden:(BOOL)hidden
153                  inFrame:(NSRect)frame;
155 // Handles when a menu item within the chevron overflow menu is selected.
156 - (void)chevronItemSelected:(id)menuItem;
158 // Updates the container's grippy cursor based on the number of hidden buttons.
159 - (void)updateGrippyCursors;
161 // Returns the associated ToolbarController.
162 - (ToolbarController*)toolbarController;
164 // Creates a message bubble anchored to the given |anchorAction|, or the wrench
165 // menu if no |anchorAction| is null.
166 - (ToolbarActionsBarBubbleMac*)createMessageBubble:
167     (scoped_ptr<ToolbarActionsBarBubbleDelegate>)delegate
168     anchorToSelf:(BOOL)anchorToSelf;
170 // Called when the window for the active bubble is closing, and sets the active
171 // bubble to nil.
172 - (void)bubbleWindowClosing:(NSNotification*)notification;
174 // Sets the current focused view. Should only be used for the overflow
175 // container.
176 - (void)setFocusedViewIndex:(NSInteger)index;
178 @end
180 namespace {
182 // A bridge between the ToolbarActionsBar and the BrowserActionsController.
183 class ToolbarActionsBarBridge : public ToolbarActionsBarDelegate {
184  public:
185   explicit ToolbarActionsBarBridge(BrowserActionsController* controller);
186   ~ToolbarActionsBarBridge() override;
188   BrowserActionsController* controller_for_test() { return controller_; }
190  private:
191   // ToolbarActionsBarDelegate:
192   void AddViewForAction(ToolbarActionViewController* action,
193                         size_t index) override;
194   void RemoveViewForAction(ToolbarActionViewController* action) override;
195   void RemoveAllViews() override;
196   void Redraw(bool order_changed) override;
197   void ResizeAndAnimate(gfx::Tween::Type tween_type,
198                         int target_width,
199                         bool suppress_chevron) override;
200   void SetChevronVisibility(bool chevron_visible) override;
201   int GetWidth(GetWidthTime get_width_time) const override;
202   bool IsAnimating() const override;
203   void StopAnimating() override;
204   int GetChevronWidth() const override;
205   void ShowExtensionMessageBubble(
206       scoped_ptr<extensions::ExtensionMessageBubbleController> controller,
207       ToolbarActionViewController* anchor_action) override;
209   // The owning BrowserActionsController; weak.
210   BrowserActionsController* controller_;
212   DISALLOW_COPY_AND_ASSIGN(ToolbarActionsBarBridge);
215 ToolbarActionsBarBridge::ToolbarActionsBarBridge(
216     BrowserActionsController* controller)
217     : controller_(controller) {
220 ToolbarActionsBarBridge::~ToolbarActionsBarBridge() {
223 void ToolbarActionsBarBridge::AddViewForAction(
224     ToolbarActionViewController* action,
225     size_t index) {
226   [controller_ addViewForAction:action
227                       withIndex:index];
230 void ToolbarActionsBarBridge::RemoveViewForAction(
231     ToolbarActionViewController* action) {
232   [controller_ removeViewForAction:action];
235 void ToolbarActionsBarBridge::RemoveAllViews() {
236   [controller_ removeAllViews];
239 void ToolbarActionsBarBridge::Redraw(bool order_changed) {
240   [controller_ redraw];
243 void ToolbarActionsBarBridge::ResizeAndAnimate(gfx::Tween::Type tween_type,
244                                                int target_width,
245                                                bool suppress_chevron) {
246   [controller_ resizeContainerToWidth:target_width];
249 void ToolbarActionsBarBridge::SetChevronVisibility(bool chevron_visible) {
250   [controller_ setChevronHidden:!chevron_visible
251                         inFrame:[[controller_ containerView] frame]];
254 int ToolbarActionsBarBridge::GetWidth(GetWidthTime get_width_time) const {
255   NSRect frame =
256       get_width_time == ToolbarActionsBarDelegate::GET_WIDTH_AFTER_ANIMATION
257           ? [[controller_ containerView] animationEndFrame]
258           : [[controller_ containerView] frame];
259   return NSWidth(frame);
262 bool ToolbarActionsBarBridge::IsAnimating() const {
263   return [[controller_ containerView] isAnimating];
266 void ToolbarActionsBarBridge::StopAnimating() {
267   // Unfortunately, animating the browser actions container affects neighboring
268   // views (like the omnibox), which could also be animating. Because of this,
269   // instead of just ending the animation, the cleanest way to terminate is to
270   // "animate" to the current frame.
271   [controller_ resizeContainerToWidth:
272       NSWidth([[controller_ containerView] frame])];
275 int ToolbarActionsBarBridge::GetChevronWidth() const {
276   return kChevronWidth;
279 void ToolbarActionsBarBridge::ShowExtensionMessageBubble(
280     scoped_ptr<extensions::ExtensionMessageBubbleController> controller,
281     ToolbarActionViewController* anchor_action) {
282   // This goop is a by-product of needing to wire together abstract classes,
283   // C++/Cocoa bridges, and ExtensionMessageBubbleController's somewhat strange
284   // Show() interface. It's ugly, but it's pretty confined, so it's probably
285   // okay (but if we ever need to expand, it might need to be reconsidered).
286   scoped_ptr<ExtensionMessageBubbleBridge> bridge(
287       new ExtensionMessageBubbleBridge(controller.Pass(),
288                                        anchor_action != nullptr));
289   ExtensionMessageBubbleBridge* weak_bridge = bridge.get();
290   ToolbarActionsBarBubbleMac* bubble =
291       [controller_ createMessageBubble:bridge.Pass()
292                           anchorToSelf:anchor_action != nil];
293   weak_bridge->SetBubble(bubble);
294   weak_bridge->controller()->Show(weak_bridge);
297 }  // namespace
299 @implementation BrowserActionsController
301 @synthesize containerView = containerView_;
302 @synthesize browser = browser_;
303 @synthesize isOverflow = isOverflow_;
304 @synthesize activeBubble = activeBubble_;
306 #pragma mark -
307 #pragma mark Public Methods
309 - (id)initWithBrowser:(Browser*)browser
310         containerView:(BrowserActionsContainerView*)container
311        mainController:(BrowserActionsController*)mainController {
312   DCHECK(browser && container);
314   if ((self = [super init])) {
315     browser_ = browser;
316     isOverflow_ = mainController != nil;
318     toolbarActionsBarBridge_.reset(new ToolbarActionsBarBridge(self));
319     ToolbarActionsBar* mainBar =
320         mainController ? [mainController toolbarActionsBar] : nullptr;
321     toolbarActionsBar_.reset(
322         new ToolbarActionsBar(toolbarActionsBarBridge_.get(),
323                               browser_,
324                               mainBar));
326     containerView_ = container;
327     [containerView_ setPostsFrameChangedNotifications:YES];
328     [[NSNotificationCenter defaultCenter]
329         addObserver:self
330            selector:@selector(containerFrameChanged:)
331                name:NSViewFrameDidChangeNotification
332              object:containerView_];
333     [[NSNotificationCenter defaultCenter]
334         addObserver:self
335            selector:@selector(containerDragStart:)
336                name:kBrowserActionGrippyDragStartedNotification
337              object:containerView_];
338     [[NSNotificationCenter defaultCenter]
339         addObserver:self
340            selector:@selector(containerDragFinished:)
341                name:kBrowserActionGrippyDragFinishedNotification
342              object:containerView_];
343     [[NSNotificationCenter defaultCenter]
344         addObserver:self
345            selector:@selector(containerAnimationEnded:)
346                name:kBrowserActionsContainerAnimationEnded
347              object:containerView_];
348     [[NSNotificationCenter defaultCenter]
349         addObserver:self
350            selector:@selector(containerKeyEvent:)
351                name:kBrowserActionsContainerReceivedKeyEvent
352              object:containerView_];
353     // Listen for a finished drag from any button to make sure each open window
354     // stays in sync.
355     [[NSNotificationCenter defaultCenter]
356       addObserver:self
357          selector:@selector(actionButtonDragFinished:)
358              name:kBrowserActionButtonDragEndNotification
359            object:nil];
361     suppressChevron_ = NO;
362     if (toolbarActionsBar_->platform_settings().chevron_enabled) {
363       chevronAnimation_.reset([[NSViewAnimation alloc] init]);
364       [chevronAnimation_ gtm_setDuration:kAnimationDuration
365                                eventMask:NSLeftMouseUpMask];
366       [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
367     }
369     if (isOverflow_)
370       toolbarActionsBar_->SetOverflowRowWidth(NSWidth([containerView_ frame]));
372     buttons_.reset([[NSMutableArray alloc] init]);
373     toolbarActionsBar_->CreateActions();
374     [self showChevronIfNecessaryInFrame:[containerView_ frame]];
375     [self updateGrippyCursors];
376     [container setIsOverflow:isOverflow_];
377     if (ExtensionToolbarIconSurfacingBubbleDelegate::ShouldShowForProfile(
378             browser_->profile())) {
379       [containerView_ setTrackingEnabled:YES];
380       [[NSNotificationCenter defaultCenter]
381           addObserver:self
382              selector:@selector(containerMouseEntered:)
383                  name:kBrowserActionsContainerMouseEntered
384                object:containerView_];
385     }
387     focusedViewIndex_ = -1;
388   }
390   return self;
393 - (void)dealloc {
394   [self browserWillBeDestroyed];
395   [super dealloc];
398 - (void)browserWillBeDestroyed {
399   [overflowMenu_ setDelegate:nil];
400   // Explicitly destroy the ToolbarActionsBar so all buttons get removed with a
401   // valid BrowserActionsController, and so we can verify state before
402   // destruction.
403   if (toolbarActionsBar_.get()) {
404     toolbarActionsBar_->DeleteActions();
405     toolbarActionsBar_.reset();
406   }
407   DCHECK_EQ(0u, [buttons_ count]);
408   [[NSNotificationCenter defaultCenter] removeObserver:self];
409   browser_ = nullptr;
412 - (void)update {
413   toolbarActionsBar_->Update();
416 - (NSUInteger)buttonCount {
417   return [buttons_ count];
420 - (NSUInteger)visibleButtonCount {
421   NSUInteger visibleCount = 0;
422   for (BrowserActionButton* button in buttons_.get())
423     visibleCount += [button superview] == containerView_;
424   return visibleCount;
427 - (gfx::Size)preferredSize {
428   return toolbarActionsBar_->GetPreferredSize();
431 - (NSPoint)popupPointForId:(const std::string&)id {
432   BrowserActionButton* button = [self buttonForId:id];
433   if (!button)
434     return NSZeroPoint;
436   NSRect bounds;
437   NSView* referenceButton = button;
438   if ([button superview] != containerView_ || isOverflow_) {
439     referenceButton = toolbarActionsBar_->platform_settings().chevron_enabled ?
440          chevronMenuButton_.get() : [[self toolbarController] wrenchButton];
441     bounds = [referenceButton bounds];
442   } else {
443     bounds = [button convertRect:[button frameAfterAnimation]
444                         fromView:[button superview]];
445   }
447   return [self popupPointForView:referenceButton withBounds:bounds];
450 - (BOOL)chevronIsHidden {
451   if (!chevronMenuButton_.get())
452     return YES;
454   if (![chevronAnimation_ isAnimating])
455     return [chevronMenuButton_ isHidden];
457   DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
459   // The chevron is animating in or out. Determine which one and have the return
460   // value reflect where the animation is headed.
461   NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
462       valueForKey:NSViewAnimationEffectKey];
463   if (effect == NSViewAnimationFadeInEffect) {
464     return NO;
465   } else if (effect == NSViewAnimationFadeOutEffect) {
466     return YES;
467   }
469   NOTREACHED();
470   return YES;
473 - (content::WebContents*)currentWebContents {
474   return browser_->tab_strip_model()->GetActiveWebContents();
477 - (BrowserActionButton*)mainButtonForId:(const std::string&)id {
478   BrowserActionsController* mainController = isOverflow_ ?
479       [[self toolbarController] browserActionsController] : self;
480   return [mainController buttonForId:id];
483 - (ToolbarActionsBar*)toolbarActionsBar {
484   return toolbarActionsBar_.get();
487 - (void)setFocusedInOverflow:(BOOL)focused {
488   BOOL isFocused = focusedViewIndex_ != -1;
489   if (isFocused != focused) {
490     int index = focused ?
491         [buttons_ count] - toolbarActionsBar_->GetIconCount() : -1;
492     [self setFocusedViewIndex:index];
493   }
496 - (gfx::Size)sizeForOverflowWidth:(int)maxWidth {
497   toolbarActionsBar_->SetOverflowRowWidth(maxWidth);
498   return [self preferredSize];
501 #pragma mark -
502 #pragma mark NSMenuDelegate
504 - (void)menuNeedsUpdate:(NSMenu*)menu {
505   [menu removeAllItems];
507   // See menu_button.h for documentation on why this is needed.
508   [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
510   NSUInteger iconCount = toolbarActionsBar_->GetIconCount();
511   NSRange hiddenButtonRange =
512       NSMakeRange(iconCount, [buttons_ count] - iconCount);
513   for (BrowserActionButton* button in
514            [buttons_ subarrayWithRange:hiddenButtonRange]) {
515     NSString* name =
516         base::SysUTF16ToNSString([button viewController]->GetActionName());
517     NSMenuItem* item =
518         [menu addItemWithTitle:name
519                         action:@selector(chevronItemSelected:)
520                  keyEquivalent:@""];
521     [item setRepresentedObject:button];
522     [item setImage:[button compositedImage]];
523     [item setTarget:self];
524     [item setEnabled:[button isEnabled]];
525   }
528 #pragma mark -
529 #pragma mark Private Methods
531 - (void)addViewForAction:(ToolbarActionViewController*)action
532                withIndex:(NSUInteger)index {
533   NSRect buttonFrame = NSMakeRect(NSMaxX([containerView_ bounds]),
534                                   0,
535                                   ToolbarActionsBar::IconWidth(false),
536                                   ToolbarActionsBar::IconHeight());
537   BrowserActionButton* newButton =
538       [[[BrowserActionButton alloc]
539          initWithFrame:buttonFrame
540         viewController:action
541             controller:self] autorelease];
542   [newButton setTarget:self];
543   [newButton setAction:@selector(browserActionClicked:)];
544   [buttons_ insertObject:newButton atIndex:index];
546   [[NSNotificationCenter defaultCenter]
547       addObserver:self
548          selector:@selector(actionButtonDragging:)
549              name:kBrowserActionButtonDraggingNotification
550            object:newButton];
552   [containerView_ setMaxDesiredWidth:toolbarActionsBar_->GetMaximumWidth()];
555 - (void)redraw {
556   if (![self updateContainerVisibility])
557     return;  // Container is hidden; no need to update.
559   scoped_ptr<ui::NinePartImageIds> highlight;
560   if (toolbarActionsBar_->is_highlighting()) {
561     if (toolbarActionsBar_->highlight_type() ==
562         ToolbarActionsModel::HIGHLIGHT_INFO)
563       highlight.reset(
564           new ui::NinePartImageIds(IMAGE_GRID(IDR_TOOLBAR_ACTION_HIGHLIGHT)));
565     else
566       highlight.reset(
567           new ui::NinePartImageIds(IMAGE_GRID(IDR_DEVELOPER_MODE_HIGHLIGHT)));
568   }
569   [containerView_ setHighlight:highlight.Pass()];
571   std::vector<ToolbarActionViewController*> toolbar_actions =
572       toolbarActionsBar_->GetActions();
573   for (NSUInteger i = 0; i < [buttons_ count]; ++i) {
574     ToolbarActionViewController* controller =
575         [[self buttonAtIndex:i] viewController];
576     if (controller != toolbar_actions[i]) {
577       size_t j = i + 1;
578       while (true) {
579         ToolbarActionViewController* other_controller =
580             [[self buttonAtIndex:j] viewController];
581         if (other_controller == toolbar_actions[i])
582           break;
583         ++j;
584       }
585       [buttons_ exchangeObjectAtIndex:i withObjectAtIndex:j];
586     }
587   }
589   [self showChevronIfNecessaryInFrame:[containerView_ frame]];
590   NSUInteger startIndex = toolbarActionsBar_->GetStartIndexInBounds();
591   NSUInteger endIndex = toolbarActionsBar_->GetEndIndexInBounds();
592   for (NSUInteger i = 0; i < [buttons_ count]; ++i) {
593     BrowserActionButton* button = [buttons_ objectAtIndex:i];
594     if ([button isBeingDragged])
595       continue;
597     [self moveButton:[buttons_ objectAtIndex:i] toIndex:i];
599     if (i >= startIndex && i < endIndex) {
600       // Make sure the button is within the visible container.
601       if ([button superview] != containerView_) {
602         // We add the subview under the sibling views so that when it
603         // "slides in", it does so under its neighbors.
604         [containerView_ addSubview:button
605                         positioned:NSWindowBelow
606                         relativeTo:nil];
607       }
608       // We need to set the alpha value in case the container has resized.
609       [button setAlphaValue:1.0];
610     } else if ([button superview] == containerView_ &&
611                ![containerView_ userIsResizing]) {
612       // If the user is resizing, all buttons are (and should be) on the
613       // container view.
614       [button removeFromSuperview];
615       [button setAlphaValue:0.0];
616     }
617   }
620 - (void)removeViewForAction:(ToolbarActionViewController*)action {
621   BrowserActionButton* button = [self buttonForId:action->GetId()];
623   [button removeFromSuperview];
624   [button onRemoved];
625   [buttons_ removeObject:button];
627   [containerView_ setMaxDesiredWidth:toolbarActionsBar_->GetMaximumWidth()];
630 - (void)removeAllViews {
631   for (BrowserActionButton* button in buttons_.get()) {
632     [button removeFromSuperview];
633     [button onRemoved];
634   }
635   [buttons_ removeAllObjects];
638 - (void)resizeContainerToWidth:(CGFloat)width {
639   // Cocoa goes a little crazy if we try and change animations while adjusting
640   // child frames (i.e., the buttons). If the toolbar is already animating,
641   // just jump to the new frame. (This typically only happens if someone is
642   // "spamming" a button to add/remove an action.)
643   BOOL animate = !toolbarActionsBar_->suppress_animation() &&
644       ![containerView_ isAnimating];
645   [self updateContainerVisibility];
646   [containerView_ resizeToWidth:width
647                         animate:animate];
648   NSRect frame = animate ? [containerView_ animationEndFrame] :
649                            [containerView_ frame];
651   [self showChevronIfNecessaryInFrame:frame];
653   [containerView_ setNeedsDisplay:YES];
655   if (!animate) {
656     [[NSNotificationCenter defaultCenter]
657         postNotificationName:kBrowserActionVisibilityChangedNotification
658                       object:self];
659   }
660   [self redraw];
661   [self updateGrippyCursors];
664 - (BOOL)updateContainerVisibility {
665   BOOL hidden = [buttons_ count] == 0;
666   if ([containerView_ isHidden] != hidden)
667     [containerView_ setHidden:hidden];
668   return !hidden;
671 - (void)updateButtonOpacity {
672   for (BrowserActionButton* button in buttons_.get()) {
673     NSRect buttonFrame = [button frameAfterAnimation];
674     // The button is fully in the container view, and should get full opacity.
675     if (NSContainsRect([containerView_ bounds], buttonFrame)) {
676       if ([button alphaValue] != 1.0)
677         [button setAlphaValue:1.0];
679       continue;
680     }
681     // The button is only partially in the container view. If the user is
682     // resizing the container, we have partial alpha so the icon fades in as
683     // space is made. Otherwise, hide the icon fully.
684     CGFloat alpha = 0.0;
685     if ([containerView_ userIsResizing]) {
686       CGFloat intersectionWidth =
687           NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
688       alpha = std::max(static_cast<CGFloat>(0.0),
689                        intersectionWidth / NSWidth(buttonFrame));
690     }
691     [button setAlphaValue:alpha];
692     [button setNeedsDisplay:YES];
693   }
696 - (void)updateButtonPositions {
697   for (NSUInteger index = 0; index < [buttons_ count]; ++index) {
698     BrowserActionButton* button = [buttons_ objectAtIndex:index];
699     NSRect buttonFrame = [self frameForIndex:index];
701     // If the button is at the proper position (or animating to it), then we
702     // don't need to update its position.
703     if (NSMinX([button frameAfterAnimation]) == NSMinX(buttonFrame))
704       continue;
706     // We set the x-origin by calculating the proper distance from the right
707     // edge in the container so that, if the container is animating, the
708     // button appears stationary.
709     buttonFrame.origin.x = NSWidth([containerView_ frame]) -
710         (toolbarActionsBar_->GetPreferredSize().width() - NSMinX(buttonFrame));
711     [button setFrame:buttonFrame animate:NO];
712   }
715 - (BrowserActionButton*)buttonForId:(const std::string&)id {
716   for (BrowserActionButton* button in buttons_.get()) {
717     if ([button viewController]->GetId() == id)
718       return button;
719   }
720   return nil;
723 - (BrowserActionButton*)buttonAtIndex:(NSUInteger)index {
724   return static_cast<BrowserActionButton*>([buttons_ objectAtIndex:index]);
727 - (void)containerFrameChanged:(NSNotification*)notification {
728   [self updateButtonPositions];
729   [self updateButtonOpacity];
730   [[containerView_ window] invalidateCursorRectsForView:containerView_];
731   [self updateChevronPositionInFrame:[containerView_ frame]];
734 - (void)containerDragStart:(NSNotification*)notification {
735   [self setChevronHidden:YES inFrame:[containerView_ frame]];
736   for (BrowserActionButton* button in buttons_.get()) {
737     if ([button superview] != containerView_) {
738       [button setAlphaValue:1.0];
739       [containerView_ addSubview:button];
740     }
741   }
744 - (void)containerDragFinished:(NSNotification*)notification {
745   for (BrowserActionButton* button in buttons_.get()) {
746     NSRect buttonFrame = [button frame];
747     if (NSContainsRect([containerView_ bounds], buttonFrame))
748       continue;
750     CGFloat intersectionWidth =
751         NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
752     // Hide the button if it's not "mostly" visible. "Mostly" here equates to
753     // having three or fewer pixels hidden.
754     if (([containerView_ grippyPinned] && intersectionWidth > 0) ||
755         (intersectionWidth <= NSWidth(buttonFrame) - 3.0)) {
756       [button setAlphaValue:0.0];
757       [button removeFromSuperview];
758     }
759   }
761   toolbarActionsBar_->OnResizeComplete(
762       toolbarActionsBar_->IconCountToWidth([self visibleButtonCount]));
764   [self updateGrippyCursors];
765   [self resizeContainerToWidth:toolbarActionsBar_->GetPreferredSize().width()];
768 - (void)containerAnimationEnded:(NSNotification*)notification {
769   if (![containerView_ isAnimating])
770     toolbarActionsBar_->OnAnimationEnded();
773 - (void)containerKeyEvent:(NSNotification*)notification {
774   DCHECK(isOverflow_);  // We only manually process key events in overflow.
776   NSDictionary* dict = [notification userInfo];
777   BrowserActionsContainerKeyAction action =
778       static_cast<BrowserActionsContainerKeyAction>(
779           [[dict objectForKey:kBrowserActionsContainerKeyEventKey] intValue]);
780   switch (action) {
781     case BROWSER_ACTIONS_DECREMENT_FOCUS:
782     case BROWSER_ACTIONS_INCREMENT_FOCUS: {
783       NSInteger newIndex = focusedViewIndex_ +
784           (action == BROWSER_ACTIONS_INCREMENT_FOCUS ? 1 : -1);
785       NSInteger minIndex =
786           [buttons_ count] - toolbarActionsBar_->GetIconCount();
787       if (newIndex >= minIndex && newIndex < static_cast<int>([buttons_ count]))
788         [self setFocusedViewIndex:newIndex];
789       break;
790     }
791     case BROWSER_ACTIONS_EXECUTE_CURRENT: {
792       if (focusedViewIndex_ != -1) {
793         BrowserActionButton* focusedButton =
794             [self buttonAtIndex:focusedViewIndex_];
795         [focusedButton performClick:focusedButton];
796       }
797       break;
798     }
799     case BROWSER_ACTIONS_INVALID_KEY_ACTION:
800       NOTREACHED();
801   }
804 - (void)containerMouseEntered:(NSNotification*)notification {
805   if (!activeBubble_ &&  // only show one bubble at a time
806       ExtensionToolbarIconSurfacingBubbleDelegate::ShouldShowForProfile(
807           browser_->profile())) {
808     scoped_ptr<ToolbarActionsBarBubbleDelegate> delegate(
809         new ExtensionToolbarIconSurfacingBubbleDelegate(browser_->profile()));
810     ToolbarActionsBarBubbleMac* bubble =
811         [self createMessageBubble:delegate.Pass()
812                      anchorToSelf:YES];
813     [bubble showWindow:nil];
814   }
815   [containerView_ setTrackingEnabled:NO];
816   [[NSNotificationCenter defaultCenter]
817       removeObserver:self
818                 name:kBrowserActionsContainerMouseEntered
819               object:containerView_];
822 - (void)actionButtonDragging:(NSNotification*)notification {
823   suppressChevron_ = YES;
824   if (![self chevronIsHidden])
825     [self setChevronHidden:YES inFrame:[containerView_ frame]];
827   // Determine what index the dragged button should lie in, alter the model and
828   // reposition the buttons.
829   BrowserActionButton* draggedButton = [notification object];
830   NSRect draggedButtonFrame = [draggedButton frame];
831   // Find the mid-point. We flip the y-coordinates so that y = 0 is at the
832   // top of the container to make row calculation more logical.
833   NSPoint midPoint =
834       NSMakePoint(NSMidX(draggedButtonFrame),
835                   NSMaxY([containerView_ bounds]) - NSMidY(draggedButtonFrame));
837   // Calculate the row index and the index in the row. We bound the latter
838   // because the view can go farther right than the right-most icon in the last
839   // row of the overflow menu.
840   NSInteger rowIndex = midPoint.y / ToolbarActionsBar::IconHeight();
841   int icons_per_row = isOverflow_ ?
842       toolbarActionsBar_->platform_settings().icons_per_overflow_menu_row :
843       toolbarActionsBar_->GetIconCount();
844   NSInteger indexInRow = std::min(icons_per_row - 1,
845       static_cast<int>(midPoint.x / ToolbarActionsBar::IconWidth(true)));
847   // Find the desired index for the button.
848   NSInteger maxIndex = [buttons_ count] - 1;
849   NSInteger offset = isOverflow_ ?
850       [buttons_ count] - toolbarActionsBar_->GetIconCount() : 0;
851   NSInteger index =
852       std::min(maxIndex, offset + rowIndex * icons_per_row + indexInRow);
854   toolbarActionsBar_->OnDragDrop([buttons_ indexOfObject:draggedButton],
855                                  index,
856                                  ToolbarActionsBar::DRAG_TO_SAME);
859 - (void)actionButtonDragFinished:(NSNotification*)notification {
860   suppressChevron_ = NO;
861   [self redraw];
864 - (NSRect)frameForIndex:(NSUInteger)index {
865   gfx::Rect frameRect = toolbarActionsBar_->GetFrameForIndex(index);
866   int iconWidth = ToolbarActionsBar::IconWidth(false);
867   // The toolbar actions bar will return an empty rect if the index is for an
868   // action that is before range we show (i.e., is for a button that's on the
869   // main bar, and this is the overflow). Set the frame to be outside the bounds
870   // of the view.
871   NSRect frame = frameRect.IsEmpty() ?
872       NSMakeRect(-iconWidth - 1, 0, iconWidth,
873                  ToolbarActionsBar::IconHeight()) :
874       NSRectFromCGRect(frameRect.ToCGRect());
875   // We need to flip the y coordinate for Cocoa's view system.
876   frame.origin.y = NSHeight([containerView_ frame]) - NSMaxY(frame);
877   return frame;
880 - (NSPoint)popupPointForView:(NSView*)view
881                   withBounds:(NSRect)bounds {
882   // Anchor point just above the center of the bottom.
883   int y = [view isFlipped] ? NSMaxY(bounds) - kBrowserActionBubbleYOffset :
884                              kBrowserActionBubbleYOffset;
885   NSPoint anchor = NSMakePoint(NSMidX(bounds), y);
886   // Convert the point to the container view's frame, and adjust for animation.
887   NSPoint anchorInContainer =
888       [containerView_ convertPoint:anchor fromView:view];
889   anchorInContainer.x -= NSMinX([containerView_ frame]) -
890       NSMinX([containerView_ animationEndFrame]);
892   return [containerView_ convertPoint:anchorInContainer toView:nil];
895 - (void)moveButton:(BrowserActionButton*)button
896            toIndex:(NSUInteger)index {
897   NSRect buttonFrame = [self frameForIndex:index];
899   CGFloat currentX = NSMinX([button frame]);
900   CGFloat xLeft = toolbarActionsBar_->GetPreferredSize().width() -
901       NSMinX(buttonFrame);
902   // We check if the button is already in the correct place for the toolbar's
903   // current size. This could mean that the button could be the correct distance
904   // from the left or from the right edge. If it has the correct distance, we
905   // don't move it, and it will be updated when the container frame changes.
906   // This way, if the user has extensions A and C installed, and installs
907   // extension B between them, extension C appears to stay stationary on the
908   // screen while the toolbar expands to the left (even though C's bounds within
909   // the container change).
910   if ((currentX == NSMinX(buttonFrame) ||
911        currentX == NSWidth([containerView_ frame]) - xLeft) &&
912       NSMinY([button frame]) == NSMinY(buttonFrame))
913     return;
915   // It's possible the button is already animating to the right place. Don't
916   // call move again, because it will stop the current animation.
917   if (!NSEqualRects(buttonFrame, [button frameAfterAnimation])) {
918     [button setFrame:buttonFrame
919              animate:!toolbarActionsBar_->suppress_animation() && !isOverflow_];
920   }
923 - (BOOL)browserActionClicked:(BrowserActionButton*)button {
924   return [button viewController]->ExecuteAction(true);
927 - (void)showChevronIfNecessaryInFrame:(NSRect)frame {
928   if (!toolbarActionsBar_->platform_settings().chevron_enabled)
929     return;
930   bool hidden = suppressChevron_ ||
931       toolbarActionsBar_->GetIconCount() == [self buttonCount];
932   [self setChevronHidden:hidden inFrame:frame];
935 - (void)updateChevronPositionInFrame:(NSRect)frame {
936   CGFloat xPos = NSWidth(frame) - kChevronWidth -
937       toolbarActionsBar_->platform_settings().right_padding;
938   NSRect buttonFrame = NSMakeRect(xPos,
939                                   0,
940                                   kChevronWidth,
941                                   ToolbarActionsBar::IconHeight());
942   [chevronAnimation_ stopAnimation];
943   [chevronMenuButton_ setFrame:buttonFrame];
946 - (void)setChevronHidden:(BOOL)hidden
947                  inFrame:(NSRect)frame {
948   if (!toolbarActionsBar_->platform_settings().chevron_enabled ||
949       hidden == [self chevronIsHidden])
950     return;
952   if (!chevronMenuButton_.get()) {
953     chevronMenuButton_.reset([[MenuButton alloc] init]);
954     [chevronMenuButton_ setOpenMenuOnClick:YES];
955     [chevronMenuButton_ setBordered:NO];
956     [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
958     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW
959                            forButtonState:image_button_cell::kDefaultState];
960     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_H
961                            forButtonState:image_button_cell::kHoverState];
962     [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_P
963                            forButtonState:image_button_cell::kPressedState];
965     overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
966     [overflowMenu_ setAutoenablesItems:NO];
967     [overflowMenu_ setDelegate:self];
968     [chevronMenuButton_ setAttachedMenu:overflowMenu_];
970     [containerView_ addSubview:chevronMenuButton_];
971   }
973   [self updateChevronPositionInFrame:frame];
975   // Stop any running animation.
976   [chevronAnimation_ stopAnimation];
978   if (toolbarActionsBar_->suppress_animation()) {
979     [chevronMenuButton_ setHidden:hidden];
980     return;
981   }
983   NSString* animationEffect;
984   if (hidden) {
985     animationEffect = NSViewAnimationFadeOutEffect;
986   } else {
987     [chevronMenuButton_ setHidden:NO];
988     animationEffect = NSViewAnimationFadeInEffect;
989   }
990   NSDictionary* animationDictionary = @{
991       NSViewAnimationTargetKey : chevronMenuButton_.get(),
992       NSViewAnimationEffectKey : animationEffect
993   };
994   [chevronAnimation_ setViewAnimations:
995       [NSArray arrayWithObject:animationDictionary]];
996   [chevronAnimation_ startAnimation];
999 - (void)chevronItemSelected:(id)menuItem {
1000   [self browserActionClicked:[menuItem representedObject]];
1003 - (void)updateGrippyCursors {
1004   [containerView_
1005       setCanDragLeft:toolbarActionsBar_->GetIconCount() != [buttons_ count]];
1006   [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
1007   [[containerView_ window] invalidateCursorRectsForView:containerView_];
1010 - (ToolbarController*)toolbarController {
1011   return [[BrowserWindowController browserWindowControllerForWindow:
1012              browser_->window()->GetNativeWindow()] toolbarController];
1015 - (ToolbarActionsBarBubbleMac*)createMessageBubble:
1016     (scoped_ptr<ToolbarActionsBarBubbleDelegate>)delegate
1017     anchorToSelf:(BOOL)anchorToSelf {
1018   DCHECK_GE([buttons_ count], 0u);
1019   NSView* anchorView =
1020       anchorToSelf ? containerView_ : [[self toolbarController] wrenchButton];
1021   NSPoint anchor = [self popupPointForView:anchorView
1022                                 withBounds:[anchorView bounds]];
1024   anchor = [[containerView_ window] convertBaseToScreen:anchor];
1025   activeBubble_ = [[ToolbarActionsBarBubbleMac alloc]
1026       initWithParentWindow:[containerView_ window]
1027                anchorPoint:anchor
1028                   delegate:delegate.Pass()];
1029   [[NSNotificationCenter defaultCenter]
1030       addObserver:self
1031          selector:@selector(bubbleWindowClosing:)
1032              name:NSWindowWillCloseNotification
1033            object:[activeBubble_ window]];
1034   return activeBubble_;
1037 - (void)bubbleWindowClosing:(NSNotification*)notification {
1038   activeBubble_ = nil;
1041 - (void)setFocusedViewIndex:(NSInteger)index {
1042   DCHECK(isOverflow_);
1043   focusedViewIndex_ = index;
1046 #pragma mark -
1047 #pragma mark Testing Methods
1049 - (BrowserActionButton*)buttonWithIndex:(NSUInteger)index {
1050   return index < [buttons_ count] ? [buttons_ objectAtIndex:index] : nil;
1053 @end