1 // Copyright (c) 2011 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/sys_string_conversions.h" 12 #include "chrome/browser/extensions/image_loading_tracker.h" 13 #include "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h" 14 #import "chrome/browser/ui/cocoa/image_utils.h" 15 #include "chrome/common/extensions/extension.h" 16 #include "chrome/common/extensions/extension_action.h" 17 #include "chrome/common/extensions/extension_resource.h" 18 #include "content/common/notification_observer.h" 19 #include "content/common/notification_registrar.h" 20 #include "content/common/notification_source.h" 21 #include "content/common/notification_type.h" 22 #include "skia/ext/skia_utils_mac.h" 23 #import "third_party/GTM/AppKit/GTMNSAnimation+Duration.h" 24 #include "ui/gfx/canvas_skia_paint.h" 25 #include "ui/gfx/rect.h" 26 #include "ui/gfx/size.h" 27 28 NSString* const kBrowserActionButtonUpdatedNotification = 29 @"BrowserActionButtonUpdatedNotification"; 30 31 NSString* const kBrowserActionButtonDraggingNotification = 32 @"BrowserActionButtonDraggingNotification"; 33 NSString* const kBrowserActionButtonDragEndNotification = 34 @"BrowserActionButtonDragEndNotification"; 35 36 static const CGFloat kBrowserActionBadgeOriginYOffset = 5; 37 38 namespace { 39 const CGFloat kAnimationDuration = 0.2; 40 } // anonymous namespace 41 42 // A helper class to bridge the asynchronous Skia bitmap loading mechanism to 43 // the extension's button. 44 class ExtensionImageTrackerBridge : public NotificationObserver, 45 public ImageLoadingTracker::Observer { 46 public: 47 ExtensionImageTrackerBridge(BrowserActionButton* owner, 48 const Extension* extension) 49 : owner_(owner), 50 tracker_(this) { 51 // The Browser Action API does not allow the default icon path to be 52 // changed at runtime, so we can load this now and cache it. 53 std::string path = extension->browser_action()->default_icon_path(); 54 if (!path.empty()) { 55 tracker_.LoadImage(extension, extension->GetResource(path), 56 gfx::Size(Extension::kBrowserActionIconMaxSize, 57 Extension::kBrowserActionIconMaxSize), 58 ImageLoadingTracker::DONT_CACHE); 59 } 60 registrar_.Add(this, NotificationType::EXTENSION_BROWSER_ACTION_UPDATED, 61 Source<ExtensionAction>(extension->browser_action())); 62 } 63 64 ~ExtensionImageTrackerBridge() {} 65 66 // ImageLoadingTracker::Observer implementation. 67 void OnImageLoaded(SkBitmap* image, const ExtensionResource& resource, 68 int index) { 69 if (image) 70 [owner_ setDefaultIcon:gfx::SkBitmapToNSImage(*image)]; 71 [owner_ updateState]; 72 } 73 74 // Overridden from NotificationObserver. 75 void Observe(NotificationType type, 76 const NotificationSource& source, 77 const NotificationDetails& details) { 78 if (type == NotificationType::EXTENSION_BROWSER_ACTION_UPDATED) 79 [owner_ updateState]; 80 else 81 NOTREACHED(); 82 } 83 84 private: 85 // Weak. Owns us. 86 BrowserActionButton* owner_; 87 88 // Loads the button's icons for us on the file thread. 89 ImageLoadingTracker tracker_; 90 91 // Used for registering to receive notifications and automatic clean up. 92 NotificationRegistrar registrar_; 93 94 DISALLOW_COPY_AND_ASSIGN(ExtensionImageTrackerBridge); 95 }; 96 97 @interface BrowserActionCell (Internals) 98 - (void)drawBadgeWithinFrame:(NSRect)frame; 99 @end 100 101 @interface BrowserActionButton (Private) 102 - (void)endDrag; 103 @end 104 105 @implementation BrowserActionButton 106 107 @synthesize isBeingDragged = isBeingDragged_; 108 @synthesize extension = extension_; 109 @synthesize tabId = tabId_; 110 111 + (Class)cellClass { 112 return [BrowserActionCell class]; 113 } 114 115 - (id)initWithFrame:(NSRect)frame 116 extension:(const Extension*)extension 117 profile:(Profile*)profile 118 tabId:(int)tabId { 119 if ((self = [super initWithFrame:frame])) { 120 BrowserActionCell* cell = [[[BrowserActionCell alloc] init] autorelease]; 121 // [NSButton setCell:] warns to NOT use setCell: other than in the 122 // initializer of a control. However, we are using a basic 123 // NSButton whose initializer does not take an NSCell as an 124 // object. To honor the assumed semantics, we do nothing with 125 // NSButton between alloc/init and setCell:. 126 [self setCell:cell]; 127 [cell setTabId:tabId]; 128 [cell setExtensionAction:extension->browser_action()]; 129 130 [self setTitle:@""]; 131 [self setButtonType:NSMomentaryChangeButton]; 132 [self setShowsBorderOnlyWhileMouseInside:YES]; 133 134 [self setMenu:[[[ExtensionActionContextMenu alloc] 135 initWithExtension:extension 136 profile:profile 137 extensionAction:extension->browser_action()] autorelease]]; 138 139 tabId_ = tabId; 140 extension_ = extension; 141 imageLoadingBridge_.reset(new ExtensionImageTrackerBridge(self, extension)); 142 143 moveAnimation_.reset([[NSViewAnimation alloc] init]); 144 [moveAnimation_ gtm_setDuration:kAnimationDuration 145 eventMask:NSLeftMouseUpMask]; 146 [moveAnimation_ setAnimationBlockingMode:NSAnimationNonblocking]; 147 148 [self updateState]; 149 } 150 151 return self; 152 } 153 154 - (BOOL)acceptsFirstResponder { 155 return YES; 156 } 157 158 - (void)mouseDown:(NSEvent*)theEvent { 159 [[self cell] setHighlighted:YES]; 160 dragCouldStart_ = YES; 161 } 162 163 - (void)mouseDragged:(NSEvent*)theEvent { 164 if (!dragCouldStart_) 165 return; 166 167 if (!isBeingDragged_) { 168 // The start of a drag. Position the button above all others. 169 [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil]; 170 } 171 isBeingDragged_ = YES; 172 NSRect buttonFrame = [self frame]; 173 // TODO(andybons): Constrain the buttons to be within the container. 174 // Clamp the button to be within its superview along the X-axis. 175 buttonFrame.origin.x += [theEvent deltaX]; 176 [self setFrame:buttonFrame]; 177 [self setNeedsDisplay:YES]; 178 [[NSNotificationCenter defaultCenter] 179 postNotificationName:kBrowserActionButtonDraggingNotification 180 object:self]; 181 } 182 183 - (void)mouseUp:(NSEvent*)theEvent { 184 dragCouldStart_ = NO; 185 // There are non-drag cases where a mouseUp: may happen 186 // (e.g. mouse-down, cmd-tab to another application, move mouse, 187 // mouse-up). 188 NSPoint location = [self convertPoint:[theEvent locationInWindow] 189 fromView:nil]; 190 if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) { 191 // Only perform the click if we didn't drag the button. 192 [self performClick:self]; 193 } else { 194 // Make sure an ESC to end a drag doesn't trigger 2 endDrags. 195 if (isBeingDragged_) { 196 [self endDrag]; 197 } else { 198 [super mouseUp:theEvent]; 199 } 200 } 201 } 202 203 - (void)endDrag { 204 isBeingDragged_ = NO; 205 [[NSNotificationCenter defaultCenter] 206 postNotificationName:kBrowserActionButtonDragEndNotification 207 object:self]; 208 [[self cell] setHighlighted:NO]; 209 } 210 211 - (void)setFrame:(NSRect)frameRect animate:(BOOL)animate { 212 if (!animate) { 213 [self setFrame:frameRect]; 214 } else { 215 if ([moveAnimation_ isAnimating]) 216 [moveAnimation_ stopAnimation]; 217 218 NSDictionary* animationDictionary = 219 [NSDictionary dictionaryWithObjectsAndKeys: 220 self, NSViewAnimationTargetKey, 221 [NSValue valueWithRect:[self frame]], NSViewAnimationStartFrameKey, 222 [NSValue valueWithRect:frameRect], NSViewAnimationEndFrameKey, 223 nil]; 224 [moveAnimation_ setViewAnimations: 225 [NSArray arrayWithObject:animationDictionary]]; 226 [moveAnimation_ startAnimation]; 227 } 228 } 229 230 - (void)setDefaultIcon:(NSImage*)image { 231 defaultIcon_.reset([image retain]); 232 } 233 234 - (void)setTabSpecificIcon:(NSImage*)image { 235 tabSpecificIcon_.reset([image retain]); 236 } 237 238 - (void)updateState { 239 if (tabId_ < 0) 240 return; 241 242 std::string tooltip = extension_->browser_action()->GetTitle(tabId_); 243 if (tooltip.empty()) { 244 [self setToolTip:nil]; 245 } else { 246 [self setToolTip:base::SysUTF8ToNSString(tooltip)]; 247 } 248 249 SkBitmap image = extension_->browser_action()->GetIcon(tabId_); 250 if (!image.isNull()) { 251 [self setTabSpecificIcon:gfx::SkBitmapToNSImage(image)]; 252 [self setImage:tabSpecificIcon_]; 253 } else if (defaultIcon_) { 254 [self setImage:defaultIcon_]; 255 } 256 257 [[self cell] setTabId:tabId_]; 258 259 [self setNeedsDisplay:YES]; 260 261 [[NSNotificationCenter defaultCenter] 262 postNotificationName:kBrowserActionButtonUpdatedNotification 263 object:self]; 264 } 265 266 - (BOOL)isAnimating { 267 return [moveAnimation_ isAnimating]; 268 } 269 270 - (NSImage*)compositedImage { 271 NSRect bounds = [self bounds]; 272 NSImage* image = [[[NSImage alloc] initWithSize:bounds.size] autorelease]; 273 [image lockFocus]; 274 275 [[NSColor clearColor] set]; 276 NSRectFill(bounds); 277 278 NSImage* actionImage = [self image]; 279 const NSSize imageSize = [actionImage size]; 280 const NSRect imageRect = 281 NSMakeRect(std::floor((NSWidth(bounds) - imageSize.width) / 2.0), 282 std::floor((NSHeight(bounds) - imageSize.height) / 2.0), 283 imageSize.width, imageSize.height); 284 [actionImage drawInRect:imageRect 285 fromRect:NSZeroRect 286 operation:NSCompositeSourceOver 287 fraction:1.0 288 neverFlipped:YES]; 289 290 bounds.origin.y += kBrowserActionBadgeOriginYOffset; 291 [[self cell] drawBadgeWithinFrame:bounds]; 292 293 [image unlockFocus]; 294 return image; 295 } 296 297 @end 298 299 @implementation BrowserActionCell 300 301 @synthesize tabId = tabId_; 302 @synthesize extensionAction = extensionAction_; 303 304 - (void)drawBadgeWithinFrame:(NSRect)frame { 305 gfx::CanvasSkiaPaint canvas(frame, false); 306 canvas.set_composite_alpha(true); 307 gfx::Rect boundingRect(NSRectToCGRect(frame)); 308 extensionAction_->PaintBadge(&canvas, boundingRect, tabId_); 309 } 310 311 - (void)drawInteriorWithFrame:(NSRect)cellFrame inView:(NSView*)controlView { 312 [NSGraphicsContext saveGraphicsState]; 313 [super drawInteriorWithFrame:cellFrame inView:controlView]; 314 cellFrame.origin.y += kBrowserActionBadgeOriginYOffset; 315 [self drawBadgeWithinFrame:cellFrame]; 316 [NSGraphicsContext restoreGraphicsState]; 317 } 318 319 @end 320