Give Cocoa browser windows a WebContentsModalDialogHost
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / extension_popup_controller.mm
blob0897b25b886446918a3d12dc10df53e09f851b37
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"
7 #include <algorithm>
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;
31 namespace {
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 }  // namespace
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;
69 @end
71 class ExtensionPopupContainer : public ExtensionViewMac::Container {
72  public:
73   explicit ExtensionPopupContainer(ExtensionPopupController* controller)
74       : controller_(controller) {
75   }
77   void OnExtensionSizeChanged(ExtensionViewMac* view,
78                               const gfx::Size& new_size) override {
79     [controller_ onSizeChanged:
80         NSMakeSize(new_size.width(), new_size.height())];
81   }
83   void OnExtensionViewDidShow(ExtensionViewMac* view) override {
84     [controller_ onViewDidShow];
85   }
87  private:
88   ExtensionPopupController* controller_; // Weak; owns this.
91 class ExtensionPopupNotificationBridge : public content::NotificationObserver {
92  public:
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_);
102   }
104   ~ExtensionPopupNotificationBridge() override {
105     content::DevToolsAgentHost::RemoveAgentStateCallback(devtools_callback_);
106   }
108   void OnDevToolsStateChanged(content::DevToolsAgentHost* agent_host,
109                               bool attached) {
110     if (agent_host->GetWebContents() != web_contents_)
111       return;
113     if (attached) {
114       // Set the flag on the controller so the popup is not hidden when
115       // the dev tools get focus.
116       [controller_ setBeingInspected:YES];
117     } else {
118       // Allow the devtools to finish detaching before we close the popup.
119       [controller_ performSelector:@selector(close)
120                         withObject:nil
121                         afterDelay:0.0];
122     }
123   }
125   void Observe(int type,
126                const content::NotificationSource& source,
127                const content::NotificationDetails& details) override {
128     switch (type) {
129       case extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD:
130         if (content::Details<ExtensionViewHost>(view_host_) == details)
131           [controller_ showDevTools];
132         break;
133       case extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE:
134         if (content::Details<ExtensionViewHost>(view_host_) == details &&
135             ![controller_ isClosing]) {
136           [controller_ close];
137         }
138         break;
139       default:
140         NOTREACHED() << "Received unexpected notification";
141         break;
142     }
143   }
145  private:
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
152   // valid.
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
171                     defer:YES]);
172   if (!window.get())
173     return nil;
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];
182   }
183   return self;
186 - (void)dealloc {
187   [[NSNotificationCenter defaultCenter] removeObserver:self];
188   [super dealloc];
191 - (void)showDevTools {
192   DevToolsWindow::OpenDevToolsWindow(host_->host_contents());
195 - (void)close {
196   // |windowWillClose:| could have already been called. http://crbug.com/279505
197   if (host_) {
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())) {
204       return;
205     }
206   }
207   [super close];
210 - (void)windowWillClose:(NSNotification *)notification {
211   [super windowWillClose:notification];
212   if (gPopup == self)
213     gPopup = nil;
214   if (host_->view())
215     static_cast<ExtensionViewMac*>(host_->view())->set_container(NULL);
216   host_.reset();
219 - (void)windowDidResignKey:(NSNotification*)notification {
220   // |windowWillClose:| could have already been called. http://crbug.com/279505
221   if (host_) {
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());
229     if (popupManager &&
230         popupManager->IsWebModalDialogActive(host_->host_contents())) {
231       ignoreWindowDidResignKey_ = YES;
232       return;
233     }
234     if (ignoreWindowDidResignKey_) {
235       ignoreWindowDidResignKey_ = NO;
236       return;
237     }
238   }
239   if (!beingInspected_)
240     [super windowDidResignKey:notification];
243 - (BOOL)isClosing {
244   return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
247 - (ExtensionViewHost*)extensionViewHost {
248   return host_.get();
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)
259                                       arrowLocation
260                           devMode:(BOOL)devMode {
261   DCHECK([NSThread isMainThread]);
262   DCHECK(browser);
263   DCHECK(host);
265   if (gPopup)
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
270   // paint.
271   gPopup = [[ExtensionPopupController alloc]
272       initWithParentWindow:browser->window()->GetNativeWindow()
273                 anchoredAt:anchoredAt
274              arrowLocation:arrowLocation
275                    devMode:devMode];
276   [gPopup setExtensionViewHost:host.Pass()];
277   return gPopup;
280 + (ExtensionPopupController*)popup {
281   return gPopup;
284 - (void)setExtensionViewHost:(scoped_ptr<ExtensionViewHost>)host {
285   DCHECK(!host_);
286   DCHECK(host);
287   host_.swap(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,
308                 source_context);
309   if (beingInspected_) {
310     // Listen for the extension to finish loading so the dev tools can be
311     // opened.
312     registrar_.Add(notificationBridge_.get(),
313                    extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD,
314                    source_context);
315   }
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))
321     return;
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];
369   } else {
370     [window setFrame:frame display:YES];
371   }
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];
378   }
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())
386     return;
388   // No need to use CA here, our caller calls us repeatedly to animate the
389   // resizing.
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.
407   if (!host_)
408     return;
410   ExtensionViewMac* extensionView =
411       static_cast<ExtensionViewMac*>(host_->view());
412   // Let the extension view know, so that it can tell plugins.
413   if (extensionView)
414     extensionView->WindowFrameChanged();
417 - (void)windowDidResize:(NSNotification*)notification {
418   [self onWindowChanged];
421 - (void)windowDidMove:(NSNotification*)notification {
422   [self onWindowChanged];
425 // Private (TestingAPI)
426 - (NSView*)view {
427   return extensionView_;
430 // Private (TestingAPI)
431 + (NSSize)minPopupSize {
432   NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
433   return minSize;
436 // Private (TestingAPI)
437 + (NSSize)maxPopupSize {
438   NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};
439   return maxSize;
442 @end