| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "chrome/browser/ui/cocoa/extensions/browser_action_button.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| |
| #include "base/logging.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "chrome/browser/extensions/extension_action.h" |
| #include "chrome/browser/extensions/extension_action_icon_factory.h" |
| #include "chrome/browser/extensions/extension_action_manager.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/ui/browser.h" |
| #include "chrome/browser/ui/cocoa/extensions/extension_action_context_menu_controller.h" |
| #include "extensions/common/extension.h" |
| #include "grit/theme_resources.h" |
| #include "skia/ext/skia_utils_mac.h" |
| #import "third_party/google_toolbox_for_mac/src/AppKit/GTMNSAnimation+Duration.h" |
| #include "ui/gfx/canvas_skia_paint.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/rect.h" |
| #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h" |
| #include "ui/gfx/size.h" |
| |
| using extensions::Extension; |
| |
| NSString* const kBrowserActionButtonDraggingNotification = |
| @"BrowserActionButtonDraggingNotification"; |
| NSString* const kBrowserActionButtonDragEndNotification = |
| @"BrowserActionButtonDragEndNotification"; |
| |
| static const CGFloat kBrowserActionBadgeOriginYOffset = 5; |
| static const CGFloat kAnimationDuration = 0.2; |
| static const CGFloat kMinimumDragDistance = 5; |
| |
| // A helper class to bridge the asynchronous Skia bitmap loading mechanism to |
| // the extension's button. |
| class ExtensionActionIconFactoryBridge |
| : public ExtensionActionIconFactory::Observer { |
| public: |
| ExtensionActionIconFactoryBridge(BrowserActionButton* owner, |
| Profile* profile, |
| const Extension* extension) |
| : owner_(owner), |
| browser_action_([[owner cell] extensionAction]), |
| icon_factory_(profile, extension, browser_action_, this) { |
| } |
| |
| ~ExtensionActionIconFactoryBridge() override {} |
| |
| // ExtensionActionIconFactory::Observer implementation. |
| void OnIconUpdated() override { [owner_ updateState]; } |
| |
| gfx::Image GetIcon(int tabId) { |
| return icon_factory_.GetIcon(tabId); |
| } |
| |
| private: |
| // Weak. Owns us. |
| BrowserActionButton* owner_; |
| |
| // The browser action whose images we're loading. |
| ExtensionAction* const browser_action_; |
| |
| // The object that will be used to get the browser action icon for us. |
| // It may load the icon asynchronously (in which case the initial icon |
| // returned by the factory will be transparent), so we have to observe it for |
| // updates to the icon. |
| ExtensionActionIconFactory icon_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ExtensionActionIconFactoryBridge); |
| }; |
| |
| @interface BrowserActionCell (Internals) |
| - (void)drawBadgeWithinFrame:(NSRect)frame; |
| @end |
| |
| @interface BrowserActionButton (Private) |
| - (void)endDrag; |
| @end |
| |
| @implementation BrowserActionButton |
| |
| @synthesize isBeingDragged = isBeingDragged_; |
| @synthesize extension = extension_; |
| @synthesize tabId = tabId_; |
| |
| + (Class)cellClass { |
| return [BrowserActionCell class]; |
| } |
| |
| - (id)initWithFrame:(NSRect)frame |
| extension:(const Extension*)extension |
| browser:(Browser*)browser |
| tabId:(int)tabId { |
| if ((self = [super initWithFrame:frame])) { |
| BrowserActionCell* cell = [[[BrowserActionCell alloc] init] autorelease]; |
| // [NSButton setCell:] warns to NOT use setCell: other than in the |
| // initializer of a control. However, we are using a basic |
| // NSButton whose initializer does not take an NSCell as an |
| // object. To honor the assumed semantics, we do nothing with |
| // NSButton between alloc/init and setCell:. |
| [self setCell:cell]; |
| [cell setTabId:tabId]; |
| ExtensionAction* browser_action = |
| extensions::ExtensionActionManager::Get(browser->profile())-> |
| GetExtensionAction(*extension); |
| CHECK(browser_action) |
| << "Don't create a BrowserActionButton if there is no browser action."; |
| [cell setExtensionAction:browser_action]; |
| [cell |
| accessibilitySetOverrideValue:base::SysUTF8ToNSString(extension->name()) |
| forAttribute:NSAccessibilityDescriptionAttribute]; |
| [cell setImageID:IDR_BROWSER_ACTION |
| forButtonState:image_button_cell::kDefaultState]; |
| [cell setImageID:IDR_BROWSER_ACTION_H |
| forButtonState:image_button_cell::kHoverState]; |
| [cell setImageID:IDR_BROWSER_ACTION_P |
| forButtonState:image_button_cell::kPressedState]; |
| [cell setImageID:IDR_BROWSER_ACTION |
| forButtonState:image_button_cell::kDisabledState]; |
| |
| [self setTitle:@""]; |
| [self setButtonType:NSMomentaryChangeButton]; |
| [self setShowsBorderOnlyWhileMouseInside:YES]; |
| |
| contextMenuController_.reset([[ExtensionActionContextMenuController alloc] |
| initWithExtension:extension |
| browser:browser |
| extensionAction:browser_action]); |
| base::scoped_nsobject<NSMenu> contextMenu( |
| [[NSMenu alloc] initWithTitle:@""]); |
| [contextMenu setDelegate:self]; |
| [self setMenu:contextMenu]; |
| |
| tabId_ = tabId; |
| extension_ = extension; |
| iconFactoryBridge_.reset(new ExtensionActionIconFactoryBridge( |
| self, browser->profile(), extension)); |
| |
| moveAnimation_.reset([[NSViewAnimation alloc] init]); |
| [moveAnimation_ gtm_setDuration:kAnimationDuration |
| eventMask:NSLeftMouseUpMask]; |
| [moveAnimation_ setAnimationBlockingMode:NSAnimationNonblocking]; |
| |
| [self updateState]; |
| } |
| |
| return self; |
| } |
| |
| - (BOOL)acceptsFirstResponder { |
| return YES; |
| } |
| |
| - (void)mouseDown:(NSEvent*)theEvent { |
| NSPoint location = [self convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| if (NSPointInRect(location, [self bounds])) { |
| [[self cell] setHighlighted:YES]; |
| dragCouldStart_ = YES; |
| dragStartPoint_ = [theEvent locationInWindow]; |
| } |
| } |
| |
| - (void)mouseDragged:(NSEvent*)theEvent { |
| if (!dragCouldStart_) |
| return; |
| |
| if (!isBeingDragged_) { |
| // Don't initiate a drag until it moves at least kMinimumDragDistance. |
| NSPoint currentPoint = [theEvent locationInWindow]; |
| CGFloat dx = currentPoint.x - dragStartPoint_.x; |
| CGFloat dy = currentPoint.y - dragStartPoint_.y; |
| if (dx*dx + dy*dy < kMinimumDragDistance*kMinimumDragDistance) |
| return; |
| |
| // The start of a drag. Position the button above all others. |
| [[self superview] addSubview:self positioned:NSWindowAbove relativeTo:nil]; |
| } |
| isBeingDragged_ = YES; |
| NSRect buttonFrame = [self frame]; |
| // TODO(andybons): Constrain the buttons to be within the container. |
| // Clamp the button to be within its superview along the X-axis. |
| buttonFrame.origin.x += [theEvent deltaX]; |
| [self setFrame:buttonFrame]; |
| [self setNeedsDisplay:YES]; |
| [[NSNotificationCenter defaultCenter] |
| postNotificationName:kBrowserActionButtonDraggingNotification |
| object:self]; |
| } |
| |
| - (void)mouseUp:(NSEvent*)theEvent { |
| dragCouldStart_ = NO; |
| // There are non-drag cases where a mouseUp: may happen |
| // (e.g. mouse-down, cmd-tab to another application, move mouse, |
| // mouse-up). |
| NSPoint location = [self convertPoint:[theEvent locationInWindow] |
| fromView:nil]; |
| if (NSPointInRect(location, [self bounds]) && !isBeingDragged_) { |
| // Only perform the click if we didn't drag the button. |
| [self performClick:self]; |
| } else { |
| // Make sure an ESC to end a drag doesn't trigger 2 endDrags. |
| if (isBeingDragged_) { |
| [self endDrag]; |
| } else { |
| [super mouseUp:theEvent]; |
| } |
| } |
| } |
| |
| - (void)endDrag { |
| isBeingDragged_ = NO; |
| [[NSNotificationCenter defaultCenter] |
| postNotificationName:kBrowserActionButtonDragEndNotification object:self]; |
| [[self cell] setHighlighted:NO]; |
| } |
| |
| - (void)setFrame:(NSRect)frameRect animate:(BOOL)animate { |
| if (!animate) { |
| [self setFrame:frameRect]; |
| } else { |
| if ([moveAnimation_ isAnimating]) |
| [moveAnimation_ stopAnimation]; |
| |
| NSDictionary* animationDictionary = |
| [NSDictionary dictionaryWithObjectsAndKeys: |
| self, NSViewAnimationTargetKey, |
| [NSValue valueWithRect:[self frame]], NSViewAnimationStartFrameKey, |
| [NSValue valueWithRect:frameRect], NSViewAnimationEndFrameKey, |
| nil]; |
| [moveAnimation_ setViewAnimations: |
| [NSArray arrayWithObject:animationDictionary]]; |
| [moveAnimation_ startAnimation]; |
| } |
| } |
| |
| - (void)updateState { |
| if (tabId_ < 0) |
| return; |
| |
| std::string tooltip = [[self cell] extensionAction]->GetTitle(tabId_); |
| if (tooltip.empty()) { |
| [self setToolTip:nil]; |
| } else { |
| [self setToolTip:base::SysUTF8ToNSString(tooltip)]; |
| } |
| |
| gfx::Image image = iconFactoryBridge_->GetIcon(tabId_); |
| |
| if (!image.IsEmpty()) |
| [self setImage:image.ToNSImage()]; |
| |
| [[self cell] setTabId:tabId_]; |
| |
| bool enabled = [[self cell] extensionAction]->GetIsVisible(tabId_); |
| [self setEnabled:enabled]; |
| |
| [self setNeedsDisplay:YES]; |
| } |
| |
| - (BOOL)isAnimating { |
| return [moveAnimation_ isAnimating]; |
| } |
| |
| - (NSImage*)compositedImage { |
| NSRect bounds = [self bounds]; |
| NSImage* image = [[[NSImage alloc] initWithSize:bounds.size] autorelease]; |
| [image lockFocus]; |
| |
| [[NSColor clearColor] set]; |
| NSRectFill(bounds); |
| |
| NSImage* actionImage = [self image]; |
| const NSSize imageSize = [actionImage size]; |
| const NSRect imageRect = |
| NSMakeRect(std::floor((NSWidth(bounds) - imageSize.width) / 2.0), |
| std::floor((NSHeight(bounds) - imageSize.height) / 2.0), |
| imageSize.width, imageSize.height); |
| [actionImage drawInRect:imageRect |
| fromRect:NSZeroRect |
| operation:NSCompositeSourceOver |
| fraction:1.0 |
| respectFlipped:YES |
| hints:nil]; |
| |
| bounds.origin.y += kBrowserActionBadgeOriginYOffset; |
| [[self cell] drawBadgeWithinFrame:bounds]; |
| |
| [image unlockFocus]; |
| return image; |
| } |
| |
| - (void)menuNeedsUpdate:(NSMenu*)menu { |
| [menu removeAllItems]; |
| [contextMenuController_ populateMenu:menu]; |
| } |
| |
| @end |
| |
| @implementation BrowserActionCell |
| |
| @synthesize tabId = tabId_; |
| @synthesize extensionAction = extensionAction_; |
| |
| - (void)drawBadgeWithinFrame:(NSRect)frame { |
| gfx::CanvasSkiaPaint canvas(frame, false); |
| canvas.set_composite_alpha(true); |
| gfx::Rect boundingRect(NSRectToCGRect(frame)); |
| extensionAction_->PaintBadge(&canvas, boundingRect, tabId_); |
| } |
| |
| - (void)drawWithFrame:(NSRect)cellFrame inView:(NSView*)controlView { |
| gfx::ScopedNSGraphicsContextSaveGState scopedGState; |
| [super drawWithFrame:cellFrame inView:controlView]; |
| CHECK(extensionAction_); |
| bool enabled = extensionAction_->GetIsVisible(tabId_); |
| const NSSize imageSize = self.image.size; |
| const NSRect imageRect = |
| NSMakeRect(std::floor((NSWidth(cellFrame) - imageSize.width) / 2.0), |
| std::floor((NSHeight(cellFrame) - imageSize.height) / 2.0), |
| imageSize.width, imageSize.height); |
| [self.image drawInRect:imageRect |
| fromRect:NSZeroRect |
| operation:NSCompositeSourceOver |
| fraction:enabled ? 1.0 : 0.4 |
| respectFlipped:YES |
| hints:nil]; |
| |
| cellFrame.origin.y += kBrowserActionBadgeOriginYOffset; |
| [self drawBadgeWithinFrame:cellFrame]; |
| } |
| |
| @end |