[Extensions UI] Fix two bugs in the toolbar actions bar
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / browser_actions_container_view.mm
blob1dc68604baee0d6847f099974d4b5c2bb7472161
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"
7 #include <algorithm>
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";
34 namespace {
35 const CGFloat kAnimationDuration = 0.2;
36 const CGFloat kGrippyWidth = 3.0;
37 const CGFloat kMinimumContainerWidth = 3.0;
38 }  // namespace
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;
47 @end
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_;
58 #pragma mark -
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]));
64     canDragLeft_ = YES;
65     canDragRight_ = YES;
66     resizable_ = YES;
68     resizeAnimation_.reset([[NSViewAnimation alloc] init]);
69     [resizeAnimation_ setDuration:kAnimationDuration];
70     [resizeAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
71     [resizeAnimation_ setDelegate:self];
73     [self setHidden:YES];
74   }
75   return self;
78 - (void)dealloc {
79   if (trackingArea_.get())
80     [self removeTrackingArea:trackingArea_.get()];
81   [super dealloc];
84 - (void)drawRect:(NSRect)rect {
85   [super drawRect:rect];
86   if (highlight_) {
87     ui::DrawNinePartImage(
88         [self bounds], *highlight_, NSCompositeSourceOver, 1.0, true);
89   }
92 - (void)viewDidMoveToWindow {
93   if (isOverflow_) {
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];
105   }
108 - (void)setTrackingEnabled:(BOOL)enabled {
109   if (enabled) {
110     trackingArea_.reset(
111         [[CrTrackingArea alloc] initWithRect:NSZeroRect
112                                      options:NSTrackingMouseEnteredAndExited |
113                                              NSTrackingActiveInActiveApp |
114                                              NSTrackingInVisibleRect
115                                        owner:self
116                                     userInfo:nil]);
117     [self addTrackingArea:trackingArea_.get()];
118   } else if (trackingArea_.get()) {
119     [self removeTrackingArea:trackingArea_.get()];
120     [trackingArea_.get() clearOwner];
121     trackingArea_.reset(nil);
122   }
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];
140     return;
141   }
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;
146   switch (key) {
147     case ui::VKEY_RIGHT:
148       action = BROWSER_ACTIONS_INCREMENT_FOCUS;
149       break;
150     case ui::VKEY_LEFT:
151       action = BROWSER_ACTIONS_DECREMENT_FOCUS;
152       break;
153     case ui::VKEY_SPACE:
154       action = BROWSER_ACTIONS_EXECUTE_CURRENT;
155       break;
156     default:
157       NOTREACHED();  // Should have weeded this case out above.
158   }
160   DCHECK_NE(BROWSER_ACTIONS_INVALID_KEY_ACTION, action);
161   NSDictionary* userInfo = @{ kBrowserActionsContainerKeyEventKey : @(action) };
162   [[NSNotificationCenter defaultCenter]
163       postNotificationName:kBrowserActionsContainerReceivedKeyEvent
164                     object:self
165                     userInfo:userInfo];
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];
174   }
177 - (void)setIsOverflow:(BOOL)isOverflow {
178   if (isOverflow_ != isOverflow) {
179     isOverflow_ = isOverflow;
180     resizable_ = !isOverflow_;
181     [self setNeedsDisplay:YES];
182   }
185 - (void)resetCursorRects {
186   [self addCursorRect:grippyRect_ cursor:[self appropriateCursorForGrippy]];
189 - (BOOL)acceptsFirstResponder {
190   return YES;
193 - (void)mouseEntered:(NSEvent*)theEvent {
194   [[NSNotificationCenter defaultCenter]
195       postNotificationName:kBrowserActionsContainerMouseEntered
196                     object:self];
199 - (void)mouseDown:(NSEvent*)theEvent {
200   initialDragPoint_ = [self convertPoint:[theEvent locationInWindow]
201                                 fromView:nil];
202   if (!resizable_ ||
203       !NSMouseInRect(initialDragPoint_, grippyRect_, [self isFlipped]))
204     return;
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
216                     object:self];
219 - (void)mouseUp:(NSEvent*)theEvent {
220   if (!userIsResizing_)
221     return;
223   [NSCursor pop];
224   [[self window] enableCursorRects];
226   userIsResizing_ = NO;
227   [[NSNotificationCenter defaultCenter]
228       postNotificationName:kBrowserActionGrippyDragFinishedNotification
229                     object:self];
232 - (void)mouseDragged:(NSEvent*)theEvent {
233   if (!userIsResizing_)
234     return;
236   NSPoint location = [self convertPoint:[theEvent locationInWindow]
237                                fromView:nil];
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_))
251     return;
253   if (NSWidth(containerFrame) <= kMinimumContainerWidth)
254     return;
256   grippyPinned_ = NSWidth(containerFrame) >= maxAllowedWidth;
257   containerFrame.origin.x += dX;
259   [self setFrame:containerFrame];
260   [self setNeedsDisplay:YES];
262   [[NSNotificationCenter defaultCenter]
263       postNotificationName:kBrowserActionGrippyDraggingNotification
264                     object:self];
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)
271               withObject:self
272               afterDelay:0];
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)
279               withObject:self
280               afterDelay:0];
283 - (void)notifyAnimationEnded {
284   [[NSNotificationCenter defaultCenter]
285       postNotificationName:kBrowserActionsContainerAnimationEnded
286                     object:self];
289 - (ViewID)viewID {
290   return VIEW_ID_BROWSER_ACTION_TOOLBAR;
293 #pragma mark -
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];
311   if (animate) {
312     NSDictionary* animationDictionary = @{
313       NSViewAnimationTargetKey : self,
314       NSViewAnimationStartFrameKey : [NSValue valueWithRect:[self frame]],
315       NSViewAnimationEndFrameKey : [NSValue valueWithRect:newFrame]
316     };
317     [resizeAnimation_ setViewAnimations:@[ animationDictionary ]];
318     [resizeAnimation_ startAnimation];
320     [[NSNotificationCenter defaultCenter]
321         postNotificationName:kBrowserActionsContainerWillAnimate
322                       object:self];
323   } else {
324     [self setFrame:newFrame];
325     [self setNeedsDisplay:YES];
326   }
329 - (NSRect)animationEndFrame {
330   if ([resizeAnimation_ isAnimating]) {
331     NSRect endFrame = [[[[resizeAnimation_ viewAnimations] objectAtIndex:0]
332         valueForKey:NSViewAnimationEndFrameKey] rectValue];
333     return endFrame;
334   } else {
335     return [self frame];
336   }
339 - (BOOL)isAnimating {
340   return [resizeAnimation_ isAnimating];
343 - (void)stopAnimation {
344   if ([resizeAnimation_ isAnimating])
345     [resizeAnimation_ stopAnimation];
348 #pragma mark -
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 {
354   NSCursor* retVal;
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];
361   } else {
362     retVal = [NSCursor resizeLeftRightCursor];
363   }
364   return retVal;
367 - (CGFloat)maxAllowedWidth {
368   return delegate_ ? delegate_->GetMaxAllowedWidth() : CGFLOAT_MAX;
371 @end