extension_action_view_controller.cc revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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.
4
5#include "chrome/browser/ui/views/extensions/extension_action_view_controller.h"
6
7#include "base/logging.h"
8#include "chrome/browser/extensions/api/commands/command_service.h"
9#include "chrome/browser/extensions/api/extension_action/extension_action_api.h"
10#include "chrome/browser/extensions/extension_action.h"
11#include "chrome/browser/profiles/profile.h"
12#include "chrome/browser/sessions/session_tab_helper.h"
13#include "chrome/browser/ui/browser.h"
14#include "chrome/browser/ui/extensions/accelerator_priority.h"
15#include "chrome/browser/ui/views/extensions/extension_action_view_delegate.h"
16#include "chrome/common/extensions/api/extension_action/action_info.h"
17#include "extensions/common/extension.h"
18#include "ui/views/controls/menu/menu_controller.h"
19#include "ui/views/controls/menu/menu_runner.h"
20#include "ui/views/view.h"
21#include "ui/views/widget/widget.h"
22
23using extensions::ActionInfo;
24using extensions::CommandService;
25
26namespace {
27
28// The ExtensionActionViewController which is currently showing its context
29// menu, if any.
30// Since only one context menu can be shown (even across browser windows), it's
31// safe to have this be a global singleton.
32ExtensionActionViewController* context_menu_owner = NULL;
33
34}  // namespace
35
36ExtensionActionViewController::ExtensionActionViewController(
37    const extensions::Extension* extension,
38    Browser* browser,
39    ExtensionAction* extension_action,
40    ExtensionActionViewDelegate* delegate)
41    : extension_(extension),
42      browser_(browser),
43      extension_action_(extension_action),
44      delegate_(delegate),
45      icon_factory_(browser->profile(), extension, extension_action, this),
46      popup_(NULL),
47      weak_factory_(this) {
48  DCHECK(extension_action);
49  DCHECK(extension_action->action_type() == ActionInfo::TYPE_PAGE ||
50         extension_action->action_type() == ActionInfo::TYPE_BROWSER);
51  DCHECK(extension);
52}
53
54ExtensionActionViewController::~ExtensionActionViewController() {
55  if (context_menu_owner == this)
56    context_menu_owner = NULL;
57  HidePopup();
58  UnregisterCommand(false);
59}
60
61void ExtensionActionViewController::InspectPopup() {
62  ExecuteAction(ExtensionPopup::SHOW_AND_INSPECT, true);
63}
64
65void ExtensionActionViewController::ExecuteActionByUser() {
66  ExecuteAction(ExtensionPopup::SHOW, true);
67}
68
69bool ExtensionActionViewController::ExecuteAction(
70    ExtensionPopup::ShowAction show_action, bool grant_tab_permissions) {
71  if (extensions::ExtensionActionAPI::Get(browser_->profile())->
72          ExecuteExtensionAction(extension_, browser_, grant_tab_permissions) ==
73      ExtensionAction::ACTION_SHOW_POPUP) {
74    GURL popup_url = extension_action_->GetPopupUrl(GetCurrentTabId());
75    return delegate_->GetPreferredPopupViewController()->ShowPopupWithUrl(
76        show_action, popup_url, grant_tab_permissions);
77  }
78  return false;
79}
80
81void ExtensionActionViewController::HidePopup() {
82  if (popup_)
83    CleanupPopup(true);
84}
85
86gfx::Image ExtensionActionViewController::GetIcon(int tab_id) {
87  return icon_factory_.GetIcon(tab_id);
88}
89
90int ExtensionActionViewController::GetCurrentTabId() const {
91  content::WebContents* web_contents = delegate_->GetCurrentWebContents();
92  return web_contents ? SessionTabHelper::IdForTab(web_contents) : -1;
93}
94
95void ExtensionActionViewController::RegisterCommand() {
96  // If we've already registered, do nothing.
97  if (action_keybinding_.get())
98    return;
99
100  extensions::Command extension_command;
101  views::FocusManager* focus_manager =
102      delegate_->GetFocusManagerForAccelerator();
103  if (focus_manager && GetExtensionCommand(&extension_command)) {
104    action_keybinding_.reset(
105        new ui::Accelerator(extension_command.accelerator()));
106    focus_manager->RegisterAccelerator(
107        *action_keybinding_,
108        GetAcceleratorPriority(extension_command.accelerator(), extension_),
109        this);
110  }
111}
112
113void ExtensionActionViewController::UnregisterCommand(bool only_if_removed) {
114  views::FocusManager* focus_manager =
115      delegate_->GetFocusManagerForAccelerator();
116  if (!focus_manager || !action_keybinding_.get())
117    return;
118
119  // If |only_if_removed| is true, it means that we only need to unregister
120  // ourselves as an accelerator if the command was removed. Otherwise, we need
121  // to unregister ourselves no matter what (likely because we are shutting
122  // down).
123  extensions::Command extension_command;
124  if (!only_if_removed || !GetExtensionCommand(&extension_command)) {
125    focus_manager->UnregisterAccelerator(*action_keybinding_, this);
126    action_keybinding_.reset();
127  }
128}
129
130void ExtensionActionViewController::OnIconUpdated() {
131  delegate_->OnIconUpdated();
132}
133
134bool ExtensionActionViewController::AcceleratorPressed(
135    const ui::Accelerator& accelerator) {
136  // We shouldn't be handling any accelerators if the view is hidden, unless
137  // this is a browser action.
138  DCHECK(extension_action_->action_type() == ActionInfo::TYPE_BROWSER ||
139         delegate_->GetAsView()->visible());
140
141  // Normal priority shortcuts must be handled via standard browser commands to
142  // be processed at the proper time.
143  if (GetAcceleratorPriority(accelerator, extension()) ==
144      ui::AcceleratorManager::kNormalPriority)
145    return false;
146
147  ExecuteActionByUser();
148  return true;
149}
150
151bool ExtensionActionViewController::CanHandleAccelerators() const {
152  // Page actions can only handle accelerators when they are visible.
153  // Browser actions can handle accelerators even when not visible, since they
154  // might be hidden in an overflow menu.
155  return extension_action_->action_type() == ActionInfo::TYPE_PAGE ?
156      delegate_->GetAsView()->visible() : true;
157}
158
159void ExtensionActionViewController::OnWidgetDestroying(views::Widget* widget) {
160  DCHECK(popup_);
161  DCHECK_EQ(popup_->GetWidget(), widget);
162  CleanupPopup(false);
163}
164
165void ExtensionActionViewController::ShowContextMenuForView(
166    views::View* source,
167    const gfx::Point& point,
168    ui::MenuSourceType source_type) {
169
170  // If there's another active menu that won't be dismissed by opening this one,
171  // then we can't show this one right away, since we can only show one nested
172  // menu at a time.
173  // If the other menu is an extension action's context menu, then we'll run
174  // this one after that one closes. If it's a different type of menu, then we
175  // close it and give up, for want of a better solution. (Luckily, this is
176  // rare).
177  // TODO(devlin): Update this when views code no longer runs menus in a nested
178  // loop.
179  if (context_menu_owner) {
180    context_menu_owner->followup_context_menu_task_ =
181        base::Bind(&ExtensionActionViewController::DoShowContextMenu,
182                   weak_factory_.GetWeakPtr(),
183                   source_type);
184  }
185  if (CloseActiveMenuIfNeeded())
186    return;
187
188  // Otherwise, no other menu is showing, and we can proceed normally.
189  DoShowContextMenu(source_type);
190}
191
192void ExtensionActionViewController::DoShowContextMenu(
193    ui::MenuSourceType source_type) {
194  if (!extension_->ShowConfigureContextMenus())
195    return;
196
197  DCHECK(!context_menu_owner);
198  context_menu_owner = this;
199
200  // We shouldn't have both a popup and a context menu showing.
201  delegate_->HideActivePopup();
202
203  // Reconstructs the menu every time because the menu's contents are dynamic.
204  scoped_refptr<ExtensionContextMenuModel> context_menu_model(
205      new ExtensionContextMenuModel(extension_, browser_, this));
206
207  gfx::Point screen_loc;
208  views::View::ConvertPointToScreen(delegate_->GetAsView(), &screen_loc);
209
210  int run_types = views::MenuRunner::HAS_MNEMONICS |
211                  views::MenuRunner::CONTEXT_MENU;
212  if (delegate_->IsShownInMenu())
213    run_types |= views::MenuRunner::IS_NESTED;
214
215  views::Widget* parent = delegate_->GetParentForContextMenu();
216
217  menu_runner_.reset(
218      new views::MenuRunner(context_menu_model.get(), run_types));
219
220  if (menu_runner_->RunMenuAt(
221          parent,
222          delegate_->GetContextMenuButton(),
223          gfx::Rect(screen_loc, delegate_->GetAsView()->size()),
224          views::MENU_ANCHOR_TOPLEFT,
225          source_type) == views::MenuRunner::MENU_DELETED) {
226    return;
227  }
228
229  context_menu_owner = NULL;
230  menu_runner_.reset();
231
232  // If another extension action wants to show its context menu, allow it to.
233  if (!followup_context_menu_task_.is_null()) {
234    base::Closure task = followup_context_menu_task_;
235    followup_context_menu_task_ = base::Closure();
236    task.Run();
237  }
238}
239
240bool ExtensionActionViewController::ShowPopupWithUrl(
241    ExtensionPopup::ShowAction show_action,
242    const GURL& popup_url,
243    bool grant_tab_permissions) {
244  // If we're already showing the popup for this browser action, just hide it
245  // and return.
246  bool already_showing = popup_ != NULL;
247
248  // Always hide the current popup, even if it's not the same.
249  // Only one popup should be visible at a time.
250  delegate_->HideActivePopup();
251
252  // Similarly, don't allow a context menu and a popup to be showing
253  // simultaneously.
254  CloseActiveMenuIfNeeded();
255
256  if (already_showing)
257    return false;
258
259  views::BubbleBorder::Arrow arrow = base::i18n::IsRTL() ?
260      views::BubbleBorder::TOP_LEFT : views::BubbleBorder::TOP_RIGHT;
261
262  views::View* reference_view = delegate_->GetReferenceViewForPopup();
263
264  popup_ = ExtensionPopup::ShowPopup(
265               popup_url, browser_, reference_view, arrow, show_action);
266  popup_->GetWidget()->AddObserver(this);
267
268  delegate_->OnPopupShown(grant_tab_permissions);
269
270  return true;
271}
272
273bool ExtensionActionViewController::GetExtensionCommand(
274    extensions::Command* command) {
275  DCHECK(command);
276  CommandService* command_service = CommandService::Get(browser_->profile());
277  if (extension_action_->action_type() == ActionInfo::TYPE_PAGE) {
278    return command_service->GetPageActionCommand(
279        extension_->id(), CommandService::ACTIVE_ONLY, command, NULL);
280  }
281  return command_service->GetBrowserActionCommand(
282      extension_->id(), CommandService::ACTIVE_ONLY, command, NULL);
283}
284
285bool ExtensionActionViewController::CloseActiveMenuIfNeeded() {
286  // If this view is shown inside another menu, there's a possibility that there
287  // is another context menu showing that we have to close before we can
288  // activate a different menu.
289  if (delegate_->IsShownInMenu()) {
290    views::MenuController* menu_controller =
291        views::MenuController::GetActiveInstance();
292    // If this is shown inside a menu, then there should always be an active
293    // menu controller.
294    DCHECK(menu_controller);
295    if (menu_controller->in_nested_run()) {
296      // There is another menu showing. Close the outermost menu (since we are
297      // shown in the same menu, we don't want to close the whole thing).
298      menu_controller->Cancel(views::MenuController::EXIT_OUTERMOST);
299      return true;
300    }
301  }
302
303  return false;
304}
305
306void ExtensionActionViewController::CleanupPopup(bool close_widget) {
307  DCHECK(popup_);
308  delegate_->CleanupPopup();
309  popup_->GetWidget()->RemoveObserver(this);
310  if (close_widget)
311    popup_->GetWidget()->Close();
312  popup_ = NULL;
313}
314