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/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";
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;
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.
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
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
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
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
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
172 - (void)bubbleWindowClosing:(NSNotification*)notification;
174 // Sets the current focused view. Should only be used for the overflow
176 - (void)setFocusedViewIndex:(NSInteger)index;
182 // A bridge between the ToolbarActionsBar and the BrowserActionsController.
183 class ToolbarActionsBarBridge : public ToolbarActionsBarDelegate {
185 explicit ToolbarActionsBarBridge(BrowserActionsController* controller);
186 ~ToolbarActionsBarBridge() override;
188 BrowserActionsController* controller_for_test() { return controller_; }
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,
199 bool suppress_chevron) override;
200 void SetChevronVisibility(bool chevron_visible) override;
201 int GetWidth() 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,
226 [controller_ addViewForAction:action
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,
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() const {
255 return NSWidth([[controller_ containerView] frame]);
258 bool ToolbarActionsBarBridge::IsAnimating() const {
259 return [[controller_ containerView] isAnimating];
262 void ToolbarActionsBarBridge::StopAnimating() {
263 // Unfortunately, animating the browser actions container affects neighboring
264 // views (like the omnibox), which could also be animating. Because of this,
265 // instead of just ending the animation, the cleanest way to terminate is to
266 // "animate" to the current frame.
267 [controller_ resizeContainerToWidth:
268 NSWidth([[controller_ containerView] frame])];
271 int ToolbarActionsBarBridge::GetChevronWidth() const {
272 return kChevronWidth;
275 void ToolbarActionsBarBridge::ShowExtensionMessageBubble(
276 scoped_ptr<extensions::ExtensionMessageBubbleController> controller,
277 ToolbarActionViewController* anchor_action) {
278 // This goop is a by-product of needing to wire together abstract classes,
279 // C++/Cocoa bridges, and ExtensionMessageBubbleController's somewhat strange
280 // Show() interface. It's ugly, but it's pretty confined, so it's probably
281 // okay (but if we ever need to expand, it might need to be reconsidered).
282 scoped_ptr<ExtensionMessageBubbleBridge> bridge(
283 new ExtensionMessageBubbleBridge(controller.Pass(),
284 anchor_action != nullptr));
285 ExtensionMessageBubbleBridge* weak_bridge = bridge.get();
286 ToolbarActionsBarBubbleMac* bubble =
287 [controller_ createMessageBubble:bridge.Pass()
288 anchorToSelf:anchor_action != nil];
289 weak_bridge->SetBubble(bubble);
290 weak_bridge->controller()->Show(weak_bridge);
295 @implementation BrowserActionsController
297 @synthesize containerView = containerView_;
298 @synthesize browser = browser_;
299 @synthesize isOverflow = isOverflow_;
300 @synthesize activeBubble = activeBubble_;
303 #pragma mark Public Methods
305 - (id)initWithBrowser:(Browser*)browser
306 containerView:(BrowserActionsContainerView*)container
307 mainController:(BrowserActionsController*)mainController {
308 DCHECK(browser && container);
310 if ((self = [super init])) {
312 isOverflow_ = mainController != nil;
314 toolbarActionsBarBridge_.reset(new ToolbarActionsBarBridge(self));
315 ToolbarActionsBar* mainBar =
316 mainController ? [mainController toolbarActionsBar] : nullptr;
317 toolbarActionsBar_.reset(
318 new ToolbarActionsBar(toolbarActionsBarBridge_.get(),
322 containerView_ = container;
323 [containerView_ setPostsFrameChangedNotifications:YES];
324 [[NSNotificationCenter defaultCenter]
326 selector:@selector(containerFrameChanged:)
327 name:NSViewFrameDidChangeNotification
328 object:containerView_];
329 [[NSNotificationCenter defaultCenter]
331 selector:@selector(containerDragStart:)
332 name:kBrowserActionGrippyDragStartedNotification
333 object:containerView_];
334 [[NSNotificationCenter defaultCenter]
336 selector:@selector(containerDragFinished:)
337 name:kBrowserActionGrippyDragFinishedNotification
338 object:containerView_];
339 [[NSNotificationCenter defaultCenter]
341 selector:@selector(containerAnimationEnded:)
342 name:kBrowserActionsContainerAnimationEnded
343 object:containerView_];
344 [[NSNotificationCenter defaultCenter]
346 selector:@selector(containerKeyEvent:)
347 name:kBrowserActionsContainerReceivedKeyEvent
348 object:containerView_];
349 // Listen for a finished drag from any button to make sure each open window
351 [[NSNotificationCenter defaultCenter]
353 selector:@selector(actionButtonDragFinished:)
354 name:kBrowserActionButtonDragEndNotification
357 suppressChevron_ = NO;
358 if (toolbarActionsBar_->platform_settings().chevron_enabled) {
359 chevronAnimation_.reset([[NSViewAnimation alloc] init]);
360 [chevronAnimation_ gtm_setDuration:kAnimationDuration
361 eventMask:NSLeftMouseUpMask];
362 [chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
366 toolbarActionsBar_->SetOverflowRowWidth(NSWidth([containerView_ frame]));
368 buttons_.reset([[NSMutableArray alloc] init]);
369 toolbarActionsBar_->CreateActions();
370 [self showChevronIfNecessaryInFrame:[containerView_ frame]];
371 [self updateGrippyCursors];
372 [container setIsOverflow:isOverflow_];
373 if (ExtensionToolbarIconSurfacingBubbleDelegate::ShouldShowForProfile(
374 browser_->profile())) {
375 [containerView_ setTrackingEnabled:YES];
376 [[NSNotificationCenter defaultCenter]
378 selector:@selector(containerMouseEntered:)
379 name:kBrowserActionsContainerMouseEntered
380 object:containerView_];
383 focusedViewIndex_ = -1;
390 [self browserWillBeDestroyed];
394 - (void)browserWillBeDestroyed {
395 [overflowMenu_ setDelegate:nil];
396 // Explicitly destroy the ToolbarActionsBar so all buttons get removed with a
397 // valid BrowserActionsController, and so we can verify state before
399 if (toolbarActionsBar_.get()) {
400 toolbarActionsBar_->DeleteActions();
401 toolbarActionsBar_.reset();
403 DCHECK_EQ(0u, [buttons_ count]);
404 [[NSNotificationCenter defaultCenter] removeObserver:self];
409 toolbarActionsBar_->Update();
412 - (NSUInteger)buttonCount {
413 return [buttons_ count];
416 - (NSUInteger)visibleButtonCount {
417 NSUInteger visibleCount = 0;
418 for (BrowserActionButton* button in buttons_.get())
419 visibleCount += [button superview] == containerView_;
423 - (gfx::Size)preferredSize {
424 return toolbarActionsBar_->GetPreferredSize();
427 - (NSPoint)popupPointForId:(const std::string&)id {
428 BrowserActionButton* button = [self buttonForId:id];
433 NSView* referenceButton = button;
434 if ([button superview] != containerView_ || isOverflow_) {
435 referenceButton = toolbarActionsBar_->platform_settings().chevron_enabled ?
436 chevronMenuButton_.get() : [[self toolbarController] wrenchButton];
437 bounds = [referenceButton bounds];
439 bounds = [button convertRect:[button frameAfterAnimation]
440 fromView:[button superview]];
443 return [self popupPointForView:referenceButton withBounds:bounds];
446 - (BOOL)chevronIsHidden {
447 if (!chevronMenuButton_.get())
450 if (![chevronAnimation_ isAnimating])
451 return [chevronMenuButton_ isHidden];
453 DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
455 // The chevron is animating in or out. Determine which one and have the return
456 // value reflect where the animation is headed.
457 NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
458 valueForKey:NSViewAnimationEffectKey];
459 if (effect == NSViewAnimationFadeInEffect) {
461 } else if (effect == NSViewAnimationFadeOutEffect) {
469 - (content::WebContents*)currentWebContents {
470 return browser_->tab_strip_model()->GetActiveWebContents();
473 - (BrowserActionButton*)mainButtonForId:(const std::string&)id {
474 BrowserActionsController* mainController = isOverflow_ ?
475 [[self toolbarController] browserActionsController] : self;
476 return [mainController buttonForId:id];
479 - (ToolbarActionsBar*)toolbarActionsBar {
480 return toolbarActionsBar_.get();
483 - (void)setFocusedInOverflow:(BOOL)focused {
484 BOOL isFocused = focusedViewIndex_ != -1;
485 if (isFocused != focused) {
486 int index = focused ?
487 [buttons_ count] - toolbarActionsBar_->GetIconCount() : -1;
488 [self setFocusedViewIndex:index];
492 - (gfx::Size)sizeForOverflowWidth:(int)maxWidth {
493 toolbarActionsBar_->SetOverflowRowWidth(maxWidth);
494 return [self preferredSize];
498 #pragma mark NSMenuDelegate
500 - (void)menuNeedsUpdate:(NSMenu*)menu {
501 [menu removeAllItems];
503 // See menu_button.h for documentation on why this is needed.
504 [menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
506 NSUInteger iconCount = toolbarActionsBar_->GetIconCount();
507 NSRange hiddenButtonRange =
508 NSMakeRange(iconCount, [buttons_ count] - iconCount);
509 for (BrowserActionButton* button in
510 [buttons_ subarrayWithRange:hiddenButtonRange]) {
512 base::SysUTF16ToNSString([button viewController]->GetActionName());
514 [menu addItemWithTitle:name
515 action:@selector(chevronItemSelected:)
517 [item setRepresentedObject:button];
518 [item setImage:[button compositedImage]];
519 [item setTarget:self];
520 [item setEnabled:[button isEnabled]];
525 #pragma mark Private Methods
527 - (void)addViewForAction:(ToolbarActionViewController*)action
528 withIndex:(NSUInteger)index {
529 NSRect buttonFrame = NSMakeRect(NSMaxX([containerView_ bounds]),
531 ToolbarActionsBar::IconWidth(false),
532 ToolbarActionsBar::IconHeight());
533 BrowserActionButton* newButton =
534 [[[BrowserActionButton alloc]
535 initWithFrame:buttonFrame
536 viewController:action
537 controller:self] autorelease];
538 [newButton setTarget:self];
539 [newButton setAction:@selector(browserActionClicked:)];
540 [buttons_ insertObject:newButton atIndex:index];
542 [[NSNotificationCenter defaultCenter]
544 selector:@selector(actionButtonDragging:)
545 name:kBrowserActionButtonDraggingNotification
548 [containerView_ setMaxDesiredWidth:toolbarActionsBar_->GetMaximumWidth()];
552 if (![self updateContainerVisibility])
553 return; // Container is hidden; no need to update.
555 scoped_ptr<ui::NinePartImageIds> highlight;
556 if (toolbarActionsBar_->is_highlighting()) {
557 if (toolbarActionsBar_->highlight_type() ==
558 ToolbarActionsModel::HIGHLIGHT_INFO)
560 new ui::NinePartImageIds(IMAGE_GRID(IDR_TOOLBAR_ACTION_HIGHLIGHT)));
563 new ui::NinePartImageIds(IMAGE_GRID(IDR_DEVELOPER_MODE_HIGHLIGHT)));
565 [containerView_ setHighlight:highlight.Pass()];
567 std::vector<ToolbarActionViewController*> toolbar_actions =
568 toolbarActionsBar_->GetActions();
569 for (NSUInteger i = 0; i < [buttons_ count]; ++i) {
570 ToolbarActionViewController* controller =
571 [[self buttonAtIndex:i] viewController];
572 if (controller != toolbar_actions[i]) {
575 ToolbarActionViewController* other_controller =
576 [[self buttonAtIndex:j] viewController];
577 if (other_controller == toolbar_actions[i])
581 [buttons_ exchangeObjectAtIndex:i withObjectAtIndex:j];
585 [self showChevronIfNecessaryInFrame:[containerView_ frame]];
586 NSUInteger minIndex = isOverflow_ ?
587 [buttons_ count] - toolbarActionsBar_->GetIconCount() : 0;
588 NSUInteger maxIndex = isOverflow_ ?
589 [buttons_ count] : toolbarActionsBar_->GetIconCount();
590 for (NSUInteger i = 0; i < [buttons_ count]; ++i) {
591 BrowserActionButton* button = [buttons_ objectAtIndex:i];
592 if ([button isBeingDragged])
595 [self moveButton:[buttons_ objectAtIndex:i] toIndex:i];
597 if (i >= minIndex && i < maxIndex) {
598 // Make sure the button is within the visible container.
599 if ([button superview] != containerView_) {
600 // We add the subview under the sibling views so that when it
601 // "slides in", it does so under its neighbors.
602 [containerView_ addSubview:button
603 positioned:NSWindowBelow
606 // We need to set the alpha value in case the container has resized.
607 [button setAlphaValue:1.0];
608 } else if ([button superview] == containerView_ &&
609 ![containerView_ userIsResizing]) {
610 // If the user is resizing, all buttons are (and should be) on the
612 [button removeFromSuperview];
613 [button setAlphaValue:0.0];
618 - (void)removeViewForAction:(ToolbarActionViewController*)action {
619 BrowserActionButton* button = [self buttonForId:action->GetId()];
621 [button removeFromSuperview];
623 [buttons_ removeObject:button];
625 [containerView_ setMaxDesiredWidth:toolbarActionsBar_->GetMaximumWidth()];
628 - (void)removeAllViews {
629 for (BrowserActionButton* button in buttons_.get()) {
630 [button removeFromSuperview];
633 [buttons_ removeAllObjects];
636 - (void)resizeContainerToWidth:(CGFloat)width {
637 // Cocoa goes a little crazy if we try and change animations while adjusting
638 // child frames (i.e., the buttons). If the toolbar is already animating,
639 // just jump to the new frame. (This typically only happens if someone is
640 // "spamming" a button to add/remove an action.)
641 BOOL animate = !toolbarActionsBar_->suppress_animation() &&
642 ![containerView_ isAnimating];
643 [self updateContainerVisibility];
644 [containerView_ resizeToWidth:width
646 NSRect frame = animate ? [containerView_ animationEndFrame] :
647 [containerView_ frame];
649 [self showChevronIfNecessaryInFrame:frame];
651 [containerView_ setNeedsDisplay:YES];
654 [[NSNotificationCenter defaultCenter]
655 postNotificationName:kBrowserActionVisibilityChangedNotification
659 [self updateGrippyCursors];
662 - (BOOL)updateContainerVisibility {
663 BOOL hidden = [buttons_ count] == 0;
664 if ([containerView_ isHidden] != hidden)
665 [containerView_ setHidden:hidden];
669 - (void)updateButtonOpacity {
670 for (BrowserActionButton* button in buttons_.get()) {
671 NSRect buttonFrame = [button frame];
672 if (NSContainsRect([containerView_ bounds], buttonFrame)) {
673 if ([button alphaValue] != 1.0)
674 [button setAlphaValue:1.0];
678 CGFloat intersectionWidth =
679 NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
680 CGFloat alpha = std::max(static_cast<CGFloat>(0.0),
681 intersectionWidth / NSWidth(buttonFrame));
682 [button setAlphaValue:alpha];
683 [button setNeedsDisplay:YES];
687 - (void)updateButtonPositions {
688 for (NSUInteger index = 0; index < [buttons_ count]; ++index) {
689 BrowserActionButton* button = [buttons_ objectAtIndex:index];
690 NSRect buttonFrame = [self frameForIndex:index];
692 // If the button is at the proper position (or animating to it), then we
693 // don't need to update its position.
694 if (NSMinX([button frameAfterAnimation]) == NSMinX(buttonFrame))
697 // We set the x-origin by calculating the proper distance from the right
698 // edge in the container so that, if the container is animating, the
699 // button appears stationary.
700 buttonFrame.origin.x = NSWidth([containerView_ frame]) -
701 (toolbarActionsBar_->GetPreferredSize().width() - NSMinX(buttonFrame));
702 [button setFrame:buttonFrame animate:NO];
706 - (BrowserActionButton*)buttonForId:(const std::string&)id {
707 for (BrowserActionButton* button in buttons_.get()) {
708 if ([button viewController]->GetId() == id)
714 - (BrowserActionButton*)buttonAtIndex:(NSUInteger)index {
715 return static_cast<BrowserActionButton*>([buttons_ objectAtIndex:index]);
718 - (void)containerFrameChanged:(NSNotification*)notification {
719 [self updateButtonPositions];
720 [self updateButtonOpacity];
721 [[containerView_ window] invalidateCursorRectsForView:containerView_];
722 [self updateChevronPositionInFrame:[containerView_ frame]];
725 - (void)containerDragStart:(NSNotification*)notification {
726 [self setChevronHidden:YES inFrame:[containerView_ frame]];
727 for (BrowserActionButton* button in buttons_.get()) {
728 if ([button superview] != containerView_) {
729 [button setAlphaValue:1.0];
730 [containerView_ addSubview:button];
735 - (void)containerDragFinished:(NSNotification*)notification {
736 for (BrowserActionButton* button in buttons_.get()) {
737 NSRect buttonFrame = [button frame];
738 if (NSContainsRect([containerView_ bounds], buttonFrame))
741 CGFloat intersectionWidth =
742 NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
743 // Hide the button if it's not "mostly" visible. "Mostly" here equates to
744 // having three or fewer pixels hidden.
745 if (([containerView_ grippyPinned] && intersectionWidth > 0) ||
746 (intersectionWidth <= NSWidth(buttonFrame) - 3.0)) {
747 [button setAlphaValue:0.0];
748 [button removeFromSuperview];
752 toolbarActionsBar_->OnResizeComplete(
753 toolbarActionsBar_->IconCountToWidth([self visibleButtonCount]));
755 [self updateGrippyCursors];
756 [self resizeContainerToWidth:toolbarActionsBar_->GetPreferredSize().width()];
759 - (void)containerAnimationEnded:(NSNotification*)notification {
760 if (![containerView_ isAnimating])
761 toolbarActionsBar_->OnAnimationEnded();
764 - (void)containerKeyEvent:(NSNotification*)notification {
765 DCHECK(isOverflow_); // We only manually process key events in overflow.
767 NSDictionary* dict = [notification userInfo];
768 BrowserActionsContainerKeyAction action =
769 static_cast<BrowserActionsContainerKeyAction>(
770 [[dict objectForKey:kBrowserActionsContainerKeyEventKey] intValue]);
772 case BROWSER_ACTIONS_DECREMENT_FOCUS:
773 case BROWSER_ACTIONS_INCREMENT_FOCUS: {
774 NSInteger newIndex = focusedViewIndex_ +
775 (action == BROWSER_ACTIONS_INCREMENT_FOCUS ? 1 : -1);
777 [buttons_ count] - toolbarActionsBar_->GetIconCount();
778 if (newIndex >= minIndex && newIndex < static_cast<int>([buttons_ count]))
779 [self setFocusedViewIndex:newIndex];
782 case BROWSER_ACTIONS_EXECUTE_CURRENT: {
783 if (focusedViewIndex_ != -1) {
784 BrowserActionButton* focusedButton =
785 [self buttonAtIndex:focusedViewIndex_];
786 [focusedButton performClick:focusedButton];
790 case BROWSER_ACTIONS_INVALID_KEY_ACTION:
795 - (void)containerMouseEntered:(NSNotification*)notification {
796 if (!activeBubble_ && // only show one bubble at a time
797 ExtensionToolbarIconSurfacingBubbleDelegate::ShouldShowForProfile(
798 browser_->profile())) {
799 scoped_ptr<ToolbarActionsBarBubbleDelegate> delegate(
800 new ExtensionToolbarIconSurfacingBubbleDelegate(browser_->profile()));
801 ToolbarActionsBarBubbleMac* bubble =
802 [self createMessageBubble:delegate.Pass()
804 [bubble showWindow:nil];
806 [containerView_ setTrackingEnabled:NO];
807 [[NSNotificationCenter defaultCenter]
809 name:kBrowserActionsContainerMouseEntered
810 object:containerView_];
813 - (void)actionButtonDragging:(NSNotification*)notification {
814 suppressChevron_ = YES;
815 if (![self chevronIsHidden])
816 [self setChevronHidden:YES inFrame:[containerView_ frame]];
818 // Determine what index the dragged button should lie in, alter the model and
819 // reposition the buttons.
820 BrowserActionButton* draggedButton = [notification object];
821 NSRect draggedButtonFrame = [draggedButton frame];
822 // Find the mid-point. We flip the y-coordinates so that y = 0 is at the
823 // top of the container to make row calculation more logical.
825 NSMakePoint(NSMidX(draggedButtonFrame),
826 NSMaxY([containerView_ bounds]) - NSMidY(draggedButtonFrame));
828 // Calculate the row index and the index in the row. We bound the latter
829 // because the view can go farther right than the right-most icon in the last
830 // row of the overflow menu.
831 NSInteger rowIndex = midPoint.y / ToolbarActionsBar::IconHeight();
832 int icons_per_row = isOverflow_ ?
833 toolbarActionsBar_->platform_settings().icons_per_overflow_menu_row :
834 toolbarActionsBar_->GetIconCount();
835 NSInteger indexInRow = std::min(icons_per_row - 1,
836 static_cast<int>(midPoint.x / ToolbarActionsBar::IconWidth(true)));
838 // Find the desired index for the button.
839 NSInteger maxIndex = [buttons_ count] - 1;
840 NSInteger offset = isOverflow_ ?
841 [buttons_ count] - toolbarActionsBar_->GetIconCount() : 0;
843 std::min(maxIndex, offset + rowIndex * icons_per_row + indexInRow);
845 toolbarActionsBar_->OnDragDrop([buttons_ indexOfObject:draggedButton],
847 ToolbarActionsBar::DRAG_TO_SAME);
850 - (void)actionButtonDragFinished:(NSNotification*)notification {
851 suppressChevron_ = NO;
855 - (NSRect)frameForIndex:(NSUInteger)index {
856 gfx::Rect frameRect = toolbarActionsBar_->GetFrameForIndex(index);
857 int iconWidth = ToolbarActionsBar::IconWidth(false);
858 // The toolbar actions bar will return an empty rect if the index is for an
859 // action that is before range we show (i.e., is for a button that's on the
860 // main bar, and this is the overflow). Set the frame to be outside the bounds
862 NSRect frame = frameRect.IsEmpty() ?
863 NSMakeRect(-iconWidth - 1, 0, iconWidth,
864 ToolbarActionsBar::IconHeight()) :
865 NSRectFromCGRect(frameRect.ToCGRect());
866 // We need to flip the y coordinate for Cocoa's view system.
867 frame.origin.y = NSHeight([containerView_ frame]) - NSMaxY(frame);
871 - (NSPoint)popupPointForView:(NSView*)view
872 withBounds:(NSRect)bounds {
873 // Anchor point just above the center of the bottom.
874 int y = [view isFlipped] ? NSMaxY(bounds) - kBrowserActionBubbleYOffset :
875 kBrowserActionBubbleYOffset;
876 NSPoint anchor = NSMakePoint(NSMidX(bounds), y);
877 // Convert the point to the container view's frame, and adjust for animation.
878 NSPoint anchorInContainer =
879 [containerView_ convertPoint:anchor fromView:view];
880 anchorInContainer.x -= NSMinX([containerView_ frame]) -
881 NSMinX([containerView_ animationEndFrame]);
883 return [containerView_ convertPoint:anchorInContainer toView:nil];
886 - (void)moveButton:(BrowserActionButton*)button
887 toIndex:(NSUInteger)index {
888 NSRect buttonFrame = [self frameForIndex:index];
890 CGFloat currentX = NSMinX([button frame]);
891 CGFloat xLeft = toolbarActionsBar_->GetPreferredSize().width() -
893 // We check if the button is already in the correct place for the toolbar's
894 // current size. This could mean that the button could be the correct distance
895 // from the left or from the right edge. If it has the correct distance, we
896 // don't move it, and it will be updated when the container frame changes.
897 // This way, if the user has extensions A and C installed, and installs
898 // extension B between them, extension C appears to stay stationary on the
899 // screen while the toolbar expands to the left (even though C's bounds within
900 // the container change).
901 if ((currentX == NSMinX(buttonFrame) ||
902 currentX == NSWidth([containerView_ frame]) - xLeft) &&
903 NSMinY([button frame]) == NSMinY(buttonFrame))
906 // It's possible the button is already animating to the right place. Don't
907 // call move again, because it will stop the current animation.
908 if (!NSEqualRects(buttonFrame, [button frameAfterAnimation])) {
909 [button setFrame:buttonFrame
910 animate:!toolbarActionsBar_->suppress_animation() && !isOverflow_];
914 - (BOOL)browserActionClicked:(BrowserActionButton*)button {
915 return [button viewController]->ExecuteAction(true);
918 - (void)showChevronIfNecessaryInFrame:(NSRect)frame {
919 if (!toolbarActionsBar_->platform_settings().chevron_enabled)
921 bool hidden = suppressChevron_ ||
922 toolbarActionsBar_->GetIconCount() == [self buttonCount];
923 [self setChevronHidden:hidden inFrame:frame];
926 - (void)updateChevronPositionInFrame:(NSRect)frame {
927 CGFloat xPos = NSWidth(frame) - kChevronWidth -
928 toolbarActionsBar_->platform_settings().right_padding;
929 NSRect buttonFrame = NSMakeRect(xPos,
932 ToolbarActionsBar::IconHeight());
933 [chevronAnimation_ stopAnimation];
934 [chevronMenuButton_ setFrame:buttonFrame];
937 - (void)setChevronHidden:(BOOL)hidden
938 inFrame:(NSRect)frame {
939 if (!toolbarActionsBar_->platform_settings().chevron_enabled ||
940 hidden == [self chevronIsHidden])
943 if (!chevronMenuButton_.get()) {
944 chevronMenuButton_.reset([[MenuButton alloc] init]);
945 [chevronMenuButton_ setOpenMenuOnClick:YES];
946 [chevronMenuButton_ setBordered:NO];
947 [chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
949 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW
950 forButtonState:image_button_cell::kDefaultState];
951 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_H
952 forButtonState:image_button_cell::kHoverState];
953 [[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_P
954 forButtonState:image_button_cell::kPressedState];
956 overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
957 [overflowMenu_ setAutoenablesItems:NO];
958 [overflowMenu_ setDelegate:self];
959 [chevronMenuButton_ setAttachedMenu:overflowMenu_];
961 [containerView_ addSubview:chevronMenuButton_];
964 [self updateChevronPositionInFrame:frame];
966 // Stop any running animation.
967 [chevronAnimation_ stopAnimation];
969 if (toolbarActionsBar_->suppress_animation()) {
970 [chevronMenuButton_ setHidden:hidden];
974 NSString* animationEffect;
976 animationEffect = NSViewAnimationFadeOutEffect;
978 [chevronMenuButton_ setHidden:NO];
979 animationEffect = NSViewAnimationFadeInEffect;
981 NSDictionary* animationDictionary = @{
982 NSViewAnimationTargetKey : chevronMenuButton_.get(),
983 NSViewAnimationEffectKey : animationEffect
985 [chevronAnimation_ setViewAnimations:
986 [NSArray arrayWithObject:animationDictionary]];
987 [chevronAnimation_ startAnimation];
990 - (void)chevronItemSelected:(id)menuItem {
991 [self browserActionClicked:[menuItem representedObject]];
994 - (void)updateGrippyCursors {
996 setCanDragLeft:toolbarActionsBar_->GetIconCount() != [buttons_ count]];
997 [containerView_ setCanDragRight:[self visibleButtonCount] > 0];
998 [[containerView_ window] invalidateCursorRectsForView:containerView_];
1001 - (ToolbarController*)toolbarController {
1002 return [[BrowserWindowController browserWindowControllerForWindow:
1003 browser_->window()->GetNativeWindow()] toolbarController];
1006 - (ToolbarActionsBarBubbleMac*)createMessageBubble:
1007 (scoped_ptr<ToolbarActionsBarBubbleDelegate>)delegate
1008 anchorToSelf:(BOOL)anchorToSelf {
1009 DCHECK_GE([buttons_ count], 0u);
1010 NSView* anchorView =
1011 anchorToSelf ? containerView_ : [[self toolbarController] wrenchButton];
1012 NSPoint anchor = [self popupPointForView:anchorView
1013 withBounds:[anchorView bounds]];
1015 anchor = [[containerView_ window] convertBaseToScreen:anchor];
1016 activeBubble_ = [[ToolbarActionsBarBubbleMac alloc]
1017 initWithParentWindow:[containerView_ window]
1019 delegate:delegate.Pass()];
1020 [[NSNotificationCenter defaultCenter]
1022 selector:@selector(bubbleWindowClosing:)
1023 name:NSWindowWillCloseNotification
1024 object:[activeBubble_ window]];
1025 return activeBubble_;
1028 - (void)bubbleWindowClosing:(NSNotification*)notification {
1029 activeBubble_ = nil;
1032 - (void)setFocusedViewIndex:(NSInteger)index {
1033 DCHECK(isOverflow_);
1034 focusedViewIndex_ = index;
1038 #pragma mark Testing Methods
1040 - (BrowserActionButton*)buttonWithIndex:(NSUInteger)index {
1041 return index < [buttons_ count] ? [buttons_ objectAtIndex:index] : nil;