[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / browser_action_button.mm
blobc6d78b817191603aca4fdb945c55d5cb298211aa
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"
7 #include <algorithm>
8 #include <cmath>
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 {
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(
56         this, chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED,
57         content::Source<ExtensionAction>(browser_action_));
58   }
60   virtual ~ExtensionActionIconFactoryBridge() {}
62   // ExtensionActionIconFactory::Observer implementation.
63   virtual void OnIconUpdated() OVERRIDE {
64     [owner_ updateState];
65   }
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 == chrome::NOTIFICATION_EXTENSION_BROWSER_ACTION_UPDATED)
73       [owner_ updateState];
74     else
75       NOTREACHED();
76   }
78   gfx::Image GetIcon(int tabId) {
79     return icon_factory_.GetIcon(tabId);
80   }
82  private:
83   // Weak. Owns us.
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;
103 @end
105 @interface BrowserActionButton (Private)
106 - (void)endDrag;
107 @end
109 @implementation BrowserActionButton
111 @synthesize isBeingDragged = isBeingDragged_;
112 @synthesize extension = extension_;
113 @synthesize tabId = tabId_;
115 + (Class)cellClass {
116   return [BrowserActionCell class];
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];
150     [self setTitle:@""];
151     [self setButtonType:NSMomentaryChangeButton];
152     [self setShowsBorderOnlyWhileMouseInside:YES];
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];
163     tabId_ = tabId;
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];
173     [self updateState];
174   }
176   return self;
179 - (BOOL)acceptsFirstResponder {
180   return YES;
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_)
191     return;
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)
199       return;
201     // The start of a drag. Position the button above all others.
202     [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil];
203   }
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
213       object:self];
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,
220   // mouse-up).
221   NSPoint location = [self convertPoint:[theEvent locationInWindow]
222                                fromView:nil];
223   if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) {
224     // Only perform the click if we didn't drag the button.
225     [self performClick:self];
226   } else {
227     // Make sure an ESC to end a drag doesn't trigger 2 endDrags.
228     if (isBeingDragged_) {
229       [self endDrag];
230     } else {
231       [super mouseUp:theEvent];
232     }
233   }
236 - (void)endDrag {
237   isBeingDragged_ = NO;
238   [[NSNotificationCenter defaultCenter]
239       postNotificationName:kBrowserActionButtonDragEndNotification object:self];
240   [[self cell] setHighlighted:NO];
243 - (void)setFrame:(NSRect)frameRect animate:(BOOL)animate {
244   if (!animate) {
245     [self setFrame:frameRect];
246   } else {
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,
255             nil];
256     [moveAnimation_ setViewAnimations:
257         [NSArray arrayWithObject:animationDictionary]];
258     [moveAnimation_ startAnimation];
259   }
262 - (void)updateState {
263   if (tabId_ < 0)
264     return;
266   std::string tooltip = [[self cell] extensionAction]->GetTitle(tabId_);
267   if (tooltip.empty()) {
268     [self setToolTip:nil];
269   } else {
270     [self setToolTip:base::SysUTF8ToNSString(tooltip)];
271   }
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];
293   [image lockFocus];
295   [[NSColor clearColor] set];
296   NSRectFill(bounds);
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
305                  fromRect:NSZeroRect
306                 operation:NSCompositeSourceOver
307                  fraction:1.0
308            respectFlipped:YES
309                     hints:nil];
311   bounds.origin.y += kBrowserActionBadgeOriginYOffset;
312   [[self cell] drawBadgeWithinFrame:bounds];
314   [image unlockFocus];
315   return image;
318 - (void)menuNeedsUpdate:(NSMenu*)menu {
319   [menu removeAllItems];
320   [contextMenuController_ populateMenu:menu];
323 @end
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
348                 fromRect:NSZeroRect
349                operation:NSCompositeSourceOver
350                 fraction:enabled ? 1.0 : 0.4
351           respectFlipped:YES
352                    hints:nil];
354   cellFrame.origin.y += kBrowserActionBadgeOriginYOffset;
355   [self drawBadgeWithinFrame:cellFrame];
358 @end