Home | History | Annotate | Download | only in extensions
      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