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