extension_action_view_controller.cc revision 03b57e008b61dfcb1fbad3aea950ae0e001748b0
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_tab_helper.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); 52 DCHECK(extension_action->action_type() == ActionInfo::TYPE_PAGE || 53 extension_action->action_type() == ActionInfo::TYPE_BROWSER); 54 DCHECK(extension); 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 ? SessionTabHelper::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