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/extensions/extension_popup_controller.h"
9 #include "base/callback.h"
10 #include "chrome/browser/chrome_notification_types.h"
11 #include "chrome/browser/devtools/devtools_window.h"
12 #include "chrome/browser/extensions/extension_view_host.h"
13 #include "chrome/browser/extensions/extension_view_host_factory.h"
14 #include "chrome/browser/profiles/profile.h"
15 #include "chrome/common/url_constants.h"
16 #include "chrome/browser/ui/browser.h"
17 #include "chrome/browser/ui/browser_finder.h"
18 #import "chrome/browser/ui/cocoa/browser_window_cocoa.h"
19 #import "chrome/browser/ui/cocoa/extensions/extension_view_mac.h"
20 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
21 #include "components/web_modal/popup_manager.h"
22 #include "content/public/browser/devtools_agent_host.h"
23 #include "content/public/browser/notification_details.h"
24 #include "content/public/browser/notification_registrar.h"
25 #include "content/public/browser/notification_source.h"
26 #include "ui/base/cocoa/window_size_constants.h"
28 using content::BrowserContext;
29 using content::RenderViewHost;
30 using content::WebContents;
33 // The duration for any animations that might be invoked by this controller.
34 const NSTimeInterval kAnimationDuration = 0.2;
36 // There should only be one extension popup showing at one time. Keep a
37 // reference to it here.
38 static ExtensionPopupController* gPopup;
40 // Given a value and a rage, clamp the value into the range.
41 CGFloat Clamp(CGFloat value, CGFloat min, CGFloat max) {
42 return std::max(min, std::min(max, value));
47 @interface ExtensionPopupController (Private)
48 // Callers should be using the public static method for initialization.
49 // NOTE: This takes ownership of |host|.
50 - (id)initWithHost:(extensions::ExtensionViewHost*)host
51 parentWindow:(NSWindow*)parentWindow
52 anchoredAt:(NSPoint)anchoredAt
53 arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
54 devMode:(BOOL)devMode;
56 // Called when the extension's hosted NSView has been resized.
57 - (void)extensionViewFrameChanged;
59 // Called when the extension's size changes.
60 - (void)onSizeChanged:(NSSize)newSize;
62 // Called when the extension view is shown.
63 - (void)onViewDidShow;
65 // Called when the window moves or resizes. Notifies the extension.
66 - (void)onWindowChanged;
70 class ExtensionPopupContainer : public ExtensionViewMac::Container {
72 explicit ExtensionPopupContainer(ExtensionPopupController* controller)
73 : controller_(controller) {
76 virtual void OnExtensionSizeChanged(
77 ExtensionViewMac* view,
78 const gfx::Size& new_size) OVERRIDE {
79 [controller_ onSizeChanged:
80 NSMakeSize(new_size.width(), new_size.height())];
83 virtual void OnExtensionViewDidShow(ExtensionViewMac* view) OVERRIDE {
84 [controller_ onViewDidShow];
88 ExtensionPopupController* controller_; // Weak; owns this.
91 class DevtoolsNotificationBridge : public content::NotificationObserver {
93 explicit DevtoolsNotificationBridge(ExtensionPopupController* controller)
94 : controller_(controller),
95 web_contents_([controller_ extensionViewHost]->host_contents()),
96 devtools_callback_(base::Bind(
97 &DevtoolsNotificationBridge::OnDevToolsStateChanged,
98 base::Unretained(this))) {
99 content::DevToolsAgentHost::AddAgentStateCallback(devtools_callback_);
102 virtual ~DevtoolsNotificationBridge() {
103 content::DevToolsAgentHost::RemoveAgentStateCallback(devtools_callback_);
106 void OnDevToolsStateChanged(content::DevToolsAgentHost* agent_host,
108 if (agent_host->GetWebContents() != web_contents_)
112 // Set the flag on the controller so the popup is not hidden when
113 // the dev tools get focus.
114 [controller_ setBeingInspected:YES];
116 // Allow the devtools to finish detaching before we close the popup.
117 [controller_ performSelector:@selector(close)
123 virtual void Observe(
125 const content::NotificationSource& source,
126 const content::NotificationDetails& details) OVERRIDE {
128 case extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING: {
129 if (content::Details<extensions::ExtensionViewHost>(
130 [controller_ extensionViewHost]) == details) {
131 [controller_ showDevTools];
136 NOTREACHED() << "Received unexpected notification";
143 ExtensionPopupController* controller_;
144 // WebContents for controller. Hold onto this separately because we need to
145 // know what it is for notifications, but our ExtensionViewHost may not be
147 WebContents* web_contents_;
148 base::Callback<void(content::DevToolsAgentHost*, bool)> devtools_callback_;
151 @implementation ExtensionPopupController
153 - (id)initWithHost:(extensions::ExtensionViewHost*)host
154 parentWindow:(NSWindow*)parentWindow
155 anchoredAt:(NSPoint)anchoredAt
156 arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
157 devMode:(BOOL)devMode {
158 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
159 initWithContentRect:ui::kWindowSizeDeterminedLater
160 styleMask:NSBorderlessWindowMask
161 backing:NSBackingStoreBuffered
166 anchoredAt = [parentWindow convertBaseToScreen:anchoredAt];
167 if ((self = [super initWithWindow:window
168 parentWindow:parentWindow
169 anchoredAt:anchoredAt])) {
171 beingInspected_ = devMode;
172 ignoreWindowDidResignKey_ = NO;
174 InfoBubbleView* view = self.bubble;
175 [view setArrowLocation:arrowLocation];
177 extensionView_ = host->view()->GetNativeView();
178 container_.reset(new ExtensionPopupContainer(self));
179 static_cast<ExtensionViewMac*>(host->view())
180 ->set_container(container_.get());
182 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
183 [center addObserver:self
184 selector:@selector(extensionViewFrameChanged)
185 name:NSViewFrameDidChangeNotification
186 object:extensionView_];
188 [view addSubview:extensionView_];
190 notificationBridge_.reset(new DevtoolsNotificationBridge(self));
191 registrar_.reset(new content::NotificationRegistrar);
192 if (beingInspected_) {
193 // Listen for the extension to finish loading so the dev tools can be
195 registrar_->Add(notificationBridge_.get(),
196 extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING,
197 content::Source<BrowserContext>(host->browser_context()));
204 [[NSNotificationCenter defaultCenter] removeObserver:self];
208 - (void)showDevTools {
209 DevToolsWindow::OpenDevToolsWindow(host_->host_contents());
213 // |windowWillClose:| could have already been called. http://crbug.com/279505
215 // TODO(gbillock): Change this API to say directly if the current popup
216 // should block tab close? This is a bit over-reaching.
217 web_modal::PopupManager* popup_manager =
218 web_modal::PopupManager::FromWebContents(host_->host_contents());
219 if (popup_manager && popup_manager->IsWebModalDialogActive(
220 host_->host_contents())) {
227 - (void)windowWillClose:(NSNotification *)notification {
228 [super windowWillClose:notification];
232 static_cast<ExtensionViewMac*>(host_->view())->set_container(NULL);
236 - (void)windowDidResignKey:(NSNotification*)notification {
237 // |windowWillClose:| could have already been called. http://crbug.com/279505
239 // When a modal dialog is opened on top of the popup and when it's closed,
240 // it steals key-ness from the popup. Don't close the popup when this
241 // happens. There's an extra windowDidResignKey: notification after the
242 // modal dialog closes that should also be ignored.
243 web_modal::PopupManager* popupManager =
244 web_modal::PopupManager::FromWebContents(
245 host_->host_contents());
247 popupManager->IsWebModalDialogActive(host_->host_contents())) {
248 ignoreWindowDidResignKey_ = YES;
251 if (ignoreWindowDidResignKey_) {
252 ignoreWindowDidResignKey_ = NO;
256 if (!beingInspected_)
257 [super windowDidResignKey:notification];
261 return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
264 - (extensions::ExtensionViewHost*)extensionViewHost {
268 - (void)setBeingInspected:(BOOL)beingInspected {
269 beingInspected_ = beingInspected;
272 + (ExtensionPopupController*)showURL:(GURL)url
273 inBrowser:(Browser*)browser
274 anchoredAt:(NSPoint)anchoredAt
275 arrowLocation:(info_bubble::BubbleArrowLocation)
277 devMode:(BOOL)devMode {
278 DCHECK([NSThread isMainThread]);
283 // If we click the browser/page action again, we should close the popup.
284 // Make Mac behavior the same with Windows and others.
286 std::string extension_id = url.host();
287 if (url.SchemeIs(content::kChromeUIScheme) &&
288 url.host() == chrome::kChromeUIExtensionInfoHost)
289 extension_id = url.path().substr(1);
290 extensions::ExtensionViewHost* host = [gPopup extensionViewHost];
291 if (extension_id == host->extension_id()) {
297 extensions::ExtensionViewHost* host =
298 extensions::ExtensionViewHostFactory::CreatePopupHost(url, browser);
305 // Takes ownership of |host|. Also will autorelease itself when the popup is
306 // closed, so no need to do that here.
307 gPopup = [[ExtensionPopupController alloc]
309 parentWindow:browser->window()->GetNativeWindow()
310 anchoredAt:anchoredAt
311 arrowLocation:arrowLocation
316 + (ExtensionPopupController*)popup {
320 - (void)extensionViewFrameChanged {
321 // If there are no changes in the width or height of the frame, then ignore.
322 if (NSEqualSizes([extensionView_ frame].size, extensionFrame_.size))
325 extensionFrame_ = [extensionView_ frame];
326 // Constrain the size of the view.
327 [extensionView_ setFrameSize:NSMakeSize(
328 Clamp(NSWidth(extensionFrame_),
329 ExtensionViewMac::kMinWidth,
330 ExtensionViewMac::kMaxWidth),
331 Clamp(NSHeight(extensionFrame_),
332 ExtensionViewMac::kMinHeight,
333 ExtensionViewMac::kMaxHeight))];
335 // Pad the window by half of the rounded corner radius to prevent the
336 // extension's view from bleeding out over the corners.
337 CGFloat inset = info_bubble::kBubbleCornerRadius / 2.0;
338 [extensionView_ setFrameOrigin:NSMakePoint(inset, inset)];
340 NSRect frame = [extensionView_ frame];
341 frame.size.height += info_bubble::kBubbleArrowHeight +
342 info_bubble::kBubbleCornerRadius;
343 frame.size.width += info_bubble::kBubbleCornerRadius;
344 frame = [extensionView_ convertRect:frame toView:nil];
345 // Adjust the origin according to the height and width so that the arrow is
346 // positioned correctly at the middle and slightly down from the button.
347 NSPoint windowOrigin = self.anchorPoint;
348 NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
349 info_bubble::kBubbleArrowWidth / 2.0,
350 info_bubble::kBubbleArrowHeight / 2.0);
351 offsets = [extensionView_ convertSize:offsets toView:nil];
352 windowOrigin.x -= NSWidth(frame) - offsets.width;
353 windowOrigin.y -= NSHeight(frame) - offsets.height;
354 frame.origin = windowOrigin;
356 // Is the window still animating in? If so, then cancel that and create a new
357 // animation setting the opacity and new frame value. Otherwise the current
358 // animation will continue after this frame is set, reverting the frame to
359 // what it was when the animation started.
360 NSWindow* window = [self window];
361 id animator = [window animator];
362 if ([window isVisible] &&
363 ([animator alphaValue] < 1.0 ||
364 !NSEqualRects([window frame], [animator frame]))) {
365 [NSAnimationContext beginGrouping];
366 [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
367 [animator setAlphaValue:1.0];
368 [animator setFrame:frame display:YES];
369 [NSAnimationContext endGrouping];
371 [window setFrame:frame display:YES];
374 // A NSViewFrameDidChangeNotification won't be sent until the extension view
375 // content is loaded. The window is hidden on init, so show it the first time
376 // the notification is fired (and consequently the view contents have loaded).
377 if (![window isVisible]) {
378 [self showWindow:self];
382 - (void)onSizeChanged:(NSSize)newSize {
383 // When we update the size, the window will become visible. Stay hidden until
384 // the host is loaded.
385 pendingSize_ = newSize;
386 if (!host_->did_stop_loading())
389 // No need to use CA here, our caller calls us repeatedly to animate the
391 NSRect frame = [extensionView_ frame];
392 frame.size = newSize;
394 // |new_size| is in pixels. Convert to view units.
395 frame.size = [extensionView_ convertSize:frame.size fromView:nil];
397 [extensionView_ setFrame:frame];
398 [extensionView_ setNeedsDisplay:YES];
401 - (void)onViewDidShow {
402 [self onSizeChanged:pendingSize_];
405 - (void)onWindowChanged {
406 // The window is positioned before creating the host, to ensure the host is
407 // created with the correct screen information.
411 ExtensionViewMac* extensionView =
412 static_cast<ExtensionViewMac*>(host_->view());
413 // Let the extension view know, so that it can tell plugins.
415 extensionView->WindowFrameChanged();
418 - (void)windowDidResize:(NSNotification*)notification {
419 [self onWindowChanged];
422 - (void)windowDidMove:(NSNotification*)notification {
423 [self onWindowChanged];
426 // Private (TestingAPI)
428 return extensionView_;
431 // Private (TestingAPI)
432 + (NSSize)minPopupSize {
433 NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
437 // Private (TestingAPI)
438 + (NSSize)maxPopupSize {
439 NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};