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/extensions/extension_action.h"
13#include "chrome/browser/extensions/extension_action_icon_factory.h"
14#include "chrome/browser/extensions/extension_action_manager.h"
15#include "chrome/browser/profiles/profile.h"
16#include "chrome/browser/ui/browser.h"
17#include "chrome/browser/ui/cocoa/extensions/extension_action_context_menu_controller.h"
18#include "extensions/common/extension.h"
19#include "grit/theme_resources.h"
20#include "skia/ext/skia_utils_mac.h"
21#import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h"
22#include "ui/gfx/canvas_skia_paint.h"
23#include "ui/gfx/image/image.h"
24#include "ui/gfx/rect.h"
25#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
26#include "ui/gfx/size.h"
27
28using extensions::Extension;
29
30NSString* const kBrowserActionButtonDraggingNotification =
31    @"BrowserActionButtonDraggingNotification";
32NSString* const kBrowserActionButtonDragEndNotification =
33    @"BrowserActionButtonDragEndNotification";
34
35static const CGFloat kBrowserActionBadgeOriginYOffset = 5;
36static const CGFloat kAnimationDuration = 0.2;
37static const CGFloat kMinimumDragDistance = 5;
38
39// A helper class to bridge the asynchronous Skia bitmap loading mechanism to
40// the extension's button.
41class ExtensionActionIconFactoryBridge
42    : public ExtensionActionIconFactory::Observer {
43 public:
44  ExtensionActionIconFactoryBridge(BrowserActionButton* owner,
45                                   Profile* profile,
46                                   const Extension* extension)
47      : owner_(owner),
48        browser_action_([[owner cell] extensionAction]),
49        icon_factory_(profile, extension, browser_action_, this) {
50  }
51
52  virtual ~ExtensionActionIconFactoryBridge() {}
53
54  // ExtensionActionIconFactory::Observer implementation.
55  virtual void OnIconUpdated() OVERRIDE {
56    [owner_ updateState];
57  }
58
59  gfx::Image GetIcon(int tabId) {
60    return icon_factory_.GetIcon(tabId);
61  }
62
63 private:
64  // Weak. Owns us.
65  BrowserActionButton* owner_;
66
67  // The browser action whose images we're loading.
68  ExtensionAction* const browser_action_;
69
70  // The object that will be used to get the browser action icon for us.
71  // It may load the icon asynchronously (in which case the initial icon
72  // returned by the factory will be transparent), so we have to observe it for
73  // updates to the icon.
74  ExtensionActionIconFactory icon_factory_;
75
76  DISALLOW_COPY_AND_ASSIGN(ExtensionActionIconFactoryBridge);
77};
78
79@interface BrowserActionCell (Internals)
80- (void)drawBadgeWithinFrame:(NSRect)frame;
81@end
82
83@interface BrowserActionButton (Private)
84- (void)endDrag;
85@end
86
87@implementation BrowserActionButton
88
89@synthesize isBeingDragged = isBeingDragged_;
90@synthesize extension = extension_;
91@synthesize tabId = tabId_;
92
93+ (Class)cellClass {
94  return [BrowserActionCell class];
95}
96
97- (id)initWithFrame:(NSRect)frame
98          extension:(const Extension*)extension
99            browser:(Browser*)browser
100              tabId:(int)tabId {
101  if ((self = [super initWithFrame:frame])) {
102    BrowserActionCell* cell = [[[BrowserActionCell alloc] init] autorelease];
103    // [NSButton setCell:] warns to NOT use setCell: other than in the
104    // initializer of a control.  However, we are using a basic
105    // NSButton whose initializer does not take an NSCell as an
106    // object.  To honor the assumed semantics, we do nothing with
107    // NSButton between alloc/init and setCell:.
108    [self setCell:cell];
109    [cell setTabId:tabId];
110    ExtensionAction* browser_action =
111        extensions::ExtensionActionManager::Get(browser->profile())->
112        GetBrowserAction(*extension);
113    CHECK(browser_action)
114        << "Don't create a BrowserActionButton if there is no browser action.";
115    [cell setExtensionAction:browser_action];
116    [cell
117        accessibilitySetOverrideValue:base::SysUTF8ToNSString(extension->name())
118        forAttribute:NSAccessibilityDescriptionAttribute];
119    [cell setImageID:IDR_BROWSER_ACTION
120      forButtonState:image_button_cell::kDefaultState];
121    [cell setImageID:IDR_BROWSER_ACTION_H
122      forButtonState:image_button_cell::kHoverState];
123    [cell setImageID:IDR_BROWSER_ACTION_P
124      forButtonState:image_button_cell::kPressedState];
125    [cell setImageID:IDR_BROWSER_ACTION
126      forButtonState:image_button_cell::kDisabledState];
127
128    [self setTitle:@""];
129    [self setButtonType:NSMomentaryChangeButton];
130    [self setShowsBorderOnlyWhileMouseInside:YES];
131
132    contextMenuController_.reset([[ExtensionActionContextMenuController alloc]
133        initWithExtension:extension
134                  browser:browser
135          extensionAction:browser_action]);
136    base::scoped_nsobject<NSMenu> contextMenu(
137        [[NSMenu alloc] initWithTitle:@""]);
138    [contextMenu setDelegate:self];
139    [self setMenu:contextMenu];
140
141    tabId_ = tabId;
142    extension_ = extension;
143    iconFactoryBridge_.reset(new ExtensionActionIconFactoryBridge(
144        self, browser->profile(), extension));
145
146    moveAnimation_.reset([[NSViewAnimation alloc] init]);
147    [moveAnimation_ gtm_setDuration:kAnimationDuration
148                          eventMask:NSLeftMouseUpMask];
149    [moveAnimation_ setAnimationBlockingMode:NSAnimationNonblocking];
150
151    [self updateState];
152  }
153
154  return self;
155}
156
157- (BOOL)acceptsFirstResponder {
158  return YES;
159}
160
161- (void)mouseDown:(NSEvent*)theEvent {
162  NSPoint location = [self convertPoint:[theEvent locationInWindow]
163                               fromView:nil];
164  if (NSPointInRect(location, [self bounds])) {
165    [[self cell] setHighlighted:YES];
166    dragCouldStart_ = YES;
167    dragStartPoint_ = [theEvent locationInWindow];
168  }
169}
170
171- (void)mouseDragged:(NSEvent*)theEvent {
172  if (!dragCouldStart_)
173    return;
174
175  if (!isBeingDragged_) {
176    // Don't initiate a drag until it moves at least kMinimumDragDistance.
177    NSPoint currentPoint = [theEvent locationInWindow];
178    CGFloat dx = currentPoint.x - dragStartPoint_.x;
179    CGFloat dy = currentPoint.y - dragStartPoint_.y;
180    if (dx*dx + dy*dy < kMinimumDragDistance*kMinimumDragDistance)
181      return;
182
183    // The start of a drag. Position the button above all others.
184    [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil];
185  }
186  isBeingDragged_ = YES;
187  NSRect buttonFrame = [self frame];
188  // TODO(andybons): Constrain the buttons to be within the container.
189  // Clamp the button to be within its superview along the X-axis.
190  buttonFrame.origin.x += [theEvent deltaX];
191  [self setFrame:buttonFrame];
192  [self setNeedsDisplay:YES];
193  [[NSNotificationCenter defaultCenter]
194      postNotificationName:kBrowserActionButtonDraggingNotification
195      object:self];
196}
197
198- (void)mouseUp:(NSEvent*)theEvent {
199  dragCouldStart_ = NO;
200  // There are non-drag cases where a mouseUp: may happen
201  // (e.g. mouse-down, cmd-tab to another application, move mouse,
202  // mouse-up).
203  NSPoint location = [self convertPoint:[theEvent locationInWindow]
204                               fromView:nil];
205  if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) {
206    // Only perform the click if we didn't drag the button.
207    [self performClick:self];
208  } else {
209    // Make sure an ESC to end a drag doesn't trigger 2 endDrags.
210    if (isBeingDragged_) {
211      [self endDrag];
212    } else {
213      [super mouseUp:theEvent];
214    }
215  }
216}
217
218- (void)endDrag {
219  isBeingDragged_ = NO;
220  [[NSNotificationCenter defaultCenter]
221      postNotificationName:kBrowserActionButtonDragEndNotification object:self];
222  [[self cell] setHighlighted:NO];
223}
224
225- (void)setFrame:(NSRect)frameRect animate:(BOOL)animate {
226  if (!animate) {
227    [self setFrame:frameRect];
228  } else {
229    if ([moveAnimation_ isAnimating])
230      [moveAnimation_ stopAnimation];
231
232    NSDictionary* animationDictionary =
233        [NSDictionary dictionaryWithObjectsAndKeys:
234            self, NSViewAnimationTargetKey,
235            [NSValue valueWithRect:[self frame]], NSViewAnimationStartFrameKey,
236            [NSValue valueWithRect:frameRect], NSViewAnimationEndFrameKey,
237            nil];
238    [moveAnimation_ setViewAnimations:
239        [NSArray arrayWithObject:animationDictionary]];
240    [moveAnimation_ startAnimation];
241  }
242}
243
244- (void)updateState {
245  if (tabId_ < 0)
246    return;
247
248  std::string tooltip = [[self cell] extensionAction]->GetTitle(tabId_);
249  if (tooltip.empty()) {
250    [self setToolTip:nil];
251  } else {
252    [self setToolTip:base::SysUTF8ToNSString(tooltip)];
253  }
254
255  gfx::Image image = iconFactoryBridge_->GetIcon(tabId_);
256
257  if (!image.IsEmpty())
258    [self setImage:image.ToNSImage()];
259
260  [[self cell] setTabId:tabId_];
261
262  bool enabled = [[self cell] extensionAction]->GetIsVisible(tabId_);
263  [self setEnabled:enabled];
264
265  [self setNeedsDisplay:YES];
266}
267
268- (BOOL)isAnimating {
269  return [moveAnimation_ isAnimating];
270}
271
272- (NSImage*)compositedImage {
273  NSRect bounds = [self bounds];
274  NSImage* image = [[[NSImage alloc] initWithSize:bounds.size] autorelease];
275  [image lockFocus];
276
277  [[NSColor clearColor] set];
278  NSRectFill(bounds);
279
280  NSImage* actionImage = [self image];
281  const NSSize imageSize = [actionImage size];
282  const NSRect imageRect =
283      NSMakeRect(std::floor((NSWidth(bounds) - imageSize.width) / 2.0),
284                 std::floor((NSHeight(bounds) - imageSize.height) / 2.0),
285                 imageSize.width, imageSize.height);
286  [actionImage drawInRect:imageRect
287                 fromRect:NSZeroRect
288                operation:NSCompositeSourceOver
289                 fraction:1.0
290           respectFlipped:YES
291                    hints:nil];
292
293  bounds.origin.y += kBrowserActionBadgeOriginYOffset;
294  [[self cell] drawBadgeWithinFrame:bounds];
295
296  [image unlockFocus];
297  return image;
298}
299
300- (void)menuNeedsUpdate:(NSMenu*)menu {
301  [menu removeAllItems];
302  [contextMenuController_ populateMenu:menu];
303}
304
305@end
306
307@implementation BrowserActionCell
308
309@synthesize tabId = tabId_;
310@synthesize extensionAction = extensionAction_;
311
312- (void)drawBadgeWithinFrame:(NSRect)frame {
313  gfx::CanvasSkiaPaint canvas(frame, false);
314  canvas.set_composite_alpha(true);
315  gfx::Rect boundingRect(NSRectToCGRect(frame));
316  extensionAction_->PaintBadge(&canvas, boundingRect, tabId_);
317}
318
319- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView {
320  gfx::ScopedNSGraphicsContextSaveGState scopedGState;
321  [super drawWithFrame:cellFrame inView:controlView];
322  CHECK(extensionAction_);
323  bool enabled = extensionAction_->GetIsVisible(tabId_);
324  const NSSize imageSize = self.image.size;
325  const NSRect imageRect =
326      NSMakeRect(std::floor((NSWidth(cellFrame) - imageSize.width) / 2.0),
327                 std::floor((NSHeight(cellFrame) - imageSize.height) / 2.0),
328                 imageSize.width, imageSize.height);
329  [self.image drawInRect:imageRect
330                fromRect:NSZeroRect
331               operation:NSCompositeSourceOver
332                fraction:enabled ? 1.0 : 0.4
333          respectFlipped:YES
334                   hints:nil];
335
336  cellFrame.origin.y += kBrowserActionBadgeOriginYOffset;
337  [self drawBadgeWithinFrame:cellFrame];
338}
339
340@end
341