Upstream version 9.37.195.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / ui / cocoa / extensions / extension_popup_controller.mm
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_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"
26
27 using content::BrowserContext;
28 using content::RenderViewHost;
29
30 namespace {
31 // The duration for any animations that might be invoked by this controller.
32 const NSTimeInterval kAnimationDuration = 0.2;
33
34 // There should only be one extension popup showing at one time. Keep a
35 // reference to it here.
36 static ExtensionPopupController* gPopup;
37
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));
41 }
42
43 }  // namespace
44
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;
53
54 // Called when the extension's hosted NSView has been resized.
55 - (void)extensionViewFrameChanged;
56
57 // Called when the extension's size changes.
58 - (void)onSizeChanged:(NSSize)newSize;
59
60 // Called when the extension view is shown.
61 - (void)onViewDidShow;
62 @end
63
64 class ExtensionPopupContainer : public ExtensionViewMac::Container {
65  public:
66   explicit ExtensionPopupContainer(ExtensionPopupController* controller)
67       : controller_(controller) {
68   }
69
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())];
75   }
76
77   virtual void OnExtensionViewDidShow(ExtensionViewMac* view) OVERRIDE {
78     [controller_ onViewDidShow];
79   }
80
81  private:
82   ExtensionPopupController* controller_; // Weak; owns this.
83 };
84
85 class DevtoolsNotificationBridge : public content::NotificationObserver {
86  public:
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(
94         devtools_callback_);
95   }
96
97   virtual ~DevtoolsNotificationBridge() {
98     content::DevToolsManager::GetInstance()->RemoveAgentStateCallback(
99         devtools_callback_);
100   }
101
102   void OnDevToolsStateChanged(content::DevToolsAgentHost* agent_host,
103                               bool attached) {
104     if (agent_host->GetRenderViewHost() != render_view_host_)
105       return;
106
107     if (attached) {
108       // Set the flag on the controller so the popup is not hidden when
109       // the dev tools get focus.
110       [controller_ setBeingInspected:YES];
111     } else {
112       // Allow the devtools to finish detaching before we close the popup.
113       [controller_ performSelector:@selector(close)
114                         withObject:nil
115                         afterDelay:0.0];
116     }
117   }
118
119   virtual void Observe(
120       int type,
121       const content::NotificationSource& source,
122       const content::NotificationDetails& details) OVERRIDE {
123     switch (type) {
124       case chrome::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING: {
125         if (content::Details<extensions::ExtensionViewHost>(
126                 [controller_ extensionViewHost]) == details) {
127           [controller_ showDevTools];
128         }
129         break;
130       }
131       default: {
132         NOTREACHED() << "Received unexpected notification";
133         break;
134       }
135     };
136   }
137
138  private:
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
142   // valid.
143   RenderViewHost* render_view_host_;
144   base::Callback<void(content::DevToolsAgentHost*, bool)> devtools_callback_;
145 };
146
147 @implementation ExtensionPopupController
148
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
158                     defer:YES]);
159   if (!window.get())
160     return nil;
161
162   anchoredAt = [parentWindow convertBaseToScreen:anchoredAt];
163   if ((self = [super initWithWindow:window
164                        parentWindow:parentWindow
165                          anchoredAt:anchoredAt])) {
166     host_.reset(host);
167     beingInspected_ = devMode;
168     ignoreWindowDidResignKey_ = NO;
169
170     InfoBubbleView* view = self.bubble;
171     [view setArrowLocation:arrowLocation];
172
173     extensionView_ = host->view()->native_view();
174     container_.reset(new ExtensionPopupContainer(self));
175     host->view()->set_container(container_.get());
176
177     NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
178     [center addObserver:self
179                selector:@selector(extensionViewFrameChanged)
180                    name:NSViewFrameDidChangeNotification
181                  object:extensionView_];
182
183     [view addSubview:extensionView_];
184
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
189       // opened.
190       registrar_->Add(notificationBridge_.get(),
191                       chrome::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING,
192                       content::Source<BrowserContext>(host->browser_context()));
193     }
194   }
195   return self;
196 }
197
198 - (void)dealloc {
199   [[NSNotificationCenter defaultCenter] removeObserver:self];
200   [super dealloc];
201 }
202
203 - (void)showDevTools {
204   DevToolsWindow::OpenDevToolsWindow(host_->render_view_host());
205 }
206
207 - (void)close {
208   // |windowWillClose:| could have already been called. http://crbug.com/279505
209   if (host_) {
210     web_modal::WebContentsModalDialogManager* modalDialogManager =
211         web_modal::WebContentsModalDialogManager::FromWebContents(
212             host_->host_contents());
213     if (modalDialogManager &&
214         modalDialogManager->IsDialogActive()) {
215       return;
216     }
217   }
218   [super close];
219 }
220
221 - (void)windowWillClose:(NSNotification *)notification {
222   [super windowWillClose:notification];
223   if (gPopup == self)
224     gPopup = nil;
225   if (host_->view())
226     host_->view()->set_container(NULL);
227   host_.reset();
228 }
229
230 - (void)windowDidResignKey:(NSNotification*)notification {
231   // |windowWillClose:| could have already been called. http://crbug.com/279505
232   if (host_) {
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;
243       return;
244     }
245     if (ignoreWindowDidResignKey_) {
246       ignoreWindowDidResignKey_ = NO;
247       return;
248     }
249   }
250   if (!beingInspected_)
251     [super windowDidResignKey:notification];
252 }
253
254 - (BOOL)isClosing {
255   return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
256 }
257
258 - (extensions::ExtensionViewHost*)extensionViewHost {
259   return host_.get();
260 }
261
262 - (void)setBeingInspected:(BOOL)beingInspected {
263   beingInspected_ = beingInspected;
264 }
265
266 + (ExtensionPopupController*)showURL:(GURL)url
267                            inBrowser:(Browser*)browser
268                           anchoredAt:(NSPoint)anchoredAt
269                        arrowLocation:(info_bubble::BubbleArrowLocation)
270                                          arrowLocation
271                              devMode:(BOOL)devMode {
272   DCHECK([NSThread isMainThread]);
273   DCHECK(browser);
274   if (!browser)
275     return nil;
276
277   extensions::ExtensionViewHost* host =
278       extensions::ExtensionViewHostFactory::CreatePopupHost(url, browser);
279   DCHECK(host);
280   if (!host)
281     return nil;
282
283   [gPopup close];
284
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]
288       initWithHost:host
289       parentWindow:browser->window()->GetNativeWindow()
290         anchoredAt:anchoredAt
291      arrowLocation:arrowLocation
292            devMode:devMode];
293   return gPopup;
294 }
295
296 + (ExtensionPopupController*)popup {
297   return gPopup;
298 }
299
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))
303     return;
304
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))];
314
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)];
319
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;
335
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];
350   } else {
351     [window setFrame:frame display:YES];
352   }
353
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];
359   }
360 }
361
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())
367     return;
368
369   // No need to use CA here, our caller calls us repeatedly to animate the
370   // resizing.
371   NSRect frame = [extensionView_ frame];
372   frame.size = newSize;
373
374   // |new_size| is in pixels. Convert to view units.
375   frame.size = [extensionView_ convertSize:frame.size fromView:nil];
376
377   [extensionView_ setFrame:frame];
378   [extensionView_ setNeedsDisplay:YES];
379 }
380
381 - (void)onViewDidShow {
382   [self onSizeChanged:pendingSize_];
383 }
384
385 - (void)windowDidResize:(NSNotification*)notification {
386   // Let the extension view know, so that it can tell plugins.
387   if (host_->view())
388     host_->view()->WindowFrameChanged();
389 }
390
391 - (void)windowDidMove:(NSNotification*)notification {
392   // Let the extension view know, so that it can tell plugins.
393   if (host_->view())
394     host_->view()->WindowFrameChanged();
395 }
396
397 // Private (TestingAPI)
398 - (NSView*)view {
399   return extensionView_;
400 }
401
402 // Private (TestingAPI)
403 + (NSSize)minPopupSize {
404   NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
405   return minSize;
406 }
407
408 // Private (TestingAPI)
409 + (NSSize)maxPopupSize {
410   NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};
411   return maxSize;
412 }
413
414 @end