1 // Copyright (c) 2012 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.
5 #import "chrome/browser/ui/cocoa/infobars/extension_infobar_controller.h"
9 #include "chrome/browser/extensions/extension_infobar_delegate.h"
10 #include "chrome/browser/extensions/extension_view_host.h"
11 #include "chrome/browser/extensions/image_loader.h"
12 #include "chrome/browser/infobars/infobar_service.h"
13 #include "chrome/browser/profiles/profile.h"
14 #include "chrome/browser/ui/browser_finder.h"
15 #import "chrome/browser/ui/cocoa/animatable_view.h"
16 #import "chrome/browser/ui/cocoa/extensions/extension_action_context_menu_controller.h"
17 #include "chrome/browser/ui/cocoa/infobars/infobar_cocoa.h"
18 #import "chrome/browser/ui/cocoa/menu_button.h"
19 #include "content/public/browser/web_contents.h"
20 #include "extensions/common/constants.h"
21 #include "extensions/common/extension.h"
22 #include "extensions/common/extension_icon_set.h"
23 #include "extensions/common/extension_resource.h"
24 #include "extensions/common/manifest_handlers/icons_handler.h"
25 #include "grit/theme_resources.h"
26 #include "skia/ext/skia_utils_mac.h"
27 #include "ui/base/resource/resource_bundle.h"
28 #include "ui/gfx/canvas.h"
29 #include "ui/gfx/image/image.h"
31 const CGFloat kBottomBorderHeightPx = 1.0;
32 const CGFloat kButtonHeightPx = 26.0;
33 const CGFloat kButtonLeftMarginPx = 2.0;
34 const CGFloat kButtonWidthPx = 34.0;
35 const CGFloat kDropArrowLeftMarginPx = 3.0;
36 const CGFloat kToolbarMinHeightPx = 36.0;
37 const CGFloat kToolbarMaxHeightPx = 72.0;
39 @interface ExtensionInfoBarController(Private)
40 // Called when the extension's hosted NSView has been resized.
41 - (void)extensionViewFrameChanged;
42 // Returns the clamped height of the extension view to be within the min and max
43 // values defined above.
44 - (CGFloat)clampedExtensionViewHeight;
45 // Adjusts the width of the extension's hosted view to match the window's width
46 // and sets the proper height for it as well.
47 - (void)adjustExtensionViewSize;
48 // Sets the image to be used in the button on the left side of the infobar.
49 - (void)setButtonImage:(NSImage*)image;
52 // A helper class to bridge the asynchronous Skia bitmap loading mechanism to
53 // the extension's button.
56 explicit InfobarBridge(ExtensionInfoBarController* owner)
58 delegate_([owner delegate]->AsExtensionInfoBarDelegate()),
59 weak_ptr_factory_(this) {
63 // Load the Extension's icon image.
65 const extensions::Extension* extension = delegate_->extension_view_host()->
67 extensions::ExtensionResource icon_resource =
68 extensions::IconsInfo::GetIconResource(
70 extension_misc::EXTENSION_ICON_BITTY,
71 ExtensionIconSet::MATCH_EXACTLY);
72 extensions::ImageLoader* loader = extensions::ImageLoader::Get(
73 delegate_->extension_view_host()->browser_context());
74 loader->LoadImageAsync(extension, icon_resource,
75 gfx::Size(extension_misc::EXTENSION_ICON_BITTY,
76 extension_misc::EXTENSION_ICON_BITTY),
77 base::Bind(&InfobarBridge::OnImageLoaded,
78 weak_ptr_factory_.GetWeakPtr()));
81 // ImageLoader callback.
82 // TODO(andybons): The infobar view implementations share a lot of the same
83 // code. Come up with a strategy to share amongst them.
84 void OnImageLoaded(const gfx::Image& image) {
86 return; // The delegate can go away while the image asynchronously loads.
88 ResourceBundle& rb = ResourceBundle::GetSharedInstance();
90 // Fall back on the default extension icon on failure.
91 const gfx::ImageSkia* icon;
93 icon = rb.GetImageSkiaNamed(IDR_EXTENSIONS_SECTION);
95 icon = image.ToImageSkia();
97 gfx::ImageSkia* drop_image = rb.GetImageSkiaNamed(IDR_APP_DROPARROW);
99 const int image_size = extension_misc::EXTENSION_ICON_BITTY;
100 scoped_ptr<gfx::Canvas> canvas(
102 gfx::Size(image_size + kDropArrowLeftMarginPx + drop_image->width(),
103 image_size), 1.0f, false));
104 canvas->DrawImageInt(*icon,
105 0, 0, icon->width(), icon->height(),
106 0, 0, image_size, image_size,
108 canvas->DrawImageInt(*drop_image,
109 image_size + kDropArrowLeftMarginPx,
111 [owner_ setButtonImage:gfx::SkBitmapToNSImage(
112 canvas->ExtractImageRep().sk_bitmap())];
117 ExtensionInfoBarController* owner_;
120 ExtensionInfoBarDelegate* delegate_;
122 base::WeakPtrFactory<InfobarBridge> weak_ptr_factory_;
124 DISALLOW_COPY_AND_ASSIGN(InfobarBridge);
128 @implementation ExtensionInfoBarController
130 - (id)initWithInfoBar:(InfoBarCocoa*)infobar {
131 if ((self = [super initWithInfoBar:infobar])) {
132 dropdownButton_.reset([[MenuButton alloc] init]);
133 [dropdownButton_ setOpenMenuOnClick:YES];
135 base::scoped_nsobject<NSMenu> contextMenu(
136 [[NSMenu alloc] initWithTitle:@""]);
137 [contextMenu setDelegate:self];
138 // See menu_button.h for documentation on why this is needed.
139 [contextMenu addItemWithTitle:@"" action:NULL keyEquivalent:@""];
140 [dropdownButton_ setAttachedMenu:contextMenu];
142 bridge_.reset(new InfobarBridge(self));
148 [[NSNotificationCenter defaultCenter] removeObserver:self];
152 - (void)addAdditionalControls {
153 [self removeButtons];
155 extensionView_ = [self delegate]->AsExtensionInfoBarDelegate()
156 ->extension_view_host()->view()->native_view();
158 // Add the extension's RenderWidgetHostView to the view hierarchy of the
159 // InfoBar and make sure to place it below the Close button.
160 [infoBarView_ addSubview:extensionView_
161 positioned:NSWindowBelow
162 relativeTo:(NSView*)closeButton_];
164 // Add the context menu button to the hierarchy.
165 [dropdownButton_ setShowsBorderOnlyWhileMouseInside:YES];
167 std::floor(NSMidY([infoBarView_ frame]) - (kButtonHeightPx / 2.0)) +
168 kBottomBorderHeightPx;
169 NSRect buttonFrame = NSMakeRect(
170 kButtonLeftMarginPx, buttonY, kButtonWidthPx, kButtonHeightPx);
171 [dropdownButton_ setFrame:buttonFrame];
172 [dropdownButton_ setAutoresizingMask:NSViewMinYMargin | NSViewMaxYMargin];
173 [infoBarView_ addSubview:dropdownButton_];
175 // Because the parent view has a bottom border, account for it during
177 NSRect extensionFrame = [extensionView_ frame];
178 extensionFrame.origin.y = kBottomBorderHeightPx;
180 [extensionView_ setFrame:extensionFrame];
181 // The extension's native view will only have a height that is non-zero if it
182 // already has been loaded and rendered, which is the case when you switch
183 // back to a tab with an extension infobar within it. The reason this is
184 // needed is because the extension view's frame will not have changed in the
185 // above case, so the NSViewFrameDidChangeNotification registered below will
187 if (NSHeight(extensionFrame) > 0.0)
188 [self infobar]->SetBarTargetHeight([self clampedExtensionViewHeight]);
190 [self adjustExtensionViewSize];
192 // These two notification handlers are here to ensure the width of the
193 // native extension view is the same as the browser window's width and that
194 // the parent infobar view matches the height of the extension's native view.
195 [[NSNotificationCenter defaultCenter]
197 selector:@selector(extensionViewFrameChanged)
198 name:NSViewFrameDidChangeNotification
199 object:extensionView_];
201 [[NSNotificationCenter defaultCenter]
203 selector:@selector(adjustExtensionViewSize)
204 name:NSViewFrameDidChangeNotification
208 - (void)infobarWillHide {
209 [[dropdownButton_ menu] cancelTracking];
210 [super infobarWillHide];
213 - (void)infobarWillClose {
214 [self disablePopUpMenu:[dropdownButton_ menu]];
215 [super infobarWillClose];
218 - (void)extensionViewFrameChanged {
219 [self adjustExtensionViewSize];
220 [self infobar]->SetBarTargetHeight([self clampedExtensionViewHeight]);
223 - (CGFloat)clampedExtensionViewHeight {
224 CGFloat height = [self delegate]->AsExtensionInfoBarDelegate()->height();
225 return std::max(kToolbarMinHeightPx, std::min(height, kToolbarMaxHeightPx));
228 - (void)adjustExtensionViewSize {
229 [extensionView_ setPostsFrameChangedNotifications:NO];
230 NSSize extensionViewSize = [extensionView_ frame].size;
231 extensionViewSize.width = NSWidth([[self view] frame]);
232 extensionViewSize.height = [self clampedExtensionViewHeight];
233 [extensionView_ setFrameSize:extensionViewSize];
234 [extensionView_ setPostsFrameChangedNotifications:YES];
237 - (void)setButtonImage:(NSImage*)image {
238 [dropdownButton_ setImage:image];
241 - (void)menuNeedsUpdate:(NSMenu*)menu {
242 DCHECK([self isOwned]);
244 if (!contextMenuController_) {
245 extensions::ExtensionViewHost* extensionViewHost =
246 [self delegate]->AsExtensionInfoBarDelegate()->extension_view_host();
247 Browser* browser = chrome::FindBrowserWithWebContents(
248 [self delegate]->AsExtensionInfoBarDelegate()->GetWebContents());
249 contextMenuController_.reset([[ExtensionActionContextMenuController alloc]
250 initWithExtension:extensionViewHost->extension()
252 extensionAction:NULL]);
255 [menu removeAllItems];
256 [contextMenuController_ populateMenu:menu];
262 scoped_ptr<infobars::InfoBar> ExtensionInfoBarDelegate::CreateInfoBar(
263 scoped_ptr<ExtensionInfoBarDelegate> delegate) {
264 scoped_ptr<InfoBarCocoa> infobar(
265 new InfoBarCocoa(delegate.PassAs<infobars::InfoBarDelegate>()));
266 base::scoped_nsobject<ExtensionInfoBarController> controller(
267 [[ExtensionInfoBarController alloc] initWithInfoBar:infobar.get()]);
268 infobar->set_controller(controller);
269 return infobar.PassAs<infobars::InfoBar>();