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"
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";
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;
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.
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
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
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
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;
148 // A bridge between the ToolbarActionsBar and the BrowserActionsController.
149 class ToolbarActionsBarBridge : public ToolbarActionsBarDelegate {
151 explicit ToolbarActionsBarBridge(BrowserActionsController* controller);
152 ~ToolbarActionsBarBridge() override;
154 BrowserActionsController* controller_for_test() { return controller_; }
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,
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)
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,
192 [controller_ addViewForAction:action
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,
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];
253 @implementation BrowserActionsController
255 @synthesize containerView = containerView_;
256 @synthesize browser = browser_;
257 @synthesize isOverflow = isOverflow_;
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])) {
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(),
279 containerView_ = container;
280 [containerView_ setPostsFrameChangedNotifications:YES];
281 [[NSNotificationCenter defaultCenter]
283 selector:@selector(containerFrameChanged:)
284 name:NSViewFrameDidChangeNotification
285 object:containerView_];
286 [[NSNotificationCenter defaultCenter]
288 selector:@selector(containerDragStart:)
289 name:kBrowserActionGrippyDragStartedNotification
290 object:containerView_];
291 [[NSNotificationCenter defaultCenter]
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
298 [[NSNotificationCenter defaultCenter]
300 selector:@selector(actionButtonDragFinished:)
301 name:kBrowserActionButtonDragEndNotification
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];
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]
324 selector:@selector(containerMouseEntered:)
325 name:kBrowserActionsContainerMouseEntered
326 object:containerView_];
334 // Explicitly destroy the ToolbarActionsBar so all buttons get removed with a
335 // valid BrowserActionsController, and so we can verify state before
337 toolbarActionsBar_->DeleteActions();
338 toolbarActionsBar_.reset();
339 DCHECK_EQ(0u, [buttons_ count]);
340 [[NSNotificationCenter defaultCenter] removeObserver:self];
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_;
359 - (gfx::Size)preferredSize {
360 return toolbarActionsBar_->GetPreferredSize();
363 - (NSPoint)popupPointForId:(const std::string&)id {
364 BrowserActionButton* button = [self buttonForId:id];
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];
375 bounds = [button convertRect:[button frameAfterAnimation]
376 fromView:[button superview]];
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())
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) {
407 } else if (effect == NSViewAnimationFadeOutEffect) {
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];
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]) {
440 base::SysUTF16ToNSString([button viewController]->GetActionName());
442 [menu addItemWithTitle:name
443 action:@selector(chevronItemSelected:)
445 [item setRepresentedObject:button];
446 [item setImage:[button compositedImage]];
447 [item setTarget:self];
448 [item setEnabled:[button isEnabled]];
453 #pragma mark Private Methods
455 - (void)addViewForAction:(ToolbarActionViewController*)action
456 withIndex:(NSUInteger)index {
457 NSRect buttonFrame = NSMakeRect(NSMaxX([containerView_ bounds]),
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]
472 selector:@selector(actionButtonDragging:)
473 name:kBrowserActionButtonDraggingNotification
476 [containerView_ setMaxWidth:toolbarActionsBar_->GetMaximumWidth()];
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]) {
488 while (toolbar_actions[i] != [[buttons_ objectAtIndex:j] viewController])
490 [buttons_ exchangeObjectAtIndex:i withObjectAtIndex: j];
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])
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
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];
524 - (void)removeViewForAction:(ToolbarActionViewController*)action {
525 BrowserActionButton* button = [self buttonForId:action->GetId()];
527 [button removeFromSuperview];
529 [buttons_ removeObject:button];
531 [containerView_ setMaxWidth:toolbarActionsBar_->GetMaximumWidth()];
534 - (void)removeAllViews {
535 for (BrowserActionButton* button in buttons_.get()) {
536 [button removeFromSuperview];
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
552 NSRect frame = animate ? [containerView_ animationEndFrame] :
553 [containerView_ frame];
555 [self showChevronIfNecessaryInFrame:frame];
557 [containerView_ setNeedsDisplay:YES];
560 [[NSNotificationCenter defaultCenter]
561 postNotificationName:kBrowserActionVisibilityChangedNotification
565 [self updateGrippyCursors];
568 - (BOOL)updateContainerVisibility {
569 BOOL hidden = [buttons_ count] == 0;
570 if ([containerView_ isHidden] != hidden)
571 [containerView_ setHidden: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];
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];
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))
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];
612 - (BrowserActionButton*)buttonForId:(const std::string&)id {
613 for (BrowserActionButton* button in buttons_.get()) {
614 if ([button viewController]->GetId() == id)
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];
637 - (void)containerDragFinished:(NSNotification*)notification {
638 for (BrowserActionButton* button in buttons_.get()) {
639 NSRect buttonFrame = [button frame];
640 if (NSContainsRect([containerView_ bounds], buttonFrame))
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];
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]
670 delegate:toolbarActionsBar_.get()];
671 [bubble showWindow:nil];
673 [containerView_ setTrackingEnabled:NO];
674 [[NSNotificationCenter defaultCenter]
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.
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;
710 std::min(maxIndex, offset + rowIndex * icons_per_row + indexInRow);
712 toolbarActionsBar_->OnDragDrop([buttons_ indexOfObject:draggedButton],
714 ToolbarActionsBar::DRAG_TO_SAME);
717 - (void)actionButtonDragFinished:(NSNotification*)notification {
718 suppressChevron_ = NO;
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,
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() -
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))
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_];
768 - (BOOL)browserActionClicked:(BrowserActionButton*)button {
769 return [button viewController]->ExecuteAction(true);
772 - (void)showChevronIfNecessaryInFrame:(NSRect)frame {
773 if (!toolbarActionsBar_->platform_settings().chevron_enabled)
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,
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])
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_];
818 [self updateChevronPositionInFrame:frame];
820 // Stop any running animation.
821 [chevronAnimation_ stopAnimation];
823 if (toolbarActionsBar_->suppress_animation()) {
824 [chevronMenuButton_ setHidden:hidden];
828 NSString* animationEffect;
830 animationEffect = NSViewAnimationFadeOutEffect;
832 [chevronMenuButton_ setHidden:NO];
833 animationEffect = NSViewAnimationFadeInEffect;
835 NSDictionary* animationDictionary = @{
836 NSViewAnimationTargetKey : chevronMenuButton_.get(),
837 NSViewAnimationEffectKey : animationEffect
839 [chevronAnimation_ setViewAnimations:
840 [NSArray arrayWithObject:animationDictionary]];
841 [chevronAnimation_ startAnimation];
844 - (void)chevronItemSelected:(id)menuItem {
845 [self browserActionClicked:[menuItem representedObject]];
848 - (void)updateGrippyCursors {
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];
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();