Popular sites on the NTP: check that experiment group StartsWith (rather than IS...
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / extensions / extension_popup_controller.mm
blob570855fb6099f2c377dfb293389048c44d2d2fdb
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/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;
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 BOOL gAnimationsEnabled = true;
47 }  // namespace
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;
71 @end
73 class ExtensionPopupContainer : public ExtensionViewMac::Container {
74  public:
75   explicit ExtensionPopupContainer(ExtensionPopupController* controller)
76       : controller_(controller) {
77   }
79   void OnExtensionSizeChanged(ExtensionViewMac* view,
80                               const gfx::Size& new_size) override {
81     [controller_ onSizeChanged:
82         NSMakeSize(new_size.width(), new_size.height())];
83   }
85   void OnExtensionViewDidShow(ExtensionViewMac* view) override {
86     [controller_ onViewDidShow];
87   }
89  private:
90   ExtensionPopupController* controller_; // Weak; owns this.
93 class ExtensionPopupNotificationBridge : public content::NotificationObserver {
94  public:
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_);
104   }
106   ~ExtensionPopupNotificationBridge() override {
107     content::DevToolsAgentHost::RemoveAgentStateCallback(devtools_callback_);
108   }
110   void OnDevToolsStateChanged(content::DevToolsAgentHost* agent_host,
111                               bool attached) {
112     if (agent_host->GetWebContents() != web_contents_)
113       return;
115     if (attached) {
116       // Set the flag on the controller so the popup is not hidden when
117       // the dev tools get focus.
118       [controller_ setBeingInspected:YES];
119     } else {
120       // Allow the devtools to finish detaching before we close the popup.
121       [controller_ performSelector:@selector(close)
122                         withObject:nil
123                         afterDelay:0.0];
124     }
125   }
127   void Observe(int type,
128                const content::NotificationSource& source,
129                const content::NotificationDetails& details) override {
130     switch (type) {
131       case extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD:
132         if (content::Details<ExtensionViewHost>(view_host_) == details)
133           [controller_ showDevTools];
134         break;
135       case extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE:
136         if (content::Details<ExtensionViewHost>(view_host_) == details &&
137             ![controller_ isClosing]) {
138           [controller_ close];
139         }
140         break;
141       default:
142         NOTREACHED() << "Received unexpected notification";
143         break;
144     }
145   }
147  private:
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
154   // valid.
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
173                     defer:NO]);
174   if (!window.get())
175     return nil;
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];
186   }
187   return self;
190 - (void)dealloc {
191   [[NSNotificationCenter defaultCenter] removeObserver:self];
192   [super dealloc];
195 - (void)showDevTools {
196   DevToolsWindow::OpenDevToolsWindow(host_->host_contents());
199 - (void)close {
200   // |windowWillClose:| could have already been called. http://crbug.com/279505
201   if (host_) {
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())
208       return;
209   }
210   [super close];
213 - (void)windowWillClose:(NSNotification *)notification {
214   [super windowWillClose:notification];
215   if (gPopup == self)
216     gPopup = nil;
217   if (host_->view())
218     static_cast<ExtensionViewMac*>(host_->view())->set_container(NULL);
219   host_.reset();
222 - (void)windowDidResignKey:(NSNotification*)notification {
223   // |windowWillClose:| could have already been called. http://crbug.com/279505
224   if (host_) {
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;
234       return;
235     }
236     if (ignoreWindowDidResignKey_) {
237       ignoreWindowDidResignKey_ = NO;
238       return;
239     }
240   }
241   if (!beingInspected_)
242     [super windowDidResignKey:notification];
245 - (BOOL)isClosing {
246   return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
249 - (ExtensionViewHost*)extensionViewHost {
250   return host_.get();
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)
261                                       arrowLocation
262                           devMode:(BOOL)devMode {
263   DCHECK([NSThread isMainThread]);
264   DCHECK(browser);
265   DCHECK(host);
267   if (gPopup)
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
272   // paint.
273   gPopup = [[ExtensionPopupController alloc]
274       initWithParentWindow:browser->window()->GetNativeWindow()
275                 anchoredAt:anchoredAt
276              arrowLocation:arrowLocation
277                    devMode:devMode];
278   [gPopup setExtensionViewHost:host.Pass()];
279   return gPopup;
282 + (ExtensionPopupController*)popup {
283   return gPopup;
286 - (void)setExtensionViewHost:(scoped_ptr<ExtensionViewHost>)host {
287   DCHECK(!host_);
288   DCHECK(host);
289   host_.swap(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,
310                 source_context);
311   if (beingInspected_) {
312     // Listen for the extension to finish loading so the dev tools can be
313     // opened.
314     registrar_.Add(notificationBridge_.get(),
315                    extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_FIRST_LOAD,
316                    source_context);
317   }
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))
323     return;
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];
371   } else {
372     [window setFrame:frame display:YES];
373   }
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];
380   }
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())
388     return;
390   // No need to use CA here, our caller calls us repeatedly to animate the
391   // resizing.
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.
409   if (!host_)
410     return;
412   ExtensionViewMac* extensionView =
413       static_cast<ExtensionViewMac*>(host_->view());
414   // Let the extension view know, so that it can tell plugins.
415   if (extensionView)
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)
433 - (NSView*)view {
434   return extensionView_;
437 // Private (TestingAPI)
438 + (NSSize)minPopupSize {
439   NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
440   return minSize;
443 // Private (TestingAPI)
444 + (NSSize)maxPopupSize {
445   NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};
446   return maxSize;
449 @end