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