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 CHECK(browser_action)
136 << "Don't create a BrowserActionButton if there is no browser action.";
137 [cell setExtensionAction:browser_action];
139 accessibilitySetOverrideValue:base::SysUTF8ToNSString(extension->name())
140 forAttribute:NSAccessibilityDescriptionAttribute];
141 [cell setImageID:IDR_BROWSER_ACTION
142 forButtonState:image_button_cell::kDefaultState];
143 [cell setImageID:IDR_BROWSER_ACTION_H
144 forButtonState:image_button_cell::kHoverState];
145 [cell setImageID:IDR_BROWSER_ACTION_P
146 forButtonState:image_button_cell::kPressedState];
147 [cell setImageID:IDR_BROWSER_ACTION
148 forButtonState:image_button_cell::kDisabledState];
151 [self setButtonType:NSMomentaryChangeButton];
152 [self setShowsBorderOnlyWhileMouseInside:YES];
154 contextMenuController_.reset([[ExtensionActionContextMenuController alloc]
155 initWithExtension:extension
157 extensionAction:browser_action]);
158 base::scoped_nsobject<NSMenu> contextMenu(
159 [[NSMenu alloc] initWithTitle:@""]);
160 [contextMenu setDelegate:self];
161 [self setMenu:contextMenu];
164 extension_ = extension;
165 iconFactoryBridge_.reset(new ExtensionActionIconFactoryBridge(
166 self, browser->profile(), extension));
168 moveAnimation_.reset([[NSViewAnimation alloc] init]);
169 [moveAnimation_ gtm_setDuration:kAnimationDuration
170 eventMask:NSLeftMouseUpMask];
171 [moveAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
179 - (BOOL)acceptsFirstResponder {
183 - (void)mouseDown:(NSEvent*)theEvent {
184 [[self cell] setHighlighted:YES];
185 dragCouldStart_ = YES;
186 dragStartPoint_ = [theEvent locationInWindow];
189 - (void)mouseDragged:(NSEvent*)theEvent {
190 if (!dragCouldStart_)
193 if (!isBeingDragged_) {
194 // Don't initiate a drag until it moves at least kMinimumDragDistance.
195 NSPoint currentPoint = [theEvent locationInWindow];
196 CGFloat dx = currentPoint.x - dragStartPoint_.x;
197 CGFloat dy = currentPoint.y - dragStartPoint_.y;
198 if (dx*dx + dy*dy < kMinimumDragDistance*kMinimumDragDistance)
201 // The start of a drag. Position the button above all others.
202 [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil];
204 isBeingDragged_ = YES;
205 NSRect buttonFrame = [self frame];
206 // TODO(andybons): Constrain the buttons to be within the container.
207 // Clamp the button to be within its superview along the X-axis.
208 buttonFrame.origin.x += [theEvent deltaX];
209 [self setFrame:buttonFrame];
210 [self setNeedsDisplay:YES];
211 [[NSNotificationCenter defaultCenter]
212 postNotificationName:kBrowserActionButtonDraggingNotification
216 - (void)mouseUp:(NSEvent*)theEvent {
217 dragCouldStart_ = NO;
218 // There are non-drag cases where a mouseUp: may happen
219 // (e.g. mouse-down, cmd-tab to another application, move mouse,
221 NSPoint location = [self convertPoint:[theEvent locationInWindow]
223 if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) {
224 // Only perform the click if we didn't drag the button.
225 [self performClick:self];
227 // Make sure an ESC to end a drag doesn't trigger 2 endDrags.
228 if (isBeingDragged_) {
231 [super mouseUp:theEvent];
237 isBeingDragged_ = NO;
238 [[NSNotificationCenter defaultCenter]
239 postNotificationName:kBrowserActionButtonDragEndNotification object:self];
240 [[self cell] setHighlighted:NO];
243 - (void)setFrame:(NSRect)frameRect animate:(BOOL)animate {
245 [self setFrame:frameRect];
247 if ([moveAnimation_ isAnimating])
248 [moveAnimation_ stopAnimation];
250 NSDictionary* animationDictionary =
251 [NSDictionary dictionaryWithObjectsAndKeys:
252 self, NSViewAnimationTargetKey,
253 [NSValue valueWithRect:[self frame]], NSViewAnimationStartFrameKey,
254 [NSValue valueWithRect:frameRect], NSViewAnimationEndFrameKey,
256 [moveAnimation_ setViewAnimations:
257 [NSArray arrayWithObject:animationDictionary]];
258 [moveAnimation_ startAnimation];
262 - (void)updateState {
266 std::string tooltip = [[self cell] extensionAction]->GetTitle(tabId_);
267 if (tooltip.empty()) {
268 [self setToolTip:nil];
270 [self setToolTip:base::SysUTF8ToNSString(tooltip)];
273 gfx::Image image = iconFactoryBridge_->GetIcon(tabId_);
275 if (!image.IsEmpty())
276 [self setImage:image.ToNSImage()];
278 [[self cell] setTabId:tabId_];
280 bool enabled = [[self cell] extensionAction]->GetIsVisible(tabId_);
281 [self setEnabled:enabled];
283 [self setNeedsDisplay:YES];
286 - (BOOL)isAnimating {
287 return [moveAnimation_ isAnimating];
290 - (NSImage*)compositedImage {
291 NSRect bounds = [self bounds];
292 NSImage* image = [[[NSImage alloc] initWithSize:bounds.size] autorelease];
295 [[NSColor clearColor] set];
298 NSImage* actionImage = [self image];
299 const NSSize imageSize = [actionImage size];
300 const NSRect imageRect =
301 NSMakeRect(std::floor((NSWidth(bounds) - imageSize.width) / 2.0),
302 std::floor((NSHeight(bounds) - imageSize.height) / 2.0),
303 imageSize.width, imageSize.height);
304 [actionImage drawInRect:imageRect
306 operation:NSCompositeSourceOver
311 bounds.origin.y += kBrowserActionBadgeOriginYOffset;
312 [[self cell] drawBadgeWithinFrame:bounds];
318 - (void)menuNeedsUpdate:(NSMenu*)menu {
319 [menu removeAllItems];
320 [contextMenuController_ populateMenu:menu];
325 @implementation BrowserActionCell
327 @synthesize tabId = tabId_;
328 @synthesize extensionAction = extensionAction_;
330 - (void)drawBadgeWithinFrame:(NSRect)frame {
331 gfx::CanvasSkiaPaint canvas(frame, false);
332 canvas.set_composite_alpha(true);
333 gfx::Rect boundingRect(NSRectToCGRect(frame));
334 extensionAction_->PaintBadge(&canvas, boundingRect, tabId_);
337 - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
338 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
339 [super drawWithFrame:cellFrame inView:controlView];
340 CHECK(extensionAction_);
341 bool enabled = extensionAction_->GetIsVisible(tabId_);
342 const NSSize imageSize = self.image.size;
343 const NSRect imageRect =
344 NSMakeRect(std::floor((NSWidth(cellFrame) - imageSize.width) / 2.0),
345 std::floor((NSHeight(cellFrame) - imageSize.height) / 2.0),
346 imageSize.width, imageSize.height);
347 [self.image drawInRect:imageRect
349 operation:NSCompositeSourceOver
350 fraction:enabled ? 1.0 : 0.4
354 cellFrame.origin.y += kBrowserActionBadgeOriginYOffset;
355 [self drawBadgeWithinFrame:cellFrame];