blob: 483d17bb157a508a4983a2ea143c93eefeb345f6 [file] [log] [blame]
// 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 <utility>
#include "base/callback.h"
#include "base/macros.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/ui/browser.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 "chrome/common/url_constants.h"
#include "components/web_modal/web_contents_modal_dialog_manager.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_source.h"
#include "extensions/browser/notification_types.h"
#include "ui/base/cocoa/cocoa_base_utils.h"
#include "ui/base/cocoa/window_size_constants.h"
using content::BrowserContext;
using content::RenderViewHost;
using content::WebContents;
using extensions::ExtensionViewHost;
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.
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));
}
BOOL gAnimationsEnabled = true;
} // namespace
@interface ExtensionPopupController (Private)
// Callers should be using the public static method for initialization.
- (id)initWithParentWindow:(NSWindow*)parentWindow
anchoredAt:(NSPoint)anchoredAt
arrowLocation:(info_bubble::BubbleArrowLocation)arrowLocation
devMode:(BOOL)devMode;
// Set the ExtensionViewHost, taking ownership.
- (void)setExtensionViewHost:(scoped_ptr<ExtensionViewHost>)host;
// 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;
@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 ExtensionPopupNotificationBridge : public content::NotificationObserver {
public:
ExtensionPopupNotificationBridge(ExtensionPopupController* controller,
ExtensionViewHost* view_host)
: controller_(controller),
view_host_(view_host),
web_contents_(view_host_->host_contents()),
devtools_callback_(base::Bind(
&ExtensionPopupNotificationBridge::OnDevToolsStateChanged,
base::Unretained(this))) {
content::DevToolsAgentHost::AddAgentStateCallback(devtools_callback_);
}
~ExtensionPopupNotificationBridge() 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_FIRST_LOAD:
if (content::Details<ExtensionViewHost>(view_host_) == details)
[controller_ showDevTools];
break;
case extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE:
if (content::Details<ExtensionViewHost>(view_host_) == details &&
![controller_ isClosing]) {
[controller_ close];
}
break;
default:
NOTREACHED() << "Received unexpected notification";
break;
}
}
private:
ExtensionPopupController* controller_;
extensions::ExtensionViewHost* view_host_;
// 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_;
DISALLOW_COPY_AND_ASSIGN(ExtensionPopupNotificationBridge);
};
@implementation ExtensionPopupController
@synthesize extensionId = extensionId_;
- (id)initWithParentWindow:(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:NO]);
if (!window.get())
return nil;
anchoredAt = ui::ConvertPointFromWindowToScreen(parentWindow, anchoredAt);
if ((self = [super initWithWindow:window
parentWindow:parentWindow
anchoredAt:anchoredAt])) {
beingInspected_ = devMode;
ignoreWindowDidResignKey_ = NO;
[[self bubble] setArrowLocation:arrowLocation];
if (!gAnimationsEnabled)
[window setAllowedAnimations:info_bubble::kAnimateNone];
}
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.
const web_modal::WebContentsModalDialogManager* manager =
web_modal::WebContentsModalDialogManager::FromWebContents(
host_->host_contents());
if (manager && manager->IsDialogActive())
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.
const web_modal::WebContentsModalDialogManager* manager =
web_modal::WebContentsModalDialogManager::FromWebContents(
host_->host_contents());
if (manager && manager->IsDialogActive()) {
ignoreWindowDidResignKey_ = YES;
return;
}
if (ignoreWindowDidResignKey_) {
ignoreWindowDidResignKey_ = NO;
return;
}
}
if (!beingInspected_)
[super windowDidResignKey:notification];
}
- (BOOL)isClosing {
return [static_cast<InfoBubbleWindow*>([self window]) isClosing];
}
- (ExtensionViewHost*)extensionViewHost {
return host_.get();
}
- (void)setBeingInspected:(BOOL)beingInspected {
beingInspected_ = beingInspected;
}
+ (ExtensionPopupController*)host:(scoped_ptr<ExtensionViewHost>)host
inBrowser:(Browser*)browser
anchoredAt:(NSPoint)anchoredAt
arrowLocation:(info_bubble::BubbleArrowLocation)
arrowLocation
devMode:(BOOL)devMode {
DCHECK([NSThread isMainThread]);
DCHECK(browser);
DCHECK(host);
if (gPopup)
[gPopup close]; // Starts the animation to fade out the popup.
// Create the popup first. This establishes an initially hidden NSWindow so
// that the renderer is able to gather correct screen metrics for the initial
// paint.
gPopup = [[ExtensionPopupController alloc]
initWithParentWindow:browser->window()->GetNativeWindow()
anchoredAt:anchoredAt
arrowLocation:arrowLocation
devMode:devMode];
[gPopup setExtensionViewHost:std::move(host)];
return gPopup;
}
+ (ExtensionPopupController*)popup {
return gPopup;
}
- (void)setExtensionViewHost:(scoped_ptr<ExtensionViewHost>)host {
DCHECK(!host_);
DCHECK(host);
host_.swap(host);
extensionId_ = host_->extension_id();
container_.reset(new ExtensionPopupContainer(self));
ExtensionViewMac* hostView = static_cast<ExtensionViewMac*>(host_->view());
hostView->set_container(container_.get());
hostView->CreateWidgetHostViewIn([self bubble]);
extensionView_ = hostView->GetNativeView();
NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:@selector(extensionViewFrameChanged)
name:NSViewFrameDidChangeNotification
object:extensionView_];
notificationBridge_.reset(
new ExtensionPopupNotificationBridge(self, host_.get()));
content::Source<BrowserContext> source_context(host_->browser_context());
registrar_.Add(notificationBridge_.get(),
extensions::NOTIFICATION_EXTENSION_HOST_VIEW_SHOULD_CLOSE,
source_context);
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_FIRST_LOAD,
source_context);
}
}
- (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_ || !host_->has_loaded_once())
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_];
}
// Private (TestingAPI)
+ (void)setAnimationsEnabledForTesting:(BOOL)enabled {
gAnimationsEnabled = enabled;
}
// 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