Upstream version 10.39.225.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/common/url_constants.h"
16 #include "chrome/browser/ui/browser.h"
17 #include "chrome/browser/ui/browser_finder.h"
18 #import "chrome/browser/ui/cocoa/browser_window_cocoa.h"
19 #import "chrome/browser/ui/cocoa/extensions/extension_view_mac.h"
20 #import "chrome/browser/ui/cocoa/info_bubble_window.h"
21 #include "components/web_modal/popup_manager.h"
22 #include "content/public/browser/devtools_agent_host.h"
23 #include "content/public/browser/notification_details.h"
24 #include "content/public/browser/notification_registrar.h"
25 #include "content/public/browser/notification_source.h"
26 #include "ui/base/cocoa/window_size_constants.h"
27
28 using content::BrowserContext;
29 using content::RenderViewHost;
30 using content::WebContents;
31
32 namespace {
33 // The duration for any animations that might be invoked by this controller.
34 const NSTimeInterval kAnimationDuration = 0.2;
35
36 // There should only be one extension popup showing at one time. Keep a
37 // reference to it here.
38 static ExtensionPopupController* gPopup;
39
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));
43 }
44
45 }  // namespace
46
47 @interface ExtensionPopupController (Private)
48 // Callers should be using the public static method for initialization.
49 // NOTE: This takes ownership of |host|.
50 - (id)initWithHost:(extensions::ExtensionViewHost*)host
51       parentWindow:(NSWindow*)parentWindow
52         anchoredAt:(NSPoint)anchoredAt
53      arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
54            devMode:(BOOL)devMode;
55
56 // Called when the extension's hosted NSView has been resized.
57 - (void)extensionViewFrameChanged;
58
59 // Called when the extension's size changes.
60 - (void)onSizeChanged:(NSSize)newSize;
61
62 // Called when the extension view is shown.
63 - (void)onViewDidShow;
64
65 // Called when the window moves or resizes. Notifies the extension.
66 - (void)onWindowChanged;
67
68 @end
69
70 class ExtensionPopupContainer : public ExtensionViewMac::Container {
71  public:
72   explicit ExtensionPopupContainer(ExtensionPopupController* controller)
73       : controller_(controller) {
74   }
75
76   virtual void OnExtensionSizeChanged(
77       ExtensionViewMac* view,
78       const gfx::Size& new_size) OVERRIDE {
79     [controller_ onSizeChanged:
80         NSMakeSize(new_size.width(), new_size.height())];
81   }
82
83   virtual void OnExtensionViewDidShow(ExtensionViewMac* view) OVERRIDE {
84     [controller_ onViewDidShow];
85   }
86
87  private:
88   ExtensionPopupController* controller_; // Weak; owns this.
89 };
90
91 class DevtoolsNotificationBridge : public content::NotificationObserver {
92  public:
93   explicit DevtoolsNotificationBridge(ExtensionPopupController* controller)
94     : controller_(controller),
95       web_contents_([controller_ extensionViewHost]->host_contents()),
96       devtools_callback_(base::Bind(
97           &DevtoolsNotificationBridge::OnDevToolsStateChanged,
98           base::Unretained(this))) {
99     content::DevToolsAgentHost::AddAgentStateCallback(devtools_callback_);
100   }
101
102   virtual ~DevtoolsNotificationBridge() {
103     content::DevToolsAgentHost::RemoveAgentStateCallback(devtools_callback_);
104   }
105
106   void OnDevToolsStateChanged(content::DevToolsAgentHost* agent_host,
107                               bool attached) {
108     if (agent_host->GetWebContents() != web_contents_)
109       return;
110
111     if (attached) {
112       // Set the flag on the controller so the popup is not hidden when
113       // the dev tools get focus.
114       [controller_ setBeingInspected:YES];
115     } else {
116       // Allow the devtools to finish detaching before we close the popup.
117       [controller_ performSelector:@selector(close)
118                         withObject:nil
119                         afterDelay:0.0];
120     }
121   }
122
123   virtual void Observe(
124       int type,
125       const content::NotificationSource& source,
126       const content::NotificationDetails& details) OVERRIDE {
127     switch (type) {
128       case extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING: {
129         if (content::Details<extensions::ExtensionViewHost>(
130                 [controller_ extensionViewHost]) == details) {
131           [controller_ showDevTools];
132         }
133         break;
134       }
135       default: {
136         NOTREACHED() << "Received unexpected notification";
137         break;
138       }
139     };
140   }
141
142  private:
143   ExtensionPopupController* controller_;
144   // WebContents for controller. Hold onto this separately because we need to
145   // know what it is for notifications, but our ExtensionViewHost may not be
146   // valid.
147   WebContents* web_contents_;
148   base::Callback<void(content::DevToolsAgentHost*, bool)> devtools_callback_;
149 };
150
151 @implementation ExtensionPopupController
152
153 - (id)initWithHost:(extensions::ExtensionViewHost*)host
154       parentWindow:(NSWindow*)parentWindow
155         anchoredAt:(NSPoint)anchoredAt
156      arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
157            devMode:(BOOL)devMode {
158   base::scoped_nsobject<InfoBubbleWindow> window([[InfoBubbleWindow alloc]
159       initWithContentRect:ui::kWindowSizeDeterminedLater
160                 styleMask:NSBorderlessWindowMask
161                   backing:NSBackingStoreBuffered
162                     defer:YES]);
163   if (!window.get())
164     return nil;
165
166   anchoredAt = [parentWindow convertBaseToScreen:anchoredAt];
167   if ((self = [super initWithWindow:window
168                        parentWindow:parentWindow
169                          anchoredAt:anchoredAt])) {
170     host_.reset(host);
171     beingInspected_ = devMode;
172     ignoreWindowDidResignKey_ = NO;
173
174     InfoBubbleView* view = self.bubble;
175     [view setArrowLocation:arrowLocation];
176
177     extensionView_ = host->view()->GetNativeView();
178     container_.reset(new ExtensionPopupContainer(self));
179     static_cast<ExtensionViewMac*>(host->view())
180         ->set_container(container_.get());
181
182     NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
183     [center addObserver:self
184                selector:@selector(extensionViewFrameChanged)
185                    name:NSViewFrameDidChangeNotification
186                  object:extensionView_];
187
188     [view addSubview:extensionView_];
189
190     notificationBridge_.reset(new DevtoolsNotificationBridge(self));
191     registrar_.reset(new content::NotificationRegistrar);
192     if (beingInspected_) {
193       // Listen for the extension to finish loading so the dev tools can be
194       // opened.
195       registrar_->Add(notificationBridge_.get(),
196                       extensions::NOTIFICATION_EXTENSION_HOST_DID_STOP_LOADING,
197                       content::Source<BrowserContext>(host->browser_context()));
198     }
199   }
200   return self;
201 }
202
203 - (void)dealloc {
204   [[NSNotificationCenter defaultCenter] removeObserver:self];
205   [super dealloc];
206 }
207
208 - (void)showDevTools {
209   DevToolsWindow::OpenDevToolsWindow(host_->host_contents());
210 }
211
212 - (void)close {
213   // |windowWillClose:| could have already been called. http://crbug.com/279505
214   if (host_) {
215     // TODO(gbillock): Change this API to say directly if the current popup
216     // should block tab close? This is a bit over-reaching.
217     web_modal::PopupManager* popup_manager =
218         web_modal::PopupManager::FromWebContents(host_->host_contents());
219     if (popup_manager && popup_manager->IsWebModalDialogActive(
220             host_->host_contents())) {
221       return;
222     }
223   }
224   [super close];
225 }
226
227 - (void)windowWillClose:(NSNotification *)notification {
228   [super windowWillClose:notification];
229   if (gPopup == self)
230     gPopup = nil;
231   if (host_->view())
232     static_cast<ExtensionViewMac*>(host_->view())->set_container(NULL);
233   host_.reset();
234 }
235
236 - (void)windowDidResignKey:(NSNotification*)notification {
237   // |windowWillClose:| could have already been called. http://crbug.com/279505
238   if (host_) {
239     // When a modal dialog is opened on top of the popup and when it's closed,
240     // it steals key-ness from the popup. Don't close the popup when this
241     // happens. There's an extra windowDidResignKey: notification after the
242     // modal dialog closes that should also be ignored.
243     web_modal::PopupManager* popupManager =
244         web_modal::PopupManager::FromWebContents(
245             host_->host_contents());
246     if (popupManager &&
247         popupManager->IsWebModalDialogActive(host_->host_contents())) {
248       ignoreWindowDidResignKey_ = YES;
249       return;
250     }
251     if (ignoreWindowDidResignKey_) {
252       ignoreWindowDidResignKey_ = NO;
253       return;
254     }
255   }
256   if (!beingInspected_)
257     [super windowDidResignKey:notification];
258 }
259
260 - (BOOL)isClosing {
261   return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
262 }
263
264 - (extensions::ExtensionViewHost*)extensionViewHost {
265   return host_.get();
266 }
267
268 - (void)setBeingInspected:(BOOL)beingInspected {
269   beingInspected_ = beingInspected;
270 }
271
272 + (ExtensionPopupController*)showURL:(GURL)url
273                            inBrowser:(Browser*)browser
274                           anchoredAt:(NSPoint)anchoredAt
275                        arrowLocation:(info_bubble::BubbleArrowLocation)
276                                          arrowLocation
277                              devMode:(BOOL)devMode {
278   DCHECK([NSThread isMainThread]);
279   DCHECK(browser);
280   if (!browser)
281     return nil;
282
283   // If we click the browser/page action again, we should close the popup.
284   // Make Mac behavior the same with Windows and others.
285   if (gPopup) {
286     std::string extension_id = url.host();
287     if (url.SchemeIs(content::kChromeUIScheme) &&
288         url.host() == chrome::kChromeUIExtensionInfoHost)
289       extension_id = url.path().substr(1);
290     extensions::ExtensionViewHost* host = [gPopup extensionViewHost];
291     if (extension_id == host->extension_id()) {
292       [gPopup close];
293       return nil;
294     }
295   }
296
297   extensions::ExtensionViewHost* host =
298       extensions::ExtensionViewHostFactory::CreatePopupHost(url, browser);
299   DCHECK(host);
300   if (!host)
301     return nil;
302
303   [gPopup close];
304
305   // Takes ownership of |host|. Also will autorelease itself when the popup is
306   // closed, so no need to do that here.
307   gPopup = [[ExtensionPopupController alloc]
308       initWithHost:host
309       parentWindow:browser->window()->GetNativeWindow()
310         anchoredAt:anchoredAt
311      arrowLocation:arrowLocation
312            devMode:devMode];
313   return gPopup;
314 }
315
316 + (ExtensionPopupController*)popup {
317   return gPopup;
318 }
319
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;
324
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))];
334
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)];
339
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;
355
356   // Is the window still animating in? If so, then cancel that and create a new
357   // animation setting the opacity and new frame value. Otherwise the current
358   // animation will continue after this frame is set, reverting the frame to
359   // what it was when the animation started.
360   NSWindow* window = [self window];
361   id animator = [window animator];
362   if ([window isVisible] &&
363       ([animator alphaValue] < 1.0 ||
364        !NSEqualRects([window frame], [animator frame]))) {
365     [NSAnimationContext beginGrouping];
366     [[NSAnimationContext currentContext] setDuration:kAnimationDuration];
367     [animator setAlphaValue:1.0];
368     [animator setFrame:frame display:YES];
369     [NSAnimationContext endGrouping];
370   } else {
371     [window setFrame:frame display:YES];
372   }
373
374   // A NSViewFrameDidChangeNotification won't be sent until the extension view
375   // content is loaded. The window is hidden on init, so show it the first time
376   // the notification is fired (and consequently the view contents have loaded).
377   if (![window isVisible]) {
378     [self showWindow:self];
379   }
380 }
381
382 - (void)onSizeChanged:(NSSize)newSize {
383   // When we update the size, the window will become visible. Stay hidden until
384   // the host is loaded.
385   pendingSize_ = newSize;
386   if (!host_->did_stop_loading())
387     return;
388
389   // No need to use CA here, our caller calls us repeatedly to animate the
390   // resizing.
391   NSRect frame = [extensionView_ frame];
392   frame.size = newSize;
393
394   // |new_size| is in pixels. Convert to view units.
395   frame.size = [extensionView_ convertSize:frame.size fromView:nil];
396
397   [extensionView_ setFrame:frame];
398   [extensionView_ setNeedsDisplay:YES];
399 }
400
401 - (void)onViewDidShow {
402   [self onSizeChanged:pendingSize_];
403 }
404
405 - (void)onWindowChanged {
406   // The window is positioned before creating the host, to ensure the host is
407   // created with the correct screen information.
408   if (!host_)
409     return;
410
411   ExtensionViewMac* extensionView =
412       static_cast<ExtensionViewMac*>(host_->view());
413   // Let the extension view know, so that it can tell plugins.
414   if (extensionView)
415     extensionView->WindowFrameChanged();
416 }
417
418 - (void)windowDidResize:(NSNotification*)notification {
419   [self onWindowChanged];
420 }
421
422 - (void)windowDidMove:(NSNotification*)notification {
423   [self onWindowChanged];
424 }
425
426 // Private (TestingAPI)
427 - (NSView*)view {
428   return extensionView_;
429 }
430
431 // Private (TestingAPI)
432 + (NSSize)minPopupSize {
433   NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight};
434   return minSize;
435 }
436
437 // Private (TestingAPI)
438 + (NSSize)maxPopupSize {
439   NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight};
440   return maxSize;
441 }
442
443 @end