blob: a28c9f6b5f0bcd7f590f55e9f254042c5a38ca41 [file] [log] [blame]
[email protected]4f8a4d12012-09-28 19:23:091// Copyright (c) 2012 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
[email protected]f5fede02014-07-29 02:48:215#include "chrome/browser/extensions/context_menu_matcher.h"
6
dchengc963c7142016-04-08 03:55:227#include "base/memory/ptr_util.h"
[email protected]112158af2013-06-07 23:46:188#include "base/strings/utf_string_conversions.h"
[email protected]4f8a4d12012-09-28 19:23:099#include "chrome/app/chrome_command_ids.h"
[email protected]a7ff4b72013-10-17 20:56:0210#include "chrome/browser/extensions/extension_util.h"
[email protected]fc103da2014-08-16 01:09:3211#include "chrome/common/extensions/api/context_menus.h"
[email protected]f5fede02014-07-29 02:48:2112#include "content/public/browser/browser_context.h"
[email protected]4f8a4d12012-09-28 19:23:0913#include "content/public/common/context_menu_params.h"
[email protected]fc103da2014-08-16 01:09:3214#include "extensions/browser/extension_registry.h"
[email protected]4f8a4d12012-09-28 19:23:0915#include "ui/gfx/favicon_size.h"
[email protected]f34efa22013-03-05 19:14:2316#include "ui/gfx/image/image.h"
[email protected]4f8a4d12012-09-28 19:23:0917
18namespace extensions {
19
[email protected]a146532b2014-07-30 11:20:0920namespace {
21
22// The range of command IDs reserved for extension's custom menus.
23// TODO(oshima): These values will be injected by embedders.
24int extensions_context_custom_first = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST;
25int extensions_context_custom_last = IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST;
26
27} // namespace
28
[email protected]4f8a4d12012-09-28 19:23:0929// static
30const size_t ContextMenuMatcher::kMaxExtensionItemTitleLength = 75;
31
[email protected]a146532b2014-07-30 11:20:0932// static
33int ContextMenuMatcher::ConvertToExtensionsCustomCommandId(int id) {
34 return extensions_context_custom_first + id;
35}
36
37// static
38bool ContextMenuMatcher::IsExtensionsCustomCommandId(int id) {
39 return id >= extensions_context_custom_first &&
40 id <= extensions_context_custom_last;
41}
42
[email protected]4f8a4d12012-09-28 19:23:0943ContextMenuMatcher::ContextMenuMatcher(
[email protected]f5fede02014-07-29 02:48:2144 content::BrowserContext* browser_context,
[email protected]4f8a4d12012-09-28 19:23:0945 ui::SimpleMenuModel::Delegate* delegate,
46 ui::SimpleMenuModel* menu_model,
47 const base::Callback<bool(const MenuItem*)>& filter)
[email protected]f5fede02014-07-29 02:48:2148 : browser_context_(browser_context),
49 menu_model_(menu_model),
50 delegate_(delegate),
[email protected]4f8a4d12012-09-28 19:23:0951 filter_(filter) {
52}
53
[email protected]439f1e32013-12-09 20:09:0954void ContextMenuMatcher::AppendExtensionItems(
[email protected]6f9d2c62014-03-10 12:12:0555 const MenuItem::ExtensionKey& extension_key,
[email protected]439f1e32013-12-09 20:09:0956 const base::string16& selection_text,
[email protected]69e1c12d2014-08-13 08:25:3457 int* index,
58 bool is_action_menu) {
[email protected]4f8a4d12012-09-28 19:23:0959 DCHECK_GE(*index, 0);
60 int max_index =
[email protected]a146532b2014-07-30 11:20:0961 extensions_context_custom_last - extensions_context_custom_first;
[email protected]0ea8fac2013-06-12 15:31:3562 if (*index >= max_index)
[email protected]4f8a4d12012-09-28 19:23:0963 return;
64
[email protected]0ea8fac2013-06-12 15:31:3565 const Extension* extension = NULL;
66 MenuItem::List items;
67 bool can_cross_incognito;
[email protected]6f9d2c62014-03-10 12:12:0568 if (!GetRelevantExtensionTopLevelItems(
[email protected]fc103da2014-08-16 01:09:3269 extension_key, &extension, &can_cross_incognito, &items))
[email protected]4f8a4d12012-09-28 19:23:0970 return;
[email protected]4f8a4d12012-09-28 19:23:0971
72 if (items.empty())
73 return;
74
75 // If this is the first extension-provided menu item, and there are other
76 // items in the menu, and the last item is not a separator add a separator.
catmullingsff2cdbc2017-08-22 22:07:0277 bool prepend_separator = *index == 0 && menu_model_->GetItemCount();
[email protected]4f8a4d12012-09-28 19:23:0978
79 // Extensions (other than platform apps) are only allowed one top-level slot
80 // (and it can't be a radio or checkbox item because we are going to put the
[email protected]69e1c12d2014-08-13 08:25:3481 // extension icon next to it), unless the context menu is an an action menu.
82 // Action menus do not include the extension action, and they only include
83 // items from one extension, so they are not placed within a submenu.
84 // Otherwise, we automatically push them into a submenu if there is more than
85 // one top-level item.
86 if (extension->is_platform_app() || is_action_menu) {
catmullingsff2cdbc2017-08-22 22:07:0287 if (prepend_separator)
88 menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]69e1c12d2014-08-13 08:25:3489 RecursivelyAppendExtensionItems(items,
90 can_cross_incognito,
91 selection_text,
92 menu_model_,
93 index,
94 is_action_menu);
[email protected]4f8a4d12012-09-28 19:23:0995 } else {
[email protected]a146532b2014-07-30 11:20:0996 int menu_id = ConvertToExtensionsCustomCommandId(*index);
97 (*index)++;
[email protected]439f1e32013-12-09 20:09:0998 base::string16 title;
[email protected]4f8a4d12012-09-28 19:23:0999 MenuItem::List submenu_items;
100
101 if (items.size() > 1 || items[0]->type() != MenuItem::NORMAL) {
catmullingsff2cdbc2017-08-22 22:07:02102 if (prepend_separator)
103 menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]04338722013-12-24 23:18:05104 title = base::UTF8ToUTF16(extension->name());
[email protected]4f8a4d12012-09-28 19:23:09105 submenu_items = items;
106 } else {
catmullingsff2cdbc2017-08-22 22:07:02107 // The top-level menu item, |item[0]|, is sandwiched between two menu
108 // separators. If the top-level menu item is visible, its preceding
109 // separator should be included in the UI model, so that both separators
110 // are shown. Otherwise if the top-level menu item is hidden, the
111 // preceding separator should be excluded, so that only one of the two
112 // separators remain.
113 if (prepend_separator && items[0]->visible())
114 menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09115 MenuItem* item = items[0];
116 extension_item_map_[menu_id] = item->id();
117 title = item->TitleWithReplacement(selection_text,
118 kMaxExtensionItemTitleLength);
119 submenu_items = GetRelevantExtensionItems(item->children(),
120 can_cross_incognito);
121 }
122
123 // Now add our item(s) to the menu_model_.
124 if (submenu_items.empty()) {
125 menu_model_->AddItem(menu_id, title);
126 } else {
127 ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
dchengc963c7142016-04-08 03:55:22128 extension_menu_models_.push_back(base::WrapUnique(submenu));
[email protected]4f8a4d12012-09-28 19:23:09129 menu_model_->AddSubMenu(menu_id, title, submenu);
limasdf7e955d22015-12-15 05:17:39130 RecursivelyAppendExtensionItems(submenu_items, can_cross_incognito,
131 selection_text, submenu, index,
[email protected]69e1c12d2014-08-13 08:25:34132 false); // is_action_menu_top_level
[email protected]4f8a4d12012-09-28 19:23:09133 }
[email protected]69e1c12d2014-08-13 08:25:34134 if (!is_action_menu)
135 SetExtensionIcon(extension_key.extension_id);
[email protected]4f8a4d12012-09-28 19:23:09136 }
137}
138
139void ContextMenuMatcher::Clear() {
140 extension_item_map_.clear();
141 extension_menu_models_.clear();
142}
143
[email protected]0ea8fac2013-06-12 15:31:35144base::string16 ContextMenuMatcher::GetTopLevelContextMenuTitle(
[email protected]6f9d2c62014-03-10 12:12:05145 const MenuItem::ExtensionKey& extension_key,
[email protected]439f1e32013-12-09 20:09:09146 const base::string16& selection_text) {
[email protected]0ea8fac2013-06-12 15:31:35147 const Extension* extension = NULL;
148 MenuItem::List items;
149 bool can_cross_incognito;
[email protected]6f9d2c62014-03-10 12:12:05150 GetRelevantExtensionTopLevelItems(
[email protected]fc103da2014-08-16 01:09:32151 extension_key, &extension, &can_cross_incognito, &items);
[email protected]0ea8fac2013-06-12 15:31:35152
153 base::string16 title;
154
155 if (items.empty() ||
156 items.size() > 1 ||
157 items[0]->type() != MenuItem::NORMAL) {
[email protected]04338722013-12-24 23:18:05158 title = base::UTF8ToUTF16(extension->name());
[email protected]0ea8fac2013-06-12 15:31:35159 } else {
160 MenuItem* item = items[0];
161 title = item->TitleWithReplacement(
162 selection_text, kMaxExtensionItemTitleLength);
163 }
164 return title;
165}
166
[email protected]4f8a4d12012-09-28 19:23:09167bool ContextMenuMatcher::IsCommandIdChecked(int command_id) const {
168 MenuItem* item = GetExtensionMenuItem(command_id);
169 if (!item)
170 return false;
171 return item->checked();
172}
173
catmullingsff2cdbc2017-08-22 22:07:02174bool ContextMenuMatcher::IsCommandIdVisible(int command_id) const {
175 MenuItem* item = GetExtensionMenuItem(command_id);
176 // The context menu code creates a top-level menu item, labeled with the
177 // extension's name, that is a container of an extension's menu items. This
178 // top-level menu item is not added to the context menu, so checking its
179 // visibility is a special case handled below. This top-level menu item should
180 // always be displayed.
Catherine Mullings783d51f2017-09-15 01:50:22181 if (!item && command_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST &&
182 command_id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) {
catmullingsff2cdbc2017-08-22 22:07:02183 return true;
Catherine Mullings783d51f2017-09-15 01:50:22184 } else if (item) {
185 return item->visible();
186 } else {
catmullingsff2cdbc2017-08-22 22:07:02187 return false;
Catherine Mullings783d51f2017-09-15 01:50:22188 }
catmullingsff2cdbc2017-08-22 22:07:02189}
190
[email protected]4f8a4d12012-09-28 19:23:09191bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const {
192 MenuItem* item = GetExtensionMenuItem(command_id);
193 if (!item)
194 return true;
195 return item->enabled();
196}
197
robcbe35ba2016-03-10 01:20:49198void ContextMenuMatcher::ExecuteCommand(
199 int command_id,
[email protected]4f8a4d12012-09-28 19:23:09200 content::WebContents* web_contents,
robcbe35ba2016-03-10 01:20:49201 content::RenderFrameHost* render_frame_host,
[email protected]4f8a4d12012-09-28 19:23:09202 const content::ContextMenuParams& params) {
[email protected]4f8a4d12012-09-28 19:23:09203 MenuItem* item = GetExtensionMenuItem(command_id);
204 if (!item)
205 return;
206
[email protected]f5fede02014-07-29 02:48:21207 MenuManager* manager = MenuManager::Get(browser_context_);
robcbe35ba2016-03-10 01:20:49208 manager->ExecuteCommand(browser_context_, web_contents, render_frame_host,
209 params, item->id());
[email protected]4f8a4d12012-09-28 19:23:09210}
211
[email protected]0ea8fac2013-06-12 15:31:35212bool ContextMenuMatcher::GetRelevantExtensionTopLevelItems(
[email protected]6f9d2c62014-03-10 12:12:05213 const MenuItem::ExtensionKey& extension_key,
[email protected]0ea8fac2013-06-12 15:31:35214 const Extension** extension,
215 bool* can_cross_incognito,
[email protected]fc103da2014-08-16 01:09:32216 MenuItem::List* items) {
217 *extension = ExtensionRegistry::Get(
218 browser_context_)->enabled_extensions().GetByID(
219 extension_key.extension_id);
[email protected]0ea8fac2013-06-12 15:31:35220 if (!*extension)
221 return false;
222
223 // Find matching items.
[email protected]f5fede02014-07-29 02:48:21224 MenuManager* manager = MenuManager::Get(browser_context_);
avi5d5b7e92016-10-21 01:11:40225 const MenuItem::OwnedList* all_items = manager->MenuItems(extension_key);
[email protected]0ea8fac2013-06-12 15:31:35226 if (!all_items || all_items->empty())
227 return false;
228
[email protected]f5fede02014-07-29 02:48:21229 *can_cross_incognito = util::CanCrossIncognito(*extension, browser_context_);
[email protected]fc103da2014-08-16 01:09:32230 *items = GetRelevantExtensionItems(*all_items, *can_cross_incognito);
[email protected]0ea8fac2013-06-12 15:31:35231
232 return true;
233}
234
[email protected]4f8a4d12012-09-28 19:23:09235MenuItem::List ContextMenuMatcher::GetRelevantExtensionItems(
avi5d5b7e92016-10-21 01:11:40236 const MenuItem::OwnedList& items,
[email protected]4f8a4d12012-09-28 19:23:09237 bool can_cross_incognito) {
238 MenuItem::List result;
avi5d5b7e92016-10-21 01:11:40239 for (auto i = items.begin(); i != items.end(); ++i) {
240 MenuItem* item = i->get();
[email protected]4f8a4d12012-09-28 19:23:09241
242 if (!filter_.Run(item))
243 continue;
244
[email protected]f5fede02014-07-29 02:48:21245 if (item->id().incognito == browser_context_->IsOffTheRecord() ||
[email protected]4f8a4d12012-09-28 19:23:09246 can_cross_incognito)
avi5d5b7e92016-10-21 01:11:40247 result.push_back(item);
[email protected]4f8a4d12012-09-28 19:23:09248 }
249 return result;
250}
251
252void ContextMenuMatcher::RecursivelyAppendExtensionItems(
253 const MenuItem::List& items,
254 bool can_cross_incognito,
[email protected]439f1e32013-12-09 20:09:09255 const base::string16& selection_text,
[email protected]4f8a4d12012-09-28 19:23:09256 ui::SimpleMenuModel* menu_model,
[email protected]69e1c12d2014-08-13 08:25:34257 int* index,
258 bool is_action_menu_top_level) {
[email protected]4f8a4d12012-09-28 19:23:09259 MenuItem::Type last_type = MenuItem::NORMAL;
260 int radio_group_id = 1;
catmullingsff2cdbc2017-08-22 22:07:02261 int num_visible_items = 0;
[email protected]4f8a4d12012-09-28 19:23:09262
avi5d5b7e92016-10-21 01:11:40263 for (auto i = items.begin(); i != items.end(); ++i) {
[email protected]4f8a4d12012-09-28 19:23:09264 MenuItem* item = *i;
265
266 // If last item was of type radio but the current one isn't, auto-insert
267 // a separator. The converse case is handled below.
268 if (last_type == MenuItem::RADIO &&
269 item->type() != MenuItem::RADIO) {
270 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
271 last_type = MenuItem::SEPARATOR;
272 }
273
[email protected]a146532b2014-07-30 11:20:09274 int menu_id = ConvertToExtensionsCustomCommandId(*index);
[email protected]69e1c12d2014-08-13 08:25:34275 // Action context menus have a limit for top level extension items to
276 // prevent control items from being pushed off the screen, since extension
277 // items will not be placed in a submenu.
[email protected]fc103da2014-08-16 01:09:32278 const int top_level_limit = api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT;
[email protected]69e1c12d2014-08-13 08:25:34279 if (menu_id >= extensions_context_custom_last ||
catmullingsff2cdbc2017-08-22 22:07:02280 (is_action_menu_top_level && num_visible_items >= top_level_limit))
[email protected]4f8a4d12012-09-28 19:23:09281 return;
[email protected]69e1c12d2014-08-13 08:25:34282
lazyboy413226da2015-05-14 22:18:20283 ++(*index);
catmullingsff2cdbc2017-08-22 22:07:02284 if (item->visible())
285 ++num_visible_items;
lazyboy413226da2015-05-14 22:18:20286
[email protected]4f8a4d12012-09-28 19:23:09287 extension_item_map_[menu_id] = item->id();
[email protected]439f1e32013-12-09 20:09:09288 base::string16 title = item->TitleWithReplacement(selection_text,
[email protected]4f8a4d12012-09-28 19:23:09289 kMaxExtensionItemTitleLength);
290 if (item->type() == MenuItem::NORMAL) {
291 MenuItem::List children =
292 GetRelevantExtensionItems(item->children(), can_cross_incognito);
293 if (children.empty()) {
294 menu_model->AddItem(menu_id, title);
295 } else {
296 ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
dchengc963c7142016-04-08 03:55:22297 extension_menu_models_.push_back(base::WrapUnique(submenu));
[email protected]4f8a4d12012-09-28 19:23:09298 menu_model->AddSubMenu(menu_id, title, submenu);
limasdf7e955d22015-12-15 05:17:39299 RecursivelyAppendExtensionItems(children, can_cross_incognito,
300 selection_text, submenu, index,
[email protected]69e1c12d2014-08-13 08:25:34301 false); // is_action_menu_top_level
[email protected]4f8a4d12012-09-28 19:23:09302 }
303 } else if (item->type() == MenuItem::CHECKBOX) {
304 menu_model->AddCheckItem(menu_id, title);
305 } else if (item->type() == MenuItem::RADIO) {
306 if (i != items.begin() &&
307 last_type != MenuItem::RADIO) {
308 radio_group_id++;
309
310 // Auto-append a separator if needed.
[email protected]00491c052013-02-08 10:53:25311 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09312 }
313
314 menu_model->AddRadioItem(menu_id, title, radio_group_id);
315 } else if (item->type() == MenuItem::SEPARATOR) {
[email protected]00491c052013-02-08 10:53:25316 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09317 }
318 last_type = item->type();
319 }
320}
321
322MenuItem* ContextMenuMatcher::GetExtensionMenuItem(int id) const {
[email protected]f5fede02014-07-29 02:48:21323 MenuManager* manager = MenuManager::Get(browser_context_);
[email protected]4f8a4d12012-09-28 19:23:09324 std::map<int, MenuItem::Id>::const_iterator i =
325 extension_item_map_.find(id);
326 if (i != extension_item_map_.end()) {
327 MenuItem* item = manager->GetItemById(i->second);
328 if (item)
329 return item;
330 }
331 return NULL;
332}
333
334void ContextMenuMatcher::SetExtensionIcon(const std::string& extension_id) {
[email protected]f5fede02014-07-29 02:48:21335 MenuManager* menu_manager = MenuManager::Get(browser_context_);
[email protected]4f8a4d12012-09-28 19:23:09336
337 int index = menu_model_->GetItemCount() - 1;
338 DCHECK_GE(index, 0);
339
estade32426e02016-12-18 01:26:17340 gfx::Image icon = menu_manager->GetIconForExtension(extension_id);
341 DCHECK_EQ(gfx::kFaviconSize, icon.Width());
342 DCHECK_EQ(gfx::kFaviconSize, icon.Height());
343 menu_model_->SetIcon(index, icon);
[email protected]4f8a4d12012-09-28 19:23:09344}
345
346} // namespace extensions