Logo Search packages:      
Sourcecode: chromium-browser version File versions  Download package

extension_popup_api.cc

// Copyright (c) 2010 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/extensions/extension_popup_api.h"

#include "base/json/json_writer.h"
#include "base/string_util.h"
#include "chrome/browser/extensions/extension_dom_ui.h"
#include "chrome/browser/extensions/extension_host.h"
#include "chrome/browser/extensions/extension_message_service.h"
#include "chrome/browser/browser.h"
#include "chrome/browser/browser_window.h"
#include "chrome/browser/profile.h"
#include "chrome/browser/renderer_host/render_view_host.h"
#include "chrome/browser/renderer_host/render_view_host_delegate.h"
#include "chrome/browser/renderer_host/render_widget_host_view.h"
#include "chrome/browser/tab_contents/tab_contents.h"
#include "chrome/browser/window_sizer.h"
#include "chrome/common/extensions/extension.h"
#include "chrome/common/notification_details.h"
#include "chrome/common/notification_service.h"
#include "chrome/common/notification_source.h"
#include "chrome/common/notification_type.h"
#include "chrome/common/url_constants.h"
#include "gfx/point.h"

#if defined(TOOLKIT_VIEWS)
#include "chrome/browser/views/bubble_border.h"
#include "chrome/browser/views/extensions/extension_popup.h"
#include "views/view.h"
#include "views/focus/focus_manager.h"
#endif  // TOOLKIT_VIEWS

namespace extension_popup_module_events {

const char kOnPopupClosed[] = "experimental.popup.onClosed.%d";

}  // namespace extension_popup_module_events

namespace {

// Errors.
const char kBadAnchorArgument[] = "Invalid anchor argument.";
const char kInvalidURLError[] = "Invalid URL.";
const char kNotAnExtension[] = "Not an extension view.";
const char kPopupsDisallowed[] =
    "Popups are only supported from toolstrip or tab-contents views.";

// Keys.
const wchar_t kUrlKey[] = L"url";
const wchar_t kWidthKey[] = L"width";
const wchar_t kHeightKey[] = L"height";
const wchar_t kTopKey[] = L"top";
const wchar_t kLeftKey[] = L"left";
const wchar_t kGiveFocusKey[] = L"giveFocus";
const wchar_t kDomAnchorKey[] = L"domAnchor";
const wchar_t kBorderStyleKey[] = L"borderStyle";

// chrome enumeration values
const char kRectangleChrome[] = "rectangle";

#if defined(TOOLKIT_VIEWS)
// Returns an updated arrow location, conditioned on the type of intersection
// between the popup window, and the screen.  |location| is the current position
// of the arrow on the popup.  |intersection| is the rect representing the
// intersection between the popup view and its working screen.  |popup_rect|
// is the rect of the popup window in screen space coordinates.
// The returned location will be horizontally or vertically inverted based on
// if the popup has been clipped horizontally or vertically.
BubbleBorder::ArrowLocation ToggleArrowLocation(
    BubbleBorder::ArrowLocation location, const gfx::Rect& intersection,
    const gfx::Rect& popup_rect) {
  // If the popup has been clipped horizontally, flip the right-left position
  // of the arrow.
  if (intersection.right() != popup_rect.right() ||
      intersection.x() != popup_rect.x()) {
    location = BubbleBorder::horizontal_mirror(location);
  }

  // If the popup has been clipped vertically, flip the bottom-top position
  // of the arrow.
  if (intersection.y() != popup_rect.y() ||
      intersection.bottom() != popup_rect.bottom()) {
    location = BubbleBorder::vertical_mirror(location);
  }

  return location;
}
#endif  // TOOLKIT_VIEWS

};  // namespace

