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/web_contents_modal_dialog_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));
45 BOOL gAnimationsEnabled = true;
49 @interface ExtensionPopupController (Private)
50 // Callers should be using the public static method for initialization.
51 - (id)initWithParentWindow:(NSWindow*)parentWindow
52 anchoredAt:(NSPoint)anchoredAt
53 arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
54 devMode:(BOOL)devMode;
56 // Set the ExtensionViewHost, taking ownership.
57 - (void)setExtensionViewHost:(scoped_ptr<ExtensionViewHost>)host;
59 // Called when the extension's hosted NSView has been resized.
60 - (void)extensionViewFrameChanged;
62 // Called when the extension's size changes.
63 - (void)onSizeChanged:(NSSize)newSize;
65 // Called when the extension view is shown.
66 - (void)onViewDidShow;
68 // Called when the window moves or resizes. Notifies the extension.
69 - (void)onWindowChanged;
73 class ExtensionPopupContainer : public ExtensionViewMac::Container {
75 explicit ExtensionPopupContainer(ExtensionPopupController* controller)
76 : controller_(controller) {
79 void OnExtensionSizeChanged(ExtensionViewMac* view,
80 const gfx::Size& new_size) override {
81 [controller_ onSizeChanged:
82 NSMakeSize(new_size.width(), new_size.height())];
85 void OnExtensionViewDidShow(ExtensionViewMac* view) override {
86 [controller_ onViewDidShow];
90 ExtensionPopupController* controller_; // Weak; owns this.
93 class ExtensionPopupNotificationBridge : public content::NotificationObserver {
95 ExtensionPopupNotificationBridge(ExtensionPopupController* controller,
96 ExtensionViewHost* view_host)
97 : controller_(controller),
98 view_host_(view_host),
99 web_contents_(view_host_->host_contents()),
100 devtools_callback_(base::Bind(
101 &ExtensionPopupNotificationBridge::OnDevToolsStateChanged,
102 base::Unretained(this))) {
103 content::DevToolsAgentHost::AddAgentStateCallback(devtools_callback_);
106 ~ExtensionPopupNotificationBridge() override {
107 content::DevToolsAgentHost::RemoveAgentStateCallback(devtools_callback_);
110 void OnDevToolsStateChanged(content::DevToolsAgentHost* agent_host,
112 if (agent_host->GetWebContents() != web_contents_)
116 // Set the flag on the controller so the popup is not hidden when
117 // the dev tools get focus.
118 [controller_ setBeingInspected:YES];
120 // Allow the devtools to finish detaching before we close the popup.
121 [controller_ performSelector:@selector(close)
127 void Observe(int type,
128 const content::NotificationSource& source,
129 const content::NotificationDetails& details) override {
131 case extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD:
132 if (content::Details<ExtensionViewHost>(view_host_) == details)
133 [controller_ showDevTools];
135 case extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE:
136 if (content::Details<ExtensionViewHost>(view_host_) == details &&
137 ![controller_ isClosing]) {
142 NOTREACHED() << "Received unexpected notification";
148 ExtensionPopupController* controller_;
150 extensions::ExtensionViewHost* view_host_;
152 // WebContents for controller. Hold onto this separately because we need to
153 // know what it is for notifications, but our ExtensionViewHost may not be
155 WebContents* web_contents_;
156 base::Callback<void(content::DevToolsAgentHost*, bool)> devtools_callback_;
158 DISALLOW_COPY_AND_ASSIGN(ExtensionPopupNotificationBridge);
161 @implementation ExtensionPopupController
163 @synthesize extensionId = extensionId_;
165 - (id)initWithParentWindow:(NSWindow*)parentWindow
166 anchoredAt:(NSPoint)anchoredAt
167 arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
168 devMode:(BOOL)devMode {
169 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
170 initWithContentRect:ui::kWindowSizeDeterminedLater
171 styleMask:NSBorderlessWindowMask
172 backing:NSBackingStoreBuffered
177 anchoredAt = [parentWindow convertBaseToScreen:anchoredAt];
178 if ((self = [super initWithWindow:window
179 parentWindow:parentWindow
180 anchoredAt:anchoredAt])) {
181 beingInspected_ = devMode;
182 ignoreWindowDidResignKey_ = NO;
183 [[self bubble] setArrowLocation:arrowLocation];
184 if (!gAnimationsEnabled)
185 [window setAllowedAnimations:info_bubble::kAnimateNone];
191 [[NSNotificationCenter defaultCenter] removeObserver:self];
195 - (void)showDevTools {
196 DevToolsWindow::OpenDevToolsWindow(host_->host_contents());
200 // |windowWillClose:| could have already been called. http://crbug.com/279505
202 // TODO(gbillock): Change this API to say directly if the current popup
203 // should block tab close? This is a bit over-reaching.
204 const web_modal::WebContentsModalDialogManager* manager =
205 web_modal::WebContentsModalDialogManager::FromWebContents(
206 host_->host_contents());
207 if (manager && manager->IsDialogActive())
213 - (void)windowWillClose:(NSNotification *)notification {
214 [super windowWillClose:notification];
218 static_cast<ExtensionViewMac*>(host_->view())->set_container(NULL);
222 - (void)windowDidResignKey:(NSNotification*)notification {
223 // |windowWillClose:| could have already been called. http://crbug.com/279505
225 // When a modal dialog is opened on top of the popup and when it's closed,
226 // it steals key-ness from the popup. Don't close the popup when this
227 // happens. There's an extra windowDidResignKey: notification after the
228 // modal dialog closes that should also be ignored.
229 const web_modal::WebContentsModalDialogManager* manager =
230 web_modal::WebContentsModalDialogManager::FromWebContents(
231 host_->host_contents());
232 if (manager && manager->IsDialogActive()) {
233 ignoreWindowDidResignKey_ = YES;
236 if (ignoreWindowDidResignKey_) {
237 ignoreWindowDidResignKey_ = NO;
241 if (!beingInspected_)
242 [super windowDidResignKey:notification];
246 return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
249 - (ExtensionViewHost*)extensionViewHost {
253 - (void)setBeingInspected:(BOOL)beingInspected {
254 beingInspected_ = beingInspected;
257 + (ExtensionPopupController*)host:(scoped_ptr<ExtensionViewHost>)host
258 inBrowser:(Browser*)browser
259 anchoredAt:(NSPoint)anchoredAt
260 arrowLocation:(info_bubble::BubbleArrowLocation)
262 devMode:(BOOL)devMode {
263 DCHECK([NSThread isMainThread]);
268 [gPopup close]; // Starts the animation to fade out the popup.
270 // Create the popup first. This establishes an initially hidden NSWindow so
271 // that the renderer is able to gather correct screen metrics for the initial
273 gPopup = [[ExtensionPopupController alloc]
274 initWithParentWindow:browser->window()->GetNativeWindow()
275 anchoredAt:anchoredAt
276 arrowLocation:arrowLocation
278 [gPopup setExtensionViewHost:host.Pass()];
282 + (ExtensionPopupController*)popup {
286 - (void)setExtensionViewHost:(scoped_ptr<ExtensionViewHost>)host {
291 extensionId_ = host_->extension_id();
292 container_.reset(new ExtensionPopupContainer(self));
293 ExtensionViewMac* hostView = static_cast<ExtensionViewMac*>(host_->view());
294 hostView->set_container(container_.get());
295 hostView->CreateWidgetHostViewIn([self bubble]);
297 extensionView_ = hostView->GetNativeView();
299 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
300 [center addObserver:self
301 selector:@selector(extensionViewFrameChanged)
302 name:NSViewFrameDidChangeNotification
303 object:extensionView_];
305 notificationBridge_.reset(
306 new ExtensionPopupNotificationBridge(self, host_.get()));
307 content::Source<BrowserContext> source_context(host_->browser_context());
308 registrar_.Add(notificationBridge_.get(),
309 extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE,
311 if (beingInspected_) {
312 // Listen for the extension to finish loading so the dev tools can be
314 registrar_.Add(notificationBridge_.get(),
315 extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD,
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 or out? If so, then cancel that and create
357 // a new animation setting the opacity and new frame value. Otherwise the
358 // current animation will continue after this frame is set, reverting the
359 // frame to what it was when the animation started.
360 NSWindow* window = [self window];
361 CGFloat targetAlpha = [self isClosing] ? 0.0 : 1.0;
362 id animator = [window animator];
363 if ([window isVisible] &&
364 ([animator alphaValue] != targetAlpha ||
365 !NSEqualRects([window frame], [animator frame]))) {
366 [NSAnimationContext beginGrouping];
367 [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
368 [animator setAlphaValue:targetAlpha];
369 [animator setFrame:frame display:YES];
370 [NSAnimationContext endGrouping];
372 [window setFrame:frame display:YES];
375 // A NSViewFrameDidChangeNotification won't be sent until the extension view
376 // content is loaded. The window is hidden on init, so show it the first time
377 // the notification is fired (and consequently the view contents have loaded).
378 if (![window isVisible]) {
379 [self showWindow:self];
383 - (void)onSizeChanged:(NSSize)newSize {
384 // When we update the size, the window will become visible. Stay hidden until
385 // the host is loaded.
386 pendingSize_ = newSize;
387 if (!host_ || !host_->has_loaded_once())
390 // No need to use CA here, our caller calls us repeatedly to animate the
392 NSRect frame = [extensionView_ frame];
393 frame.size = newSize;
395 // |new_size| is in pixels. Convert to view units.
396 frame.size = [extensionView_ convertSize:frame.size fromView:nil];
398 [extensionView_ setFrame:frame];
399 [extensionView_ setNeedsDisplay:YES];
402 - (void)onViewDidShow {
403 [self onSizeChanged:pendingSize_];
406 - (void)onWindowChanged {
407 // The window is positioned before creating the host, to ensure the host is
408 // created with the correct screen information.
412 ExtensionViewMac* extensionView =
413 static_cast<ExtensionViewMac*>(host_->view());
414 // Let the extension view know, so that it can tell plugins.
416 extensionView->WindowFrameChanged();
419 - (void)windowDidResize:(NSNotification*)notification {
420 [self onWindowChanged];
423 - (void)windowDidMove:(NSNotification*)notification {
424 [self onWindowChanged];
427 // Private (TestingAPI)
428 + (void)setAnimationsEnabledForTesting:(BOOL)enabled {
429 gAnimationsEnabled = enabled;
432 // Private (TestingAPI)
434 return extensionView_;
437 // Private (TestingAPI)
438 + (NSSize)minPopupSize {
439 NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
443 // Private (TestingAPI)
444 + (NSSize)maxPopupSize {
445 NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};