Home | History | Annotate | Download | only in extensions
      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 
     23 using extensions::ActionInfo;
     24 using extensions::CommandService;
     25 
     26 namespace {
     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.
     32 ExtensionActionViewController* context_menu_owner = NULL;
     33 
     34 }  // namespace
     35 
     36 ExtensionActionViewController::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 
     54 ExtensionActionViewController::~ExtensionActionViewController() {
     55   if (context_menu_owner == this)
     56     context_menu_owner = NULL;
     57   HidePopup();
     58   UnregisterCommand(false);
     59 }
     60 
     61 void ExtensionActionViewController::InspectPopup() {
     62   ExecuteAction(ExtensionPopup::SHOW_AND_INSPECT, true);
     63 }
     64 
     65 void ExtensionActionViewController::ExecuteActionByUser() {
     66   ExecuteAction(ExtensionPopup::SHOW, true);
     67 }
     68 
     69 bool 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 
     81 void ExtensionActionViewController::HidePopup() {
     82   if (popup_)
     83     CleanupPopup(true);
     84 }
     85 
     86 gfx::Image ExtensionActionViewController::GetIcon(int tab_id) {
     87   return icon_factory_.GetIcon(tab_id);
     88 }
     89 
     90 int ExtensionActionViewController::GetCurrentTabId() const {
     91   content::WebContents* web_contents = delegate_->GetCurrentWebContents();
     92   return web_contents ? SessionTabHelper::IdForTab(web_contents) : -1;
     93 }
     94 
     95 void 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 
    113 void 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 
    130 void ExtensionActionViewController::OnIconUpdated() {
    131   delegate_->OnIconUpdated();
    132 }
    133 
    134 bool 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 
    151 bool 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 
    159 void ExtensionActionViewController::OnWidgetDestroying(views::Widget* widget) {
    160   DCHECK(popup_);
    161   DCHECK_EQ(popup_->GetWidget(), widget);
    162   CleanupPopup(false);
    163 }
    164 
    165 void 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 
    192 void 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 
    240 bool 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 
    273 bool 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 
    285 bool 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 
    306 void 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