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 25namespace { 26const CGFloat kAnimationDuration = 0.12; 27const CGFloat kBottomBorderHeightPx = 1.0; 28const CGFloat kButtonHeightPx = 26.0; 29const CGFloat kButtonLeftMarginPx = 2.0; 30const CGFloat kButtonWidthPx = 34.0; 31const CGFloat kDropArrowLeftMarginPx = 3.0; 32const CGFloat kToolbarMinHeightPx = 36.0; 33const 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. 51class 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 262InfoBar* 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