blob: 38857b0b34bd8f46cabb7d845129b8c0e7ebb81d [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
#include <stddef.h>
#include <string>
#include <utility>
#include "base/macros.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/browser/extensions/extension_message_bubble_controller.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#import "chrome/browser/ui/cocoa/browser_window_controller.h"
#import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
#import "chrome/browser/ui/cocoa/extensions/browser_actions_container_view.h"
#import "chrome/browser/ui/cocoa/extensions/extension_message_bubble_bridge.h"
#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
#import "chrome/browser/ui/cocoa/extensions/toolbar_actions_bar_bubble_mac.h"
#import "chrome/browser/ui/cocoa/image_button_cell.h"
#import "chrome/browser/ui/cocoa/menu_button.h"
#import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
#include "chrome/browser/ui/extensions/extension_toolbar_icon_surfacing_bubble_delegate.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_bar.h"
#include "chrome/browser/ui/toolbar/toolbar_actions_bar_delegate.h"
#include "grit/theme_resources.h"
#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
#include "ui/base/cocoa/appkit_utils.h"
NSString* const kBrowserActionVisibilityChangedNotification =
@"BrowserActionVisibilityChangedNotification";
namespace {
const CGFloat kAnimationDuration = 0.2;
const CGFloat kChevronWidth = 18;
// How far to inset from the bottom of the view to get the top border
// of the popup 2px below the bottom of the Omnibox.
const CGFloat kBrowserActionBubbleYOffset = 3.0;
} // namespace
@interface BrowserActionsController(Private)
// Creates and adds a view for the given |action| at |index|.
- (void)addViewForAction:(ToolbarActionViewController*)action
withIndex:(NSUInteger)index;
// Removes the view for the given |action| from the ccontainer.
- (void)removeViewForAction:(ToolbarActionViewController*)action;
// Removes views for all actions.
- (void)removeAllViews;
// Redraws the BrowserActionsContainerView and updates the button order to match
// the order in the ToolbarActionsBar.
- (void)redraw;
// Resizes the container to the specified |width|, and animates according to
// the ToolbarActionsBar.
- (void)resizeContainerToWidth:(CGFloat)width;
// Sets the container to be either hidden or visible based on whether there are
// any actions to show.
// Returns whether the container is visible.
- (BOOL)updateContainerVisibility;
// During container resizing, buttons become more transparent as they are pushed
// off the screen. This method updates each button's opacity determined by the
// position of the button.
- (void)updateButtonOpacity;
// When the container is resizing, there's a chance that the buttons' frames
// need to be adjusted (for instance, if an action is added to the left, the
// frames of the actions to the right should gradually move right in the
// container). Adjust the frames accordingly.
- (void)updateButtonPositions;
// Returns the existing button associated with the given id; nil if it cannot be
// found.
- (BrowserActionButton*)buttonForId:(const std::string&)id;
// Returns the button at the given index. This is just a wrapper around
// [NSArray objectAtIndex:], since that technically defaults to returning ids
// (and can cause compile errors).
- (BrowserActionButton*)buttonAtIndex:(NSUInteger)index;
// Notification handlers for events registered by the class.
// Updates each button's opacity, the cursor rects and chevron position.
- (void)containerFrameChanged:(NSNotification*)notification;
// Hides the chevron and unhides every hidden button so that dragging the
// container out smoothly shows the Browser Action buttons.
- (void)containerDragStart:(NSNotification*)notification;
// Determines which buttons need to be hidden based on the new size, hides them
// and updates the chevron overflow menu. Also fires a notification to let the
// toolbar know that the drag has finished.
- (void)containerDragFinished:(NSNotification*)notification;
// Shows the toolbar info bubble, if it should be displayed.
- (void)containerMouseEntered:(NSNotification*)notification;
// Notifies the controlling ToolbarActionsBar that any running animation has
// ended.
- (void)containerAnimationEnded:(NSNotification*)notification;
// Processes a key event from the container.
- (void)containerKeyEvent:(NSNotification*)notification;
// Adjusts the position of the surrounding action buttons depending on where the
// button is within the container.
- (void)actionButtonDragging:(NSNotification*)notification;
// Updates the position of the Browser Actions within the container. This fires
// when _any_ Browser Action button is done dragging to keep all open windows in
// sync visually.
- (void)actionButtonDragFinished:(NSNotification*)notification;
// Returns the frame that the button with the given |index| should have.
- (NSRect)frameForIndex:(NSUInteger)index;
// Returns the popup point for the given |view| with |bounds|.
- (NSPoint)popupPointForView:(NSView*)view
withBounds:(NSRect)bounds;
// Moves the given button both visually and within the toolbar model to the
// specified index.
- (void)moveButton:(BrowserActionButton*)button
toIndex:(NSUInteger)index;
// Handles clicks for BrowserActionButtons.
- (BOOL)browserActionClicked:(BrowserActionButton*)button;
// The reason |frame| is specified in these chevron functions is because the
// container may be animating and the end frame of the animation should be
// passed instead of the current frame (which may be off and cause the chevron
// to jump at the end of its animation).
// Shows the overflow chevron button depending on whether there are any hidden
// extensions within the frame given.
- (void)showChevronIfNecessaryInFrame:(NSRect)frame;
// Moves the chevron to its correct position within |frame|.
- (void)updateChevronPositionInFrame:(NSRect)frame;
// Shows or hides the chevron in the given |frame|.
- (void)setChevronHidden:(BOOL)hidden
inFrame:(NSRect)frame;
// Handles when a menu item within the chevron overflow menu is selected.
- (void)chevronItemSelected:(id)menuItem;
// Updates the container's grippy cursor based on the number of hidden buttons.
- (void)updateGrippyCursors;
// Returns the associated ToolbarController.
- (ToolbarController*)toolbarController;
// Creates a message bubble anchored to the given |anchorAction|, or the app
// menu if no |anchorAction| is null.
- (ToolbarActionsBarBubbleMac*)createMessageBubble:
(scoped_ptr<ToolbarActionsBarBubbleDelegate>)delegate
anchorToSelf:(BOOL)anchorToSelf;
// Called when the window for the active bubble is closing, and sets the active
// bubble to nil.
- (void)bubbleWindowClosing:(NSNotification*)notification;
// Sets the current focused view. Should only be used for the overflow
// container.
- (void)setFocusedViewIndex:(NSInteger)index;
@end
namespace {
// A bridge between the ToolbarActionsBar and the BrowserActionsController.
class ToolbarActionsBarBridge : public ToolbarActionsBarDelegate {
public:
explicit ToolbarActionsBarBridge(BrowserActionsController* controller);
~ToolbarActionsBarBridge() override;
BrowserActionsController* controller_for_test() { return controller_; }
private:
// ToolbarActionsBarDelegate:
void AddViewForAction(ToolbarActionViewController* action,
size_t index) override;
void RemoveViewForAction(ToolbarActionViewController* action) override;
void RemoveAllViews() override;
void Redraw(bool order_changed) override;
void ResizeAndAnimate(gfx::Tween::Type tween_type,
int target_width,
bool suppress_chevron) override;
void SetChevronVisibility(bool chevron_visible) override;
int GetWidth(GetWidthTime get_width_time) const override;
bool IsAnimating() const override;
void StopAnimating() override;
int GetChevronWidth() const override;
void ShowExtensionMessageBubble(
scoped_ptr<extensions::ExtensionMessageBubbleController> controller,
ToolbarActionViewController* anchor_action) override;
// The owning BrowserActionsController; weak.
BrowserActionsController* controller_;
DISALLOW_COPY_AND_ASSIGN(ToolbarActionsBarBridge);
};
ToolbarActionsBarBridge::ToolbarActionsBarBridge(
BrowserActionsController* controller)
: controller_(controller) {
}
ToolbarActionsBarBridge::~ToolbarActionsBarBridge() {
}
void ToolbarActionsBarBridge::AddViewForAction(
ToolbarActionViewController* action,
size_t index) {
[controller_ addViewForAction:action
withIndex:index];
}
void ToolbarActionsBarBridge::RemoveViewForAction(
ToolbarActionViewController* action) {
[controller_ removeViewForAction:action];
}
void ToolbarActionsBarBridge::RemoveAllViews() {
[controller_ removeAllViews];
}
void ToolbarActionsBarBridge::Redraw(bool order_changed) {
[controller_ redraw];
}
void ToolbarActionsBarBridge::ResizeAndAnimate(gfx::Tween::Type tween_type,
int target_width,
bool suppress_chevron) {
[controller_ resizeContainerToWidth:target_width];
}
void ToolbarActionsBarBridge::SetChevronVisibility(bool chevron_visible) {
[controller_ setChevronHidden:!chevron_visible
inFrame:[[controller_ containerView] frame]];
}
int ToolbarActionsBarBridge::GetWidth(GetWidthTime get_width_time) const {
NSRect frame =
get_width_time == ToolbarActionsBarDelegate::GET_WIDTH_AFTER_ANIMATION
? [[controller_ containerView] animationEndFrame]
: [[controller_ containerView] frame];
return NSWidth(frame);
}
bool ToolbarActionsBarBridge::IsAnimating() const {
return [[controller_ containerView] isAnimating];
}
void ToolbarActionsBarBridge::StopAnimating() {
// Unfortunately, animating the browser actions container affects neighboring
// views (like the omnibox), which could also be animating. Because of this,
// instead of just ending the animation, the cleanest way to terminate is to
// "animate" to the current frame.
[controller_ resizeContainerToWidth:
NSWidth([[controller_ containerView] frame])];
}
int ToolbarActionsBarBridge::GetChevronWidth() const {
return kChevronWidth;
}
void ToolbarActionsBarBridge::ShowExtensionMessageBubble(
scoped_ptr<extensions::ExtensionMessageBubbleController> bubble_controller,
ToolbarActionViewController* anchor_action) {
// This goop is a by-product of needing to wire together abstract classes,
// C++/Cocoa bridges, and ExtensionMessageBubbleController's somewhat strange
// Show() interface. It's ugly, but it's pretty confined, so it's probably
// okay (but if we ever need to expand, it might need to be reconsidered).
extensions::ExtensionMessageBubbleController* weak_controller =
bubble_controller.get();
scoped_ptr<ExtensionMessageBubbleBridge> bridge(
new ExtensionMessageBubbleBridge(std::move(bubble_controller),
anchor_action != nullptr));
ToolbarActionsBarBubbleMac* bubble =
[controller_ createMessageBubble:std::move(bridge)
anchorToSelf:anchor_action != nil];
weak_controller->OnShown();
[bubble showWindow:nil];
}
} // namespace
@implementation BrowserActionsController
@synthesize containerView = containerView_;
@synthesize browser = browser_;
@synthesize isOverflow = isOverflow_;
@synthesize activeBubble = activeBubble_;
#pragma mark -
#pragma mark Public Methods
- (id)initWithBrowser:(Browser*)browser
containerView:(BrowserActionsContainerView*)container
mainController:(BrowserActionsController*)mainController {
DCHECK(browser && container);
if ((self = [super init])) {
browser_ = browser;
isOverflow_ = mainController != nil;
toolbarActionsBarBridge_.reset(new ToolbarActionsBarBridge(self));
ToolbarActionsBar* mainBar =
mainController ? [mainController toolbarActionsBar] : nullptr;
toolbarActionsBar_.reset(
new ToolbarActionsBar(toolbarActionsBarBridge_.get(),
browser_,
mainBar));
containerView_ = container;
[containerView_ setPostsFrameChangedNotifications:YES];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(containerFrameChanged:)
name:NSViewFrameDidChangeNotification
object:containerView_];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(containerDragStart:)
name:kBrowserActionGrippyDragStartedNotification
object:containerView_];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(containerDragFinished:)
name:kBrowserActionGrippyDragFinishedNotification
object:containerView_];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(containerAnimationEnded:)
name:kBrowserActionsContainerAnimationEnded
object:containerView_];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(containerKeyEvent:)
name:kBrowserActionsContainerReceivedKeyEvent
object:containerView_];
// Listen for a finished drag from any button to make sure each open window
// stays in sync.
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(actionButtonDragFinished:)
name:kBrowserActionButtonDragEndNotification
object:nil];
suppressChevron_ = NO;
if (toolbarActionsBar_->platform_settings().chevron_enabled) {
chevronAnimation_.reset([[NSViewAnimation alloc] init]);
[chevronAnimation_ gtm_setDuration:kAnimationDuration
eventMask:NSLeftMouseUpMask];
[chevronAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
}
if (isOverflow_)
toolbarActionsBar_->SetOverflowRowWidth(NSWidth([containerView_ frame]));
buttons_.reset([[NSMutableArray alloc] init]);
toolbarActionsBar_->CreateActions();
[self showChevronIfNecessaryInFrame:[containerView_ frame]];
[self updateGrippyCursors];
[container setIsOverflow:isOverflow_];
if (ExtensionToolbarIconSurfacingBubbleDelegate::ShouldShowForProfile(
browser_->profile())) {
[containerView_ setTrackingEnabled:YES];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(containerMouseEntered:)
name:kBrowserActionsContainerMouseEntered
object:containerView_];
}
focusedViewIndex_ = -1;
}
return self;
}
- (void)dealloc {
[self browserWillBeDestroyed];
[super dealloc];
}
- (void)browserWillBeDestroyed {
[overflowMenu_ setDelegate:nil];
// Explicitly destroy the ToolbarActionsBar so all buttons get removed with a
// valid BrowserActionsController, and so we can verify state before
// destruction.
if (toolbarActionsBar_.get()) {
toolbarActionsBar_->DeleteActions();
toolbarActionsBar_.reset();
}
DCHECK_EQ(0u, [buttons_ count]);
[[NSNotificationCenter defaultCenter] removeObserver:self];
browser_ = nullptr;
}
- (void)update {
toolbarActionsBar_->Update();
}
- (NSUInteger)buttonCount {
return [buttons_ count];
}
- (NSUInteger)visibleButtonCount {
NSUInteger visibleCount = 0;
for (BrowserActionButton* button in buttons_.get())
visibleCount += [button superview] == containerView_;
return visibleCount;
}
- (gfx::Size)preferredSize {
return toolbarActionsBar_->GetPreferredSize();
}
- (NSPoint)popupPointForId:(const std::string&)id {
BrowserActionButton* button = [self buttonForId:id];
if (!button)
return NSZeroPoint;
NSRect bounds;
NSView* referenceButton = button;
if ([button superview] != containerView_ || isOverflow_) {
referenceButton = toolbarActionsBar_->platform_settings().chevron_enabled ?
chevronMenuButton_.get() : [[self toolbarController] appMenuButton];
bounds = [referenceButton bounds];
} else {
bounds = [button convertRect:[button frameAfterAnimation]
fromView:[button superview]];
}
return [self popupPointForView:referenceButton withBounds:bounds];
}
- (BOOL)chevronIsHidden {
if (!chevronMenuButton_.get())
return YES;
if (![chevronAnimation_ isAnimating])
return [chevronMenuButton_ isHidden];
DCHECK([[chevronAnimation_ viewAnimations] count] > 0);
// The chevron is animating in or out. Determine which one and have the return
// value reflect where the animation is headed.
NSString* effect = [[[chevronAnimation_ viewAnimations] objectAtIndex:0]
valueForKey:NSViewAnimationEffectKey];
if (effect == NSViewAnimationFadeInEffect) {
return NO;
} else if (effect == NSViewAnimationFadeOutEffect) {
return YES;
}
NOTREACHED();
return YES;
}
- (content::WebContents*)currentWebContents {
return browser_->tab_strip_model()->GetActiveWebContents();
}
- (BrowserActionButton*)mainButtonForId:(const std::string&)id {
BrowserActionsController* mainController = isOverflow_ ?
[[self toolbarController] browserActionsController] : self;
return [mainController buttonForId:id];
}
- (ToolbarActionsBar*)toolbarActionsBar {
return toolbarActionsBar_.get();
}
- (void)setFocusedInOverflow:(BOOL)focused {
BOOL isFocused = focusedViewIndex_ != -1;
if (isFocused != focused) {
int index = focused ?
[buttons_ count] - toolbarActionsBar_->GetIconCount() : -1;
[self setFocusedViewIndex:index];
}
}
- (gfx::Size)sizeForOverflowWidth:(int)maxWidth {
toolbarActionsBar_->SetOverflowRowWidth(maxWidth);
return [self preferredSize];
}
#pragma mark -
#pragma mark NSMenuDelegate
- (void)menuNeedsUpdate:(NSMenu*)menu {
[menu removeAllItems];
// See menu_button.h for documentation on why this is needed.
[menu addItemWithTitle:@"" action:nil keyEquivalent:@""];
NSUInteger iconCount = toolbarActionsBar_->GetIconCount();
NSRange hiddenButtonRange =
NSMakeRange(iconCount, [buttons_ count] - iconCount);
for (BrowserActionButton* button in
[buttons_ subarrayWithRange:hiddenButtonRange]) {
NSString* name =
base::SysUTF16ToNSString([button viewController]->GetActionName());
NSMenuItem* item =
[menu addItemWithTitle:name
action:@selector(chevronItemSelected:)
keyEquivalent:@""];
[item setRepresentedObject:button];
[item setImage:[button compositedImage]];
[item setTarget:self];
[item setEnabled:[button isEnabled]];
}
}
#pragma mark -
#pragma mark Private Methods
- (void)addViewForAction:(ToolbarActionViewController*)action
withIndex:(NSUInteger)index {
NSRect buttonFrame = NSMakeRect(NSMaxX([containerView_ bounds]),
0,
ToolbarActionsBar::IconWidth(false),
ToolbarActionsBar::IconHeight());
BrowserActionButton* newButton =
[[[BrowserActionButton alloc]
initWithFrame:buttonFrame
viewController:action
controller:self] autorelease];
[newButton setTarget:self];
[newButton setAction:@selector(browserActionClicked:)];
[buttons_ insertObject:newButton atIndex:index];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(actionButtonDragging:)
name:kBrowserActionButtonDraggingNotification
object:newButton];
[containerView_ setMaxDesiredWidth:toolbarActionsBar_->GetMaximumWidth()];
}
- (void)redraw {
if (![self updateContainerVisibility])
return; // Container is hidden; no need to update.
scoped_ptr<ui::NinePartImageIds> highlight;
if (toolbarActionsBar_->is_highlighting()) {
if (toolbarActionsBar_->highlight_type() ==
ToolbarActionsModel::HIGHLIGHT_INFO)
highlight.reset(
new ui::NinePartImageIds(IMAGE_GRID(IDR_TOOLBAR_ACTION_HIGHLIGHT)));
else
highlight.reset(
new ui::NinePartImageIds(IMAGE_GRID(IDR_DEVELOPER_MODE_HIGHLIGHT)));
}
[containerView_ setHighlight:std::move(highlight)];
std::vector<ToolbarActionViewController*> toolbar_actions =
toolbarActionsBar_->GetActions();
for (NSUInteger i = 0; i < [buttons_ count]; ++i) {
ToolbarActionViewController* controller =
[[self buttonAtIndex:i] viewController];
if (controller != toolbar_actions[i]) {
size_t j = i + 1;
while (true) {
ToolbarActionViewController* other_controller =
[[self buttonAtIndex:j] viewController];
if (other_controller == toolbar_actions[i])
break;
++j;
}
[buttons_ exchangeObjectAtIndex:i withObjectAtIndex:j];
}
}
[self showChevronIfNecessaryInFrame:[containerView_ frame]];
NSUInteger startIndex = toolbarActionsBar_->GetStartIndexInBounds();
NSUInteger endIndex = toolbarActionsBar_->GetEndIndexInBounds();
for (NSUInteger i = 0; i < [buttons_ count]; ++i) {
BrowserActionButton* button = [buttons_ objectAtIndex:i];
if ([button isBeingDragged])
continue;
[self moveButton:[buttons_ objectAtIndex:i] toIndex:i];
if (i >= startIndex && i < endIndex) {
// Make sure the button is within the visible container.
if ([button superview] != containerView_) {
// We add the subview under the sibling views so that when it
// "slides in", it does so under its neighbors.
[containerView_ addSubview:button
positioned:NSWindowBelow
relativeTo:nil];
}
// We need to set the alpha value in case the container has resized.
[button setAlphaValue:1.0];
} else if ([button superview] == containerView_ &&
![containerView_ userIsResizing]) {
// If the user is resizing, all buttons are (and should be) on the
// container view.
[button removeFromSuperview];
[button setAlphaValue:0.0];
}
}
}
- (void)removeViewForAction:(ToolbarActionViewController*)action {
BrowserActionButton* button = [self buttonForId:action->GetId()];
[button removeFromSuperview];
[button onRemoved];
[buttons_ removeObject:button];
[containerView_ setMaxDesiredWidth:toolbarActionsBar_->GetMaximumWidth()];
}
- (void)removeAllViews {
for (BrowserActionButton* button in buttons_.get()) {
[button removeFromSuperview];
[button onRemoved];
}
[buttons_ removeAllObjects];
}
- (void)resizeContainerToWidth:(CGFloat)width {
// Cocoa goes a little crazy if we try and change animations while adjusting
// child frames (i.e., the buttons). If the toolbar is already animating,
// just jump to the new frame. (This typically only happens if someone is
// "spamming" a button to add/remove an action.)
BOOL animate = !toolbarActionsBar_->suppress_animation() &&
![containerView_ isAnimating];
[self updateContainerVisibility];
[containerView_ resizeToWidth:width
animate:animate];
NSRect frame = animate ? [containerView_ animationEndFrame] :
[containerView_ frame];
[self showChevronIfNecessaryInFrame:frame];
[containerView_ setNeedsDisplay:YES];
if (!animate) {
[[NSNotificationCenter defaultCenter]
postNotificationName:kBrowserActionVisibilityChangedNotification
object:self];
}
[self redraw];
[self updateGrippyCursors];
}
- (BOOL)updateContainerVisibility {
BOOL hidden = [buttons_ count] == 0;
if ([containerView_ isHidden] != hidden)
[containerView_ setHidden:hidden];
return !hidden;
}
- (void)updateButtonOpacity {
for (BrowserActionButton* button in buttons_.get()) {
NSRect buttonFrame = [button frameAfterAnimation];
// The button is fully in the container view, and should get full opacity.
if (NSContainsRect([containerView_ bounds], buttonFrame)) {
if ([button alphaValue] != 1.0)
[button setAlphaValue:1.0];
continue;
}
// The button is only partially in the container view. If the user is
// resizing the container, we have partial alpha so the icon fades in as
// space is made. Otherwise, hide the icon fully.
CGFloat alpha = 0.0;
if ([containerView_ userIsResizing]) {
CGFloat intersectionWidth =
NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
alpha = std::max(static_cast<CGFloat>(0.0),
intersectionWidth / NSWidth(buttonFrame));
}
[button setAlphaValue:alpha];
[button setNeedsDisplay:YES];
}
}
- (void)updateButtonPositions {
for (NSUInteger index = 0; index < [buttons_ count]; ++index) {
BrowserActionButton* button = [buttons_ objectAtIndex:index];
NSRect buttonFrame = [self frameForIndex:index];
// If the button is at the proper position (or animating to it), then we
// don't need to update its position.
if (NSMinX([button frameAfterAnimation]) == NSMinX(buttonFrame))
continue;
// We set the x-origin by calculating the proper distance from the right
// edge in the container so that, if the container is animating, the
// button appears stationary.
buttonFrame.origin.x = NSWidth([containerView_ frame]) -
(toolbarActionsBar_->GetPreferredSize().width() - NSMinX(buttonFrame));
[button setFrame:buttonFrame animate:NO];
}
}
- (BrowserActionButton*)buttonForId:(const std::string&)id {
for (BrowserActionButton* button in buttons_.get()) {
if ([button viewController]->GetId() == id)
return button;
}
return nil;
}
- (BrowserActionButton*)buttonAtIndex:(NSUInteger)index {
return static_cast<BrowserActionButton*>([buttons_ objectAtIndex:index]);
}
- (void)containerFrameChanged:(NSNotification*)notification {
[self updateButtonPositions];
[self updateButtonOpacity];
[[containerView_ window] invalidateCursorRectsForView:containerView_];
[self updateChevronPositionInFrame:[containerView_ frame]];
}
- (void)containerDragStart:(NSNotification*)notification {
[self setChevronHidden:YES inFrame:[containerView_ frame]];
for (BrowserActionButton* button in buttons_.get()) {
if ([button superview] != containerView_) {
[button setAlphaValue:1.0];
[containerView_ addSubview:button];
}
}
}
- (void)containerDragFinished:(NSNotification*)notification {
for (BrowserActionButton* button in buttons_.get()) {
NSRect buttonFrame = [button frame];
if (NSContainsRect([containerView_ bounds], buttonFrame))
continue;
CGFloat intersectionWidth =
NSWidth(NSIntersectionRect([containerView_ bounds], buttonFrame));
// Hide the button if it's not "mostly" visible. "Mostly" here equates to
// having three or fewer pixels hidden.
if (([containerView_ grippyPinned] && intersectionWidth > 0) ||
(intersectionWidth <= NSWidth(buttonFrame) - 3.0)) {
[button setAlphaValue:0.0];
[button removeFromSuperview];
}
}
toolbarActionsBar_->OnResizeComplete(
toolbarActionsBar_->IconCountToWidth([self visibleButtonCount]));
[self updateGrippyCursors];
[self resizeContainerToWidth:toolbarActionsBar_->GetPreferredSize().width()];
}
- (void)containerAnimationEnded:(NSNotification*)notification {
if (![containerView_ isAnimating])
toolbarActionsBar_->OnAnimationEnded();
}
- (void)containerKeyEvent:(NSNotification*)notification {
DCHECK(isOverflow_); // We only manually process key events in overflow.
NSDictionary* dict = [notification userInfo];
BrowserActionsContainerKeyAction action =
static_cast<BrowserActionsContainerKeyAction>(
[[dict objectForKey:kBrowserActionsContainerKeyEventKey] intValue]);
switch (action) {
case BROWSER_ACTIONS_DECREMENT_FOCUS:
case BROWSER_ACTIONS_INCREMENT_FOCUS: {
NSInteger newIndex = focusedViewIndex_ +
(action == BROWSER_ACTIONS_INCREMENT_FOCUS ? 1 : -1);
NSInteger minIndex =
[buttons_ count] - toolbarActionsBar_->GetIconCount();
if (newIndex >= minIndex && newIndex < static_cast<int>([buttons_ count]))
[self setFocusedViewIndex:newIndex];
break;
}
case BROWSER_ACTIONS_EXECUTE_CURRENT: {
if (focusedViewIndex_ != -1) {
BrowserActionButton* focusedButton =
[self buttonAtIndex:focusedViewIndex_];
[focusedButton performClick:focusedButton];
}
break;
}
case BROWSER_ACTIONS_INVALID_KEY_ACTION:
NOTREACHED();
}
}
- (void)containerMouseEntered:(NSNotification*)notification {
if (!activeBubble_ && // only show one bubble at a time
toolbarActionsBar_->show_icon_surfacing_bubble()) {
scoped_ptr<ToolbarActionsBarBubbleDelegate> delegate(
new ExtensionToolbarIconSurfacingBubbleDelegate(browser_->profile()));
ToolbarActionsBarBubbleMac* bubble =
[self createMessageBubble:std::move(delegate) anchorToSelf:YES];
[bubble showWindow:nil];
}
[containerView_ setTrackingEnabled:NO];
[[NSNotificationCenter defaultCenter]
removeObserver:self
name:kBrowserActionsContainerMouseEntered
object:containerView_];
}
- (void)actionButtonDragging:(NSNotification*)notification {
suppressChevron_ = YES;
if (![self chevronIsHidden])
[self setChevronHidden:YES inFrame:[containerView_ frame]];
// Determine what index the dragged button should lie in, alter the model and
// reposition the buttons.
BrowserActionButton* draggedButton = [notification object];
NSRect draggedButtonFrame = [draggedButton frame];
// Find the mid-point. We flip the y-coordinates so that y = 0 is at the
// top of the container to make row calculation more logical.
NSPoint midPoint =
NSMakePoint(NSMidX(draggedButtonFrame),
NSMaxY([containerView_ bounds]) - NSMidY(draggedButtonFrame));
// Calculate the row index and the index in the row. We bound the latter
// because the view can go farther right than the right-most icon in the last
// row of the overflow menu.
NSInteger rowIndex = midPoint.y / ToolbarActionsBar::IconHeight();
int icons_per_row = isOverflow_ ?
toolbarActionsBar_->platform_settings().icons_per_overflow_menu_row :
toolbarActionsBar_->GetIconCount();
NSInteger indexInRow = std::min(icons_per_row - 1,
static_cast<int>(midPoint.x / ToolbarActionsBar::IconWidth(true)));
// Find the desired index for the button.
NSInteger maxIndex = [buttons_ count] - 1;
NSInteger offset = isOverflow_ ?
[buttons_ count] - toolbarActionsBar_->GetIconCount() : 0;
NSInteger index =
std::min(maxIndex, offset + rowIndex * icons_per_row + indexInRow);
toolbarActionsBar_->OnDragDrop([buttons_ indexOfObject:draggedButton],
index,
ToolbarActionsBar::DRAG_TO_SAME);
}
- (void)actionButtonDragFinished:(NSNotification*)notification {
suppressChevron_ = NO;
[self redraw];
}
- (NSRect)frameForIndex:(NSUInteger)index {
gfx::Rect frameRect = toolbarActionsBar_->GetFrameForIndex(index);
int iconWidth = ToolbarActionsBar::IconWidth(false);
// The toolbar actions bar will return an empty rect if the index is for an
// action that is before range we show (i.e., is for a button that's on the
// main bar, and this is the overflow). Set the frame to be outside the bounds
// of the view.
NSRect frame = frameRect.IsEmpty() ?
NSMakeRect(-iconWidth - 1, 0, iconWidth,
ToolbarActionsBar::IconHeight()) :
NSRectFromCGRect(frameRect.ToCGRect());
// We need to flip the y coordinate for Cocoa's view system.
frame.origin.y = NSHeight([containerView_ frame]) - NSMaxY(frame);
return frame;
}
- (NSPoint)popupPointForView:(NSView*)view
withBounds:(NSRect)bounds {
// Anchor point just above the center of the bottom.
int y = [view isFlipped] ? NSMaxY(bounds) - kBrowserActionBubbleYOffset :
kBrowserActionBubbleYOffset;
NSPoint anchor = NSMakePoint(NSMidX(bounds), y);
// Convert the point to the container view's frame, and adjust for animation.
NSPoint anchorInContainer =
[containerView_ convertPoint:anchor fromView:view];
anchorInContainer.x -= NSMinX([containerView_ frame]) -
NSMinX([containerView_ animationEndFrame]);
return [containerView_ convertPoint:anchorInContainer toView:nil];
}
- (void)moveButton:(BrowserActionButton*)button
toIndex:(NSUInteger)index {
NSRect buttonFrame = [self frameForIndex:index];
CGFloat currentX = NSMinX([button frame]);
CGFloat xLeft = toolbarActionsBar_->GetPreferredSize().width() -
NSMinX(buttonFrame);
// We check if the button is already in the correct place for the toolbar's
// current size. This could mean that the button could be the correct distance
// from the left or from the right edge. If it has the correct distance, we
// don't move it, and it will be updated when the container frame changes.
// This way, if the user has extensions A and C installed, and installs
// extension B between them, extension C appears to stay stationary on the
// screen while the toolbar expands to the left (even though C's bounds within
// the container change).
if ((currentX == NSMinX(buttonFrame) ||
currentX == NSWidth([containerView_ frame]) - xLeft) &&
NSMinY([button frame]) == NSMinY(buttonFrame)) {
// If the button is in the right place, but animating, we need to stop the
// animation.
if ([button isAnimating])
[button stopAnimation];
return;
}
// It's possible the button is already animating to the right place. Don't
// call move again, because it will stop the current animation.
if (!NSEqualRects(buttonFrame, [button frameAfterAnimation])) {
[button setFrame:buttonFrame
animate:!toolbarActionsBar_->suppress_animation() && !isOverflow_];
}
}
- (BOOL)browserActionClicked:(BrowserActionButton*)button {
return [button viewController]->ExecuteAction(true);
}
- (void)showChevronIfNecessaryInFrame:(NSRect)frame {
if (!toolbarActionsBar_->platform_settings().chevron_enabled)
return;
bool hidden = suppressChevron_ ||
toolbarActionsBar_->GetIconCount() == [self buttonCount];
[self setChevronHidden:hidden inFrame:frame];
}
- (void)updateChevronPositionInFrame:(NSRect)frame {
CGFloat xPos = NSWidth(frame) - kChevronWidth -
toolbarActionsBar_->platform_settings().item_spacing;
NSRect buttonFrame = NSMakeRect(xPos,
0,
kChevronWidth,
ToolbarActionsBar::IconHeight());
[chevronAnimation_ stopAnimation];
[chevronMenuButton_ setFrame:buttonFrame];
}
- (void)setChevronHidden:(BOOL)hidden
inFrame:(NSRect)frame {
if (!toolbarActionsBar_->platform_settings().chevron_enabled ||
hidden == [self chevronIsHidden])
return;
if (!chevronMenuButton_.get()) {
chevronMenuButton_.reset([[MenuButton alloc] init]);
[chevronMenuButton_ setOpenMenuOnClick:YES];
[chevronMenuButton_ setBordered:NO];
[chevronMenuButton_ setShowsBorderOnlyWhileMouseInside:YES];
[[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW
forButtonState:image_button_cell::kDefaultState];
[[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_H
forButtonState:image_button_cell::kHoverState];
[[chevronMenuButton_ cell] setImageID:IDR_BROWSER_ACTIONS_OVERFLOW_P
forButtonState:image_button_cell::kPressedState];
overflowMenu_.reset([[NSMenu alloc] initWithTitle:@""]);
[overflowMenu_ setAutoenablesItems:NO];
[overflowMenu_ setDelegate:self];
[chevronMenuButton_ setAttachedMenu:overflowMenu_];
[containerView_ addSubview:chevronMenuButton_];
}
[self updateChevronPositionInFrame:frame];
// Stop any running animation.
[chevronAnimation_ stopAnimation];
if (toolbarActionsBar_->suppress_animation()) {
[chevronMenuButton_ setHidden:hidden];
return;
}
NSString* animationEffect;
if (hidden) {
animationEffect = NSViewAnimationFadeOutEffect;
} else {
[chevronMenuButton_ setHidden:NO];
animationEffect = NSViewAnimationFadeInEffect;
}
NSDictionary* animationDictionary = @{
NSViewAnimationTargetKey : chevronMenuButton_.get(),
NSViewAnimationEffectKey : animationEffect
};
[chevronAnimation_ setViewAnimations:
[NSArray arrayWithObject:animationDictionary]];
[chevronAnimation_ startAnimation];
}
- (void)chevronItemSelected:(id)menuItem {
[self browserActionClicked:[menuItem representedObject]];
}
- (void)updateGrippyCursors {
[containerView_
setCanDragLeft:toolbarActionsBar_->GetIconCount() != [buttons_ count]];
[containerView_ setCanDragRight:[self visibleButtonCount] > 0];
[[containerView_ window] invalidateCursorRectsForView:containerView_];
}
- (ToolbarController*)toolbarController {
return [[BrowserWindowController browserWindowControllerForWindow:
browser_->window()->GetNativeWindow()] toolbarController];
}
- (ToolbarActionsBarBubbleMac*)createMessageBubble:
(scoped_ptr<ToolbarActionsBarBubbleDelegate>)delegate
anchorToSelf:(BOOL)anchorToSelf {
DCHECK_GE([buttons_ count], 0u);
NSView* anchorView =
anchorToSelf ? containerView_ : [[self toolbarController] appMenuButton];
NSPoint anchor = [self popupPointForView:anchorView
withBounds:[anchorView bounds]];
anchor = [[containerView_ window] convertBaseToScreen:anchor];
activeBubble_ = [[ToolbarActionsBarBubbleMac alloc]
initWithParentWindow:[containerView_ window]
anchorPoint:anchor
delegate:std::move(delegate)];
[[NSNotificationCenter defaultCenter]
addObserver:self
selector:@selector(bubbleWindowClosing:)
name:NSWindowWillCloseNotification
object:[activeBubble_ window]];
return activeBubble_;
}
- (void)bubbleWindowClosing:(NSNotification*)notification {
activeBubble_ = nil;
}
- (void)setFocusedViewIndex:(NSInteger)index {
DCHECK(isOverflow_);
focusedViewIndex_ = index;
}
#pragma mark -
#pragma mark Testing Methods
- (BrowserActionButton*)buttonWithIndex:(NSUInteger)index {
return index < [buttons_ count] ? [buttons_ objectAtIndex:index] : nil;
}
@end