// 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/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/profiles/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/ui/browser_list.h" #include "chrome/browser/ui/browser_window.h" #include "chrome/browser/ui/views/frame/browser_view.h" #include "chrome/browser/ui/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 "third_party/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(host->profile())); // Listen for the containing view calling window.close(); registrar_.Add(this, NotificationType::EXTENSION_HOST_VIEW_SHOULD_CLOSE, Source(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(border_widget_)->MakeTransparent(); static_cast(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(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(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(host()) != details) return; Close(); break; case NotificationType::DEVTOOLS_WINDOW_CLOSING: // Make sure its the devtools window that inspecting our popup. if (Details(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 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); } }