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_action_button.h"
10 #include "base/logging.h"
11 #include "base/strings/sys_string_conversions.h"
12 #include "chrome/browser/profiles/profile.h"
13 #include "chrome/browser/ui/browser.h"
14 #import "chrome/browser/ui/cocoa/extensions/browser_actions_controller.h"
15 #import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu_controller.h"
16 #import "chrome/browser/ui/cocoa/toolbar/toolbar_action_view_delegate_cocoa.h"
17 #include "chrome/browser/ui/toolbar/toolbar_action_view_controller.h"
18 #include "grit/theme_resources.h"
19 #include "skia/ext/skia_utils_mac.h"
20 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
21 #include "ui/gfx/canvas_skia_paint.h"
22 #include "ui/gfx/image/image.h"
23 #include "ui/gfx/rect.h"
24 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
26 NSString* const kBrowserActionButtonDraggingNotification =
27 @"BrowserActionButtonDraggingNotification";
28 NSString* const kBrowserActionButtonDragEndNotification =
29 @"BrowserActionButtonDragEndNotification";
31 static const CGFloat kBrowserActionBadgeOriginYOffset = 5;
32 static const CGFloat kAnimationDuration = 0.2;
33 static const CGFloat kMinimumDragDistance = 5;
35 // A class to bridge the ToolbarActionViewController and the
36 // BrowserActionButton.
37 class ToolbarActionViewDelegateBridge : public ToolbarActionViewDelegateCocoa {
39 ToolbarActionViewDelegateBridge(BrowserActionButton* owner,
40 BrowserActionsController* controller);
41 ~ToolbarActionViewDelegateBridge();
43 ExtensionActionContextMenuController* menuController() {
44 return menuController_;
48 // ToolbarActionViewDelegateCocoa:
49 ToolbarActionViewController* GetPreferredPopupViewController() override;
50 content::WebContents* GetCurrentWebContents() const override;
51 void UpdateState() override;
52 NSPoint GetPopupPoint() override;
53 void SetContextMenuController(
54 ExtensionActionContextMenuController* menuController) override;
56 // The owning button. Weak.
57 BrowserActionButton* owner_;
59 // The BrowserActionsController that owns the button. Weak.
60 BrowserActionsController* controller_;
62 // The context menu controller. Weak.
63 ExtensionActionContextMenuController* menuController_;
65 DISALLOW_COPY_AND_ASSIGN(ToolbarActionViewDelegateBridge);
68 ToolbarActionViewDelegateBridge::ToolbarActionViewDelegateBridge(
69 BrowserActionButton* owner,
70 BrowserActionsController* controller)
72 controller_(controller),
73 menuController_(nil) {
76 ToolbarActionViewDelegateBridge::~ToolbarActionViewDelegateBridge() {
79 ToolbarActionViewController*
80 ToolbarActionViewDelegateBridge::GetPreferredPopupViewController() {
81 return [owner_ viewController];
84 content::WebContents* ToolbarActionViewDelegateBridge::GetCurrentWebContents()
86 return [controller_ currentWebContents];
89 void ToolbarActionViewDelegateBridge::UpdateState() {
93 NSPoint ToolbarActionViewDelegateBridge::GetPopupPoint() {
94 return [controller_ popupPointForId:[owner_ viewController]->GetId()];
97 void ToolbarActionViewDelegateBridge::SetContextMenuController(
98 ExtensionActionContextMenuController* menuController) {
99 menuController_ = menuController;
102 @interface BrowserActionCell (Internals)
103 - (void)drawBadgeWithinFrame:(NSRect)frame
104 forWebContents:(content::WebContents*)webContents;
107 @interface BrowserActionButton (Private)
111 @implementation BrowserActionButton
113 @synthesize isBeingDragged = isBeingDragged_;
116 return [BrowserActionCell class];
119 - (id)initWithFrame:(NSRect)frame
120 viewController:(scoped_ptr<ToolbarActionViewController>)viewController
121 controller:(BrowserActionsController*)controller {
122 if ((self = [super initWithFrame:frame])) {
123 BrowserActionCell* cell = [[[BrowserActionCell alloc] init] autorelease];
124 // [NSButton setCell:] warns to NOT use setCell: other than in the
125 // initializer of a control. However, we are using a basic
126 // NSButton whose initializer does not take an NSCell as an
127 // object. To honor the assumed semantics, we do nothing with
128 // NSButton between alloc/init and setCell:.
131 browserActionsController_ = controller;
132 viewControllerDelegate_.reset(
133 new ToolbarActionViewDelegateBridge(self, controller));
134 viewController_ = viewController.Pass();
135 viewController_->SetDelegate(viewControllerDelegate_.get());
137 [cell setBrowserActionsController:controller];
138 [cell setViewController:viewController_.get()];
140 accessibilitySetOverrideValue:base::SysUTF16ToNSString(
141 viewController_->GetAccessibleName([controller currentWebContents]))
142 forAttribute:NSAccessibilityDescriptionAttribute];
143 [cell setImageID:IDR_BROWSER_ACTION
144 forButtonState:image_button_cell::kDefaultState];
145 [cell setImageID:IDR_BROWSER_ACTION_H
146 forButtonState:image_button_cell::kHoverState];
147 [cell setImageID:IDR_BROWSER_ACTION_P
148 forButtonState:image_button_cell::kPressedState];
149 [cell setImageID:IDR_BROWSER_ACTION
150 forButtonState:image_button_cell::kDisabledState];
153 [self setButtonType:NSMomentaryChangeButton];
154 [self setShowsBorderOnlyWhileMouseInside:YES];
156 base::scoped_nsobject<NSMenu> contextMenu(
157 [[NSMenu alloc] initWithTitle:@""]);
158 [contextMenu setDelegate:self];
159 [self setMenu:contextMenu];
161 moveAnimation_.reset([[NSViewAnimation alloc] init]);
162 [moveAnimation_ gtm_setDuration:kAnimationDuration
163 eventMask:NSLeftMouseUpMask];
164 [moveAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
172 - (BOOL)acceptsFirstResponder {
176 - (void)mouseDown:(NSEvent*)theEvent {
177 NSPoint location = [self convertPoint:[theEvent locationInWindow]
179 if (NSPointInRect(location, [self bounds])) {
180 [[self cell] setHighlighted:YES];
181 dragCouldStart_ = YES;
182 dragStartPoint_ = [theEvent locationInWindow];
186 - (void)mouseDragged:(NSEvent*)theEvent {
187 if (!dragCouldStart_)
190 if (!isBeingDragged_) {
191 // Don't initiate a drag until it moves at least kMinimumDragDistance.
192 NSPoint currentPoint = [theEvent locationInWindow];
193 CGFloat dx = currentPoint.x - dragStartPoint_.x;
194 CGFloat dy = currentPoint.y - dragStartPoint_.y;
195 if (dx*dx + dy*dy < kMinimumDragDistance*kMinimumDragDistance)
198 // The start of a drag. Position the button above all others.
199 [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil];
201 isBeingDragged_ = YES;
202 NSRect buttonFrame = [self frame];
203 // TODO(andybons): Constrain the buttons to be within the container.
204 // Clamp the button to be within its superview along the X-axis.
205 buttonFrame.origin.x += [theEvent deltaX];
206 [self setFrame:buttonFrame];
207 [self setNeedsDisplay:YES];
208 [[NSNotificationCenter defaultCenter]
209 postNotificationName:kBrowserActionButtonDraggingNotification
213 - (void)mouseUp:(NSEvent*)theEvent {
214 dragCouldStart_ = NO;
215 // There are non-drag cases where a mouseUp: may happen
216 // (e.g. mouse-down, cmd-tab to another application, move mouse,
218 NSPoint location = [self convertPoint:[theEvent locationInWindow]
220 if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) {
221 // Only perform the click if we didn't drag the button.
222 [self performClick:self];
224 // Make sure an ESC to end a drag doesn't trigger 2 endDrags.
225 if (isBeingDragged_) {
228 [super mouseUp:theEvent];
234 isBeingDragged_ = NO;
235 [[NSNotificationCenter defaultCenter]
236 postNotificationName:kBrowserActionButtonDragEndNotification object:self];
237 [[self cell] setHighlighted:NO];
240 - (void)setFrame:(NSRect)frameRect animate:(BOOL)animate {
242 [self setFrame:frameRect];
244 if ([moveAnimation_ isAnimating])
245 [moveAnimation_ stopAnimation];
247 NSDictionary* animationDictionary =
248 [NSDictionary dictionaryWithObjectsAndKeys:
249 self, NSViewAnimationTargetKey,
250 [NSValue valueWithRect:[self frame]], NSViewAnimationStartFrameKey,
251 [NSValue valueWithRect:frameRect], NSViewAnimationEndFrameKey,
253 [moveAnimation_ setViewAnimations:
254 [NSArray arrayWithObject:animationDictionary]];
255 [moveAnimation_ startAnimation];
259 - (void)updateState {
260 content::WebContents* webContents =
261 [browserActionsController_ currentWebContents];
265 base::string16 tooltip = viewController_->GetTooltip(webContents);
266 [self setToolTip:(tooltip.empty() ? nil : base::SysUTF16ToNSString(tooltip))];
268 gfx::Image image = viewController_->GetIcon(webContents);
270 if (!image.IsEmpty())
271 [self setImage:image.ToNSImage()];
273 [self setEnabled:viewController_->IsEnabled(webContents)];
275 [self setNeedsDisplay:YES];
278 - (BOOL)isAnimating {
279 return [moveAnimation_ isAnimating];
282 - (ToolbarActionViewController*)viewController {
283 return viewController_.get();
286 - (NSImage*)compositedImage {
287 NSRect bounds = [self bounds];
288 NSImage* image = [[[NSImage alloc] initWithSize:bounds.size] autorelease];
291 [[NSColor clearColor] set];
294 NSImage* actionImage = [self image];
295 const NSSize imageSize = [actionImage size];
296 const NSRect imageRect =
297 NSMakeRect(std::floor((NSWidth(bounds) - imageSize.width) / 2.0),
298 std::floor((NSHeight(bounds) - imageSize.height) / 2.0),
299 imageSize.width, imageSize.height);
300 [actionImage drawInRect:imageRect
302 operation:NSCompositeSourceOver
307 bounds.origin.y += kBrowserActionBadgeOriginYOffset;
308 [[self cell] drawBadgeWithinFrame:bounds
310 [browserActionsController_ currentWebContents]];
316 - (void)menuNeedsUpdate:(NSMenu*)menu {
317 [menu removeAllItems];
318 // |menuController()| can be nil if we don't show context menus for the given
320 if (viewControllerDelegate_->menuController())
321 [viewControllerDelegate_->menuController() populateMenu:menu];
326 @implementation BrowserActionCell
328 @synthesize browserActionsController = browserActionsController_;
329 @synthesize viewController = viewController_;
331 - (void)drawBadgeWithinFrame:(NSRect)frame
332 forWebContents:(content::WebContents*)webContents {
333 gfx::CanvasSkiaPaint canvas(frame, false);
334 canvas.set_composite_alpha(true);
335 gfx::Rect boundingRect(NSRectToCGRect(frame));
336 viewController_->PaintExtra(&canvas, boundingRect, webContents);
339 - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
340 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
341 [super drawWithFrame:cellFrame inView:controlView];
342 DCHECK(viewController_);
343 content::WebContents* webContents =
344 [browserActionsController_ currentWebContents];
345 bool enabled = viewController_->IsEnabled(webContents);
346 const NSSize imageSize = self.image.size;
347 const NSRect imageRect =
348 NSMakeRect(std::floor((NSWidth(cellFrame) - imageSize.width) / 2.0),
349 std::floor((NSHeight(cellFrame) - imageSize.height) / 2.0),
350 imageSize.width, imageSize.height);
351 [self.image drawInRect:imageRect
353 operation:NSCompositeSourceOver
354 fraction:enabled ? 1.0 : 0.4
358 cellFrame.origin.y += kBrowserActionBadgeOriginYOffset;
359 [self drawBadgeWithinFrame:cellFrame
360 forWebContents:webContents];