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/chrome_notification_types.h"
13 #include "chrome/browser/extensions/extension_action.h"
14 #include "chrome/browser/extensions/extension_action_icon_factory.h"
15 #include "chrome/browser/extensions/extension_action_manager.h"
16 #include "chrome/browser/ui/browser.h"
17 #include "chrome/browser/ui/cocoa/extensions/extension_action_context_menu_controller.h"
18 #include "content/public/browser/notification_observer.h"
19 #include "content/public/browser/notification_registrar.h"
20 #include "content/public/browser/notification_source.h"
21 #include "extensions/common/extension.h"
22 #include "grit/theme_resources.h"
23 #include "skia/ext/skia_utils_mac.h"
24 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
25 #include "ui/gfx/canvas_skia_paint.h"
26 #include "ui/gfx/image/image.h"
27 #include "ui/gfx/rect.h"
28 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
29 #include "ui/gfx/size.h"
31 using extensions::Extension;
33 NSString* const kBrowserActionButtonDraggingNotification =
34 @"BrowserActionButtonDraggingNotification";
35 NSString* const kBrowserActionButtonDragEndNotification =
36 @"BrowserActionButtonDragEndNotification";
38 static const CGFloat kBrowserActionBadgeOriginYOffset = 5;
39 static const CGFloat kAnimationDuration = 0.2;
40 static const CGFloat kMinimumDragDistance = 5;
42 // A helper class to bridge the asynchronous Skia bitmap loading mechanism to
43 // the extension's button.
44 class ExtensionActionIconFactoryBridge
45 : public content::NotificationObserver,
46 public ExtensionActionIconFactory::Observer {
48 ExtensionActionIconFactoryBridge(BrowserActionButton* owner,
50 const Extension* extension)
52 browser_action_([[owner cell] extensionAction]),
53 icon_factory_(profile, extension, browser_action_, this) {
55 extensions::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED,
56 content::Source<ExtensionAction>(browser_action_));
59 virtual ~ExtensionActionIconFactoryBridge() {}
61 // ExtensionActionIconFactory::Observer implementation.
62 virtual void OnIconUpdated() OVERRIDE {
66 // Overridden from content::NotificationObserver.
69 const content::NotificationSource& source,
70 const content::NotificationDetails& details) OVERRIDE {
71 if (type == extensions::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED)
77 gfx::Image GetIcon(int tabId) {
78 return icon_factory_.GetIcon(tabId);
83 BrowserActionButton* owner_;
85 // The browser action whose images we're loading.
86 ExtensionAction* const browser_action_;
88 // The object that will be used to get the browser action icon for us.
89 // It may load the icon asynchronously (in which case the initial icon
90 // returned by the factory will be transparent), so we have to observe it for
91 // updates to the icon.
92 ExtensionActionIconFactory icon_factory_;
94 // Used for registering to receive notifications and automatic clean up.
95 content::NotificationRegistrar registrar_;
97 DISALLOW_COPY_AND_ASSIGN(ExtensionActionIconFactoryBridge);
100 @interface BrowserActionCell (Internals)
101 - (void)drawBadgeWithinFrame:(NSRect)frame;
104 @interface BrowserActionButton (Private)
108 @implementation BrowserActionButton
110 @synthesize isBeingDragged = isBeingDragged_;
111 @synthesize extension = extension_;
112 @synthesize tabId = tabId_;
115 return [BrowserActionCell class];
118 - (id)initWithFrame:(NSRect)frame
119 extension:(const Extension*)extension
120 browser:(Browser*)browser
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:.
130 [cell setTabId:tabId];
131 ExtensionAction* browser_action =
132 extensions::ExtensionActionManager::Get(browser->profile())->
133 GetBrowserAction(*extension);
134 CHECK(browser_action)
135 << "Don't create a BrowserActionButton if there is no browser action.";
136 [cell setExtensionAction:browser_action];
138 accessibilitySetOverrideValue:base::SysUTF8ToNSString(extension->name())
139 forAttribute:NSAccessibilityDescriptionAttribute];
140 [cell setImageID:IDR_BROWSER_ACTION
141 forButtonState:image_button_cell::kDefaultState];
142 [cell setImageID:IDR_BROWSER_ACTION_H
143 forButtonState:image_button_cell::kHoverState];
144 [cell setImageID:IDR_BROWSER_ACTION_P
145 forButtonState:image_button_cell::kPressedState];
146 [cell setImageID:IDR_BROWSER_ACTION
147 forButtonState:image_button_cell::kDisabledState];
150 [self setButtonType:NSMomentaryChangeButton];
151 [self setShowsBorderOnlyWhileMouseInside:YES];
153 contextMenuController_.reset([[ExtensionActionContextMenuController alloc]
154 initWithExtension:extension
156 extensionAction:browser_action]);
157 base::scoped_nsobject<NSMenu> contextMenu(
158 [[NSMenu alloc] initWithTitle:@""]);
159 [contextMenu setDelegate:self];
160 [self setMenu:contextMenu];
163 extension_ = extension;
164 iconFactoryBridge_.reset(new ExtensionActionIconFactoryBridge(
165 self, browser->profile(), extension));
167 moveAnimation_.reset([[NSViewAnimation alloc] init]);
168 [moveAnimation_ gtm_setDuration:kAnimationDuration
169 eventMask:NSLeftMouseUpMask];
170 [moveAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
178 - (BOOL)acceptsFirstResponder {
182 - (void)mouseDown:(NSEvent*)theEvent {
183 NSPoint location = [self convertPoint:[theEvent locationInWindow]
185 if (NSPointInRect(location, [self bounds])) {
186 [[self cell] setHighlighted:YES];
187 dragCouldStart_ = YES;
188 dragStartPoint_ = [theEvent locationInWindow];
192 - (void)mouseDragged:(NSEvent*)theEvent {
193 if (!dragCouldStart_)
196 if (!isBeingDragged_) {
197 // Don't initiate a drag until it moves at least kMinimumDragDistance.
198 NSPoint currentPoint = [theEvent locationInWindow];
199 CGFloat dx = currentPoint.x - dragStartPoint_.x;
200 CGFloat dy = currentPoint.y - dragStartPoint_.y;
201 if (dx*dx + dy*dy < kMinimumDragDistance*kMinimumDragDistance)
204 // The start of a drag. Position the button above all others.
205 [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil];
207 isBeingDragged_ = YES;
208 NSRect buttonFrame = [self frame];
209 // TODO(andybons): Constrain the buttons to be within the container.
210 // Clamp the button to be within its superview along the X-axis.
211 buttonFrame.origin.x += [theEvent deltaX];
212 [self setFrame:buttonFrame];
213 [self setNeedsDisplay:YES];
214 [[NSNotificationCenter defaultCenter]
215 postNotificationName:kBrowserActionButtonDraggingNotification
219 - (void)mouseUp:(NSEvent*)theEvent {
220 dragCouldStart_ = NO;
221 // There are non-drag cases where a mouseUp: may happen
222 // (e.g. mouse-down, cmd-tab to another application, move mouse,
224 NSPoint location = [self convertPoint:[theEvent locationInWindow]
226 if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) {
227 // Only perform the click if we didn't drag the button.
228 [self performClick:self];
230 // Make sure an ESC to end a drag doesn't trigger 2 endDrags.
231 if (isBeingDragged_) {
234 [super mouseUp:theEvent];
240 isBeingDragged_ = NO;
241 [[NSNotificationCenter defaultCenter]
242 postNotificationName:kBrowserActionButtonDragEndNotification object:self];
243 [[self cell] setHighlighted:NO];
246 - (void)setFrame:(NSRect)frameRect animate:(BOOL)animate {
248 [self setFrame:frameRect];
250 if ([moveAnimation_ isAnimating])
251 [moveAnimation_ stopAnimation];
253 NSDictionary* animationDictionary =
254 [NSDictionary dictionaryWithObjectsAndKeys:
255 self, NSViewAnimationTargetKey,
256 [NSValue valueWithRect:[self frame]], NSViewAnimationStartFrameKey,
257 [NSValue valueWithRect:frameRect], NSViewAnimationEndFrameKey,
259 [moveAnimation_ setViewAnimations:
260 [NSArray arrayWithObject:animationDictionary]];
261 [moveAnimation_ startAnimation];
265 - (void)updateState {
269 std::string tooltip = [[self cell] extensionAction]->GetTitle(tabId_);
270 if (tooltip.empty()) {
271 [self setToolTip:nil];
273 [self setToolTip:base::SysUTF8ToNSString(tooltip)];
276 gfx::Image image = iconFactoryBridge_->GetIcon(tabId_);
278 if (!image.IsEmpty())
279 [self setImage:image.ToNSImage()];
281 [[self cell] setTabId:tabId_];
283 bool enabled = [[self cell] extensionAction]->GetIsVisible(tabId_);
284 [self setEnabled:enabled];
286 [self setNeedsDisplay:YES];
289 - (BOOL)isAnimating {
290 return [moveAnimation_ isAnimating];
293 - (NSImage*)compositedImage {
294 NSRect bounds = [self bounds];
295 NSImage* image = [[[NSImage alloc] initWithSize:bounds.size] autorelease];
298 [[NSColor clearColor] set];
301 NSImage* actionImage = [self image];
302 const NSSize imageSize = [actionImage size];
303 const NSRect imageRect =
304 NSMakeRect(std::floor((NSWidth(bounds) - imageSize.width) / 2.0),
305 std::floor((NSHeight(bounds) - imageSize.height) / 2.0),
306 imageSize.width, imageSize.height);
307 [actionImage drawInRect:imageRect
309 operation:NSCompositeSourceOver
314 bounds.origin.y += kBrowserActionBadgeOriginYOffset;
315 [[self cell] drawBadgeWithinFrame:bounds];
321 - (void)menuNeedsUpdate:(NSMenu*)menu {
322 [menu removeAllItems];
323 [contextMenuController_ populateMenu:menu];
328 @implementation BrowserActionCell
330 @synthesize tabId = tabId_;
331 @synthesize extensionAction = extensionAction_;
333 - (void)drawBadgeWithinFrame:(NSRect)frame {
334 gfx::CanvasSkiaPaint canvas(frame, false);
335 canvas.set_composite_alpha(true);
336 gfx::Rect boundingRect(NSRectToCGRect(frame));
337 extensionAction_->PaintBadge(&canvas, boundingRect, tabId_);
340 - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
341 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
342 [super drawWithFrame:cellFrame inView:controlView];
343 CHECK(extensionAction_);
344 bool enabled = extensionAction_->GetIsVisible(tabId_);
345 const NSSize imageSize = self.image.size;
346 const NSRect imageRect =
347 NSMakeRect(std::floor((NSWidth(cellFrame) - imageSize.width) / 2.0),
348 std::floor((NSHeight(cellFrame) - imageSize.height) / 2.0),
349 imageSize.width, imageSize.height);
350 [self.image drawInRect:imageRect
352 operation:NSCompositeSourceOver
353 fraction:enabled ? 1.0 : 0.4
357 cellFrame.origin.y += kBrowserActionBadgeOriginYOffset;
358 [self drawBadgeWithinFrame:cellFrame];