#if defined(TOOLKIT_VIEWS)
// ExtensionPopupHost objects implement the environment necessary to host
// an ExtensionPopup views for the popup api. Its main job is to handle
// its lifetime and to fire the popup-closed event when the popup is closed.
// Because the close-on-focus-lost behavior is different from page action
// and browser action, it also manages its own focus change listening. The
// difference in close-on-focus-lost is that in the page action and browser
// action cases, the popup closes when the focus leaves the popup or any of its
// children. In this case, the popup closes when the focus leaves the popups
// containing view or any of *its* children.
class ExtensionPopupHost : public ExtensionPopup::Observer,
                           public views::WidgetFocusChangeListener,
                           public base::RefCounted<ExtensionPopupHost>,
                           public NotificationObserver {
 public:
  explicit ExtensionPopupHost(ExtensionFunctionDispatcher* dispatcher)
      : dispatcher_(dispatcher), popup_(NULL) {
    AddRef();  // Balanced in DispatchPopupClosedEvent().
    views::FocusManager::GetWidgetFocusManager()->AddFocusChangeListener(this);
  }

  ~ExtensionPopupHost() {
    views::FocusManager::GetWidgetFocusManager()->
        RemoveFocusChangeListener(this);
  }

  void set_popup(ExtensionPopup* popup) {
    popup_ = popup;

    // Now that a popup has been assigned, listen for subsequent popups being
    // created in the same extension - we want to disallow more than one
    // concurrently displayed popup windows.
    registrar_.Add(
        this,
        NotificationType::EXTENSION_HOST_CREATED,
        Source<ExtensionProcessManager>(
            dispatcher_->profile()->GetExtensionProcessManager()));

    registrar_.Add(
        this,
        NotificationType::RENDER_VIEW_HOST_WILL_CLOSE_RENDER_VIEW,
        Source<RenderViewHost>(dispatcher_->render_view_host()));

    registrar_.Add(
        this,
        NotificationType::EXTENSION_FUNCTION_DISPATCHER_DESTROYED,
        Source<Profile>(dispatcher_->profile()));
  }

  // Overridden from ExtensionPopup::Observer
  virtual void ExtensionPopupIsClosing(ExtensionPopup* popup) {
    // Unregister the automation resource routing registered upon host
    // creation.
    AutomationResourceRoutingDelegate* router =
        GetRoutingFromDispatcher(dispatcher_);
    if (router)
      router->UnregisterRenderViewHost(popup_->host()->render_view_host());
  }

  virtual void ExtensionPopupClosed(void* popup_token) {
    if (popup_ == popup_token) {
      popup_ = NULL;
      DispatchPopupClosedEvent();
    }
  }

  virtual void ExtensionHostCreated(ExtensionHost* host) {
    // Pop-up views should share the same automation routing configuration as
    // their hosting views, so register the RenderViewHost of the pop-up with
    // the AutomationResourceRoutingDelegate interface of the dispatcher.
    AutomationResourceRoutingDelegate* router =
        GetRoutingFromDispatcher(dispatcher_);
    if (router)
      router->RegisterRenderViewHost(host->render_view_host());
  }

  virtual void ExtensionPopupResized(ExtensionPopup* popup) {
    // Reposition the location of the arrow on the popup so that the popup
    // better fits on the working monitor.
    gfx::Rect popup_rect = popup->GetOuterBounds();
    if (popup_rect.IsEmpty())
      return;

    scoped_ptr<WindowSizer::MonitorInfoProvider> monitor_provider(
        WindowSizer::CreateDefaultMonitorInfoProvider());
    gfx::Rect monitor_bounds(
        monitor_provider->GetMonitorWorkAreaMatching(popup_rect));
    gfx::Rect intersection = monitor_bounds.Intersect(popup_rect);

    // If the popup is totally out of the bounds of the monitor, then toggling
    // the arrow location will not result in an un-clipped window.
    if (intersection.IsEmpty())
      return;

    if (!intersection.Equals(popup_rect)) {
      // The popup was clipped by the monitor.  Toggle the arrow position
      // to see if that improves visibility.  Note:  The assignment and
      // re-assignment of the arrow-position will not trigger an intermittent
      // display.
      BubbleBorder::ArrowLocation previous_location = popup->arrow_position();
      BubbleBorder::ArrowLocation flipped_location = ToggleArrowLocation(
          previous_location, intersection, popup_rect);
      popup->SetArrowPosition(flipped_location);

      // Double check that toggling the position actually improved the
      // situation - the popup will be contained entirely in its working monitor
      // bounds.
      gfx::Rect flipped_bounds = popup->GetOuterBounds();
      gfx::Rect updated_monitor_bounds =
          monitor_provider->GetMonitorWorkAreaMatching(flipped_bounds);
      if (!updated_monitor_bounds.Contains(flipped_bounds))
        popup->SetArrowPosition(previous_location);
    }
  }

  virtual void DispatchPopupClosedEvent() {
    if (dispatcher_) {
      PopupEventRouter::OnPopupClosed(
          dispatcher_->profile(),
          dispatcher_->render_view_host()->routing_id());
      dispatcher_ = NULL;
    }
    Release();  // Balanced in ctor.
  }

  // Overridden from views::WidgetFocusChangeListener
  virtual void NativeFocusWillChange(gfx::NativeView focused_before,
                                     gfx::NativeView focused_now) {
    // If the popup doesn't exist, then do nothing.
    if (!popup_)
      return;

    // If no view is to be focused, then Chrome was deactivated, so hide the
    // popup.
    if (focused_now) {
      gfx::NativeView host_view =
          dispatcher_->delegate()->GetNativeViewOfHost();

      // If the widget hosting the popup contains the newly focused view, then
      // don't dismiss the pop-up.
      ExtensionView* view = popup_->host()->view();
      if (view) {
        views::Widget* popup_root_widget = view->GetWidget();
        if (popup_root_widget &&
            popup_root_widget->ContainsNativeView(focused_now))
          return;
      }

      // If the widget or RenderWidgetHostView hosting the extension that
      // launched the pop-up is receiving focus, then don't dismiss the popup.
      views::Widget* host_widget =
          views::Widget::GetWidgetFromNativeView(host_view);
      if (host_widget && host_widget->ContainsNativeView(focused_now))
        return;

      RenderWidgetHostView* render_host_view =
          RenderWidgetHostView::GetRenderWidgetHostViewFromNativeView(
              host_view);
      if (render_host_view &&
          render_host_view->ContainsNativeView(focused_now))
        return;
    }

    // We are careful here to let the current event loop unwind before
    // causing the popup to be closed.
    MessageLoop::current()->PostTask(FROM_HERE, NewRunnableMethod(popup_,
        &ExtensionPopup::Close));
  }

  // Overridden from NotificationObserver
  virtual void Observe(NotificationType type,
                       const NotificationSource& source,
                       const NotificationDetails& details) {
    if (NotificationType::EXTENSION_HOST_CREATED == type) {
      Details<ExtensionHost> details_host(details);
      // Disallow multiple pop-ups from the same extension, by closing
      // the presently opened popup during construction of any new popups.
      if (ViewType::EXTENSION_POPUP == details_host->GetRenderViewType() &&
          popup_->host()->extension() == details_host->extension() &&
          Details<ExtensionHost>(popup_->host()) != details) {
        popup_->Close();
      }
    } else if (NotificationType::RENDER_VIEW_HOST_WILL_CLOSE_RENDER_VIEW ==
        type) {
      if (Source<RenderViewHost>(dispatcher_->render_view_host()) == source) {
        // If the parent render view is about to be closed, signal closure
        // of the popup.
        popup_->Close();
      }
    } else if (NotificationType::EXTENSION_FUNCTION_DISPATCHER_DESTROYED ==
        type) {
      // Popups should not outlive the dispatchers that launched them.
      // Normally, long-lived popups will be dismissed in response to the
      // RENDER_VIEW_WILL_CLOSE_BY_RENDER_VIEW_HOST message.  Unfortunately,
      // if the hosting view invokes window.close(), there is no communication
      // back to the browser until the entire view has been torn down, at which
      // time the dispatcher will be invoked.
      // Note:  The onClosed event will not be fired, but because the hosting
      // view has already been torn down, it is already too late to process it.
      // TODO(twiz):  Add a communication path between the renderer and browser
      // for RenderView closure notifications initiatied within the renderer.
      if (Details<ExtensionFunctionDispatcher>(dispatcher_) == details) {
        dispatcher_ = NULL;
        popup_->Close();
      }
    }
  }

 private:
  // Returns the AutomationResourceRoutingDelegate interface for |dispatcher|.
  static AutomationResourceRoutingDelegate*
      GetRoutingFromDispatcher(ExtensionFunctionDispatcher* dispatcher) {
    if (!dispatcher)
      return NULL;

    RenderViewHost* render_view_host = dispatcher->render_view_host();
    RenderViewHostDelegate* delegate =
        render_view_host ? render_view_host->delegate() : NULL;

    return delegate ? delegate->GetAutomationResourceRoutingDelegate() : NULL;
  }

  // A pointer to the dispatcher that handled the request that opened this
  // popup view.
  ExtensionFunctionDispatcher* dispatcher_;

  // A pointer to the popup.
  ExtensionPopup* popup_;

  NotificationRegistrar registrar_;

  DISALLOW_COPY_AND_ASSIGN(ExtensionPopupHost);
};
#endif  // TOOLKIT_VIEWS

