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