blob: 454bd60dda3841c714f431eaaa5acc4a8f44e527 [file] [log] [blame]
// Copyright (c) 2009 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.
#include "chrome/browser/views/extensions/extension_popup.h"
#include "chrome/browser/browser_list.h"
#include "chrome/browser/browser_window.h"
#include "chrome/browser/debugger/devtools_manager.h"
#include "chrome/browser/debugger/devtools_toggle_action.h"
#include "chrome/browser/extensions/extension_host.h"
#include "chrome/browser/extensions/extension_process_manager.h"
#include "chrome/browser/profile.h"
#include "chrome/browser/renderer_host/render_widget_host_view.h"
#include "chrome/browser/renderer_host/render_view_host.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/views/frame/browser_view.h"
#include "chrome/browser/window_sizer.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/notification_details.h"
#include "chrome/common/notification_source.h"
#include "chrome/common/notification_type.h"
#include "third_party/skia/include/core/SkColor.h"
#include "views/widget/root_view.h"
#include "views/window/window.h"
#if defined(OS_LINUX)
#include "views/widget/widget_gtk.h"
#endif
#if defined(OS_CHROMEOS)
#include "chrome/browser/chromeos/wm_ipc.h"
#include "cros/chromeos_wm_ipc_enums.h"
#endif
using views::Widget;
// The minimum, and default maximum dimensions of the popup.
// The minimum is just a little larger than the size of the button itself.
// The default maximum is an arbitrary number that should be smaller than most
// screens.
const int ExtensionPopup::kMinWidth = 25;
const int ExtensionPopup::kMinHeight = 25;
const int ExtensionPopup::kMaxWidth = 800;
const int ExtensionPopup::kMaxHeight = 600;
namespace {
// The width, in pixels, of the black-border on a popup.
const int kPopupBorderWidth = 1;
const int kPopupBubbleCornerRadius = BubbleBorder::GetCornerRadius() / 2;
} // namespace
ExtensionPopup::ExtensionPopup(ExtensionHost* host,
views::Widget* frame,
const gfx::Rect& relative_to,
BubbleBorder::ArrowLocation arrow_location,
bool activate_on_show,
bool inspect_with_devtools,
PopupChrome chrome,
Observer* observer)
: BrowserBubble(host->view(),
frame,
gfx::Point(),
RECTANGLE_CHROME == chrome), // If no bubble chrome is to
// be displayed, then enable a
// drop-shadow on the bubble
// widget.
relative_to_(relative_to),
extension_host_(host),
activate_on_show_(activate_on_show),
inspect_with_devtools_(inspect_with_devtools),
close_on_lost_focus_(true),
closing_(false),
border_widget_(NULL),
border_(NULL),
border_view_(NULL),
popup_chrome_(chrome),
max_size_(kMaxWidth, kMaxHeight),
observer_(observer),
anchor_position_(arrow_location),
instance_lifetime_(new InternalRefCounter()){
AddRef(); // Balanced in Close();
set_delegate(this);
host->view()->SetContainer(this);
// We wait to show the popup until the contained host finishes loading.
registrar_.Add(this,
NotificationType::EXTENSION_HOST_DID_STOP_LOADING,
Source<Profile>(host->profile()));
// Listen for the containing view calling window.close();
registrar_.Add(this, NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE,
Source<Profile>(host->profile()));
// TODO(erikkay) Some of this border code is derived from InfoBubble.
// We should see if we can unify these classes.
// Keep relative_to_ in frame-relative coordinates to aid in drag
// positioning.
gfx::Point origin = relative_to_.origin();
views::View::ConvertPointToView(NULL, frame_->GetRootView(), &origin);
relative_to_.set_origin(origin);
// The bubble chrome requires a separate window, so construct it here.
if (BUBBLE_CHROME == popup_chrome_) {
gfx::NativeView native_window = frame->GetNativeView();
#if defined(OS_LINUX)
border_widget_ = new views::WidgetGtk(views::WidgetGtk::TYPE_WINDOW);
static_cast<views::WidgetGtk*>(border_widget_)->MakeTransparent();
static_cast<views::WidgetGtk*>(border_widget_)->make_transient_to_parent();
#else
border_widget_ = Widget::CreatePopupWidget(Widget::Transparent,
Widget::NotAcceptEvents,
Widget::DeleteOnDestroy,
Widget::MirrorOriginInRTL);
#endif
border_widget_->Init(native_window, bounds());
#if defined(OS_CHROMEOS)
chromeos::WmIpc::instance()->SetWindowType(
border_widget_->GetNativeView(),
chromeos::WM_IPC_WINDOW_CHROME_INFO_BUBBLE,
NULL);
#endif
border_ = new BubbleBorder(arrow_location);
border_view_ = new views::View;
border_view_->set_background(new BubbleBackground(border_));
border_view_->set_border(border_);
border_widget_->SetContentsView(border_view_);
// Ensure that the popup contents are always displayed ontop of the border
// widget.
border_widget_->MoveAbove(popup_);
} else {
// Otherwise simply set a black-border on the view containing the popup
// extension view.
views::Border* border = views::Border::CreateSolidBorder(kPopupBorderWidth,
SK_ColorBLACK);
view()->set_border(border);
}
}
ExtensionPopup::~ExtensionPopup() {
// The widget is set to delete on destroy, so no leak here.
if (border_widget_)
border_widget_->Close();
}
void ExtensionPopup::SetArrowPosition(
BubbleBorder::ArrowLocation arrow_location) {
DCHECK_NE(BubbleBorder::NONE, arrow_location) <<
"Extension popups must be positioned relative to an arrow.";
anchor_position_ = arrow_location;
if (border_)
border_->set_arrow_location(anchor_position_);
}
void ExtensionPopup::Hide() {
BrowserBubble::Hide();
if (border_widget_)
border_widget_->Hide();
}
void ExtensionPopup::Show(bool activate) {
if (visible())
return;
#if defined(OS_WIN)
if (frame_->GetWindow())
frame_->GetWindow()->DisableInactiveRendering();
#endif
ResizeToView();
// Show the border first, then the popup overlaid on top.
if (border_widget_)
border_widget_->Show();
BrowserBubble::Show(activate);
}
void ExtensionPopup::ResizeToView() {
if (observer_)
observer_->ExtensionPopupResized(this);
gfx::Rect rect = GetOuterBounds();
gfx::Point origin = rect.origin();
views::View::ConvertPointToView(NULL, frame_->GetRootView(), &origin);
if (border_widget_) {
// Set the bubble-chrome widget according to the outer bounds of the entire
// popup.
border_widget_->SetBounds(rect);
// Now calculate the inner bounds. This is a bit more convoluted than
// it should be because BrowserBubble coordinates are in Browser coordinates
// while |rect| is in screen coordinates.
gfx::Insets border_insets;
border_->GetInsets(&border_insets);
origin.set_x(origin.x() + border_insets.left() + kPopupBubbleCornerRadius);
origin.set_y(origin.y() + border_insets.top() + kPopupBubbleCornerRadius);
gfx::Size new_size = view()->size();
SetBounds(origin.x(), origin.y(), new_size.width(), new_size.height());
} else {
SetBounds(origin.x(), origin.y(), rect.width(), rect.height());
}
}
void ExtensionPopup::BubbleBrowserWindowMoved(BrowserBubble* bubble) {
ResizeToView();
}
void ExtensionPopup::BubbleBrowserWindowClosing(BrowserBubble* bubble) {
if (!closing_)
Close();
}
void ExtensionPopup::BubbleGotFocus(BrowserBubble* bubble) {
// Forward the focus to the renderer.
host()->render_view_host()->view()->Focus();
}
void ExtensionPopup::BubbleLostFocus(BrowserBubble* bubble,
bool lost_focus_to_child) {
if (closing_ || // We are already closing.
inspect_with_devtools_ || // The popup is being inspected.
!close_on_lost_focus_ || // Our client is handling focus listening.
lost_focus_to_child) // A child of this view got focus.
return;
// When we do close on BubbleLostFocus, we do it in the next event loop
// because a subsequent event in this loop may also want to close this popup
// and if so, we want to allow that. Example: Clicking the same browser
// action button that opened the popup. If we closed immediately, the
// browser action container would fail to discover that the same button
// was pressed.
MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod(this,
&ExtensionPopup::Close));
}
void ExtensionPopup::Observe(NotificationType type,
const NotificationSource& source,
const NotificationDetails& details) {
switch (type.value) {
case NotificationType::EXTENSION_HOST_DID_STOP_LOADING:
// Once we receive did stop loading, the content will be complete and
// the width will have been computed. Now it's safe to show.
if (extension_host_.get() == Details<ExtensionHost>(details).ptr()) {
Show(activate_on_show_);
if (inspect_with_devtools_) {
// Listen for the the devtools window closing.
registrar_.Add(this, NotificationType::DEVTOOLS_WINDOW_CLOSING,
Source<Profile>(extension_host_->profile()));
DevToolsManager::GetInstance()->ToggleDevToolsWindow(
extension_host_->render_view_host(),
DEVTOOLS_TOGGLE_ACTION_SHOW_CONSOLE);
}
}
break;
case NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE:
// If we aren't the host of the popup, then disregard the notification.
if (Details<ExtensionHost>(host()) != details)
return;
Close();
break;
case NotificationType::DEVTOOLS_WINDOW_CLOSING:
// Make sure its the devtools window that inspecting our popup.
if (Details<RenderViewHost>(extension_host_->render_view_host()) !=
details)
return;
// If the devtools window is closing, we post a task to ourselves to
// close the popup. This gives the devtools window a chance to finish
// detaching from the inspected RenderViewHost.
MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod(this,
&ExtensionPopup::Close));
break;
default:
NOTREACHED() << L"Received unexpected notification";
}
}
void ExtensionPopup::OnExtensionPreferredSizeChanged(ExtensionView* view) {
// Constrain the size to popup min/max.
gfx::Size sz = view->GetPreferredSize();
// Enforce that the popup never resizes to larger than the working monitor
// bounds.
scoped_ptr<WindowSizer::MonitorInfoProvider> monitor_provider(
WindowSizer::CreateDefaultMonitorInfoProvider());
gfx::Rect monitor_bounds(
monitor_provider->GetMonitorWorkAreaMatching(relative_to_));
int max_width = std::min(max_size_.width(), monitor_bounds.width());
int max_height = std::min(max_size_.height(), monitor_bounds.height());
view->SetBounds(view->x(), view->y(),
std::max(kMinWidth, std::min(max_width, sz.width())),
std::max(kMinHeight, std::min(max_height, sz.height())));
// If popup_chrome_ == RECTANGLE_CHROME, the border is drawn in the client
// area of the ExtensionView, rather than in a window which sits behind it.
// In this case, the actual size of the view must be enlarged so that the
// web contents portion of the view gets its full PreferredSize area.
if (view->border()) {
gfx::Insets border_insets;
view->border()->GetInsets(&border_insets);
gfx::Rect bounds(view->bounds());
gfx::Size size(bounds.size());
size.Enlarge(border_insets.width(), border_insets.height());
view->SetBounds(bounds.x(), bounds.y(), size.width(), size.height());
}
ResizeToView();
}
gfx::Rect ExtensionPopup::GetOuterBounds() const {
gfx::Rect relative_rect = relative_to_;
gfx::Point origin = relative_rect.origin();
views::View::ConvertPointToScreen(frame_->GetRootView(), &origin);
relative_rect.set_origin(origin);
gfx::Size contents_size = view()->size();
// If the popup has a bubble-chrome, then let the BubbleBorder compute
// the bounds.
if (BUBBLE_CHROME == popup_chrome_) {
// The rounded corners cut off more of the view than the border insets
// claim. Since we can't clip the ExtensionView's corners, we need to
// increase the inset by half the corner radius as well as lying about the
// size of the contents size to compensate.
contents_size.Enlarge(2 * kPopupBubbleCornerRadius,
2 * kPopupBubbleCornerRadius);
return border_->GetBounds(relative_rect, contents_size);
}
// Position the bounds according to the location of the |anchor_position_|.
int y;
if (BubbleBorder::is_arrow_on_top(anchor_position_))
y = relative_rect.bottom();
else
y = relative_rect.y() - contents_size.height();
int x;
if (BubbleBorder::is_arrow_on_left(anchor_position_))
x = relative_rect.x();
else
// Note that if the arrow is on the right, that the x position of the popup
// is assigned so that the rightmost edge of the popup is aligned with the
// rightmost edge of the relative region.
x = relative_rect.right() - contents_size.width();
return gfx::Rect(x, y, contents_size.width(), contents_size.height());
}
// static
ExtensionPopup* ExtensionPopup::Show(
const GURL& url,
Browser* browser,
Profile* profile,
gfx::NativeWindow frame_window,
const gfx::Rect& relative_to,
BubbleBorder::ArrowLocation arrow_location,
bool activate_on_show,
bool inspect_with_devtools,
PopupChrome chrome,
Observer* observer) {
DCHECK(profile);
DCHECK(frame_window);
ExtensionProcessManager* manager = profile->GetExtensionProcessManager();
DCHECK(manager);
if (!manager)
return NULL;
// If no Browser instance was given, attempt to look up one matching the given
// profile.
if (!browser)
browser = BrowserList::FindBrowserWithProfile(profile);
Widget* frame_widget = Widget::GetWidgetFromNativeWindow(frame_window);
DCHECK(frame_widget);
if (!frame_widget)
return NULL;
ExtensionHost* host = manager->CreatePopup(url, browser);
if (observer)
observer->ExtensionHostCreated(host);
ExtensionPopup* popup = new ExtensionPopup(host, frame_widget, relative_to,
arrow_location, activate_on_show,
inspect_with_devtools, chrome,
observer);
if (observer)
observer->ExtensionPopupCreated(popup);
// If the host had somehow finished loading, then we'd miss the notification
// and not show. This seems to happen in single-process mode.
if (host->did_stop_loading())
popup->Show(activate_on_show);
return popup;
}
void ExtensionPopup::Close() {
if (closing_)
return;
closing_ = true;
DetachFromBrowser();
if (observer_)
observer_->ExtensionPopupIsClosing(this);
Release(); // Balanced in ctor.
}
void ExtensionPopup::Release() {
bool final_release = instance_lifetime_->HasOneRef();
instance_lifetime_->Release();
if (final_release) {
DCHECK(closing_) << "ExtensionPopup to be destroyed before being closed.";
ExtensionPopup::Observer* observer = observer_;
delete this;
// |this| is passed only as a 'cookie'. The observer API explicitly takes a
// void* argument to emphasize this.
if (observer)
observer->ExtensionPopupClosed(this);
}
}