1 // Copyright (c) 2011 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_container_view.h"
9 #include "base/basictypes.h"
10 #import "chrome/browser/ui/cocoa/view_id_util.h"
11 #include "grit/theme_resources.h"
12 #include "ui/base/cocoa/appkit_utils.h"
13 #include "ui/events/keycodes/keyboard_code_conversion_mac.h"
15 NSString* const kBrowserActionGrippyDragStartedNotification =
16 @"BrowserActionGrippyDragStartedNotification";
17 NSString* const kBrowserActionGrippyDraggingNotification =
18 @"BrowserActionGrippyDraggingNotification";
19 NSString* const kBrowserActionGrippyDragFinishedNotification =
20 @"BrowserActionGrippyDragFinishedNotification";
21 NSString* const kBrowserActionsContainerWillAnimate =
22 @"BrowserActionsContainerWillAnimate";
23 NSString* const kBrowserActionsContainerMouseEntered =
24 @"BrowserActionsContainerMouseEntered";
25 NSString* const kBrowserActionsContainerAnimationEnded =
26 @"BrowserActionsContainerAnimationEnded";
27 NSString* const kTranslationWithDelta =
28 @"TranslationWithDelta";
29 NSString* const kBrowserActionsContainerReceivedKeyEvent =
30 @"BrowserActionsContainerReceivedKeyEvent";
31 NSString* const kBrowserActionsContainerKeyEventKey =
32 @"BrowserActionsContainerKeyEventKey";
35 const CGFloat kAnimationDuration = 0.2;
36 const CGFloat kGrippyWidth = 3.0;
37 const CGFloat kMinimumContainerWidth = 3.0;
40 @interface BrowserActionsContainerView(Private)
41 // Returns the cursor that should be shown when hovering over the grippy based
42 // on |canDragLeft_| and |canDragRight_|.
43 - (NSCursor*)appropriateCursorForGrippy;
45 // Returns the maximum allowed size for the container.
46 - (CGFloat)maxAllowedWidth;
49 @implementation BrowserActionsContainerView
51 @synthesize canDragLeft = canDragLeft_;
52 @synthesize canDragRight = canDragRight_;
53 @synthesize grippyPinned = grippyPinned_;
54 @synthesize maxDesiredWidth = maxDesiredWidth_;
55 @synthesize userIsResizing = userIsResizing_;
56 @synthesize delegate = delegate_;
59 #pragma mark Overridden Class Functions
61 - (id)initWithFrame:(NSRect)frameRect {
62 if ((self = [super initWithFrame:frameRect])) {
63 grippyRect_ = NSMakeRect(0.0, 0.0, kGrippyWidth, NSHeight([self bounds]));
68 resizeAnimation_.reset([[NSViewAnimation alloc] init]);
69 [resizeAnimation_ setDuration:kAnimationDuration];
70 [resizeAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
71 [resizeAnimation_ setDelegate:self];
79 if (trackingArea_.get())
80 [self removeTrackingArea:trackingArea_.get()];
84 - (void)drawRect:(NSRect)rect {
85 [super drawRect:rect];
87 ui::DrawNinePartImage(
88 [self bounds], *highlight_, NSCompositeSourceOver, 1.0, true);
92 - (void)viewDidMoveToWindow {
94 // Yet another Cocoa oddity: Custom views in menu items in Cocoa, by
95 // default, won't receive key events. However, if we make this the first
96 // responder when it's moved to a window, it will, and it will behave
97 // properly (i.e., will only receive key events if the menu item is
98 // highlighted, not for any key event in the menu). More strangely,
99 // setting this to be first responder at any other time (such as calling
100 // [[containerView window] makeFirstResponder:containerView] when the menu
101 // item is highlighted) does *not* work (it messes up the currently-
102 // highlighted item).
103 // Since this seems to have the right behavior, use it.
104 [[self window] makeFirstResponder:self];
108 - (void)setTrackingEnabled:(BOOL)enabled {
111 [[CrTrackingArea alloc] initWithRect:NSZeroRect
112 options:NSTrackingMouseEnteredAndExited |
113 NSTrackingActiveInActiveApp |
114 NSTrackingInVisibleRect
117 [self addTrackingArea:trackingArea_.get()];
118 } else if (trackingArea_.get()) {
119 [self removeTrackingArea:trackingArea_.get()];
120 [trackingArea_.get() clearOwner];
121 trackingArea_.reset(nil);
125 - (void)keyDown:(NSEvent*)theEvent {
126 // If this is the overflow container, we handle three key events: left, right,
127 // and space. Left and right navigate the actions within the container, and
128 // space activates the current one. We have to handle this ourselves, because
129 // Cocoa doesn't treat custom views with subviews in menu items differently
130 // than any other menu item, so it would otherwise be impossible to navigate
131 // to a particular action from the keyboard.
132 ui::KeyboardCode key = ui::KeyboardCodeFromNSEvent(theEvent);
133 BOOL shouldProcess = isOverflow_ &&
134 (key == ui::VKEY_RIGHT || key == ui::VKEY_LEFT || key == ui::VKEY_SPACE);
136 // If this isn't the overflow container, or isn't one of the keys we process,
137 // forward the event on.
138 if (!shouldProcess) {
139 [super keyDown:theEvent];
143 // TODO(devlin): The keyboard navigation should be adjusted for RTL, but right
144 // now we only ever display the extension items in the same way (LTR) on Mac.
145 BrowserActionsContainerKeyAction action = BROWSER_ACTIONS_INVALID_KEY_ACTION;
148 action = BROWSER_ACTIONS_INCREMENT_FOCUS;
151 action = BROWSER_ACTIONS_DECREMENT_FOCUS;
154 action = BROWSER_ACTIONS_EXECUTE_CURRENT;
157 NOTREACHED(); // Should have weeded this case out above.
160 DCHECK_NE(BROWSER_ACTIONS_INVALID_KEY_ACTION, action);
161 NSDictionary* userInfo = @{ kBrowserActionsContainerKeyEventKey : @(action) };
162 [[NSNotificationCenter defaultCenter]
163 postNotificationName:kBrowserActionsContainerReceivedKeyEvent
166 [super keyDown:theEvent];
169 - (void)setHighlight:(scoped_ptr<ui::NinePartImageIds>)highlight {
170 if (highlight || highlight_) {
171 highlight_ = highlight.Pass();
172 resizable_ = highlight.get() != nullptr;
173 [self setNeedsDisplay:YES];
177 - (void)setIsOverflow:(BOOL)isOverflow {
178 if (isOverflow_ != isOverflow) {
179 isOverflow_ = isOverflow;
180 resizable_ = !isOverflow_;
181 [self setNeedsDisplay:YES];
185 - (void)resetCursorRects {
186 [self addCursorRect:grippyRect_ cursor:[self appropriateCursorForGrippy]];
189 - (BOOL)acceptsFirstResponder {
193 - (void)mouseEntered:(NSEvent*)theEvent {
194 [[NSNotificationCenter defaultCenter]
195 postNotificationName:kBrowserActionsContainerMouseEntered
199 - (void)mouseDown:(NSEvent*)theEvent {
200 initialDragPoint_ = [self convertPoint:[theEvent locationInWindow]
203 !NSMouseInRect(initialDragPoint_, grippyRect_, [self isFlipped]))
206 userIsResizing_ = YES;
208 [[self appropriateCursorForGrippy] push];
209 // Disable cursor rects so that the Omnibox and other UI elements don't push
210 // cursors while the user is dragging. The cursor should be grippy until
211 // the |-mouseUp:| message is received.
212 [[self window] disableCursorRects];
214 [[NSNotificationCenter defaultCenter]
215 postNotificationName:kBrowserActionGrippyDragStartedNotification
219 - (void)mouseUp:(NSEvent*)theEvent {
220 if (!userIsResizing_)
224 [[self window] enableCursorRects];
226 userIsResizing_ = NO;
227 [[NSNotificationCenter defaultCenter]
228 postNotificationName:kBrowserActionGrippyDragFinishedNotification
232 - (void)mouseDragged:(NSEvent*)theEvent {
233 if (!userIsResizing_)
236 NSPoint location = [self convertPoint:[theEvent locationInWindow]
238 NSRect containerFrame = [self frame];
239 CGFloat dX = [theEvent deltaX];
240 CGFloat withDelta = location.x - dX;
241 canDragRight_ = (withDelta >= initialDragPoint_.x) &&
242 (NSWidth(containerFrame) > kMinimumContainerWidth);
243 CGFloat maxAllowedWidth = [self maxAllowedWidth];
244 containerFrame.size.width =
245 std::max(NSWidth(containerFrame) - dX, kMinimumContainerWidth);
246 canDragLeft_ = withDelta <= initialDragPoint_.x &&
247 NSWidth(containerFrame) < maxDesiredWidth_ &&
248 NSWidth(containerFrame) < maxAllowedWidth;
250 if ((dX < 0.0 && !canDragLeft_) || (dX > 0.0 && !canDragRight_))
253 if (NSWidth(containerFrame) <= kMinimumContainerWidth)
256 grippyPinned_ = NSWidth(containerFrame) >= maxAllowedWidth;
257 containerFrame.origin.x += dX;
259 [self setFrame:containerFrame];
260 [self setNeedsDisplay:YES];
262 [[NSNotificationCenter defaultCenter]
263 postNotificationName:kBrowserActionGrippyDraggingNotification
267 - (void)animationDidEnd:(NSAnimation*)animation {
268 // We notify asynchronously so that the animation fully finishes before any
269 // listeners do work.
270 [self performSelector:@selector(notifyAnimationEnded)
275 - (void)animationDidStop:(NSAnimation*)animation {
276 // We notify asynchronously so that the animation fully finishes before any
277 // listeners do work.
278 [self performSelector:@selector(notifyAnimationEnded)
283 - (void)notifyAnimationEnded {
284 [[NSNotificationCenter defaultCenter]
285 postNotificationName:kBrowserActionsContainerAnimationEnded
290 return VIEW_ID_BROWSER_ACTION_TOOLBAR;
294 #pragma mark Public Methods
296 - (void)resizeToWidth:(CGFloat)width animate:(BOOL)animate {
297 width = std::max(width, kMinimumContainerWidth);
298 NSRect frame = [self frame];
300 CGFloat maxAllowedWidth = [self maxAllowedWidth];
301 width = std::min(maxAllowedWidth, width);
303 CGFloat dX = frame.size.width - width;
304 frame.size.width = width;
305 NSRect newFrame = NSOffsetRect(frame, dX, 0);
307 grippyPinned_ = width == maxAllowedWidth;
309 [self stopAnimation];
312 NSDictionary* animationDictionary = @{
313 NSViewAnimationTargetKey : self,
314 NSViewAnimationStartFrameKey : [NSValue valueWithRect:[self frame]],
315 NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame]
317 [resizeAnimation_ setViewAnimations:@[ animationDictionary ]];
318 [resizeAnimation_ startAnimation];
320 [[NSNotificationCenter defaultCenter]
321 postNotificationName:kBrowserActionsContainerWillAnimate
324 [self setFrame:newFrame];
325 [self setNeedsDisplay:YES];
329 - (NSRect)animationEndFrame {
330 if ([resizeAnimation_ isAnimating]) {
331 NSRect endFrame = [[[[resizeAnimation_ viewAnimations] objectAtIndex:0]
332 valueForKey:NSViewAnimationEndFrameKey] rectValue];
339 - (BOOL)isAnimating {
340 return [resizeAnimation_ isAnimating];
343 - (void)stopAnimation {
344 if ([resizeAnimation_ isAnimating])
345 [resizeAnimation_ stopAnimation];
349 #pragma mark Private Methods
351 // Returns the cursor to display over the grippy hover region depending on the
352 // current drag state.
353 - (NSCursor*)appropriateCursorForGrippy {
355 if (!resizable_ || (!canDragLeft_ && !canDragRight_)) {
356 retVal = [NSCursor arrowCursor];
357 } else if (!canDragLeft_) {
358 retVal = [NSCursor resizeRightCursor];
359 } else if (!canDragRight_) {
360 retVal = [NSCursor resizeLeftCursor];
362 retVal = [NSCursor resizeLeftRightCursor];
367 - (CGFloat)maxAllowedWidth {
368 return delegate_ ? delegate_->GetMaxAllowedWidth() : CGFLOAT_MAX;