1 // Copyright (c) 2011 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 "chrome/browser/debugger/devtools_manager.h" 10 #include "chrome/browser/extensions/extension_host.h" 11 #include "chrome/browser/extensions/extension_process_manager.h" 12 #include "chrome/browser/profiles/profile.h" 13 #include "chrome/browser/ui/browser.h" 14 #import "chrome/browser/ui/cocoa/browser_window_cocoa.h" 15 #import "chrome/browser/ui/cocoa/extensions/extension_view_mac.h" 16 #import "chrome/browser/ui/cocoa/info_bubble_window.h" 17 #include "content/common/notification_details.h" 18 #include "content/common/notification_registrar.h" 19 #include "content/common/notification_source.h" 20 21 namespace { 22 // The duration for any animations that might be invoked by this controller. 23 const NSTimeInterval kAnimationDuration = 0.2; 24 25 // There should only be one extension popup showing at one time. Keep a 26 // reference to it here. 27 static ExtensionPopupController* gPopup; 28 29 // Given a value and a rage, clamp the value into the range. 30 CGFloat Clamp(CGFloat value, CGFloat min, CGFloat max) { 31 return std::max(min, std::min(max, value)); 32 } 33 34 } // namespace 35 36 class DevtoolsNotificationBridge : public NotificationObserver { 37 public: 38 explicit DevtoolsNotificationBridge(ExtensionPopupController* controller) 39 : controller_(controller) {} 40 41 void Observe(NotificationType type, 42 const NotificationSource& source, 43 const NotificationDetails& details) { 44 switch (type.value) { 45 case NotificationType::EXTENSION_HOST_DID_STOP_LOADING: { 46 if (Details<ExtensionHost>([controller_ extensionHost]) == details) 47 [controller_ showDevTools]; 48 break; 49 } 50 case NotificationType::DEVTOOLS_WINDOW_CLOSING: { 51 RenderViewHost* rvh = [controller_ extensionHost]->render_view_host(); 52 if (Details<RenderViewHost>(rvh) == details) 53 // Allow the devtools to finish detaching before we close the popup 54 [controller_ performSelector:@selector(close) 55 withObject:nil 56 afterDelay:0.0]; 57 break; 58 } 59 default: { 60 NOTREACHED() << "Received unexpected notification"; 61 break; 62 } 63 }; 64 } 65 66 private: 67 ExtensionPopupController* controller_; 68 }; 69 70 @interface ExtensionPopupController(Private) 71 // Callers should be using the public static method for initialization. 72 // NOTE: This takes ownership of |host|. 73 - (id)initWithHost:(ExtensionHost*)host 74 parentWindow:(NSWindow*)parentWindow 75 anchoredAt:(NSPoint)anchoredAt 76 arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation 77 devMode:(BOOL)devMode; 78 79 // Called when the extension's hosted NSView has been resized. 80 - (void)extensionViewFrameChanged; 81 @end 82 83 @implementation ExtensionPopupController 84 85 - (id)initWithHost:(ExtensionHost*)host 86 parentWindow:(NSWindow*)parentWindow 87 anchoredAt:(NSPoint)anchoredAt 88 arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation 89 devMode:(BOOL)devMode { 90 91 parentWindow_ = parentWindow; 92 anchor_ = [parentWindow convertBaseToScreen:anchoredAt]; 93 host_.reset(host); 94 beingInspected_ = devMode; 95 96 scoped_nsobject<InfoBubbleView> view([[InfoBubbleView alloc] init]); 97 if (!view.get()) 98 return nil; 99 [view setArrowLocation:arrowLocation]; 100 101 host->view()->set_is_toolstrip(NO); 102 103 extensionView_ = host->view()->native_view(); 104 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 105 [center addObserver:self 106 selector:@selector(extensionViewFrameChanged) 107 name:NSViewFrameDidChangeNotification 108 object:extensionView_]; 109 110 // Watch to see if the parent window closes, and if so, close this one. 111 [center addObserver:self 112 selector:@selector(parentWindowWillClose:) 113 name:NSWindowWillCloseNotification 114 object:parentWindow_]; 115 116 [view addSubview:extensionView_]; 117 scoped_nsobject<InfoBubbleWindow> window( 118 [[InfoBubbleWindow alloc] 119 initWithContentRect:NSZeroRect 120 styleMask:NSBorderlessWindowMask 121 backing:NSBackingStoreBuffered 122 defer:YES]); 123 if (!window.get()) 124 return nil; 125 126 [window setDelegate:self]; 127 [window setContentView:view]; 128 self = [super initWithWindow:window]; 129 if (beingInspected_) { 130 // Listen for the the devtools window closing. 131 notificationBridge_.reset(new DevtoolsNotificationBridge(self)); 132 registrar_.reset(new NotificationRegistrar); 133 registrar_->Add(notificationBridge_.get(), 134 NotificationType::DEVTOOLS_WINDOW_CLOSING, 135 Source<Profile>(host->profile())); 136 registrar_->Add(notificationBridge_.get(), 137 NotificationType::EXTENSION_HOST_DID_STOP_LOADING, 138 Source<Profile>(host->profile())); 139 } 140 return self; 141 } 142 143 - (void)showDevTools { 144 DevToolsManager::GetInstance()->OpenDevToolsWindow(host_->render_view_host()); 145 } 146 147 - (void)dealloc { 148 [[NSNotificationCenter defaultCenter] removeObserver:self]; 149 [super dealloc]; 150 } 151 152 - (void)parentWindowWillClose:(NSNotification*)notification { 153 [self close]; 154 } 155 156 - (void)windowWillClose:(NSNotification *)notification { 157 [[NSNotificationCenter defaultCenter] removeObserver:self]; 158 [gPopup autorelease]; 159 gPopup = nil; 160 } 161 162 - (void)windowDidResignKey:(NSNotification *)notification { 163 NSWindow* window = [self window]; 164 DCHECK_EQ([notification object], window); 165 // If the window isn't visible, it is already closed, and this notification 166 // has been sent as part of the closing operation, so no need to close. 167 if ([window isVisible] && !beingInspected_) { 168 [self close]; 169 } 170 } 171 172 - (void)close { 173 [parentWindow_ removeChildWindow:[self window]]; 174 175 // No longer have a parent window, so nil out the pointer and deregister for 176 // notifications. 177 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 178 [center removeObserver:self 179 name:NSWindowWillCloseNotification 180 object:parentWindow_]; 181 parentWindow_ = nil; 182 [super close]; 183 } 184 185 - (BOOL)isClosing { 186 return [static_cast<InfoBubbleWindow*>([self window]) isClosing]; 187 } 188 189 - (ExtensionHost*)extensionHost { 190 return host_.get(); 191 } 192 193 + (ExtensionPopupController*)showURL:(GURL)url 194 inBrowser:(Browser*)browser 195 anchoredAt:(NSPoint)anchoredAt 196 arrowLocation:(info_bubble::BubbleArrowLocation) 197 arrowLocation 198 devMode:(BOOL)devMode { 199 DCHECK([NSThread isMainThread]); 200 DCHECK(browser); 201 if (!browser) 202 return nil; 203 204 ExtensionProcessManager* manager = 205 browser->profile()->GetExtensionProcessManager(); 206 DCHECK(manager); 207 if (!manager) 208 return nil; 209 210 ExtensionHost* host = manager->CreatePopup(url, browser); 211 DCHECK(host); 212 if (!host) 213 return nil; 214 215 // Make absolutely sure that no popups are leaked. 216 if (gPopup) { 217 if ([[gPopup window] isVisible]) 218 [gPopup close]; 219 220 [gPopup autorelease]; 221 gPopup = nil; 222 } 223 DCHECK(!gPopup); 224 225 // Takes ownership of |host|. Also will autorelease itself when the popup is 226 // closed, so no need to do that here. 227 gPopup = [[ExtensionPopupController alloc] 228 initWithHost:host 229 parentWindow:browser->window()->GetNativeHandle() 230 anchoredAt:anchoredAt 231 arrowLocation:arrowLocation 232 devMode:devMode]; 233 return gPopup; 234 } 235 236 + (ExtensionPopupController*)popup { 237 return gPopup; 238 } 239 240 - (void)extensionViewFrameChanged { 241 // If there are no changes in the width or height of the frame, then ignore. 242 if (NSEqualSizes([extensionView_ frame].size, extensionFrame_.size)) 243 return; 244 245 extensionFrame_ = [extensionView_ frame]; 246 // Constrain the size of the view. 247 [extensionView_ setFrameSize:NSMakeSize( 248 Clamp(NSWidth(extensionFrame_), 249 ExtensionViewMac::kMinWidth, 250 ExtensionViewMac::kMaxWidth), 251 Clamp(NSHeight(extensionFrame_), 252 ExtensionViewMac::kMinHeight, 253 ExtensionViewMac::kMaxHeight))]; 254 255 // Pad the window by half of the rounded corner radius to prevent the 256 // extension's view from bleeding out over the corners. 257 CGFloat inset = info_bubble::kBubbleCornerRadius / 2.0; 258 [extensionView_ setFrameOrigin:NSMakePoint(inset, inset)]; 259 260 NSRect frame = [extensionView_ frame]; 261 frame.size.height += info_bubble::kBubbleArrowHeight + 262 info_bubble::kBubbleCornerRadius; 263 frame.size.width += info_bubble::kBubbleCornerRadius; 264 frame = [extensionView_ convertRectToBase:frame]; 265 // Adjust the origin according to the height and width so that the arrow is 266 // positioned correctly at the middle and slightly down from the button. 267 NSPoint windowOrigin = anchor_; 268 NSSize offsets = NSMakeSize(info_bubble::kBubbleArrowXOffset + 269 info_bubble::kBubbleArrowWidth / 2.0, 270 info_bubble::kBubbleArrowHeight / 2.0); 271 offsets = [extensionView_ convertSize:offsets toView:nil]; 272 windowOrigin.x -= NSWidth(frame) - offsets.width; 273 windowOrigin.y -= NSHeight(frame) - offsets.height; 274 frame.origin = windowOrigin; 275 276 // Is the window still animating in? If so, then cancel that and create a new 277 // animation setting the opacity and new frame value. Otherwise the current 278 // animation will continue after this frame is set, reverting the frame to 279 // what it was when the animation started. 280 NSWindow* window = [self window]; 281 if ([window isVisible] && [[window animator] alphaValue] < 1.0) { 282 [NSAnimationContext beginGrouping]; 283 [[NSAnimationContext currentContext] setDuration:kAnimationDuration]; 284 [[window animator] setAlphaValue:1.0]; 285 [[window animator] setFrame:frame display:YES]; 286 [NSAnimationContext endGrouping]; 287 } else { 288 [window setFrame:frame display:YES]; 289 } 290 291 // A NSViewFrameDidChangeNotification won't be sent until the extension view 292 // content is loaded. The window is hidden on init, so show it the first time 293 // the notification is fired (and consequently the view contents have loaded). 294 if (![window isVisible]) { 295 [self showWindow:self]; 296 } 297 } 298 299 // We want this to be a child of a browser window. addChildWindow: (called from 300 // this function) will bring the window on-screen; unfortunately, 301 // [NSWindowController showWindow:] will also bring it on-screen (but will cause 302 // unexpected changes to the window's position). We cannot have an 303 // addChildWindow: and a subsequent showWindow:. Thus, we have our own version. 304 - (void)showWindow:(id)sender { 305 [parentWindow_ addChildWindow:[self window] ordered:NSWindowAbove]; 306 [[self window] makeKeyAndOrderFront:self]; 307 } 308 309 - (void)windowDidResize:(NSNotification*)notification { 310 // Let the extension view know, so that it can tell plugins. 311 if (host_->view()) 312 host_->view()->WindowFrameChanged(); 313 } 314 315 - (void)windowDidMove:(NSNotification*)notification { 316 // Let the extension view know, so that it can tell plugins. 317 if (host_->view()) 318 host_->view()->WindowFrameChanged(); 319 } 320 321 // Private (TestingAPI) 322 - (NSView*)view { 323 return extensionView_; 324 } 325 326 // Private (TestingAPI) 327 + (NSSize)minPopupSize { 328 NSSize minSize = {ExtensionViewMac::kMinWidth, ExtensionViewMac::kMinHeight}; 329 return minSize; 330 } 331 332 // Private (TestingAPI) 333 + (NSSize)maxPopupSize { 334 NSSize maxSize = {ExtensionViewMac::kMaxWidth, ExtensionViewMac::kMaxHeight}; 335 return maxSize; 336 } 337 338 @end 339