browser_action_button.mm revision 5f1c94371a64b3196d4be9466099bb892df9b88e
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.
4
5#import "chrome/browser/ui/cocoa/extensions/browser_action_button.h"
6
7#include <algorithm>
8#include <cmath>
9
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"
31
32using extensions::Extension;
33
34NSString* const kBrowserActionButtonDraggingNotification =
35    @"BrowserActionButtonDraggingNotification";
36NSString* const kBrowserActionButtonDragEndNotification =
37    @"BrowserActionButtonDragEndNotification";
38
39static const CGFloat kBrowserActionBadgeOriginYOffset = 5;
40static const CGFloat kAnimationDuration = 0.2;
41static const CGFloat kMinimumDragDistance = 5;
42
43// A helper class to bridge the asynchronous Skia bitmap loading mechanism to
44// the extension's button.
45class ExtensionActionIconFactoryBridge
46    : public content::NotificationObserver,
47      public ExtensionActionIconFactory::Observer {
48 public:
49  ExtensionActionIconFactoryBridge(BrowserActionButton* owner,
50                                   Profile* profile,
51                                   const Extension* extension)
52      : owner_(owner),
53        browser_action_([[owner cell] extensionAction]),
54        icon_factory_(profile, extension, browser_action_, this) {
55    registrar_.Add(this,
56                   extensions::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED,
57                   content::Source<ExtensionAction>(browser_action_));
58  }
59
60  virtual ~ExtensionActionIconFactoryBridge() {}
61
62  // ExtensionActionIconFactory::Observer implementation.
63  virtual void OnIconUpdated() OVERRIDE {
64    [owner_ updateState];
65  }
66
67  // Overridden from content::NotificationObserver.
68  virtual void Observe(
69      int type,
70      const content::NotificationSource& source,
71      const content::NotificationDetails& details) OVERRIDE {
72    if (type == extensions::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED)
73      [owner_ updateState];
74    else
75      NOTREACHED();
76  }
77
78  gfx::Image GetIcon(int tabId) {
79    return icon_factory_.GetIcon(tabId);
80  }
81
82 private:
83  // Weak. Owns us.
84  BrowserActionButton* owner_;
85
86  // The browser action whose images we're loading.
87  ExtensionAction* const browser_action_;
88
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_;
94
95  // Used for registering to receive notifications and automatic clean up.
96  content::NotificationRegistrar registrar_;
97
98  DISALLOW_COPY_AND_ASSIGN(ExtensionActionIconFactoryBridge);
99};
100
101@interface BrowserActionCell (Internals)
102- (void)drawBadgeWithinFrame:(NSRect)frame;
103@end
104
105@interface BrowserActionButton (Private)
106- (void)endDrag;
107@end
108
109@implementation BrowserActionButton
110
111@synthesize isBeingDragged = isBeingDragged_;
112@synthesize extension = extension_;
113@synthesize tabId = tabId_;
114
115+ (Class)cellClass {
116  return [BrowserActionCell class];
117}
118
119- (id)initWithFrame:(NSRect)frame
120          extension:(const Extension*)extension
121            browser:(Browser*)browser
122              tabId:(int)tabId {
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:.
130    [self setCell:cell];
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];
138    [cell
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];
149
150    [self setTitle:@""];
151    [self setButtonType:NSMomentaryChangeButton];
152    [self setShowsBorderOnlyWhileMouseInside:YES];
153
154    contextMenuController_.reset([[ExtensionActionContextMenuController alloc]
155        initWithExtension:extension
156                  browser:browser
157          extensionAction:browser_action]);
158    base::scoped_nsobject<NSMenu> contextMenu(
159        [[NSMenu alloc] initWithTitle:@""]);
160    [contextMenu setDelegate:self];
161    [self setMenu:contextMenu];
162
163    tabId_ = tabId;
164    extension_ = extension;
165    iconFactoryBridge_.reset(new ExtensionActionIconFactoryBridge(
166        self, browser->profile(), extension));
167
168    moveAnimation_.reset([[NSViewAnimation alloc] init]);
169    [moveAnimation_ gtm_setDuration:kAnimationDuration
170                          eventMask:NSLeftMouseUpMask];
171    [moveAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
172
173    [self updateState];
174  }
175
176  return self;
177}
178
179- (BOOL)acceptsFirstResponder {
180  return YES;
181}
182
183- (void)mouseDown:(NSEvent*)theEvent {
184  NSPoint location = [self convertPoint:[theEvent locationInWindow]
185                               fromView:nil];
186  if (NSPointInRect(location, [self bounds])) {
187    [[self cell] setHighlighted:YES];
188    dragCouldStart_ = YES;
189    dragStartPoint_ = [theEvent locationInWindow];
190  }
191}
192
193- (void)mouseDragged:(NSEvent*)theEvent {
194  if (!dragCouldStart_)
195    return;
196
197  if (!isBeingDragged_) {
198    // Don't initiate a drag until it moves at least kMinimumDragDistance.
199    NSPoint currentPoint = [theEvent locationInWindow];
200    CGFloat dx = currentPoint.x - dragStartPoint_.x;
201    CGFloat dy = currentPoint.y - dragStartPoint_.y;
202    if (dx*dx + dy*dy < kMinimumDragDistance*kMinimumDragDistance)
203      return;
204
205    // The start of a drag. Position the button above all others.
206    [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil];
207  }
208  isBeingDragged_ = YES;
209  NSRect buttonFrame = [self frame];
210  // TODO(andybons): Constrain the buttons to be within the container.
211  // Clamp the button to be within its superview along the X-axis.
212  buttonFrame.origin.x += [theEvent deltaX];
213  [self setFrame:buttonFrame];
214  [self setNeedsDisplay:YES];
215  [[NSNotificationCenter defaultCenter]
216      postNotificationName:kBrowserActionButtonDraggingNotification
217      object:self];
218}
219
220- (void)mouseUp:(NSEvent*)theEvent {
221  dragCouldStart_ = NO;
222  // There are non-drag cases where a mouseUp: may happen
223  // (e.g. mouse-down, cmd-tab to another application, move mouse,
224  // mouse-up).
225  NSPoint location = [self convertPoint:[theEvent locationInWindow]
226                               fromView:nil];
227  if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) {
228    // Only perform the click if we didn't drag the button.
229    [self performClick:self];
230  } else {
231    // Make sure an ESC to end a drag doesn't trigger 2 endDrags.
232    if (isBeingDragged_) {
233      [self endDrag];
234    } else {
235      [super mouseUp:theEvent];
236    }
237  }
238}
239
240- (void)endDrag {
241  isBeingDragged_ = NO;
242  [[NSNotificationCenter defaultCenter]
243      postNotificationName:kBrowserActionButtonDragEndNotification object:self];
244  [[self cell] setHighlighted:NO];
245}
246
247- (void)setFrame:(NSRect)frameRect animate:(BOOL)animate {
248  if (!animate) {
249    [self setFrame:frameRect];
250  } else {
251    if ([moveAnimation_ isAnimating])
252      [moveAnimation_ stopAnimation];
253
254    NSDictionary* animationDictionary =
255        [NSDictionary dictionaryWithObjectsAndKeys:
256            self, NSViewAnimationTargetKey,
257            [NSValue valueWithRect:[self frame]], NSViewAnimationStartFrameKey,
258            [NSValue valueWithRect:frameRect], NSViewAnimationEndFrameKey,
259            nil];
260    [moveAnimation_ setViewAnimations:
261        [NSArray arrayWithObject:animationDictionary]];
262    [moveAnimation_ startAnimation];
263  }
264}
265
266- (void)updateState {
267  if (tabId_ < 0)
268    return;
269
270  std::string tooltip = [[self cell] extensionAction]->GetTitle(tabId_);
271  if (tooltip.empty()) {
272    [self setToolTip:nil];
273  } else {
274    [self setToolTip:base::SysUTF8ToNSString(tooltip)];
275  }
276
277  gfx::Image image = iconFactoryBridge_->GetIcon(tabId_);
278
279  if (!image.IsEmpty())
280    [self setImage:image.ToNSImage()];
281
282  [[self cell] setTabId:tabId_];
283
284  bool enabled = [[self cell] extensionAction]->GetIsVisible(tabId_);
285  [self setEnabled:enabled];
286
287  [self setNeedsDisplay:YES];
288}
289
290- (BOOL)isAnimating {
291  return [moveAnimation_ isAnimating];
292}
293
294- (NSImage*)compositedImage {
295  NSRect bounds = [self bounds];
296  NSImage* image = [[[NSImage alloc] initWithSize:bounds.size] autorelease];
297  [image lockFocus];
298
299  [[NSColor clearColor] set];
300  NSRectFill(bounds);
301
302  NSImage* actionImage = [self image];
303  const NSSize imageSize = [actionImage size];
304  const NSRect imageRect =
305      NSMakeRect(std::floor((NSWidth(bounds) - imageSize.width) / 2.0),
306                 std::floor((NSHeight(bounds) - imageSize.height) / 2.0),
307                 imageSize.width, imageSize.height);
308  [actionImage drawInRect:imageRect
309                 fromRect:NSZeroRect
310                operation:NSCompositeSourceOver
311                 fraction:1.0
312           respectFlipped:YES
313                    hints:nil];
314
315  bounds.origin.y += kBrowserActionBadgeOriginYOffset;
316  [[self cell] drawBadgeWithinFrame:bounds];
317
318  [image unlockFocus];
319  return image;
320}
321
322- (void)menuNeedsUpdate:(NSMenu*)menu {
323  [menu removeAllItems];
324  [contextMenuController_ populateMenu:menu];
325}
326
327@end
328
329@implementation BrowserActionCell
330
331@synthesize tabId = tabId_;
332@synthesize extensionAction = extensionAction_;
333
334- (void)drawBadgeWithinFrame:(NSRect)frame {
335  gfx::CanvasSkiaPaint canvas(frame, false);
336  canvas.set_composite_alpha(true);
337  gfx::Rect boundingRect(NSRectToCGRect(frame));
338  extensionAction_->PaintBadge(&canvas, boundingRect, tabId_);
339}
340
341- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
342  gfx::ScopedNSGraphicsContextSaveGState scopedGState;
343  [super drawWithFrame:cellFrame inView:controlView];
344  CHECK(extensionAction_);
345  bool enabled = extensionAction_->GetIsVisible(tabId_);
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
352                fromRect:NSZeroRect
353               operation:NSCompositeSourceOver
354                fraction:enabled ? 1.0 : 0.4
355          respectFlipped:YES
356                   hints:nil];
357
358  cellFrame.origin.y += kBrowserActionBadgeOriginYOffset;
359  [self drawBadgeWithinFrame:cellFrame];
360}
361
362@end
363