blob: ab803af5ef18eb2d9c70f33ba5fabb82e7fc3601 [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
Jorge Gilc99913a52018-08-01 23:50:377#include <string>
8
dchengc963c7142016-04-08 03:55:229#include "base/memory/ptr_util.h"
[email protected]112158af2013-06-07 23:46:1810#include "base/strings/utf_string_conversions.h"
[email protected]4f8a4d12012-09-28 19:23:0911#include "chrome/app/chrome_command_ids.h"
[email protected]fc103da2014-08-16 01:09:3212#include "chrome/common/extensions/api/context_menus.h"
[email protected]f5fede02014-07-29 02:48:2113#include "content/public/browser/browser_context.h"
[email protected]4f8a4d12012-09-28 19:23:0914#include "content/public/common/context_menu_params.h"
[email protected]fc103da2014-08-16 01:09:3215#include "extensions/browser/extension_registry.h"
Clark DuVall1d816192019-07-19 19:54:4216#include "extensions/browser/extension_util.h"
[email protected]4f8a4d12012-09-28 19:23:0917#include "ui/gfx/favicon_size.h"
[email protected]f34efa22013-03-05 19:14:2318#include "ui/gfx/image/image.h"
[email protected]4f8a4d12012-09-28 19:23:0919
20namespace extensions {
21
[email protected]a146532b2014-07-30 11:20:0922namespace {
23
24// The range of command IDs reserved for extension's custom menus.
25// TODO(oshima): These values will be injected by embedders.
26int extensions_context_custom_first = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST;
27int extensions_context_custom_last = IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST;
28
29} // namespace
30
[email protected]4f8a4d12012-09-28 19:23:0931// static
32const size_t ContextMenuMatcher::kMaxExtensionItemTitleLength = 75;
33
[email protected]a146532b2014-07-30 11:20:0934// static
35int ContextMenuMatcher::ConvertToExtensionsCustomCommandId(int id) {
36 return extensions_context_custom_first + id;
37}
38
39// static
40bool ContextMenuMatcher::IsExtensionsCustomCommandId(int id) {
41 return id >= extensions_context_custom_first &&
42 id <= extensions_context_custom_last;
43}
44
[email protected]4f8a4d12012-09-28 19:23:0945ContextMenuMatcher::ContextMenuMatcher(
[email protected]f5fede02014-07-29 02:48:2146 content::BrowserContext* browser_context,
[email protected]4f8a4d12012-09-28 19:23:0947 ui::SimpleMenuModel::Delegate* delegate,
48 ui::SimpleMenuModel* menu_model,
49 const base::Callback<bool(const MenuItem*)>& filter)
[email protected]f5fede02014-07-29 02:48:2150 : browser_context_(browser_context),
51 menu_model_(menu_model),
52 delegate_(delegate),
Jorge Gilc99913a52018-08-01 23:50:3753 filter_(filter),
54 is_smart_text_selection_enabled_(false) {}
[email protected]4f8a4d12012-09-28 19:23:0955
[email protected]439f1e32013-12-09 20:09:0956void ContextMenuMatcher::AppendExtensionItems(
[email protected]6f9d2c62014-03-10 12:12:0557 const MenuItem::ExtensionKey& extension_key,
[email protected]439f1e32013-12-09 20:09:0958 const base::string16& selection_text,
[email protected]69e1c12d2014-08-13 08:25:3459 int* index,
60 bool is_action_menu) {
[email protected]4f8a4d12012-09-28 19:23:0961 DCHECK_GE(*index, 0);
62 int max_index =
[email protected]a146532b2014-07-30 11:20:0963 extensions_context_custom_last - extensions_context_custom_first;
[email protected]0ea8fac2013-06-12 15:31:3564 if (*index >= max_index)
[email protected]4f8a4d12012-09-28 19:23:0965 return;
66
[email protected]0ea8fac2013-06-12 15:31:3567 const Extension* extension = NULL;
68 MenuItem::List items;
69 bool can_cross_incognito;
[email protected]6f9d2c62014-03-10 12:12:0570 if (!GetRelevantExtensionTopLevelItems(
[email protected]fc103da2014-08-16 01:09:3271 extension_key, &extension, &can_cross_incognito, &items))
[email protected]4f8a4d12012-09-28 19:23:0972 return;
[email protected]4f8a4d12012-09-28 19:23:0973
74 if (items.empty())
75 return;
76
Alex Newcomer1ee2d562018-12-06 02:05:0577 bool prepend_separator = false;
78
79#if !defined(OS_CHROMEOS)
[email protected]4f8a4d12012-09-28 19:23:0980 // If this is the first extension-provided menu item, and there are other
81 // items in the menu, and the last item is not a separator add a separator.
Jorge Gilc99913a52018-08-01 23:50:3782 // Also, don't add separators when Smart Text Selection is enabled. Smart
83 // actions are grouped with extensions and the separator logic is
84 // handled by them.
Alex Newcomer1ee2d562018-12-06 02:05:0585 prepend_separator = *index == 0 && menu_model_->GetItemCount() &&
86 !is_smart_text_selection_enabled_;
87#endif
[email protected]4f8a4d12012-09-28 19:23:0988
89 // Extensions (other than platform apps) are only allowed one top-level slot
90 // (and it can't be a radio or checkbox item because we are going to put the
[email protected]69e1c12d2014-08-13 08:25:3491 // extension icon next to it), unless the context menu is an an action menu.
92 // Action menus do not include the extension action, and they only include
93 // items from one extension, so they are not placed within a submenu.
94 // Otherwise, we automatically push them into a submenu if there is more than
95 // one top-level item.
96 if (extension->is_platform_app() || is_action_menu) {
catmullingsff2cdbc2017-08-22 22:07:0297 if (prepend_separator)
98 menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]69e1c12d2014-08-13 08:25:3499 RecursivelyAppendExtensionItems(items,
100 can_cross_incognito,
101 selection_text,
102 menu_model_,
103 index,
104 is_action_menu);
[email protected]4f8a4d12012-09-28 19:23:09105 } else {
[email protected]a146532b2014-07-30 11:20:09106 int menu_id = ConvertToExtensionsCustomCommandId(*index);
107 (*index)++;
[email protected]439f1e32013-12-09 20:09:09108 base::string16 title;
[email protected]4f8a4d12012-09-28 19:23:09109 MenuItem::List submenu_items;
110
111 if (items.size() > 1 || items[0]->type() != MenuItem::NORMAL) {
catmullingsff2cdbc2017-08-22 22:07:02112 if (prepend_separator)
113 menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]04338722013-12-24 23:18:05114 title = base::UTF8ToUTF16(extension->name());
[email protected]4f8a4d12012-09-28 19:23:09115 submenu_items = items;
116 } else {
catmullingsff2cdbc2017-08-22 22:07:02117 // The top-level menu item, |item[0]|, is sandwiched between two menu
118 // separators. If the top-level menu item is visible, its preceding
119 // separator should be included in the UI model, so that both separators
120 // are shown. Otherwise if the top-level menu item is hidden, the
121 // preceding separator should be excluded, so that only one of the two
122 // separators remain.
123 if (prepend_separator && items[0]->visible())
124 menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09125 MenuItem* item = items[0];
126 extension_item_map_[menu_id] = item->id();
127 title = item->TitleWithReplacement(selection_text,
128 kMaxExtensionItemTitleLength);
129 submenu_items = GetRelevantExtensionItems(item->children(),
130 can_cross_incognito);
131 }
132
133 // Now add our item(s) to the menu_model_.
134 if (submenu_items.empty()) {
135 menu_model_->AddItem(menu_id, title);
136 } else {
137 ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
dchengc963c7142016-04-08 03:55:22138 extension_menu_models_.push_back(base::WrapUnique(submenu));
[email protected]4f8a4d12012-09-28 19:23:09139 menu_model_->AddSubMenu(menu_id, title, submenu);
limasdf7e955d22015-12-15 05:17:39140 RecursivelyAppendExtensionItems(submenu_items, can_cross_incognito,
141 selection_text, submenu, index,
[email protected]69e1c12d2014-08-13 08:25:34142 false); // is_action_menu_top_level
[email protected]4f8a4d12012-09-28 19:23:09143 }
[email protected]69e1c12d2014-08-13 08:25:34144 if (!is_action_menu)
145 SetExtensionIcon(extension_key.extension_id);
[email protected]4f8a4d12012-09-28 19:23:09146 }
147}
148
149void ContextMenuMatcher::Clear() {
150 extension_item_map_.clear();
151 extension_menu_models_.clear();
152}
153
[email protected]0ea8fac2013-06-12 15:31:35154base::string16 ContextMenuMatcher::GetTopLevelContextMenuTitle(
[email protected]6f9d2c62014-03-10 12:12:05155 const MenuItem::ExtensionKey& extension_key,
[email protected]439f1e32013-12-09 20:09:09156 const base::string16& selection_text) {
[email protected]0ea8fac2013-06-12 15:31:35157 const Extension* extension = NULL;
158 MenuItem::List items;
159 bool can_cross_incognito;
[email protected]6f9d2c62014-03-10 12:12:05160 GetRelevantExtensionTopLevelItems(
[email protected]fc103da2014-08-16 01:09:32161 extension_key, &extension, &can_cross_incognito, &items);
[email protected]0ea8fac2013-06-12 15:31:35162
163 base::string16 title;
164
165 if (items.empty() ||
166 items.size() > 1 ||
167 items[0]->type() != MenuItem::NORMAL) {
[email protected]04338722013-12-24 23:18:05168 title = base::UTF8ToUTF16(extension->name());
[email protected]0ea8fac2013-06-12 15:31:35169 } else {
170 MenuItem* item = items[0];
171 title = item->TitleWithReplacement(
172 selection_text, kMaxExtensionItemTitleLength);
173 }
174 return title;
175}
176
[email protected]4f8a4d12012-09-28 19:23:09177bool ContextMenuMatcher::IsCommandIdChecked(int command_id) const {
178 MenuItem* item = GetExtensionMenuItem(command_id);
179 if (!item)
180 return false;
181 return item->checked();
182}
183
catmullingsff2cdbc2017-08-22 22:07:02184bool ContextMenuMatcher::IsCommandIdVisible(int command_id) const {
185 MenuItem* item = GetExtensionMenuItem(command_id);
186 // The context menu code creates a top-level menu item, labeled with the
187 // extension's name, that is a container of an extension's menu items. This
188 // top-level menu item is not added to the context menu, so checking its
189 // visibility is a special case handled below. This top-level menu item should
190 // always be displayed.
Catherine Mullings8e7c1212017-10-02 23:18:19191 if (!item && ContextMenuMatcher::IsExtensionsCustomCommandId(command_id)) {
catmullingsff2cdbc2017-08-22 22:07:02192 return true;
Catherine Mullings783d51f2017-09-15 01:50:22193 } else if (item) {
194 return item->visible();
195 } else {
catmullingsff2cdbc2017-08-22 22:07:02196 return false;
Catherine Mullings783d51f2017-09-15 01:50:22197 }
catmullingsff2cdbc2017-08-22 22:07:02198}
199
[email protected]4f8a4d12012-09-28 19:23:09200bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const {
201 MenuItem* item = GetExtensionMenuItem(command_id);
202 if (!item)
203 return true;
204 return item->enabled();
205}
206
robcbe35ba2016-03-10 01:20:49207void ContextMenuMatcher::ExecuteCommand(
208 int command_id,
[email protected]4f8a4d12012-09-28 19:23:09209 content::WebContents* web_contents,
robcbe35ba2016-03-10 01:20:49210 content::RenderFrameHost* render_frame_host,
[email protected]4f8a4d12012-09-28 19:23:09211 const content::ContextMenuParams& params) {
[email protected]4f8a4d12012-09-28 19:23:09212 MenuItem* item = GetExtensionMenuItem(command_id);
213 if (!item)
214 return;
215
[email protected]f5fede02014-07-29 02:48:21216 MenuManager* manager = MenuManager::Get(browser_context_);
robcbe35ba2016-03-10 01:20:49217 manager->ExecuteCommand(browser_context_, web_contents, render_frame_host,
218 params, item->id());
[email protected]4f8a4d12012-09-28 19:23:09219}
220
[email protected]0ea8fac2013-06-12 15:31:35221bool ContextMenuMatcher::GetRelevantExtensionTopLevelItems(
[email protected]6f9d2c62014-03-10 12:12:05222 const MenuItem::ExtensionKey& extension_key,
[email protected]0ea8fac2013-06-12 15:31:35223 const Extension** extension,
224 bool* can_cross_incognito,
[email protected]fc103da2014-08-16 01:09:32225 MenuItem::List* items) {
226 *extension = ExtensionRegistry::Get(
227 browser_context_)->enabled_extensions().GetByID(
228 extension_key.extension_id);
[email protected]0ea8fac2013-06-12 15:31:35229 if (!*extension)
230 return false;
231
232 // Find matching items.
[email protected]f5fede02014-07-29 02:48:21233 MenuManager* manager = MenuManager::Get(browser_context_);
avi5d5b7e92016-10-21 01:11:40234 const MenuItem::OwnedList* all_items = manager->MenuItems(extension_key);
[email protected]0ea8fac2013-06-12 15:31:35235 if (!all_items || all_items->empty())
236 return false;
237
[email protected]f5fede02014-07-29 02:48:21238 *can_cross_incognito = util::CanCrossIncognito(*extension, browser_context_);
[email protected]fc103da2014-08-16 01:09:32239 *items = GetRelevantExtensionItems(*all_items, *can_cross_incognito);
[email protected]0ea8fac2013-06-12 15:31:35240
241 return true;
242}
243
[email protected]4f8a4d12012-09-28 19:23:09244MenuItem::List ContextMenuMatcher::GetRelevantExtensionItems(
avi5d5b7e92016-10-21 01:11:40245 const MenuItem::OwnedList& items,
[email protected]4f8a4d12012-09-28 19:23:09246 bool can_cross_incognito) {
247 MenuItem::List result;
avi5d5b7e92016-10-21 01:11:40248 for (auto i = items.begin(); i != items.end(); ++i) {
249 MenuItem* item = i->get();
[email protected]4f8a4d12012-09-28 19:23:09250
251 if (!filter_.Run(item))
252 continue;
253
[email protected]f5fede02014-07-29 02:48:21254 if (item->id().incognito == browser_context_->IsOffTheRecord() ||
[email protected]4f8a4d12012-09-28 19:23:09255 can_cross_incognito)
avi5d5b7e92016-10-21 01:11:40256 result.push_back(item);
[email protected]4f8a4d12012-09-28 19:23:09257 }
258 return result;
259}
260
261void ContextMenuMatcher::RecursivelyAppendExtensionItems(
262 const MenuItem::List& items,
263 bool can_cross_incognito,
[email protected]439f1e32013-12-09 20:09:09264 const base::string16& selection_text,
[email protected]4f8a4d12012-09-28 19:23:09265 ui::SimpleMenuModel* menu_model,
[email protected]69e1c12d2014-08-13 08:25:34266 int* index,
267 bool is_action_menu_top_level) {
[email protected]4f8a4d12012-09-28 19:23:09268 MenuItem::Type last_type = MenuItem::NORMAL;
269 int radio_group_id = 1;
catmullingsff2cdbc2017-08-22 22:07:02270 int num_visible_items = 0;
[email protected]4f8a4d12012-09-28 19:23:09271
Alex Newcomer1ee2d562018-12-06 02:05:05272 bool enable_separators = false;
273
274#if !defined(OS_CHROMEOS)
275 enable_separators = true;
276#endif
Alex Newcomera2f222a2018-03-20 20:08:35277
avi5d5b7e92016-10-21 01:11:40278 for (auto i = items.begin(); i != items.end(); ++i) {
[email protected]4f8a4d12012-09-28 19:23:09279 MenuItem* item = *i;
280
281 // If last item was of type radio but the current one isn't, auto-insert
282 // a separator. The converse case is handled below.
Alex Newcomera2f222a2018-03-20 20:08:35283 if (last_type == MenuItem::RADIO && item->type() != MenuItem::RADIO &&
284 enable_separators) {
[email protected]4f8a4d12012-09-28 19:23:09285 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
286 last_type = MenuItem::SEPARATOR;
287 }
288
[email protected]a146532b2014-07-30 11:20:09289 int menu_id = ConvertToExtensionsCustomCommandId(*index);
[email protected]69e1c12d2014-08-13 08:25:34290 // Action context menus have a limit for top level extension items to
291 // prevent control items from being pushed off the screen, since extension
292 // items will not be placed in a submenu.
[email protected]fc103da2014-08-16 01:09:32293 const int top_level_limit = api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT;
[email protected]69e1c12d2014-08-13 08:25:34294 if (menu_id >= extensions_context_custom_last ||
catmullingsff2cdbc2017-08-22 22:07:02295 (is_action_menu_top_level && num_visible_items >= top_level_limit))
[email protected]4f8a4d12012-09-28 19:23:09296 return;
[email protected]69e1c12d2014-08-13 08:25:34297
lazyboy413226da2015-05-14 22:18:20298 ++(*index);
catmullingsff2cdbc2017-08-22 22:07:02299 if (item->visible())
300 ++num_visible_items;
lazyboy413226da2015-05-14 22:18:20301
[email protected]4f8a4d12012-09-28 19:23:09302 extension_item_map_[menu_id] = item->id();
[email protected]439f1e32013-12-09 20:09:09303 base::string16 title = item->TitleWithReplacement(selection_text,
[email protected]4f8a4d12012-09-28 19:23:09304 kMaxExtensionItemTitleLength);
305 if (item->type() == MenuItem::NORMAL) {
306 MenuItem::List children =
307 GetRelevantExtensionItems(item->children(), can_cross_incognito);
308 if (children.empty()) {
309 menu_model->AddItem(menu_id, title);
310 } else {
311 ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
dchengc963c7142016-04-08 03:55:22312 extension_menu_models_.push_back(base::WrapUnique(submenu));
[email protected]4f8a4d12012-09-28 19:23:09313 menu_model->AddSubMenu(menu_id, title, submenu);
limasdf7e955d22015-12-15 05:17:39314 RecursivelyAppendExtensionItems(children, can_cross_incognito,
315 selection_text, submenu, index,
[email protected]69e1c12d2014-08-13 08:25:34316 false); // is_action_menu_top_level
[email protected]4f8a4d12012-09-28 19:23:09317 }
318 } else if (item->type() == MenuItem::CHECKBOX) {
319 menu_model->AddCheckItem(menu_id, title);
320 } else if (item->type() == MenuItem::RADIO) {
321 if (i != items.begin() &&
322 last_type != MenuItem::RADIO) {
323 radio_group_id++;
324
325 // Auto-append a separator if needed.
Alex Newcomera2f222a2018-03-20 20:08:35326 if (enable_separators)
327 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09328 }
329
330 menu_model->AddRadioItem(menu_id, title, radio_group_id);
Alex Newcomera2f222a2018-03-20 20:08:35331 } else if (item->type() == MenuItem::SEPARATOR && enable_separators) {
[email protected]00491c052013-02-08 10:53:25332 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09333 }
334 last_type = item->type();
335 }
336}
337
338MenuItem* ContextMenuMatcher::GetExtensionMenuItem(int id) const {
[email protected]f5fede02014-07-29 02:48:21339 MenuManager* manager = MenuManager::Get(browser_context_);
jdoerrie13cd648c82018-10-02 21:21:02340 auto i = extension_item_map_.find(id);
[email protected]4f8a4d12012-09-28 19:23:09341 if (i != extension_item_map_.end()) {
342 MenuItem* item = manager->GetItemById(i->second);
343 if (item)
344 return item;
345 }
346 return NULL;
347}
348
349void ContextMenuMatcher::SetExtensionIcon(const std::string& extension_id) {
[email protected]f5fede02014-07-29 02:48:21350 MenuManager* menu_manager = MenuManager::Get(browser_context_);
[email protected]4f8a4d12012-09-28 19:23:09351
352 int index = menu_model_->GetItemCount() - 1;
353 DCHECK_GE(index, 0);
354
estade32426e02016-12-18 01:26:17355 gfx::Image icon = menu_manager->GetIconForExtension(extension_id);
356 DCHECK_EQ(gfx::kFaviconSize, icon.Width());
357 DCHECK_EQ(gfx::kFaviconSize, icon.Height());
358 menu_model_->SetIcon(index, icon);
[email protected]4f8a4d12012-09-28 19:23:09359}
360
361} // namespace extensions