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