extension_popup_controller.mm revision 7dbb3d5cf0c15f500944d211057644d6a2f37371
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.
4
5#import "chrome/browser/ui/cocoa/extensions/extension_popup_controller.h"
6
7#include <algorithm>
8
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_host.h"
13#include "chrome/browser/extensions/extension_process_manager.h"
14#include "chrome/browser/extensions/extension_system.h"
15#include "chrome/browser/profiles/profile.h"
16#include "chrome/browser/ui/browser.h"
17#import "chrome/browser/ui/cocoa/browser_window_cocoa.h"
18#import "chrome/browser/ui/cocoa/extensions/extension_view_mac.h"
19#import "chrome/browser/ui/cocoa/info_bubble_window.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"
26
27using content::RenderViewHost;
28
29namespace {
30// The duration for any animations that might be invoked by this controller.
31const NSTimeInterval kAnimationDuration = 0.2;
32
33// There should only be one extension popup showing at one time. Keep a
34// reference to it here.
35static ExtensionPopupController* gPopup;
36
37// Given a value and a rage, clamp the value into the range.
38CGFloat Clamp(CGFloat value, CGFloat min, CGFloat max) {
39  return std::max(min, std::min(max, value));
40}
41
42}  // namespace
43
44@interface ExtensionPopupController (Private)
45// Callers should be using the public static method for initialization.
46// NOTE: This takes ownership of |host|.
47- (id)initWithHost:(extensions::ExtensionHost*)host
48      parentWindow:(NSWindow*)parentWindow
49        anchoredAt:(NSPoint)anchoredAt
50     arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
51           devMode:(BOOL)devMode;
52
53// Called when the extension's hosted NSView has been resized.
54- (void)extensionViewFrameChanged;
55
56// Called when the extension's size changes.
57- (void)onSizeChanged:(NSSize)newSize;
58
59// Called when the extension view is shown.
60- (void)onViewDidShow;
61@end
62
63class ExtensionPopupContainer : public ExtensionViewMac::Container {
64 public:
65  explicit ExtensionPopupContainer(ExtensionPopupController* controller)
66      : controller_(controller) {
67  }
68
69  virtual void OnExtensionSizeChanged(
70      ExtensionViewMac* view,
71      const gfx::Size& new_size) OVERRIDE {
72    [controller_ onSizeChanged:
73        NSMakeSize(new_size.width(), new_size.height())];
74  }
75
76  virtual void OnExtensionViewDidShow(ExtensionViewMac* view) OVERRIDE {
77    [controller_ onViewDidShow];
78  }
79
80 private:
81  ExtensionPopupController* controller_; // Weak; owns this.
82};
83
84class DevtoolsNotificationBridge : public content::NotificationObserver {
85 public:
86  explicit DevtoolsNotificationBridge(ExtensionPopupController* controller)
87    : controller_(controller),
88      render_view_host_([controller_ extensionHost]->render_view_host()),
89      devtools_callback_(base::Bind(
90          &DevtoolsNotificationBridge::OnDevToolsStateChanged,
91          base::Unretained(this))) {
92    content::DevToolsManager::GetInstance()->AddAgentStateCallback(
93        devtools_callback_);
94  }
95
96  virtual ~DevtoolsNotificationBridge() {
97    content::DevToolsManager::GetInstance()->RemoveAgentStateCallback(
98        devtools_callback_);
99  }
100
101  void OnDevToolsStateChanged(content::DevToolsAgentHost* agent_host,
102                              bool attached) {
103    if (agent_host->GetRenderViewHost() != render_view_host_)
104      return;
105
106    if (attached) {
107      // Set the flag on the controller so the popup is not hidden when
108      // the dev tools get focus.
109      [controller_ setBeingInspected:YES];
110    } else {
111      // Allow the devtools to finish detaching before we close the popup.
112      [controller_ performSelector:@selector(close)
113                        withObject:nil
114                        afterDelay:0.0];
115    }
116  }
117
118  virtual void Observe(
119      int type,
120      const content::NotificationSource& source,
121      const content::NotificationDetails& details) OVERRIDE {
122    switch (type) {
123      case chrome::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING: {
124        if (content::Details<extensions::ExtensionHost>(
125                [controller_ extensionHost]) == details) {
126          [controller_ showDevTools];
127        }
128        break;
129      }
130      default: {
131        NOTREACHED() << "Received unexpected notification";
132        break;
133      }
134    };
135  }
136
137 private:
138  ExtensionPopupController* controller_;
139  // RenderViewHost for controller. Hold onto this separately because we need to
140  // know what it is for notifications, but our ExtensionHost may not be valid.
141  RenderViewHost* render_view_host_;
142  base::Callback<void(content::DevToolsAgentHost*, bool)> devtools_callback_;
143};
144
145@implementation ExtensionPopupController
146
147- (id)initWithHost:(extensions::ExtensionHost*)host
148      parentWindow:(NSWindow*)parentWindow
149        anchoredAt:(NSPoint)anchoredAt
150     arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
151           devMode:(BOOL)devMode {
152  base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
153      initWithContentRect:ui::kWindowSizeDeterminedLater
154                styleMask:NSBorderlessWindowMask
155                  backing:NSBackingStoreBuffered
156                    defer:YES]);
157  if (!window.get())
158    return nil;
159
160  anchoredAt = [parentWindow convertBaseToScreen:anchoredAt];
161  if ((self = [super initWithWindow:window
162                       parentWindow:parentWindow
163                         anchoredAt:anchoredAt])) {
164    host_.reset(host);
165    beingInspected_ = devMode;
166
167    InfoBubbleView* view = self.bubble;
168    [view setArrowLocation:arrowLocation];
169
170    extensionView_ = host->view()->native_view();
171    container_.reset(new ExtensionPopupContainer(self));
172    host->view()->set_container(container_.get());
173
174    NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
175    [center addObserver:self
176               selector:@selector(extensionViewFrameChanged)
177                   name:NSViewFrameDidChangeNotification
178                 object:extensionView_];
179
180    [view addSubview:extensionView_];
181
182    notificationBridge_.reset(new DevtoolsNotificationBridge(self));
183    registrar_.reset(new content::NotificationRegistrar);
184    if (beingInspected_) {
185      // Listen for the extension to finish loading so the dev tools can be
186      // opened.
187      registrar_->Add(notificationBridge_.get(),
188                      chrome::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING,
189                      content::Source<Profile>(host->profile()));
190    }
191  }
192  return self;
193}
194
195- (void)dealloc {
196  [[NSNotificationCenter defaultCenter] removeObserver:self];
197  [super dealloc];
198}
199
200- (void)showDevTools {
201  DevToolsWindow::OpenDevToolsWindow(host_->render_view_host());
202}
203
204- (void)windowWillClose:(NSNotification *)notification {
205  [super windowWillClose:notification];
206  gPopup = nil;
207  if (host_->view())
208    host_->view()->set_container(NULL);
209  host_.reset();
210}
211
212- (void)windowDidResignKey:(NSNotification*)notification {
213  if (!beingInspected_)
214    [super windowDidResignKey:notification];
215}
216
217- (BOOL)isClosing {
218  return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
219}
220
221- (extensions::ExtensionHost*)extensionHost {
222  return host_.get();
223}
224
225- (void)setBeingInspected:(BOOL)beingInspected {
226  beingInspected_ = beingInspected;
227}
228
229+ (ExtensionPopupController*)showURL:(GURL)url
230                           inBrowser:(Browser*)browser
231                          anchoredAt:(NSPoint)anchoredAt
232                       arrowLocation:(info_bubble::BubbleArrowLocation)
233                                         arrowLocation
234                             devMode:(BOOL)devMode {
235  DCHECK([NSThread isMainThread]);
236  DCHECK(browser);
237  if (!browser)
238    return nil;
239
240  ExtensionProcessManager* manager =
241      extensions::ExtensionSystem::Get(browser->profile())->process_manager();
242  DCHECK(manager);
243  if (!manager)
244    return nil;
245
246  extensions::ExtensionHost* host = manager->CreatePopupHost(url, browser);
247  DCHECK(host);
248  if (!host)
249    return nil;
250
251  // Make absolutely sure that no popups are leaked.
252  if (gPopup) {
253    if ([[gPopup window] isVisible])
254      [gPopup close];
255
256    [gPopup autorelease];
257    gPopup = nil;
258  }
259  DCHECK(!gPopup);
260
261  // Takes ownership of |host|. Also will autorelease itself when the popup is
262  // closed, so no need to do that here.
263  gPopup = [[ExtensionPopupController alloc]
264      initWithHost:host
265      parentWindow:browser->window()->GetNativeWindow()
266        anchoredAt:anchoredAt
267     arrowLocation:arrowLocation
268           devMode:devMode];
269  return gPopup;
270}
271
272+ (ExtensionPopupController*)popup {
273  return gPopup;
274}
275
276- (void)extensionViewFrameChanged {
277  // If there are no changes in the width or height of the frame, then ignore.
278  if (NSEqualSizes([extensionView_ frame].size, extensionFrame_.size))
279    return;
280
281  extensionFrame_ = [extensionView_ frame];
282  // Constrain the size of the view.
283  [extensionView_ setFrameSize:NSMakeSize(
284      Clamp(NSWidth(extensionFrame_),
285            ExtensionViewMac::kMinWidth,
286            ExtensionViewMac::kMaxWidth),
287      Clamp(NSHeight(extensionFrame_),
288            ExtensionViewMac::kMinHeight,
289            ExtensionViewMac::kMaxHeight))];
290
291  // Pad the window by half of the rounded corner radius to prevent the
292  // extension's view from bleeding out over the corners.
293  CGFloat inset = info_bubble::kBubbleCornerRadius / 2.0;
294  [extensionView_ setFrameOrigin:NSMakePoint(inset, inset)];
295
296  NSRect frame = [extensionView_ frame];
297  frame.size.height += info_bubble::kBubbleArrowHeight +
298                       info_bubble::kBubbleCornerRadius;
299  frame.size.width += info_bubble::kBubbleCornerRadius;
300  frame = [extensionView_ convertRect:frame toView:nil];
301  // Adjust the origin according to the height and width so that the arrow is
302  // positioned correctly at the middle and slightly down from the button.
303  NSPoint windowOrigin = self.anchorPoint;
304  NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset +
305                                  info_bubble::kBubbleArrowWidth / 2.0,
306                              info_bubble::kBubbleArrowHeight / 2.0);
307  offsets = [extensionView_ convertSize:offsets toView:nil];
308  windowOrigin.x -= NSWidth(frame) - offsets.width;
309  windowOrigin.y -= NSHeight(frame) - offsets.height;
310  frame.origin = windowOrigin;
311
312  // Is the window still animating in? If so, then cancel that and create a new
313  // animation setting the opacity and new frame value. Otherwise the current
314  // animation will continue after this frame is set, reverting the frame to
315  // what it was when the animation started.
316  NSWindow* window = [self window];
317  if ([window isVisible] && [[window animator] alphaValue] < 1.0) {
318    [NSAnimationContext beginGrouping];
319    [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
320    [[window animator] setAlphaValue:1.0];
321    [[window animator] setFrame:frame display:YES];
322    [NSAnimationContext endGrouping];
323  } else {
324    [window setFrame:frame display:YES];
325  }
326
327  // A NSViewFrameDidChangeNotification won't be sent until the extension view
328  // content is loaded. The window is hidden on init, so show it the first time
329  // the notification is fired (and consequently the view contents have loaded).
330  if (![window isVisible]) {
331    [self showWindow:self];
332  }
333}
334
335- (void)onSizeChanged:(NSSize)newSize {
336  // When we update the size, the window will become visible. Stay hidden until
337  // the host is loaded.
338  pendingSize_ = newSize;
339  if (!host_->did_stop_loading())
340    return;
341
342  // No need to use CA here, our caller calls us repeatedly to animate the
343  // resizing.
344  NSRect frame = [extensionView_ frame];
345  frame.size = newSize;
346
347  // |new_size| is in pixels. Convert to view units.
348  frame.size = [extensionView_ convertSize:frame.size fromView:nil];
349
350  [extensionView_ setFrame:frame];
351  [extensionView_ setNeedsDisplay:YES];
352}
353
354- (void)onViewDidShow {
355  [self onSizeChanged:pendingSize_];
356}
357
358- (void)windowDidResize:(NSNotification*)notification {
359  // Let the extension view know, so that it can tell plugins.
360  if (host_->view())
361    host_->view()->WindowFrameChanged();
362}
363
364- (void)windowDidMove:(NSNotification*)notification {
365  // Let the extension view know, so that it can tell plugins.
366  if (host_->view())
367    host_->view()->WindowFrameChanged();
368}
369
370// Private (TestingAPI)
371- (NSView*)view {
372  return extensionView_;
373}
374
375// Private (TestingAPI)
376+ (NSSize)minPopupSize {
377  NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
378  return minSize;
379}
380
381// Private (TestingAPI)
382+ (NSSize)maxPopupSize {
383  NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};
384  return maxSize;
385}
386
387@end
388