blob: c9bac7c563f153ac93979e114ac7f13c567758f9 [file] [log] [blame]
// Copyright 2014 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/ui/views/extensions/extension_action_platform_delegate_views.h"
#include "base/logging.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/extensions/api/commands/command_service.h"
#include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
#include "chrome/browser/extensions/extension_action.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_tab_helper.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/extensions/accelerator_priority.h"
#include "chrome/browser/ui/views/toolbar/toolbar_action_view_delegate_views.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/common/extensions/api/extension_action/action_info.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_source.h"
#include "extensions/browser/notification_types.h"
#include "extensions/common/extension.h"
#include "extensions/common/manifest_constants.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
using extensions::ActionInfo;
using extensions::CommandService;
namespace {
// The ExtensionActionPlatformDelegateViews which is currently showing its
// context menu, if any.
// Since only one context menu can be shown (even across browser windows), it's
// safe to have this be a global singleton.
ExtensionActionPlatformDelegateViews* context_menu_owner = NULL;
} // namespace
// static
scoped_ptr<ExtensionActionPlatformDelegate>
ExtensionActionPlatformDelegate::Create(
ExtensionActionViewController* controller) {
return make_scoped_ptr(new ExtensionActionPlatformDelegateViews(controller));
}
ExtensionActionPlatformDelegateViews::ExtensionActionPlatformDelegateViews(
ExtensionActionViewController* controller)
: controller_(controller),
popup_(nullptr),
weak_factory_(this) {
content::NotificationSource notification_source = content::Source<Profile>(
controller_->browser()->profile()->GetOriginalProfile());
registrar_.Add(this,
extensions::NOTIFICATION_EXTENSION_COMMAND_ADDED,
notification_source);
registrar_.Add(this,
extensions::NOTIFICATION_EXTENSION_COMMAND_REMOVED,
notification_source);
}
ExtensionActionPlatformDelegateViews::~ExtensionActionPlatformDelegateViews() {
if (context_menu_owner == this)
context_menu_owner = NULL;
controller_->HidePopup();
UnregisterCommand(false);
}
gfx::NativeView ExtensionActionPlatformDelegateViews::GetPopupNativeView() {
return popup_ ? popup_->GetWidget()->GetNativeView() : nullptr;
}
bool ExtensionActionPlatformDelegateViews::IsMenuRunning() const {
return menu_runner_.get() != NULL;
}
void ExtensionActionPlatformDelegateViews::RegisterCommand() {
// If we've already registered, do nothing.
if (action_keybinding_.get())
return;
extensions::Command extension_command;
views::FocusManager* focus_manager =
GetDelegateViews()->GetFocusManagerForAccelerator();
if (focus_manager && controller_->GetExtensionCommand(&extension_command)) {
action_keybinding_.reset(
new ui::Accelerator(extension_command.accelerator()));
focus_manager->RegisterAccelerator(
*action_keybinding_,
GetAcceleratorPriority(extension_command.accelerator(),
controller_->extension()),
this);
}
}
void ExtensionActionPlatformDelegateViews::OnDelegateSet() {
GetDelegateViews()->GetAsView()->set_context_menu_controller(this);
}
bool ExtensionActionPlatformDelegateViews::IsShowingPopup() const {
return popup_ != nullptr;
}
void ExtensionActionPlatformDelegateViews::CloseActivePopup() {
GetDelegateViews()->HideActivePopup();
}
void ExtensionActionPlatformDelegateViews::CloseOwnPopup() {
// We should only be asked to close the popup if we're showing one.
DCHECK(popup_);
CleanupPopup(true);
}
bool ExtensionActionPlatformDelegateViews::ShowPopupWithUrl(
ExtensionActionViewController::PopupShowAction show_action,
const GURL& popup_url,
bool grant_tab_permissions) {
views::BubbleBorder::Arrow arrow = base::i18n::IsRTL() ?
views::BubbleBorder::TOP_LEFT : views::BubbleBorder::TOP_RIGHT;
views::View* reference_view = GetDelegateViews()->GetReferenceViewForPopup();
ExtensionPopup::ShowAction popup_show_action =
show_action == ExtensionActionViewController::SHOW_POPUP ?
ExtensionPopup::SHOW : ExtensionPopup::SHOW_AND_INSPECT;
popup_ = ExtensionPopup::ShowPopup(popup_url,
controller_->browser(),
reference_view,
arrow,
popup_show_action);
popup_->GetWidget()->AddObserver(this);
GetDelegateViews()->OnPopupShown(grant_tab_permissions);
return true;
}
void ExtensionActionPlatformDelegateViews::Observe(
int type,
const content::NotificationSource& source,
const content::NotificationDetails& details) {
DCHECK(type == extensions::NOTIFICATION_EXTENSION_COMMAND_ADDED ||
type == extensions::NOTIFICATION_EXTENSION_COMMAND_REMOVED);
std::pair<const std::string, const std::string>* payload =
content::Details<std::pair<const std::string, const std::string> >(
details).ptr();
if (controller_->extension()->id() == payload->first &&
payload->second ==
extensions::manifest_values::kBrowserActionCommandEvent) {
if (type == extensions::NOTIFICATION_EXTENSION_COMMAND_ADDED)
RegisterCommand();
else
UnregisterCommand(true);
}
}
bool ExtensionActionPlatformDelegateViews::AcceleratorPressed(
const ui::Accelerator& accelerator) {
// We shouldn't be handling any accelerators if the view is hidden, unless
// this is a browser action.
DCHECK(controller_->extension_action()->action_type() ==
ActionInfo::TYPE_BROWSER ||
GetDelegateViews()->GetAsView()->visible());
// Normal priority shortcuts must be handled via standard browser commands to
// be processed at the proper time.
if (GetAcceleratorPriority(accelerator, controller_->extension()) ==
ui::AcceleratorManager::kNormalPriority)
return false;
controller_->ExecuteAction(true);
return true;
}
bool ExtensionActionPlatformDelegateViews::CanHandleAccelerators() const {
// Page actions can only handle accelerators when they are visible.
// Browser actions can handle accelerators even when not visible, since they
// might be hidden in an overflow menu.
return controller_->extension_action()->action_type() ==
ActionInfo::TYPE_PAGE ? GetDelegateViews()->GetAsView()->visible() :
true;
}
void ExtensionActionPlatformDelegateViews::OnWidgetDestroying(
views::Widget* widget) {
DCHECK(popup_);
DCHECK_EQ(popup_->GetWidget(), widget);
CleanupPopup(false);
}
void ExtensionActionPlatformDelegateViews::ShowContextMenuForView(
views::View* source,
const gfx::Point& point,
ui::MenuSourceType source_type) {
// If there's another active menu that won't be dismissed by opening this one,
// then we can't show this one right away, since we can only show one nested
// menu at a time.
// If the other menu is an extension action's context menu, then we'll run
// this one after that one closes. If it's a different type of menu, then we
// close it and give up, for want of a better solution. (Luckily, this is
// rare).
// TODO(devlin): Update this when views code no longer runs menus in a nested
// loop.
if (context_menu_owner) {
context_menu_owner->followup_context_menu_task_ =
base::Bind(&ExtensionActionPlatformDelegateViews::DoShowContextMenu,
weak_factory_.GetWeakPtr(),
source_type);
}
if (CloseActiveMenuIfNeeded())
return;
// Otherwise, no other menu is showing, and we can proceed normally.
DoShowContextMenu(source_type);
}
void ExtensionActionPlatformDelegateViews::DoShowContextMenu(
ui::MenuSourceType source_type) {
if (!controller_->extension()->ShowConfigureContextMenus())
return;
DCHECK(!context_menu_owner);
context_menu_owner = this;
// We shouldn't have both a popup and a context menu showing.
GetDelegateViews()->HideActivePopup();
// Reconstructs the menu every time because the menu's contents are dynamic.
scoped_refptr<ExtensionContextMenuModel> context_menu_model(
new ExtensionContextMenuModel(
controller_->extension(), controller_->browser(), controller_));
gfx::Point screen_loc;
views::View::ConvertPointToScreen(GetDelegateViews()->GetAsView(),
&screen_loc);
int run_types =
views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU;
if (GetDelegateViews()->IsShownInMenu())
run_types |= views::MenuRunner::IS_NESTED;
views::Widget* parent = GetDelegateViews()->GetParentForContextMenu();
menu_runner_.reset(
new views::MenuRunner(context_menu_model.get(), run_types));
if (menu_runner_->RunMenuAt(
parent,
GetDelegateViews()->GetContextMenuButton(),
gfx::Rect(screen_loc, GetDelegateViews()->GetAsView()->size()),
views::MENU_ANCHOR_TOPLEFT,
source_type) == views::MenuRunner::MENU_DELETED) {
return;
}
context_menu_owner = NULL;
menu_runner_.reset();
// If another extension action wants to show its context menu, allow it to.
if (!followup_context_menu_task_.is_null()) {
base::Closure task = followup_context_menu_task_;
followup_context_menu_task_ = base::Closure();
task.Run();
}
}
void ExtensionActionPlatformDelegateViews::UnregisterCommand(
bool only_if_removed) {
views::FocusManager* focus_manager =
GetDelegateViews()->GetFocusManagerForAccelerator();
if (!focus_manager || !action_keybinding_.get())
return;
// If |only_if_removed| is true, it means that we only need to unregister
// ourselves as an accelerator if the command was removed. Otherwise, we need
// to unregister ourselves no matter what (likely because we are shutting
// down).
extensions::Command extension_command;
if (!only_if_removed ||
!controller_->GetExtensionCommand(&extension_command)) {
focus_manager->UnregisterAccelerator(*action_keybinding_, this);
action_keybinding_.reset();
}
}
bool ExtensionActionPlatformDelegateViews::CloseActiveMenuIfNeeded() {
// If this view is shown inside another menu, there's a possibility that there
// is another context menu showing that we have to close before we can
// activate a different menu.
if (GetDelegateViews()->IsShownInMenu()) {
views::MenuController* menu_controller =
views::MenuController::GetActiveInstance();
// If this is shown inside a menu, then there should always be an active
// menu controller.
DCHECK(menu_controller);
if (menu_controller->in_nested_run()) {
// There is another menu showing. Close the outermost menu (since we are
// shown in the same menu, we don't want to close the whole thing).
menu_controller->Cancel(views::MenuController::EXIT_OUTERMOST);
return true;
}
}
return false;
}
void ExtensionActionPlatformDelegateViews::CleanupPopup(bool close_widget) {
DCHECK(popup_);
GetDelegateViews()->CleanupPopup();
popup_->GetWidget()->RemoveObserver(this);
if (close_widget)
popup_->GetWidget()->Close();
popup_ = NULL;
}
ToolbarActionViewDelegateViews*
ExtensionActionPlatformDelegateViews::GetDelegateViews() const {
return static_cast<ToolbarActionViewDelegateViews*>(
controller_->view_delegate());
}