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/devtools/devtools_window.h"
11 #include "chrome/browser/extensions/extension_view_host.h"
12 #include "chrome/browser/extensions/extension_view_host_factory.h"
13 #include "chrome/browser/ui/browser.h"
14 #import "chrome/browser/ui/cocoa/browser_window_cocoa.h"
15 #import "chrome/browser/ui/cocoa/extensions/extension_view_mac.h"
16 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
17 #include "chrome/common/url_constants.h"
18 #include "components/web_modal/popup_manager.h"
19 #include "content/public/browser/browser_context.h"
20 #include "content/public/browser/devtools_agent_host.h"
21 #include "content/public/browser/notification_details.h"
22 #include "content/public/browser/notification_source.h"
23 #include "extensions/browser/notification_types.h"
24 #include "ui/base/cocoa/window_size_constants.h"
26 using content::BrowserContext;
27 using content::RenderViewHost;
28 using content::WebContents;
29 using extensions::ExtensionViewHost;
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 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 - (id)initWithParentWindow:(NSWindow*)parentWindow
50 anchoredAt:(NSPoint)anchoredAt
51 arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
52 devMode:(BOOL)devMode;
54 // Set the ExtensionViewHost, taking ownership.
55 - (void)setExtensionViewHost:(scoped_ptr<ExtensionViewHost>)host;
57 // Called when the extension's hosted NSView has been resized.
58 - (void)extensionViewFrameChanged;
60 // Called when the extension's size changes.
61 - (void)onSizeChanged:(NSSize)newSize;
63 // Called when the extension view is shown.
64 - (void)onViewDidShow;
66 // Called when the window moves or resizes. Notifies the extension.
67 - (void)onWindowChanged;
71 class ExtensionPopupContainer : public ExtensionViewMac::Container {
73 explicit ExtensionPopupContainer(ExtensionPopupController* controller)
74 : controller_(controller) {
77 void OnExtensionSizeChanged(ExtensionViewMac* view,
78 const gfx::Size& new_size) override {
79 [controller_ onSizeChanged:
80 NSMakeSize(new_size.width(), new_size.height())];
83 void OnExtensionViewDidShow(ExtensionViewMac* view) override {
84 [controller_ onViewDidShow];
88 ExtensionPopupController* controller_; // Weak; owns this.
91 class ExtensionPopupNotificationBridge : public content::NotificationObserver {
93 ExtensionPopupNotificationBridge(ExtensionPopupController* controller,
94 ExtensionViewHost* view_host)
95 : controller_(controller),
96 view_host_(view_host),
97 web_contents_(view_host_->host_contents()),
98 devtools_callback_(base::Bind(
99 &ExtensionPopupNotificationBridge::OnDevToolsStateChanged,
100 base::Unretained(this))) {
101 content::DevToolsAgentHost::AddAgentStateCallback(devtools_callback_);
104 ~ExtensionPopupNotificationBridge() override {
105 content::DevToolsAgentHost::RemoveAgentStateCallback(devtools_callback_);
108 void OnDevToolsStateChanged(content::DevToolsAgentHost* agent_host,
110 if (agent_host->GetWebContents() != web_contents_)
114 // Set the flag on the controller so the popup is not hidden when
115 // the dev tools get focus.
116 [controller_ setBeingInspected:YES];
118 // Allow the devtools to finish detaching before we close the popup.
119 [controller_ performSelector:@selector(close)
125 void Observe(int type,
126 const content::NotificationSource& source,
127 const content::NotificationDetails& details) override {
129 case extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD:
130 if (content::Details<ExtensionViewHost>(view_host_) == details)
131 [controller_ showDevTools];
133 case extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE:
134 if (content::Details<ExtensionViewHost>(view_host_) == details &&
135 ![controller_ isClosing]) {
140 NOTREACHED() << "Received unexpected notification";
146 ExtensionPopupController* controller_;
148 extensions::ExtensionViewHost* view_host_;
150 // WebContents for controller. Hold onto this separately because we need to
151 // know what it is for notifications, but our ExtensionViewHost may not be
153 WebContents* web_contents_;
154 base::Callback<void(content::DevToolsAgentHost*, bool)> devtools_callback_;
156 DISALLOW_COPY_AND_ASSIGN(ExtensionPopupNotificationBridge);
159 @implementation ExtensionPopupController
161 @synthesize extensionId = extensionId_;
163 - (id)initWithParentWindow:(NSWindow*)parentWindow
164 anchoredAt:(NSPoint)anchoredAt
165 arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
166 devMode:(BOOL)devMode {
167 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
168 initWithContentRect:ui::kWindowSizeDeterminedLater
169 styleMask:NSBorderlessWindowMask
170 backing:NSBackingStoreBuffered
175 anchoredAt = [parentWindow convertBaseToScreen:anchoredAt];
176 if ((self = [super initWithWindow:window
177 parentWindow:parentWindow
178 anchoredAt:anchoredAt])) {
179 beingInspected_ = devMode;
180 ignoreWindowDidResignKey_ = NO;
181 [[self bubble] setArrowLocation:arrowLocation];
187 [[NSNotificationCenter defaultCenter] removeObserver:self];
191 - (void)showDevTools {
192 DevToolsWindow::OpenDevToolsWindow(host_->host_contents());
196 // |windowWillClose:| could have already been called. http://crbug.com/279505
198 // TODO(gbillock): Change this API to say directly if the current popup
199 // should block tab close? This is a bit over-reaching.
200 web_modal::PopupManager* popup_manager =
201 web_modal::PopupManager::FromWebContents(host_->host_contents());
202 if (popup_manager && popup_manager->IsWebModalDialogActive(
203 host_->host_contents())) {
210 - (void)windowWillClose:(NSNotification *)notification {
211 [super windowWillClose:notification];
215 static_cast<ExtensionViewMac*>(host_->view())->set_container(NULL);
219 - (void)windowDidResignKey:(NSNotification*)notification {
220 // |windowWillClose:| could have already been called. http://crbug.com/279505
222 // When a modal dialog is opened on top of the popup and when it's closed,
223 // it steals key-ness from the popup. Don't close the popup when this
224 // happens. There's an extra windowDidResignKey: notification after the
225 // modal dialog closes that should also be ignored.
226 web_modal::PopupManager* popupManager =
227 web_modal::PopupManager::FromWebContents(
228 host_->host_contents());
230 popupManager->IsWebModalDialogActive(host_->host_contents())) {
231 ignoreWindowDidResignKey_ = YES;
234 if (ignoreWindowDidResignKey_) {
235 ignoreWindowDidResignKey_ = NO;
239 if (!beingInspected_)
240 [super windowDidResignKey:notification];
244 return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
247 - (ExtensionViewHost*)extensionViewHost {
251 - (void)setBeingInspected:(BOOL)beingInspected {
252 beingInspected_ = beingInspected;
255 + (ExtensionPopupController*)host:(scoped_ptr<ExtensionViewHost>)host
256 inBrowser:(Browser*)browser
257 anchoredAt:(NSPoint)anchoredAt
258 arrowLocation:(info_bubble::BubbleArrowLocation)
260 devMode:(BOOL)devMode {
261 DCHECK([NSThread isMainThread]);
266 [gPopup close]; // Starts the animation to fade out the popup.
268 // Create the popup first. This establishes an initially hidden NSWindow so
269 // that the renderer is able to gather correct screen metrics for the initial
271 gPopup = [[ExtensionPopupController alloc]
272 initWithParentWindow:browser->window()->GetNativeWindow()
273 anchoredAt:anchoredAt
274 arrowLocation:arrowLocation
276 [gPopup setExtensionViewHost:host.Pass()];
280 + (ExtensionPopupController*)popup {
284 - (void)setExtensionViewHost:(scoped_ptr<ExtensionViewHost>)host {
289 extensionId_ = host_->extension_id();
290 container_.reset(new ExtensionPopupContainer(self));
291 ExtensionViewMac* hostView = static_cast<ExtensionViewMac*>(host_->view());
292 hostView->set_container(container_.get());
293 hostView->CreateWidgetHostViewIn([self bubble]);
295 extensionView_ = hostView->GetNativeView();
297 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
298 [center addObserver:self
299 selector:@selector(extensionViewFrameChanged)
300 name:NSViewFrameDidChangeNotification
301 object:extensionView_];
303 notificationBridge_.reset(
304 new ExtensionPopupNotificationBridge(self, host_.get()));
305 content::Source<BrowserContext> source_context(host_->browser_context());
306 registrar_.Add(notificationBridge_.get(),
307 extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE,
309 if (beingInspected_) {
310 // Listen for the extension to finish loading so the dev tools can be
312 registrar_.Add(notificationBridge_.get(),
313 extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD,
318 - (void)extensionViewFrameChanged {
319 // If there are no changes in the width or height of the frame, then ignore.
320 if (NSEqualSizes([extensionView_ frame].size, extensionFrame_.size))
323 extensionFrame_ = [extensionView_ frame];
324 // Constrain the size of the view.
325 [extensionView_ setFrameSize:NSMakeSize(
326 Clamp(NSWidth(extensionFrame_),
327 ExtensionViewMac::kMinWidth,
328 ExtensionViewMac::kMaxWidth),
329 Clamp(NSHeight(extensionFrame_),
330 ExtensionViewMac::kMinHeight,
331 ExtensionViewMac::kMaxHeight))];
333 // Pad the window by half of the rounded corner radius to prevent the
334 // extension's view from bleeding out over the corners.
335 CGFloat inset = info_bubble::kBubbleCornerRadius / 2.0;
336 [extensionView_ setFrameOrigin:NSMakePoint(inset, inset)];
338 NSRect frame = [extensionView_ frame];
339 frame.size.height += info_bubble::kBubbleArrowHeight +
340 info_bubble::kBubbleCornerRadius;
341 frame.size.width += info_bubble::kBubbleCornerRadius;
342 frame = [extensionView_ convertRect:frame toView:nil];
343 // Adjust the origin according to the height and width so that the arrow is
344 // positioned correctly at the middle and slightly down from the button.
345 NSPoint windowOrigin = self.anchorPoint;
346 NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
347 info_bubble::kBubbleArrowWidth / 2.0,
348 info_bubble::kBubbleArrowHeight / 2.0);
349 offsets = [extensionView_ convertSize:offsets toView:nil];
350 windowOrigin.x -= NSWidth(frame) - offsets.width;
351 windowOrigin.y -= NSHeight(frame) - offsets.height;
352 frame.origin = windowOrigin;
354 // Is the window still animating in or out? If so, then cancel that and create
355 // a new animation setting the opacity and new frame value. Otherwise the
356 // current animation will continue after this frame is set, reverting the
357 // frame to what it was when the animation started.
358 NSWindow* window = [self window];
359 CGFloat targetAlpha = [self isClosing] ? 0.0 : 1.0;
360 id animator = [window animator];
361 if ([window isVisible] &&
362 ([animator alphaValue] != targetAlpha ||
363 !NSEqualRects([window frame], [animator frame]))) {
364 [NSAnimationContext beginGrouping];
365 [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
366 [animator setAlphaValue:targetAlpha];
367 [animator setFrame:frame display:YES];
368 [NSAnimationContext endGrouping];
370 [window setFrame:frame display:YES];
373 // A NSViewFrameDidChangeNotification won't be sent until the extension view
374 // content is loaded. The window is hidden on init, so show it the first time
375 // the notification is fired (and consequently the view contents have loaded).
376 if (![window isVisible]) {
377 [self showWindow:self];
381 - (void)onSizeChanged:(NSSize)newSize {
382 // When we update the size, the window will become visible. Stay hidden until
383 // the host is loaded.
384 pendingSize_ = newSize;
385 if (!host_ || !host_->has_loaded_once())
388 // No need to use CA here, our caller calls us repeatedly to animate the
390 NSRect frame = [extensionView_ frame];
391 frame.size = newSize;
393 // |new_size| is in pixels. Convert to view units.
394 frame.size = [extensionView_ convertSize:frame.size fromView:nil];
396 [extensionView_ setFrame:frame];
397 [extensionView_ setNeedsDisplay:YES];
400 - (void)onViewDidShow {
401 [self onSizeChanged:pendingSize_];
404 - (void)onWindowChanged {
405 // The window is positioned before creating the host, to ensure the host is
406 // created with the correct screen information.
410 ExtensionViewMac* extensionView =
411 static_cast<ExtensionViewMac*>(host_->view());
412 // Let the extension view know, so that it can tell plugins.
414 extensionView->WindowFrameChanged();
417 - (void)windowDidResize:(NSNotification*)notification {
418 [self onWindowChanged];
421 - (void)windowDidMove:(NSNotification*)notification {
422 [self onWindowChanged];
425 // Private (TestingAPI)
427 return extensionView_;
430 // Private (TestingAPI)
431 + (NSSize)minPopupSize {
432 NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
436 // Private (TestingAPI)
437 + (NSSize)maxPopupSize {
438 NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};