PopupShowFunction::PopupShowFunction()
#if defined (TOOLKIT_VIEWS)
    : popup_(NULL)
#endif
{}

void PopupShowFunction::Run() {
#if defined(TOOLKIT_VIEWS)
  if (!RunImpl()) {
    SendResponse(false);
  } else {
    // If the contents of the popup are already available, then immediately
    // send the response.  Otherwise wait for the EXTENSION_POPUP_VIEW_READY
    // notification.
    if (popup_->host() && popup_->host()->document_element_available()) {
      SendResponse(true);
    } else {
      AddRef();
      registrar_.Add(this, NotificationType::EXTENSION_POPUP_VIEW_READY,
                     NotificationService::AllSources());
      registrar_.Add(this, NotificationType::EXTENSION_HOST_DESTROYED,
                     NotificationService::AllSources());
    }
  }
#else
  SendResponse(false);
#endif
}

bool PopupShowFunction::RunImpl() {
  // Popups may only be displayed from TAB_CONTENTS and EXTENSION_TOOLSTRIP
  // views.
  ViewType::Type view_type =
      dispatcher()->render_view_host()->delegate()->GetRenderViewType();
  if (ViewType::EXTENSION_TOOLSTRIP != view_type &&
      ViewType::TAB_CONTENTS != view_type) {
    error_ = kPopupsDisallowed;
    return false;
  }

  std::string url_string;
  EXTENSION_FUNCTION_VALIDATE(args_->GetString(0, &url_string));

  DictionaryValue* show_details = NULL;
  EXTENSION_FUNCTION_VALIDATE(args_->GetDictionary(1, &show_details));

  DictionaryValue* dom_anchor = NULL;
  EXTENSION_FUNCTION_VALIDATE(show_details->GetDictionary(kDomAnchorKey,
                                                          &dom_anchor));

  int dom_top, dom_left;
  EXTENSION_FUNCTION_VALIDATE(dom_anchor->GetInteger(kTopKey,
                                                     &dom_top));
  EXTENSION_FUNCTION_VALIDATE(dom_anchor->GetInteger(kLeftKey,
                                                     &dom_left));

  int dom_width, dom_height;
  EXTENSION_FUNCTION_VALIDATE(dom_anchor->GetInteger(kWidthKey,
                                                     &dom_width));
  EXTENSION_FUNCTION_VALIDATE(dom_anchor->GetInteger(kHeightKey,
                                                     &dom_height));
  EXTENSION_FUNCTION_VALIDATE(dom_top >= 0 && dom_left >= 0 &&
                              dom_width >= 0 && dom_height >= 0);

  // The default behaviour is to give the focus to the pop-up window.
  bool give_focus = true;
  if (show_details->HasKey(kGiveFocusKey)) {
    EXTENSION_FUNCTION_VALIDATE(show_details->GetBoolean(kGiveFocusKey,
                                                         &give_focus));
  }

#if defined(TOOLKIT_VIEWS)
  // The default behaviour is to provide the bubble-chrome to the popup.
  ExtensionPopup::PopupChrome chrome = ExtensionPopup::BUBBLE_CHROME;
  if (show_details->HasKey(kBorderStyleKey)) {
    std::string chrome_string;
    EXTENSION_FUNCTION_VALIDATE(show_details->GetString(kBorderStyleKey,
                                                        &chrome_string));
    if (chrome_string == kRectangleChrome)
      chrome = ExtensionPopup::RECTANGLE_CHROME;
  }
#endif

  GURL url = dispatcher()->url().Resolve(url_string);
  if (!url.is_valid()) {
    error_ = kInvalidURLError;
    return false;
  }

  // Disallow non-extension requests, or requests outside of the requesting
  // extension view's extension.
  const std::string& extension_id = url.host();
  if (extension_id != GetExtension()->id() ||
      !url.SchemeIs(chrome::kExtensionScheme)) {
    error_ = kInvalidURLError;
    return false;
  }

  gfx::Point origin(dom_left, dom_top);
  if (!dispatcher()->render_view_host()->view()) {
    error_ = kNotAnExtension;
    return false;
  }

  gfx::Rect content_bounds =
      dispatcher()->render_view_host()->view()->GetViewBounds();
  origin.Offset(content_bounds.x(), content_bounds.y());
  gfx::Rect rect(origin.x(), origin.y(), dom_width, dom_height);

  // Get the correct native window to pass to ExtensionPopup.
  // ExtensionFunctionDispatcher::Delegate may provide a custom implementation
  // of this.
  gfx::NativeWindow window =
      dispatcher()->delegate()->GetCustomFrameNativeWindow();
  if (!window)
    window = GetCurrentBrowser()->window()->GetNativeHandle();

#if defined(TOOLKIT_VIEWS)
  // Pop-up from extension views (ExtensionShelf, etc.), and drop-down when
  // in a TabContents view.
  BubbleBorder::ArrowLocation arrow_location =
      view_type == ViewType::TAB_CONTENTS ?
          BubbleBorder::TOP_LEFT : BubbleBorder::BOTTOM_LEFT;

  // ExtensionPopupHost manages it's own lifetime.
  ExtensionPopupHost* popup_host = new ExtensionPopupHost(dispatcher());
  popup_ = ExtensionPopup::Show(url,
                                GetCurrentBrowser(),
                                dispatcher()->profile(),
                                window,
                                rect,
                                arrow_location,
                                give_focus,
                                false,  // inspect_with_devtools
                                chrome,
                                popup_host);  // ExtensionPopup::Observer

  // popup_host will handle focus change listening and close the popup when
  // focus leaves the containing views hierarchy.
  popup_->set_close_on_lost_focus(false);
  popup_host->set_popup(popup_);
#endif  // defined(TOOLKIT_VIEWS)
  return true;
}

