1 // Copyright 2014 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/views/extensions/extension_action_platform_delegate_views.h"
7 #include "base/logging.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "chrome/browser/extensions/api/commands/command_service.h"
10 #include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
11 #include "chrome/browser/extensions/extension_action.h"
12 #include "chrome/browser/profiles/profile.h"
13 #include "chrome/browser/sessions/session_tab_helper.h"
14 #include "chrome/browser/ui/browser.h"
15 #include "chrome/browser/ui/extensions/accelerator_priority.h"
16 #include "chrome/browser/ui/views/toolbar/toolbar_action_view_delegate_views.h"
17 #include "chrome/browser/ui/views/toolbar/toolbar_view.h"
18 #include "chrome/common/extensions/api/extension_action/action_info.h"
19 #include "content/public/browser/notification_details.h"
20 #include "content/public/browser/notification_source.h"
21 #include "extensions/browser/notification_types.h"
22 #include "extensions/common/extension.h"
23 #include "extensions/common/manifest_constants.h"
24 #include "ui/gfx/image/image_skia.h"
25 #include "ui/gfx/image/image_skia_operations.h"
26 #include "ui/views/controls/menu/menu_controller.h"
27 #include "ui/views/controls/menu/menu_runner.h"
28 #include "ui/views/view.h"
29 #include "ui/views/widget/widget.h"
31 using extensions::ActionInfo;
32 using extensions::CommandService;
36 // The ExtensionActionPlatformDelegateViews which is currently showing its
37 // context menu, if any.
38 // Since only one context menu can be shown (even across browser windows), it's
39 // safe to have this be a global singleton.
40 ExtensionActionPlatformDelegateViews* context_menu_owner = NULL;
45 scoped_ptr<ExtensionActionPlatformDelegate>
46 ExtensionActionPlatformDelegate::Create(
47 ExtensionActionViewController* controller) {
48 return make_scoped_ptr(new ExtensionActionPlatformDelegateViews(controller));
51 ExtensionActionPlatformDelegateViews::ExtensionActionPlatformDelegateViews(
52 ExtensionActionViewController* controller)
53 : controller_(controller),
56 content::NotificationSource notification_source = content::Source<Profile>(
57 controller_->browser()->profile()->GetOriginalProfile());
59 extensions::NOTIFICATION_EXTENSION_COMMAND_ADDED,
62 extensions::NOTIFICATION_EXTENSION_COMMAND_REMOVED,
66 ExtensionActionPlatformDelegateViews::~ExtensionActionPlatformDelegateViews() {
67 if (context_menu_owner == this)
68 context_menu_owner = NULL;
69 controller_->HidePopup();
70 UnregisterCommand(false);
73 gfx::NativeView ExtensionActionPlatformDelegateViews::GetPopupNativeView() {
74 return popup_ ? popup_->GetWidget()->GetNativeView() : nullptr;
77 bool ExtensionActionPlatformDelegateViews::IsMenuRunning() const {
78 return menu_runner_.get() != NULL;
81 void ExtensionActionPlatformDelegateViews::RegisterCommand() {
82 // If we've already registered, do nothing.
83 if (action_keybinding_.get())
86 extensions::Command extension_command;
87 views::FocusManager* focus_manager =
88 GetDelegateViews()->GetFocusManagerForAccelerator();
89 if (focus_manager && controller_->GetExtensionCommand(&extension_command)) {
90 action_keybinding_.reset(
91 new ui::Accelerator(extension_command.accelerator()));
92 focus_manager->RegisterAccelerator(
94 GetAcceleratorPriority(extension_command.accelerator(),
95 controller_->extension()),
100 void ExtensionActionPlatformDelegateViews::OnDelegateSet() {
101 GetDelegateViews()->GetAsView()->set_context_menu_controller(this);
104 bool ExtensionActionPlatformDelegateViews::IsShowingPopup() const {
105 return popup_ != nullptr;
108 void ExtensionActionPlatformDelegateViews::CloseActivePopup() {
109 GetDelegateViews()->HideActivePopup();
112 void ExtensionActionPlatformDelegateViews::CloseOwnPopup() {
113 // We should only be asked to close the popup if we're showing one.
118 bool ExtensionActionPlatformDelegateViews::ShowPopupWithUrl(
119 ExtensionActionViewController::PopupShowAction show_action,
120 const GURL& popup_url,
121 bool grant_tab_permissions) {
122 // TOP_RIGHT is correct for both RTL and LTR, because the views platform
123 // performs the flipping in RTL cases.
124 views::BubbleBorder::Arrow arrow = views::BubbleBorder::TOP_RIGHT;
126 views::View* reference_view = GetDelegateViews()->GetReferenceViewForPopup();
128 ExtensionPopup::ShowAction popup_show_action =
129 show_action == ExtensionActionViewController::SHOW_POPUP ?
130 ExtensionPopup::SHOW : ExtensionPopup::SHOW_AND_INSPECT;
131 popup_ = ExtensionPopup::ShowPopup(popup_url,
132 controller_->browser(),
136 popup_->GetWidget()->AddObserver(this);
138 GetDelegateViews()->OnPopupShown(grant_tab_permissions);
143 void ExtensionActionPlatformDelegateViews::Observe(
145 const content::NotificationSource& source,
146 const content::NotificationDetails& details) {
147 DCHECK(type == extensions::NOTIFICATION_EXTENSION_COMMAND_ADDED ||
148 type == extensions::NOTIFICATION_EXTENSION_COMMAND_REMOVED);
149 std::pair<const std::string, const std::string>* payload =
150 content::Details<std::pair<const std::string, const std::string> >(
152 if (controller_->extension()->id() == payload->first &&
154 extensions::manifest_values::kBrowserActionCommandEvent) {
155 if (type == extensions::NOTIFICATION_EXTENSION_COMMAND_ADDED)
158 UnregisterCommand(true);
162 bool ExtensionActionPlatformDelegateViews::AcceleratorPressed(
163 const ui::Accelerator& accelerator) {
164 // We shouldn't be handling any accelerators if the view is hidden, unless
165 // this is a browser action.
166 DCHECK(controller_->extension_action()->action_type() ==
167 ActionInfo::TYPE_BROWSER ||
168 GetDelegateViews()->GetAsView()->visible());
170 // Normal priority shortcuts must be handled via standard browser commands to
171 // be processed at the proper time.
172 if (GetAcceleratorPriority(accelerator, controller_->extension()) ==
173 ui::AcceleratorManager::kNormalPriority)
176 controller_->ExecuteAction(true);
180 bool ExtensionActionPlatformDelegateViews::CanHandleAccelerators() const {
181 // Page actions can only handle accelerators when they are visible.
182 // Browser actions can handle accelerators even when not visible, since they
183 // might be hidden in an overflow menu.
184 return controller_->extension_action()->action_type() ==
185 ActionInfo::TYPE_PAGE ? GetDelegateViews()->GetAsView()->visible() :
189 void ExtensionActionPlatformDelegateViews::OnWidgetDestroying(
190 views::Widget* widget) {
192 DCHECK_EQ(popup_->GetWidget(), widget);
196 void ExtensionActionPlatformDelegateViews::ShowContextMenuForView(
198 const gfx::Point& point,
199 ui::MenuSourceType source_type) {
200 // If there's another active menu that won't be dismissed by opening this one,
201 // then we can't show this one right away, since we can only show one nested
203 // If the other menu is an extension action's context menu, then we'll run
204 // this one after that one closes. If it's a different type of menu, then we
205 // close it and give up, for want of a better solution. (Luckily, this is
207 // TODO(devlin): Update this when views code no longer runs menus in a nested
209 if (context_menu_owner) {
210 context_menu_owner->followup_context_menu_task_ =
211 base::Bind(&ExtensionActionPlatformDelegateViews::DoShowContextMenu,
212 weak_factory_.GetWeakPtr(),
215 if (CloseActiveMenuIfNeeded())
218 // Otherwise, no other menu is showing, and we can proceed normally.
219 DoShowContextMenu(source_type);
222 void ExtensionActionPlatformDelegateViews::DoShowContextMenu(
223 ui::MenuSourceType source_type) {
224 if (!controller_->extension()->ShowConfigureContextMenus())
227 DCHECK(!context_menu_owner);
228 context_menu_owner = this;
230 // We shouldn't have both a popup and a context menu showing.
231 GetDelegateViews()->HideActivePopup();
233 // Reconstructs the menu every time because the menu's contents are dynamic.
234 scoped_refptr<ExtensionContextMenuModel> context_menu_model(
235 new ExtensionContextMenuModel(
236 controller_->extension(), controller_->browser(), controller_));
238 gfx::Point screen_loc;
239 views::View::ConvertPointToScreen(GetDelegateViews()->GetAsView(),
243 views::MenuRunner::HAS_MNEMONICS | views::MenuRunner::CONTEXT_MENU;
244 if (GetDelegateViews()->IsShownInMenu())
245 run_types |= views::MenuRunner::IS_NESTED;
247 views::Widget* parent = GetDelegateViews()->GetParentForContextMenu();
250 new views::MenuRunner(context_menu_model.get(), run_types));
252 if (menu_runner_->RunMenuAt(
254 GetDelegateViews()->GetContextMenuButton(),
255 gfx::Rect(screen_loc, GetDelegateViews()->GetAsView()->size()),
256 views::MENU_ANCHOR_TOPLEFT,
257 source_type) == views::MenuRunner::MENU_DELETED) {
261 context_menu_owner = NULL;
262 menu_runner_.reset();
264 // If another extension action wants to show its context menu, allow it to.
265 if (!followup_context_menu_task_.is_null()) {
266 base::Closure task = followup_context_menu_task_;
267 followup_context_menu_task_ = base::Closure();
272 void ExtensionActionPlatformDelegateViews::UnregisterCommand(
273 bool only_if_removed) {
274 views::FocusManager* focus_manager =
275 GetDelegateViews()->GetFocusManagerForAccelerator();
276 if (!focus_manager || !action_keybinding_.get())
279 // If |only_if_removed| is true, it means that we only need to unregister
280 // ourselves as an accelerator if the command was removed. Otherwise, we need
281 // to unregister ourselves no matter what (likely because we are shutting
283 extensions::Command extension_command;
284 if (!only_if_removed ||
285 !controller_->GetExtensionCommand(&extension_command)) {
286 focus_manager->UnregisterAccelerator(*action_keybinding_, this);
287 action_keybinding_.reset();
291 bool ExtensionActionPlatformDelegateViews::CloseActiveMenuIfNeeded() {
292 // If this view is shown inside another menu, there's a possibility that there
293 // is another context menu showing that we have to close before we can
294 // activate a different menu.
295 if (GetDelegateViews()->IsShownInMenu()) {
296 views::MenuController* menu_controller =
297 views::MenuController::GetActiveInstance();
298 // If this is shown inside a menu, then there should always be an active
300 DCHECK(menu_controller);
301 if (menu_controller->in_nested_run()) {
302 // There is another menu showing. Close the outermost menu (since we are
303 // shown in the same menu, we don't want to close the whole thing).
304 menu_controller->Cancel(views::MenuController::EXIT_OUTERMOST);
312 void ExtensionActionPlatformDelegateViews::CleanupPopup(bool close_widget) {
314 GetDelegateViews()->CleanupPopup();
315 popup_->GetWidget()->RemoveObserver(this);
317 popup_->GetWidget()->Close();
321 ToolbarActionViewDelegateViews*
322 ExtensionActionPlatformDelegateViews::GetDelegateViews() const {
323 return static_cast<ToolbarActionViewDelegateViews*>(
324 controller_->view_delegate());