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