Home | History | Annotate | Download | only in extensions
      1 // Copyright (c) 2010 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/extension_infobar_controller.h"
      6 
      7 #include <cmath>
      8 
      9 #include "chrome/browser/extensions/extension_host.h"
     10 #include "chrome/browser/extensions/extension_infobar_delegate.h"
     11 #include "chrome/browser/extensions/image_loading_tracker.h"
     12 #import "chrome/browser/ui/cocoa/animatable_view.h"
     13 #import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu.h"
     14 #import "chrome/browser/ui/cocoa/menu_button.h"
     15 #include "chrome/browser/ui/cocoa/infobars/infobar.h"
     16 #include "chrome/common/extensions/extension.h"
     17 #include "chrome/common/extensions/extension_icon_set.h"
     18 #include "chrome/common/extensions/extension_resource.h"
     19 #include "content/browser/tab_contents/tab_contents.h"
     20 #include "grit/theme_resources.h"
     21 #include "skia/ext/skia_utils_mac.h"
     22 #include "ui/base/resource/resource_bundle.h"
     23 #include "ui/gfx/canvas_skia.h"
     24 
     25 namespace {
     26 const CGFloat kAnimationDuration = 0.12;
     27 const CGFloat kBottomBorderHeightPx = 1.0;
     28 const CGFloat kButtonHeightPx = 26.0;
     29 const CGFloat kButtonLeftMarginPx = 2.0;
     30 const CGFloat kButtonWidthPx = 34.0;
     31 const CGFloat kDropArrowLeftMarginPx = 3.0;
     32 const CGFloat kToolbarMinHeightPx = 36.0;
     33 const CGFloat kToolbarMaxHeightPx = 72.0;
     34 }  // namespace
     35 
     36 @interface ExtensionInfoBarController(Private)
     37 // Called when the extension's hosted NSView has been resized.
     38 - (void)extensionViewFrameChanged;
     39 // Returns the clamped height of the extension view to be within the min and max
     40 // values defined above.
     41 - (CGFloat)clampedExtensionViewHeight;
     42 // Adjusts the width of the extension's hosted view to match the window's width
     43 // and sets the proper height for it as well.
     44 - (void)adjustExtensionViewSize;
     45 // Sets the image to be used in the button on the left side of the infobar.
     46 - (void)setButtonImage:(NSImage*)image;
     47 @end
     48 
     49 // A helper class to bridge the asynchronous Skia bitmap loading mechanism to
     50 // the extension's button.
     51 class InfobarBridge : public ExtensionInfoBarDelegate::DelegateObserver,
     52                       public ImageLoadingTracker::Observer {
     53  public:
     54   explicit InfobarBridge(ExtensionInfoBarController* owner)
     55       : owner_(owner),
     56         delegate_([owner delegate]->AsExtensionInfoBarDelegate()),
     57         tracker_(this) {
     58     delegate_->set_observer(this);
     59     LoadIcon();
     60   }
     61 
     62   virtual ~InfobarBridge() {
     63     if (delegate_)
     64       delegate_->set_observer(NULL);
     65   }
     66 
     67   // Load the Extension's icon image.
     68   void LoadIcon() {
     69     const Extension* extension = delegate_->extension_host()->extension();
     70     ExtensionResource icon_resource = extension->GetIconResource(
     71         Extension::EXTENSION_ICON_BITTY, ExtensionIconSet::MATCH_EXACTLY);
     72     if (!icon_resource.relative_path().empty()) {
     73       tracker_.LoadImage(extension, icon_resource,
     74                          gfx::Size(Extension::EXTENSION_ICON_BITTY,
     75                                    Extension::EXTENSION_ICON_BITTY),
     76                          ImageLoadingTracker::DONT_CACHE);
     77     } else {
     78       OnImageLoaded(NULL, icon_resource, 0);
     79     }
     80   }
     81 
     82   // ImageLoadingTracker::Observer implementation.
     83   // TODO(andybons): The infobar view implementations share a lot of the same
     84   // code. Come up with a strategy to share amongst them.
     85   virtual void OnImageLoaded(
     86       SkBitmap* image, const ExtensionResource& resource, int index) {
     87     if (!delegate_)
     88       return;  // The delegate can go away while the image asynchronously loads.
     89 
     90     ResourceBundle& rb = ResourceBundle::GetSharedInstance();
     91 
     92     // Fall back on the default extension icon on failure.
     93     SkBitmap* icon;
     94     if (!image || image->empty())
     95       icon = rb.GetBitmapNamed(IDR_EXTENSIONS_SECTION);
     96     else
     97       icon = image;
     98 
     99     SkBitmap* drop_image = rb.GetBitmapNamed(IDR_APP_DROPARROW);
    100 
    101     const int image_size = Extension::EXTENSION_ICON_BITTY;
    102     scoped_ptr<gfx::CanvasSkia> canvas(
    103         new gfx::CanvasSkia(
    104             image_size + kDropArrowLeftMarginPx + drop_image->width(),
    105             image_size, false));
    106     canvas->DrawBitmapInt(*icon,
    107                           0, 0, icon->width(), icon->height(),
    108                           0, 0, image_size, image_size,
    109                           false);
    110     canvas->DrawBitmapInt(*drop_image,
    111                           image_size + kDropArrowLeftMarginPx,
    112                           image_size / 2);
    113     [owner_ setButtonImage:gfx::SkBitmapToNSImage(canvas->ExtractBitmap())];
    114   }
    115 
    116   // Overridden from ExtensionInfoBarDelegate::DelegateObserver:
    117   virtual void OnDelegateDeleted() {
    118     delegate_ = NULL;
    119   }
    120 
    121  private:
    122   // Weak. Owns us.
    123   ExtensionInfoBarController* owner_;
    124 
    125   // Weak.
    126   ExtensionInfoBarDelegate* delegate_;
    127 
    128   // Loads the extensions's icon on the file thread.
    129   ImageLoadingTracker tracker_;
    130 
    131   DISALLOW_COPY_AND_ASSIGN(InfobarBridge);
    132 };
    133 
    134 
    135 @implementation ExtensionInfoBarController
    136 
    137 - (id)initWithDelegate:(InfoBarDelegate*)delegate
    138                 window:(NSWindow*)window {
    139   if ((self = [super initWithDelegate:delegate])) {
    140     window_ = window;
    141     dropdownButton_.reset([[MenuButton alloc] init]);
    142     [dropdownButton_ setOpenMenuOnClick:YES];
    143 
    144     ExtensionHost* extensionHost = delegate_->AsExtensionInfoBarDelegate()->
    145         extension_host();
    146     contextMenu_.reset([[ExtensionActionContextMenu alloc]
    147         initWithExtension:extensionHost->extension()
    148                   profile:extensionHost->profile()
    149           extensionAction:NULL]);
    150     // See menu_button.h for documentation on why this is needed.
    151     NSMenuItem* dummyItem =
    152         [[[NSMenuItem alloc] initWithTitle:@""
    153                                     action:nil
    154                              keyEquivalent:@""] autorelease];
    155     [contextMenu_ insertItem:dummyItem atIndex:0];
    156     [dropdownButton_ setAttachedMenu:contextMenu_.get()];
    157 
    158     bridge_.reset(new InfobarBridge(self));
    159   }
    160   return self;
    161 }
    162 
    163 - (void)dealloc {
    164   [[NSNotificationCenter defaultCenter] removeObserver:self];
    165   [super dealloc];
    166 }
    167 
    168 - (void)addAdditionalControls {
    169   [self removeButtons];
    170 
    171   extensionView_ = delegate_->AsExtensionInfoBarDelegate()->extension_host()->
    172       view()->native_view();
    173 
    174   // Add the extension's RenderWidgetHostViewMac to the view hierarchy of the
    175   // InfoBar and make sure to place it below the Close button.
    176   [infoBarView_ addSubview:extensionView_
    177                 positioned:NSWindowBelow
    178                 relativeTo:(NSView*)closeButton_];
    179 
    180   // Add the context menu button to the hierarchy.
    181   [dropdownButton_ setShowsBorderOnlyWhileMouseInside:YES];
    182   CGFloat buttonY =
    183       std::floor(NSMidY([infoBarView_ frame]) - (kButtonHeightPx / 2.0)) +
    184           kBottomBorderHeightPx;
    185   NSRect buttonFrame = NSMakeRect(
    186       kButtonLeftMarginPx, buttonY, kButtonWidthPx, kButtonHeightPx);
    187   [dropdownButton_ setFrame:buttonFrame];
    188   [dropdownButton_ setAutoresizingMask:NSViewMinYMargin | NSViewMaxYMargin];
    189   [infoBarView_ addSubview:dropdownButton_];
    190 
    191   // Because the parent view has a bottom border, account for it during
    192   // positioning.
    193   NSRect extensionFrame = [extensionView_ frame];
    194   extensionFrame.origin.y = kBottomBorderHeightPx;
    195 
    196   [extensionView_ setFrame:extensionFrame];
    197   // The extension's native view will only have a height that is non-zero if it
    198   // already has been loaded and rendered, which is the case when you switch
    199   // back to a tab with an extension infobar within it. The reason this is
    200   // needed is because the extension view's frame will not have changed in the
    201   // above case, so the NSViewFrameDidChangeNotification registered below will
    202   // never fire.
    203   if (NSHeight(extensionFrame) > 0.0) {
    204     NSSize infoBarSize = [[self view] frame].size;
    205     infoBarSize.height = [self clampedExtensionViewHeight] +
    206         kBottomBorderHeightPx;
    207     [[self view] setFrameSize:infoBarSize];
    208     [infoBarView_ setFrameSize:infoBarSize];
    209   }
    210 
    211   [self adjustExtensionViewSize];
    212 
    213   // These two notification handlers are here to ensure the width of the
    214   // native extension view is the same as the browser window's width and that
    215   // the parent infobar view matches the height of the extension's native view.
    216   [[NSNotificationCenter defaultCenter]
    217       addObserver:self
    218          selector:@selector(extensionViewFrameChanged)
    219              name:NSViewFrameDidChangeNotification
    220            object:extensionView_];
    221 
    222   [[NSNotificationCenter defaultCenter]
    223       addObserver:self
    224          selector:@selector(adjustWidthToFitWindow)
    225              name:NSWindowDidResizeNotification
    226            object:window_];
    227 }
    228 
    229 - (void)extensionViewFrameChanged {
    230   [self adjustExtensionViewSize];
    231 
    232   AnimatableView* view = [self animatableView];
    233   NSRect infoBarFrame = [view frame];
    234   CGFloat newHeight = [self clampedExtensionViewHeight] + kBottomBorderHeightPx;
    235   [infoBarView_ setPostsFrameChangedNotifications:NO];
    236   infoBarFrame.size.height = newHeight;
    237   [infoBarView_ setFrame:infoBarFrame];
    238   [infoBarView_ setPostsFrameChangedNotifications:YES];
    239   [view animateToNewHeight:newHeight duration:kAnimationDuration];
    240 }
    241 
    242 - (CGFloat)clampedExtensionViewHeight {
    243   return std::max(kToolbarMinHeightPx,
    244       std::min(NSHeight([extensionView_ frame]), kToolbarMaxHeightPx));
    245 }
    246 
    247 - (void)adjustExtensionViewSize {
    248   [extensionView_ setPostsFrameChangedNotifications:NO];
    249   NSSize extensionViewSize = [extensionView_ frame].size;
    250   extensionViewSize.width = NSWidth([window_ frame]);
    251   extensionViewSize.height = [self clampedExtensionViewHeight];
    252   [extensionView_ setFrameSize:extensionViewSize];
    253   [extensionView_ setPostsFrameChangedNotifications:YES];
    254 }
    255 
    256 - (void)setButtonImage:(NSImage*)image {
    257   [dropdownButton_ setImage:image];
    258 }
    259 
    260 @end
    261 
    262 InfoBar* ExtensionInfoBarDelegate::CreateInfoBar() {
    263   NSWindow* window = [(NSView*)tab_contents_->GetContentNativeView() window];
    264   ExtensionInfoBarController* controller =
    265       [[ExtensionInfoBarController alloc] initWithDelegate:this
    266                                                     window:window];
    267   return new InfoBar(controller);
    268 }
    269