Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / browser_action_button.mm
blob6879d92b07715b6dd6afb073c881072f8904aaab
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     [cell setExtensionAction:browser_action];
136     [cell
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];
148     [self setTitle:@""];
149     [self setButtonType:NSMomentaryChangeButton];
150     [self setShowsBorderOnlyWhileMouseInside:YES];
152     contextMenuController_.reset([[ExtensionActionContextMenuController alloc]
153         initWithExtension:extension
154                   browser:browser
155           extensionAction:browser_action]);
156     base::scoped_nsobject<NSMenu> contextMenu(
157         [[NSMenu alloc] initWithTitle:@""]);
158     [contextMenu setDelegate:self];
159     [self setMenu:contextMenu];
161     tabId_ = tabId;
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];
171     [self updateState];
172   }
174   return self;
177 - (BOOL)acceptsFirstResponder {
178   return YES;
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_)
189     return;
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)
197       return;
199     // The start of a drag. Position the button above all others.
200     [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil];
201   }
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
211       object:self];
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,
218   // mouse-up).
219   NSPoint location = [self convertPoint:[theEvent locationInWindow]
220                                fromView:nil];
221   if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) {
222     // Only perform the click if we didn't drag the button.
223     [self performClick:self];
224   } else {
225     // Make sure an ESC to end a drag doesn't trigger 2 endDrags.
226     if (isBeingDragged_) {
227       [self endDrag];
228     } else {
229       [super mouseUp:theEvent];
230     }
231   }
234 - (void)endDrag {
235   isBeingDragged_ = NO;
236   [[NSNotificationCenter defaultCenter]
237       postNotificationName:kBrowserActionButtonDragEndNotification object:self];
238   [[self cell] setHighlighted:NO];
241 - (void)setFrame:(NSRect)frameRect animate:(BOOL)animate {
242   if (!animate) {
243     [self setFrame:frameRect];
244   } else {
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,
253             nil];
254     [moveAnimation_ setViewAnimations:
255         [NSArray arrayWithObject:animationDictionary]];
256     [moveAnimation_ startAnimation];
257   }
260 - (void)updateState {
261   if (tabId_ < 0)
262     return;
264   std::string tooltip = [[self cell] extensionAction]->GetTitle(tabId_);
265   if (tooltip.empty()) {
266     [self setToolTip:nil];
267   } else {
268     [self setToolTip:base::SysUTF8ToNSString(tooltip)];
269   }
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];
291   [image lockFocus];
293   [[NSColor clearColor] set];
294   NSRectFill(bounds);
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
303                  fromRect:NSZeroRect
304                 operation:NSCompositeSourceOver
305                  fraction:1.0
306            respectFlipped:YES
307                     hints:nil];
309   bounds.origin.y += kBrowserActionBadgeOriginYOffset;
310   [[self cell] drawBadgeWithinFrame:bounds];
312   [image unlockFocus];
313   return image;
316 - (void)menuNeedsUpdate:(NSMenu*)menu {
317   [menu removeAllItems];
318   [contextMenuController_ populateMenu:menu];
321 @end
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
345                 fromRect:NSZeroRect
346                operation:NSCompositeSourceOver
347                 fraction:enabled ? 1.0 : 0.4
348           respectFlipped:YES
349                    hints:nil];
351   cellFrame.origin.y += kBrowserActionBadgeOriginYOffset;
352   [self drawBadgeWithinFrame:cellFrame];
355 @end