void PopupShowFunction::Observe(NotificationType type,
                                const NotificationSource& source,
                                const NotificationDetails& details) {
#if defined(TOOLKIT_VIEWS)
  DCHECK(type == NotificationType::EXTENSION_POPUP_VIEW_READY ||
         type == NotificationType::EXTENSION_HOST_DESTROYED);
  DCHECK(popup_ != NULL);

  // Wait for notification that the popup view is ready (and onload has been
  // called), before completing the API call.
  if (popup_ && type == NotificationType::EXTENSION_POPUP_VIEW_READY &&
      Details<ExtensionHost>(popup_->host()) == details) {
    SendResponse(true);
    Release();  // Balanced in Run().
  } else if (popup_ && type == NotificationType::EXTENSION_HOST_DESTROYED &&
             Details<ExtensionHost>(popup_->host()) == details) {
    // If the host was destroyed, then report failure, and release the remaining
    // reference.
    SendResponse(false);
    Release();  // Balanced in Run().
  }
#endif  // defined(TOOLKIT_VIEWS)
}

// static
void PopupEventRouter::OnPopupClosed(Profile* profile,
                                     int routing_id) {
  std::string full_event_name = StringPrintf(
      extension_popup_module_events::kOnPopupClosed,
      routing_id);

  profile->GetExtensionMessageService()->DispatchEventToRenderers(
      full_event_name,
      base::JSONWriter::kEmptyArray,
      profile->IsOffTheRecord(),
      GURL());
}

Generated by  Doxygen 1.6.0   Back to index