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