extension_popup_controller.mm revision ddb351dbec246cf1fab5ec20d2d5520909041de1
1// Copyright (c) 2011 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.
4
5#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
6
7#include <algorithm>
8
9#include "chrome/browser/debugger/devtools_manager.h"
10#include "chrome/browser/extensions/extension_host.h"
11#include "chrome/browser/extensions/extension_process_manager.h"
12#include "chrome/browser/profiles/profile.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 "content/common/notification_details.h"
18#include "content/common/notification_registrar.h"
19#include "content/common/notification_source.h"
20
21namespace {
22// The duration for any animations that might be invoked by this controller.
23const NSTimeInterval kAnimationDuration = 0.2;
24
25// There should only be one extension popup showing at one time. Keep a
26// reference to it here.
27static ExtensionPopupController* gPopup;
28
29// Given a value and a rage, clamp the value into the range.
30CGFloat Clamp(CGFloat value, CGFloat min, CGFloat max) {
31  return std::max(min, std::min(max, value));
32}
33
34}  // namespace
35
36class DevtoolsNotificationBridge : public NotificationObserver {
37 public:
38  explicit DevtoolsNotificationBridge(ExtensionPopupController* controller)
39    : controller_(controller) {}
40
41  void Observe(NotificationType type,
42               const NotificationSource& source,
43               const NotificationDetails& details) {
44    switch (type.value) {
45      case NotificationType::EXTENSION_HOST_DID_STOP_LOADING: {
46        if (Details<ExtensionHost>([controller_ extensionHost]) == details)
47          [controller_ showDevTools];
48        break;
49      }
50      case NotificationType::DEVTOOLS_WINDOW_CLOSING: {
51        RenderViewHost* rvh = [controller_ extensionHost]->render_view_host();
52        if (Details<RenderViewHost>(rvh) == details)
53          // Allow the devtools to finish detaching before we close the popup
54          [controller_ performSelector:@selector(close)
55                            withObject:nil
56                            afterDelay:0.0];
57        break;
58      }
59      default: {
60        NOTREACHED() << "Received unexpected notification";
61        break;
62      }
63    };
64  }
65
66 private:
67  ExtensionPopupController* controller_;
68};
69
70@interface ExtensionPopupController(Private)
71// Callers should be using the public static method for initialization.
72// NOTE: This takes ownership of |host|.
73- (id)initWithHost:(ExtensionHost*)host
74      parentWindow:(NSWindow*)parentWindow
75        anchoredAt:(NSPoint)anchoredAt
76     arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
77           devMode:(BOOL)devMode;
78
79// Called when the extension's hosted NSView has been resized.
80- (void)extensionViewFrameChanged;
81@end
82
83@implementation ExtensionPopupController
84
85- (id)initWithHost:(ExtensionHost*)host
86      parentWindow:(NSWindow*)parentWindow
87        anchoredAt:(NSPoint)anchoredAt
88     arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
89           devMode:(BOOL)devMode {
90
91  parentWindow_ = parentWindow;
92  anchor_ = [parentWindow convertBaseToScreen:anchoredAt];
93  host_.reset(host);
94  beingInspected_ = devMode;
95
96  scoped_nsobject<InfoBubbleView> view([[InfoBubbleView alloc] init]);
97  if (!view.get())
98    return nil;
99  [view setArrowLocation:arrowLocation];
100
101  host->view()->set_is_toolstrip(NO);
102
103  extensionView_ = host->view()->native_view();
104  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
105  [center addObserver:self
106             selector:@selector(extensionViewFrameChanged)
107                 name:NSViewFrameDidChangeNotification
108               object:extensionView_];
109
110  // Watch to see if the parent window closes, and if so, close this one.
111  [center addObserver:self
112             selector:@selector(parentWindowWillClose:)
113                 name:NSWindowWillCloseNotification
114               object:parentWindow_];
115
116  [view addSubview:extensionView_];
117  scoped_nsobject<InfoBubbleWindow> window(
118      [[InfoBubbleWindow alloc]
119          initWithContentRect:NSZeroRect
120                    styleMask:NSBorderlessWindowMask
121                      backing:NSBackingStoreBuffered
122                        defer:YES]);
123  if (!window.get())
124    return nil;
125
126  [window setDelegate:self];
127  [window setContentView:view];
128  self = [super initWithWindow:window];
129  if (beingInspected_) {
130    // Listen for the the devtools window closing.
131    notificationBridge_.reset(new DevtoolsNotificationBridge(self));
132    registrar_.reset(new NotificationRegistrar);
133    registrar_->Add(notificationBridge_.get(),
134                    NotificationType::DEVTOOLS_WINDOW_CLOSING,
135                    Source<Profile>(host->profile()));
136    registrar_->Add(notificationBridge_.get(),
137                    NotificationType::EXTENSION_HOST_DID_STOP_LOADING,
138                    Source<Profile>(host->profile()));
139  }
140  return self;
141}
142
143- (void)showDevTools {
144  DevToolsManager::GetInstance()->OpenDevToolsWindow(host_->render_view_host());
145}
146
147- (void)dealloc {
148  [[NSNotificationCenter defaultCenter] removeObserver:self];
149  [super dealloc];
150}
151
152- (void)parentWindowWillClose:(NSNotification*)notification {
153  [self close];
154}
155
156- (void)windowWillClose:(NSNotification *)notification {
157  [[NSNotificationCenter defaultCenter] removeObserver:self];
158  [gPopup autorelease];
159  gPopup = nil;
160}
161
162- (void)windowDidResignKey:(NSNotification *)notification {
163  NSWindow* window = [self window];
164  DCHECK_EQ([notification object], window);
165  // If the window isn't visible, it is already closed, and this notification
166  // has been sent as part of the closing operation, so no need to close.
167  if ([window isVisible] && !beingInspected_) {
168    [self close];
169  }
170}
171
172- (void)close {
173  [parentWindow_ removeChildWindow:[self window]];
174
175  // No longer have a parent window, so nil out the pointer and deregister for
176  // notifications.
177  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
178  [center removeObserver:self
179                    name:NSWindowWillCloseNotification
180                  object:parentWindow_];
181  parentWindow_ = nil;
182  [super close];
183}
184
185- (BOOL)isClosing {
186  return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
187}
188
189- (ExtensionHost*)extensionHost {
190  return host_.get();
191}
192
193+ (ExtensionPopupController*)showURL:(GURL)url
194                           inBrowser:(Browser*)browser
195                          anchoredAt:(NSPoint)anchoredAt
196                       arrowLocation:(info_bubble::BubbleArrowLocation)
197                                         arrowLocation
198                             devMode:(BOOL)devMode {
199  DCHECK([NSThread isMainThread]);
200  DCHECK(browser);
201  if (!browser)
202    return nil;
203
204  ExtensionProcessManager* manager =
205      browser->profile()->GetExtensionProcessManager();
206  DCHECK(manager);
207  if (!manager)
208    return nil;
209
210  ExtensionHost* host = manager->CreatePopup(url, browser);
211  DCHECK(host);
212  if (!host)
213    return nil;
214
215  // Make absolutely sure that no popups are leaked.
216  if (gPopup) {
217    if ([[gPopup window] isVisible])
218      [gPopup close];
219
220    [gPopup autorelease];
221    gPopup = nil;
222  }
223  DCHECK(!gPopup);
224
225  // Takes ownership of |host|. Also will autorelease itself when the popup is
226  // closed, so no need to do that here.
227  gPopup = [[ExtensionPopupController alloc]
228      initWithHost:host
229      parentWindow:browser->window()->GetNativeHandle()
230        anchoredAt:anchoredAt
231     arrowLocation:arrowLocation
232           devMode:devMode];
233  return gPopup;
234}
235
236+ (ExtensionPopupController*)popup {
237  return gPopup;
238}
239
240- (void)extensionViewFrameChanged {
241  // If there are no changes in the width or height of the frame, then ignore.
242  if (NSEqualSizes([extensionView_ frame].size, extensionFrame_.size))
243    return;
244
245  extensionFrame_ = [extensionView_ frame];
246  // Constrain the size of the view.
247  [extensionView_ setFrameSize:NSMakeSize(
248      Clamp(NSWidth(extensionFrame_),
249            ExtensionViewMac::kMinWidth,
250            ExtensionViewMac::kMaxWidth),
251      Clamp(NSHeight(extensionFrame_),
252            ExtensionViewMac::kMinHeight,
253            ExtensionViewMac::kMaxHeight))];
254
255  // Pad the window by half of the rounded corner radius to prevent the
256  // extension's view from bleeding out over the corners.
257  CGFloat inset = info_bubble::kBubbleCornerRadius / 2.0;
258  [extensionView_ setFrameOrigin:NSMakePoint(inset, inset)];
259
260  NSRect frame = [extensionView_ frame];
261  frame.size.height += info_bubble::kBubbleArrowHeight +
262                       info_bubble::kBubbleCornerRadius;
263  frame.size.width += info_bubble::kBubbleCornerRadius;
264  frame = [extensionView_ convertRectToBase:frame];
265  // Adjust the origin according to the height and width so that the arrow is
266  // positioned correctly at the middle and slightly down from the button.
267  NSPoint windowOrigin = anchor_;
268  NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
269                                  info_bubble::kBubbleArrowWidth / 2.0,
270                              info_bubble::kBubbleArrowHeight / 2.0);
271  offsets = [extensionView_ convertSize:offsets toView:nil];
272  windowOrigin.x -= NSWidth(frame) - offsets.width;
273  windowOrigin.y -= NSHeight(frame) - offsets.height;
274  frame.origin = windowOrigin;
275
276  // Is the window still animating in? If so, then cancel that and create a new
277  // animation setting the opacity and new frame value. Otherwise the current
278  // animation will continue after this frame is set, reverting the frame to
279  // what it was when the animation started.
280  NSWindow* window = [self window];
281  if ([window isVisible] && [[window animator] alphaValue] < 1.0) {
282    [NSAnimationContext beginGrouping];
283    [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
284    [[window animator] setAlphaValue:1.0];
285    [[window animator] setFrame:frame display:YES];
286    [NSAnimationContext endGrouping];
287  } else {
288    [window setFrame:frame display:YES];
289  }
290
291  // A NSViewFrameDidChangeNotification won't be sent until the extension view
292  // content is loaded. The window is hidden on init, so show it the first time
293  // the notification is fired (and consequently the view contents have loaded).
294  if (![window isVisible]) {
295    [self showWindow:self];
296  }
297}
298
299// We want this to be a child of a browser window. addChildWindow: (called from
300// this function) will bring the window on-screen; unfortunately,
301// [NSWindowController showWindow:] will also bring it on-screen (but will cause
302// unexpected changes to the window's position). We cannot have an
303// addChildWindow: and a subsequent showWindow:. Thus, we have our own version.
304- (void)showWindow:(id)sender {
305  [parentWindow_ addChildWindow:[self window] ordered:NSWindowAbove];
306  [[self window] makeKeyAndOrderFront:self];
307}
308
309- (void)windowDidResize:(NSNotification*)notification {
310  // Let the extension view know, so that it can tell plugins.
311  if (host_->view())
312    host_->view()->WindowFrameChanged();
313}
314
315- (void)windowDidMove:(NSNotification*)notification {
316  // Let the extension view know, so that it can tell plugins.
317  if (host_->view())
318    host_->view()->WindowFrameChanged();
319}
320
321// Private (TestingAPI)
322- (NSView*)view {
323  return extensionView_;
324}
325
326// Private (TestingAPI)
327+ (NSSize)minPopupSize {
328  NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
329  return minSize;
330}
331
332// Private (TestingAPI)
333+ (NSSize)maxPopupSize {
334  NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};
335  return maxSize;
336}
337
338@end
339