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/browser/ui/browser.h"
16 #import "chrome/browser/ui/cocoa/browser_window_cocoa.h"
17 #import "chrome/browser/ui/cocoa/extensions/extension_view_mac.h"
18 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
19 #include "components/web_modal/web_contents_modal_dialog_manager.h"
20 #include "content/public/browser/devtools_agent_host.h"
21 #include "content/public/browser/devtools_manager.h"
22 #include "content/public/browser/notification_details.h"
23 #include "content/public/browser/notification_registrar.h"
24 #include "content/public/browser/notification_source.h"
25 #include "ui/base/cocoa/window_size_constants.h"
27 using content::BrowserContext;
28 using content::RenderViewHost;
31 // The duration for any animations that might be invoked by this controller.
32 const NSTimeInterval kAnimationDuration = 0.2;
34 // There should only be one extension popup showing at one time. Keep a
35 // reference to it here.
36 static ExtensionPopupController* gPopup;
38 // Given a value and a rage, clamp the value into the range.
39 CGFloat Clamp(CGFloat value, CGFloat min, CGFloat max) {
40 return std::max(min, std::min(max, value));
45 @interface ExtensionPopupController (Private)
46 // Callers should be using the public static method for initialization.
47 // NOTE: This takes ownership of |host|.
48 - (id)initWithHost:(extensions::ExtensionViewHost*)host
49 parentWindow:(NSWindow*)parentWindow
50 anchoredAt:(NSPoint)anchoredAt
51 arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
52 devMode:(BOOL)devMode;
54 // Called when the extension's hosted NSView has been resized.
55 - (void)extensionViewFrameChanged;
57 // Called when the extension's size changes.
58 - (void)onSizeChanged:(NSSize)newSize;
60 // Called when the extension view is shown.
61 - (void)onViewDidShow;
64 class ExtensionPopupContainer : public ExtensionViewMac::Container {
66 explicit ExtensionPopupContainer(ExtensionPopupController* controller)
67 : controller_(controller) {
70 virtual void OnExtensionSizeChanged(
71 ExtensionViewMac* view,
72 const gfx::Size& new_size) OVERRIDE {
73 [controller_ onSizeChanged:
74 NSMakeSize(new_size.width(), new_size.height())];
77 virtual void OnExtensionViewDidShow(ExtensionViewMac* view) OVERRIDE {
78 [controller_ onViewDidShow];
82 ExtensionPopupController* controller_; // Weak; owns this.
85 class DevtoolsNotificationBridge : public content::NotificationObserver {
87 explicit DevtoolsNotificationBridge(ExtensionPopupController* controller)
88 : controller_(controller),
89 render_view_host_([controller_ extensionViewHost]->render_view_host()),
90 devtools_callback_(base::Bind(
91 &DevtoolsNotificationBridge::OnDevToolsStateChanged,
92 base::Unretained(this))) {
93 content::DevToolsManager::GetInstance()->AddAgentStateCallback(
97 virtual ~DevtoolsNotificationBridge() {
98 content::DevToolsManager::GetInstance()->RemoveAgentStateCallback(
102 void OnDevToolsStateChanged(content::DevToolsAgentHost* agent_host,
104 if (agent_host->GetRenderViewHost() != render_view_host_)
108 // Set the flag on the controller so the popup is not hidden when
109 // the dev tools get focus.
110 [controller_ setBeingInspected:YES];
112 // Allow the devtools to finish detaching before we close the popup.
113 [controller_ performSelector:@selector(close)
119 virtual void Observe(
121 const content::NotificationSource& source,
122 const content::NotificationDetails& details) OVERRIDE {
124 case chrome::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING: {
125 if (content::Details<extensions::ExtensionViewHost>(
126 [controller_ extensionViewHost]) == details) {
127 [controller_ showDevTools];
132 NOTREACHED() << "Received unexpected notification";
139 ExtensionPopupController* controller_;
140 // RenderViewHost for controller. Hold onto this separately because we need to
141 // know what it is for notifications, but our ExtensionViewHost may not be
143 RenderViewHost* render_view_host_;
144 base::Callback<void(content::DevToolsAgentHost*, bool)> devtools_callback_;
147 @implementation ExtensionPopupController
149 - (id)initWithHost:(extensions::ExtensionViewHost*)host
150 parentWindow:(NSWindow*)parentWindow
151 anchoredAt:(NSPoint)anchoredAt
152 arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
153 devMode:(BOOL)devMode {
154 base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
155 initWithContentRect:ui::kWindowSizeDeterminedLater
156 styleMask:NSBorderlessWindowMask
157 backing:NSBackingStoreBuffered
162 anchoredAt = [parentWindow convertBaseToScreen:anchoredAt];
163 if ((self = [super initWithWindow:window
164 parentWindow:parentWindow
165 anchoredAt:anchoredAt])) {
167 beingInspected_ = devMode;
168 ignoreWindowDidResignKey_ = NO;
170 InfoBubbleView* view = self.bubble;
171 [view setArrowLocation:arrowLocation];
173 extensionView_ = host->view()->native_view();
174 container_.reset(new ExtensionPopupContainer(self));
175 host->view()->set_container(container_.get());
177 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
178 [center addObserver:self
179 selector:@selector(extensionViewFrameChanged)
180 name:NSViewFrameDidChangeNotification
181 object:extensionView_];
183 [view addSubview:extensionView_];
185 notificationBridge_.reset(new DevtoolsNotificationBridge(self));
186 registrar_.reset(new content::NotificationRegistrar);
187 if (beingInspected_) {
188 // Listen for the extension to finish loading so the dev tools can be
190 registrar_->Add(notificationBridge_.get(),
191 chrome::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING,
192 content::Source<BrowserContext>(host->browser_context()));
199 [[NSNotificationCenter defaultCenter] removeObserver:self];
203 - (void)showDevTools {
204 DevToolsWindow::OpenDevToolsWindow(host_->render_view_host());
208 // |windowWillClose:| could have already been called. http://crbug.com/279505
210 web_modal::WebContentsModalDialogManager* modalDialogManager =
211 web_modal::WebContentsModalDialogManager::FromWebContents(
212 host_->host_contents());
213 if (modalDialogManager &&
214 modalDialogManager->IsDialogActive()) {
221 - (void)windowWillClose:(NSNotification *)notification {
222 [super windowWillClose:notification];
226 host_->view()->set_container(NULL);
230 - (void)windowDidResignKey:(NSNotification*)notification {
231 // |windowWillClose:| could have already been called. http://crbug.com/279505
233 // When a modal dialog is opened on top of the popup and when it's closed,
234 // it steals key-ness from the popup. Don't close the popup when this
235 // happens. There's an extra windowDidResignKey: notification after the
236 // modal dialog closes that should also be ignored.
237 web_modal::WebContentsModalDialogManager* modalDialogManager =
238 web_modal::WebContentsModalDialogManager::FromWebContents(
239 host_->host_contents());
240 if (modalDialogManager &&
241 modalDialogManager->IsDialogActive()) {
242 ignoreWindowDidResignKey_ = YES;
245 if (ignoreWindowDidResignKey_) {
246 ignoreWindowDidResignKey_ = NO;
250 if (!beingInspected_)
251 [super windowDidResignKey:notification];
255 return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
258 - (extensions::ExtensionViewHost*)extensionViewHost {
262 - (void)setBeingInspected:(BOOL)beingInspected {
263 beingInspected_ = beingInspected;
266 + (ExtensionPopupController*)showURL:(GURL)url
267 inBrowser:(Browser*)browser
268 anchoredAt:(NSPoint)anchoredAt
269 arrowLocation:(info_bubble::BubbleArrowLocation)
271 devMode:(BOOL)devMode {
272 DCHECK([NSThread isMainThread]);
277 extensions::ExtensionViewHost* host =
278 extensions::ExtensionViewHostFactory::CreatePopupHost(url, browser);
285 // Takes ownership of |host|. Also will autorelease itself when the popup is
286 // closed, so no need to do that here.
287 gPopup = [[ExtensionPopupController alloc]
289 parentWindow:browser->window()->GetNativeWindow()
290 anchoredAt:anchoredAt
291 arrowLocation:arrowLocation
296 + (ExtensionPopupController*)popup {
300 - (void)extensionViewFrameChanged {
301 // If there are no changes in the width or height of the frame, then ignore.
302 if (NSEqualSizes([extensionView_ frame].size, extensionFrame_.size))
305 extensionFrame_ = [extensionView_ frame];
306 // Constrain the size of the view.
307 [extensionView_ setFrameSize:NSMakeSize(
308 Clamp(NSWidth(extensionFrame_),
309 ExtensionViewMac::kMinWidth,
310 ExtensionViewMac::kMaxWidth),
311 Clamp(NSHeight(extensionFrame_),
312 ExtensionViewMac::kMinHeight,
313 ExtensionViewMac::kMaxHeight))];
315 // Pad the window by half of the rounded corner radius to prevent the
316 // extension's view from bleeding out over the corners.
317 CGFloat inset = info_bubble::kBubbleCornerRadius / 2.0;
318 [extensionView_ setFrameOrigin:NSMakePoint(inset, inset)];
320 NSRect frame = [extensionView_ frame];
321 frame.size.height += info_bubble::kBubbleArrowHeight +
322 info_bubble::kBubbleCornerRadius;
323 frame.size.width += info_bubble::kBubbleCornerRadius;
324 frame = [extensionView_ convertRect:frame toView:nil];
325 // Adjust the origin according to the height and width so that the arrow is
326 // positioned correctly at the middle and slightly down from the button.
327 NSPoint windowOrigin = self.anchorPoint;
328 NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
329 info_bubble::kBubbleArrowWidth / 2.0,
330 info_bubble::kBubbleArrowHeight / 2.0);
331 offsets = [extensionView_ convertSize:offsets toView:nil];
332 windowOrigin.x -= NSWidth(frame) - offsets.width;
333 windowOrigin.y -= NSHeight(frame) - offsets.height;
334 frame.origin = windowOrigin;
336 // Is the window still animating in? If so, then cancel that and create a new
337 // animation setting the opacity and new frame value. Otherwise the current
338 // animation will continue after this frame is set, reverting the frame to
339 // what it was when the animation started.
340 NSWindow* window = [self window];
341 id animator = [window animator];
342 if ([window isVisible] &&
343 ([animator alphaValue] < 1.0 ||
344 !NSEqualRects([window frame], [animator frame]))) {
345 [NSAnimationContext beginGrouping];
346 [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
347 [animator setAlphaValue:1.0];
348 [animator setFrame:frame display:YES];
349 [NSAnimationContext endGrouping];
351 [window setFrame:frame display:YES];
354 // A NSViewFrameDidChangeNotification won't be sent until the extension view
355 // content is loaded. The window is hidden on init, so show it the first time
356 // the notification is fired (and consequently the view contents have loaded).
357 if (![window isVisible]) {
358 [self showWindow:self];
362 - (void)onSizeChanged:(NSSize)newSize {
363 // When we update the size, the window will become visible. Stay hidden until
364 // the host is loaded.
365 pendingSize_ = newSize;
366 if (!host_->did_stop_loading())
369 // No need to use CA here, our caller calls us repeatedly to animate the
371 NSRect frame = [extensionView_ frame];
372 frame.size = newSize;
374 // |new_size| is in pixels. Convert to view units.
375 frame.size = [extensionView_ convertSize:frame.size fromView:nil];
377 [extensionView_ setFrame:frame];
378 [extensionView_ setNeedsDisplay:YES];
381 - (void)onViewDidShow {
382 [self onSizeChanged:pendingSize_];
385 - (void)windowDidResize:(NSNotification*)notification {
386 // Let the extension view know, so that it can tell plugins.
388 host_->view()->WindowFrameChanged();
391 - (void)windowDidMove:(NSNotification*)notification {
392 // Let the extension view know, so that it can tell plugins.
394 host_->view()->WindowFrameChanged();
397 // Private (TestingAPI)
399 return extensionView_;
402 // Private (TestingAPI)
403 + (NSSize)minPopupSize {
404 NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
408 // Private (TestingAPI)
409 + (NSSize)maxPopupSize {
410 NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};