blob: 251d33777189e27eecc95f65dbaf42b87806582b [file] [log] [blame]
// Copyright (c) 2011 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_container_view.h"
#include <algorithm>
#include <utility>
#import "chrome/browser/ui/cocoa/l10n_util.h"
#import "chrome/browser/ui/cocoa/view_id_util.h"
#include "ui/base/cocoa/appkit_utils.h"
#include "ui/events/keycodes/keyboard_code_conversion_mac.h"
NSString* const kBrowserActionGrippyDragStartedNotification =
@"BrowserActionGrippyDragStartedNotification";
NSString* const kBrowserActionGrippyDraggingNotification =
@"BrowserActionGrippyDraggingNotification";
NSString* const kBrowserActionGrippyDragFinishedNotification =
@"BrowserActionGrippyDragFinishedNotification";
NSString* const kBrowserActionsContainerWillAnimate =
@"BrowserActionsContainerWillAnimate";
NSString* const kBrowserActionsContainerAnimationEnded =
@"BrowserActionsContainerAnimationEnded";
NSString* const kTranslationWithDelta =
@"TranslationWithDelta";
NSString* const kBrowserActionsContainerReceivedKeyEvent =
@"BrowserActionsContainerReceivedKeyEvent";
NSString* const kBrowserActionsContainerKeyEventKey =
@"BrowserActionsContainerKeyEventKey";
namespace {
const CGFloat kAnimationDuration = 0.2;
const CGFloat kGrippyWidth = 3.0;
} // namespace
@interface BrowserActionsContainerView(Private)
// Returns the cursor that should be shown when hovering over the grippy based
// on |canDragLeft_| and |canDragRight_|.
- (NSCursor*)appropriateCursorForGrippy;
@end
@implementation BrowserActionsContainerView
@synthesize minWidth = minWidth_;
@synthesize maxWidth = maxWidth_;
@synthesize grippyPinned = grippyPinned_;
@synthesize userIsResizing = userIsResizing_;
#pragma mark -
#pragma mark Overridden Class Functions
- (id)initWithFrame:(NSRect)frameRect {
if ((self = [super initWithFrame:frameRect])) {
grippyRect_ = NSMakeRect(0.0, 0.0, kGrippyWidth, NSHeight([self bounds]));
if (cocoa_l10n_util::ShouldDoExperimentalRTLLayout())
grippyRect_.origin.x = NSWidth(frameRect) - NSWidth(grippyRect_);
resizable_ = YES;
resizeAnimation_.reset([[NSViewAnimation alloc] init]);
[resizeAnimation_ setDuration:kAnimationDuration];
[resizeAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
[resizeAnimation_ setDelegate:self];
[self setHidden:YES];
}
return self;
}
- (void)drawRect:(NSRect)rect {
[super drawRect:rect];
if (highlight_) {
ui::DrawNinePartImage(
[self bounds], *highlight_, NSCompositeSourceOver, 1.0, true);
}
}
- (void)viewDidMoveToWindow {
if (isOverflow_) {
// Yet another Cocoa oddity: Custom views in menu items in Cocoa, by
// default, won't receive key events. However, if we make this the first
// responder when it's moved to a window, it will, and it will behave
// properly (i.e., will only receive key events if the menu item is
// highlighted, not for any key event in the menu). More strangely,
// setting this to be first responder at any other time (such as calling
// [[containerView window] makeFirstResponder:containerView] when the menu
// item is highlighted) does *not* work (it messes up the currently-
// highlighted item).
// Since this seems to have the right behavior, use it.
[[self window] makeFirstResponder:self];
}
}
- (void)keyDown:(NSEvent*)theEvent {
// If this is the overflow container, we handle three key events: left, right,
// and space. Left and right navigate the actions within the container, and
// space activates the current one. We have to handle this ourselves, because
// Cocoa doesn't treat custom views with subviews in menu items differently
// than any other menu item, so it would otherwise be impossible to navigate
// to a particular action from the keyboard.
ui::KeyboardCode key = ui::KeyboardCodeFromNSEvent(theEvent);
BOOL shouldProcess = isOverflow_ &&
(key == ui::VKEY_RIGHT || key == ui::VKEY_LEFT || key == ui::VKEY_SPACE);
// If this isn't the overflow container, or isn't one of the keys we process,
// forward the event on.
if (!shouldProcess) {
[super keyDown:theEvent];
return;
}
// TODO(devlin): The keyboard navigation should be adjusted for RTL, but right
// now we only ever display the extension items in the same way (LTR) on Mac.
BrowserActionsContainerKeyAction action = BROWSER_ACTIONS_INVALID_KEY_ACTION;
switch (key) {
case ui::VKEY_RIGHT:
action = BROWSER_ACTIONS_INCREMENT_FOCUS;
break;
case ui::VKEY_LEFT:
action = BROWSER_ACTIONS_DECREMENT_FOCUS;
break;
case ui::VKEY_SPACE:
action = BROWSER_ACTIONS_EXECUTE_CURRENT;
break;
default:
NOTREACHED(); // Should have weeded this case out above.
}
DCHECK_NE(BROWSER_ACTIONS_INVALID_KEY_ACTION, action);
NSDictionary* userInfo = @{ kBrowserActionsContainerKeyEventKey : @(action) };
[[NSNotificationCenter defaultCenter]
postNotificationName:kBrowserActionsContainerReceivedKeyEvent
object:self
userInfo:userInfo];
[super keyDown:theEvent];
}
- (void)setHighlight:(std::unique_ptr<ui::NinePartImageIds>)highlight {
if (highlight || highlight_) {
highlight_ = std::move(highlight);
// We don't allow resizing when the container is highlighting.
resizable_ = highlight.get() == nullptr;
[self setNeedsDisplay:YES];
}
}
- (BOOL)isHighlighting {
return highlight_.get() != nullptr;
}
- (void)setIsOverflow:(BOOL)isOverflow {
if (isOverflow_ != isOverflow) {
isOverflow_ = isOverflow;
resizable_ = !isOverflow_;
[self setNeedsDisplay:YES];
}
}
- (void)resetCursorRects {
if (cocoa_l10n_util::ShouldDoExperimentalRTLLayout())
grippyRect_.origin.x = NSWidth([self frame]) - NSWidth(grippyRect_);
[self addCursorRect:grippyRect_ cursor:[self appropriateCursorForGrippy]];
}
- (BOOL)acceptsFirstResponder {
// The overflow container needs to receive key events to handle in-item
// navigation. The top-level container should not become first responder,
// allowing focus travel to proceed to the first action.
return isOverflow_;
}
- (void)mouseDown:(NSEvent*)theEvent {
NSPoint location =
[self convertPoint:[theEvent locationInWindow] fromView:nil];
if (!resizable_ || !NSMouseInRect(location, grippyRect_, [self isFlipped]))
return;
dragOffset_ = location.x - (cocoa_l10n_util::ShouldDoExperimentalRTLLayout()
? NSWidth(self.frame)
: 0);
userIsResizing_ = YES;
[[self appropriateCursorForGrippy] push];
// Disable cursor rects so that the Omnibox and other UI elements don't push
// cursors while the user is dragging. The cursor should be grippy until
// the |-mouseUp:| message is received.
[[self window] disableCursorRects];
[[NSNotificationCenter defaultCenter]
postNotificationName:kBrowserActionGrippyDragStartedNotification
object:self];
}
- (void)mouseUp:(NSEvent*)theEvent {
if (!userIsResizing_)
return;
[NSCursor pop];
[[self window] enableCursorRects];
userIsResizing_ = NO;
[[NSNotificationCenter defaultCenter]
postNotificationName:kBrowserActionGrippyDragFinishedNotification
object:self];
}
- (void)mouseDragged:(NSEvent*)theEvent {
if (!userIsResizing_)
return;
const CGFloat translation =
[self convertPoint:[theEvent locationInWindow] fromView:nil].x -
dragOffset_;
const CGFloat targetWidth = (cocoa_l10n_util::ShouldDoExperimentalRTLLayout()
? translation
: NSWidth(self.frame) - translation);
[self resizeToWidth:targetWidth animate:NO];
[[NSNotificationCenter defaultCenter]
postNotificationName:kBrowserActionGrippyDraggingNotification
object:self];
}
- (void)animationDidEnd:(NSAnimation*)animation {
// We notify asynchronously so that the animation fully finishes before any
// listeners do work.
[self performSelector:@selector(notifyAnimationEnded)
withObject:self
afterDelay:0];
}
- (void)animationDidStop:(NSAnimation*)animation {
// We notify asynchronously so that the animation fully finishes before any
// listeners do work.
[self performSelector:@selector(notifyAnimationEnded)
withObject:self
afterDelay:0];
}
- (void)notifyAnimationEnded {
[[NSNotificationCenter defaultCenter]
postNotificationName:kBrowserActionsContainerAnimationEnded
object:self];
}
- (ViewID)viewID {
return VIEW_ID_BROWSER_ACTION_TOOLBAR;
}
#pragma mark -
#pragma mark Public Methods
- (void)resizeToWidth:(CGFloat)width animate:(BOOL)animate {
width = std::min(std::max(width, minWidth_), maxWidth_);
NSRect newFrame = [self frame];
if (!cocoa_l10n_util::ShouldDoExperimentalRTLLayout())
newFrame.origin.x += NSWidth(newFrame) - width;
newFrame.size.width = width;
grippyPinned_ = width == maxWidth_;
[self stopAnimation];
if (animate) {
NSDictionary* animationDictionary = @{
NSViewAnimationTargetKey : self,
NSViewAnimationStartFrameKey : [NSValue valueWithRect:[self frame]],
NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame]
};
[resizeAnimation_ setViewAnimations:@[ animationDictionary ]];
[resizeAnimation_ startAnimation];
[[NSNotificationCenter defaultCenter]
postNotificationName:kBrowserActionsContainerWillAnimate
object:self];
} else {
[self setFrame:newFrame];
[self setNeedsDisplay:YES];
}
}
- (NSRect)animationEndFrame {
if ([resizeAnimation_ isAnimating]) {
NSRect endFrame = [[[[resizeAnimation_ viewAnimations] objectAtIndex:0]
valueForKey:NSViewAnimationEndFrameKey] rectValue];
return endFrame;
} else {
return [self frame];
}
}
- (BOOL)isAnimating {
return [resizeAnimation_ isAnimating];
}
- (void)stopAnimation {
if ([resizeAnimation_ isAnimating])
[resizeAnimation_ stopAnimation];
}
#pragma mark -
#pragma mark Private Methods
// Returns the cursor to display over the grippy hover region depending on the
// current drag state.
- (NSCursor*)appropriateCursorForGrippy {
if (resizable_) {
const CGFloat width = NSWidth(self.frame);
const BOOL isRTL = cocoa_l10n_util::ShouldDoExperimentalRTLLayout();
const BOOL canDragLeft = width != (isRTL ? minWidth_ : maxWidth_);
const BOOL canDragRight = width != (isRTL ? maxWidth_ : minWidth_);
if (canDragLeft && canDragRight)
return [NSCursor resizeLeftRightCursor];
if (canDragLeft)
return [NSCursor resizeLeftCursor];
if (canDragRight)
return [NSCursor resizeRightCursor];
}
return [NSCursor arrowCursor];
}
@end