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