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