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/base/resource/resource_bundle.h"
26 #include "ui/gfx/canvas_skia_paint.h"
27 #include "ui/gfx/image/image.h"
28 #include "ui/gfx/rect.h"
29 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
30 #include "ui/gfx/size.h"
32 using extensions::Extension;
34 NSString* const kBrowserActionButtonDraggingNotification =
35 @"BrowserActionButtonDraggingNotification";
36 NSString* const kBrowserActionButtonDragEndNotification =
37 @"BrowserActionButtonDragEndNotification";
39 static const CGFloat kBrowserActionBadgeOriginYOffset = 5;
40 static const CGFloat kAnimationDuration = 0.2;
41 static const CGFloat kMinimumDragDistance = 5;
43 // A helper class to bridge the asynchronous Skia bitmap loading mechanism to
44 // the extension's button.
45 class ExtensionActionIconFactoryBridge
46 : public content::NotificationObserver,
47 public ExtensionActionIconFactory::Observer {
49 ExtensionActionIconFactoryBridge(BrowserActionButton* owner,
51 const Extension* extension)
53 browser_action_([[owner cell] extensionAction]),
54 icon_factory_(profile, extension, browser_action_, this) {
56 this, chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED,
57 content::Source<ExtensionAction>(browser_action_));
60 virtual ~ExtensionActionIconFactoryBridge() {}
62 // ExtensionActionIconFactory::Observer implementation.
63 virtual void OnIconUpdated() OVERRIDE {
67 // Overridden from content::NotificationObserver.
70 const content::NotificationSource& source,
71 const content::NotificationDetails& details) OVERRIDE {
72 if (type == chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED)
78 gfx::Image GetIcon(int tabId) {
79 return icon_factory_.GetIcon(tabId);
84 BrowserActionButton* owner_;
86 // The browser action whose images we're loading.
87 ExtensionAction* const browser_action_;
89 // The object that will be used to get the browser action icon for us.
90 // It may load the icon asynchronously (in which case the initial icon
91 // returned by the factory will be transparent), so we have to observe it for
92 // updates to the icon.
93 ExtensionActionIconFactory icon_factory_;
95 // Used for registering to receive notifications and automatic clean up.
96 content::NotificationRegistrar registrar_;
98 DISALLOW_COPY_AND_ASSIGN(ExtensionActionIconFactoryBridge);
101 @interface BrowserActionCell (Internals)
102 - (void)drawBadgeWithinFrame:(NSRect)frame;
105 @interface BrowserActionButton (Private)
109 @implementation BrowserActionButton
111 @synthesize isBeingDragged = isBeingDragged_;
112 @synthesize extension = extension_;
113 @synthesize tabId = tabId_;
116 return [BrowserActionCell class];
119 - (id)initWithFrame:(NSRect)frame
120 extension:(const Extension*)extension
121 browser:(Browser*)browser
123 if ((self = [super initWithFrame:frame])) {
124 BrowserActionCell* cell = [[[BrowserActionCell alloc] init] autorelease];
125 // [NSButton setCell:] warns to NOT use setCell: other than in the
126 // initializer of a control. However, we are using a basic
127 // NSButton whose initializer does not take an NSCell as an
128 // object. To honor the assumed semantics, we do nothing with
129 // NSButton between alloc/init and setCell:.
131 [cell setTabId:tabId];
132 ExtensionAction* browser_action =
133 extensions::ExtensionActionManager::Get(browser->profile())->
134 GetBrowserAction(*extension);
135 [cell setExtensionAction:browser_action];
137 accessibilitySetOverrideValue:base::SysUTF8ToNSString(extension->name())
138 forAttribute:NSAccessibilityDescriptionAttribute];
139 [cell setImageID:IDR_BROWSER_ACTION
140 forButtonState:image_button_cell::kDefaultState];
141 [cell setImageID:IDR_BROWSER_ACTION_H
142 forButtonState:image_button_cell::kHoverState];
143 [cell setImageID:IDR_BROWSER_ACTION_P
144 forButtonState:image_button_cell::kPressedState];
145 [cell setImageID:IDR_BROWSER_ACTION
146 forButtonState:image_button_cell::kDisabledState];
149 [self setButtonType:NSMomentaryChangeButton];
150 [self setShowsBorderOnlyWhileMouseInside:YES];
152 contextMenuController_.reset([[ExtensionActionContextMenuController alloc]
153 initWithExtension:extension
155 extensionAction:browser_action]);
156 base::scoped_nsobject<NSMenu> contextMenu(
157 [[NSMenu alloc] initWithTitle:@""]);
158 [contextMenu setDelegate:self];
159 [self setMenu:contextMenu];
162 extension_ = extension;
163 iconFactoryBridge_.reset(new ExtensionActionIconFactoryBridge(
164 self, browser->profile(), extension));
166 moveAnimation_.reset([[NSViewAnimation alloc] init]);
167 [moveAnimation_ gtm_setDuration:kAnimationDuration
168 eventMask:NSLeftMouseUpMask];
169 [moveAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
177 - (BOOL)acceptsFirstResponder {
181 - (void)mouseDown:(NSEvent*)theEvent {
182 [[self cell] setHighlighted:YES];
183 dragCouldStart_ = YES;
184 dragStartPoint_ = [theEvent locationInWindow];
187 - (void)mouseDragged:(NSEvent*)theEvent {
188 if (!dragCouldStart_)
191 if (!isBeingDragged_) {
192 // Don't initiate a drag until it moves at least kMinimumDragDistance.
193 NSPoint currentPoint = [theEvent locationInWindow];
194 CGFloat dx = currentPoint.x - dragStartPoint_.x;
195 CGFloat dy = currentPoint.y - dragStartPoint_.y;
196 if (dx*dx + dy*dy < kMinimumDragDistance*kMinimumDragDistance)
199 // The start of a drag. Position the button above all others.
200 [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil];
202 isBeingDragged_ = YES;
203 NSRect buttonFrame = [self frame];
204 // TODO(andybons): Constrain the buttons to be within the container.
205 // Clamp the button to be within its superview along the X-axis.
206 buttonFrame.origin.x += [theEvent deltaX];
207 [self setFrame:buttonFrame];
208 [self setNeedsDisplay:YES];
209 [[NSNotificationCenter defaultCenter]
210 postNotificationName:kBrowserActionButtonDraggingNotification
214 - (void)mouseUp:(NSEvent*)theEvent {
215 dragCouldStart_ = NO;
216 // There are non-drag cases where a mouseUp: may happen
217 // (e.g. mouse-down, cmd-tab to another application, move mouse,
219 NSPoint location = [self convertPoint:[theEvent locationInWindow]
221 if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) {
222 // Only perform the click if we didn't drag the button.
223 [self performClick:self];
225 // Make sure an ESC to end a drag doesn't trigger 2 endDrags.
226 if (isBeingDragged_) {
229 [super mouseUp:theEvent];
235 isBeingDragged_ = NO;
236 [[NSNotificationCenter defaultCenter]
237 postNotificationName:kBrowserActionButtonDragEndNotification object:self];
238 [[self cell] setHighlighted:NO];
241 - (void)setFrame:(NSRect)frameRect animate:(BOOL)animate {
243 [self setFrame:frameRect];
245 if ([moveAnimation_ isAnimating])
246 [moveAnimation_ stopAnimation];
248 NSDictionary* animationDictionary =
249 [NSDictionary dictionaryWithObjectsAndKeys:
250 self, NSViewAnimationTargetKey,
251 [NSValue valueWithRect:[self frame]], NSViewAnimationStartFrameKey,
252 [NSValue valueWithRect:frameRect], NSViewAnimationEndFrameKey,
254 [moveAnimation_ setViewAnimations:
255 [NSArray arrayWithObject:animationDictionary]];
256 [moveAnimation_ startAnimation];
260 - (void)updateState {
264 std::string tooltip = [[self cell] extensionAction]->GetTitle(tabId_);
265 if (tooltip.empty()) {
266 [self setToolTip:nil];
268 [self setToolTip:base::SysUTF8ToNSString(tooltip)];
271 gfx::Image image = iconFactoryBridge_->GetIcon(tabId_);
273 if (!image.IsEmpty())
274 [self setImage:image.ToNSImage()];
276 [[self cell] setTabId:tabId_];
278 bool enabled = [[self cell] extensionAction]->GetIsVisible(tabId_);
279 [self setEnabled:enabled];
281 [self setNeedsDisplay:YES];
284 - (BOOL)isAnimating {
285 return [moveAnimation_ isAnimating];
288 - (NSImage*)compositedImage {
289 NSRect bounds = [self bounds];
290 NSImage* image = [[[NSImage alloc] initWithSize:bounds.size] autorelease];
293 [[NSColor clearColor] set];
296 NSImage* actionImage = [self image];
297 const NSSize imageSize = [actionImage size];
298 const NSRect imageRect =
299 NSMakeRect(std::floor((NSWidth(bounds) - imageSize.width) / 2.0),
300 std::floor((NSHeight(bounds) - imageSize.height) / 2.0),
301 imageSize.width, imageSize.height);
302 [actionImage drawInRect:imageRect
304 operation:NSCompositeSourceOver
309 bounds.origin.y += kBrowserActionBadgeOriginYOffset;
310 [[self cell] drawBadgeWithinFrame:bounds];
316 - (void)menuNeedsUpdate:(NSMenu*)menu {
317 [menu removeAllItems];
318 [contextMenuController_ populateMenu:menu];
323 @implementation BrowserActionCell
325 @synthesize tabId = tabId_;
326 @synthesize extensionAction = extensionAction_;
328 - (void)drawBadgeWithinFrame:(NSRect)frame {
329 gfx::CanvasSkiaPaint canvas(frame, false);
330 canvas.set_composite_alpha(true);
331 gfx::Rect boundingRect(NSRectToCGRect(frame));
332 extensionAction_->PaintBadge(&canvas, boundingRect, tabId_);
335 - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
336 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
337 [super drawWithFrame:cellFrame inView:controlView];
338 bool enabled = extensionAction_->GetIsVisible(tabId_);
339 const NSSize imageSize = self.image.size;
340 const NSRect imageRect =
341 NSMakeRect(std::floor((NSWidth(cellFrame) - imageSize.width) / 2.0),
342 std::floor((NSHeight(cellFrame) - imageSize.height) / 2.0),
343 imageSize.width, imageSize.height);
344 [self.image drawInRect:imageRect
346 operation:NSCompositeSourceOver
347 fraction:enabled ? 1.0 : 0.4
351 cellFrame.origin.y += kBrowserActionBadgeOriginYOffset;
352 [self drawBadgeWithinFrame:cellFrame];