blob: 7b6a229c1b21c8cb3e1f5fb1ee0b453a78ea7300 [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.
181 if (command_id == IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && !item)
182 return true;
183 if (!item)
184 return false;
185 return item->visible();
186}
187
[email protected]4f8a4d12012-09-28 19:23:09188bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const {
189 MenuItem* item = GetExtensionMenuItem(command_id);
190 if (!item)
191 return true;
192 return item->enabled();
193}
194
robcbe35ba2016-03-10 01:20:49195void ContextMenuMatcher::ExecuteCommand(
196 int command_id,
[email protected]4f8a4d12012-09-28 19:23:09197 content::WebContents* web_contents,
robcbe35ba2016-03-10 01:20:49198 content::RenderFrameHost* render_frame_host,
[email protected]4f8a4d12012-09-28 19:23:09199 const content::ContextMenuParams& params) {
[email protected]4f8a4d12012-09-28 19:23:09200 MenuItem* item = GetExtensionMenuItem(command_id);
201 if (!item)
202 return;
203
[email protected]f5fede02014-07-29 02:48:21204 MenuManager* manager = MenuManager::Get(browser_context_);
robcbe35ba2016-03-10 01:20:49205 manager->ExecuteCommand(browser_context_, web_contents, render_frame_host,
206 params, item->id());
[email protected]4f8a4d12012-09-28 19:23:09207}
208
[email protected]0ea8fac2013-06-12 15:31:35209bool ContextMenuMatcher::GetRelevantExtensionTopLevelItems(
[email protected]6f9d2c62014-03-10 12:12:05210 const MenuItem::ExtensionKey& extension_key,
[email protected]0ea8fac2013-06-12 15:31:35211 const Extension** extension,
212 bool* can_cross_incognito,
[email protected]fc103da2014-08-16 01:09:32213 MenuItem::List* items) {
214 *extension = ExtensionRegistry::Get(
215 browser_context_)->enabled_extensions().GetByID(
216 extension_key.extension_id);
[email protected]0ea8fac2013-06-12 15:31:35217 if (!*extension)
218 return false;
219
220 // Find matching items.
[email protected]f5fede02014-07-29 02:48:21221 MenuManager* manager = MenuManager::Get(browser_context_);
avi5d5b7e92016-10-21 01:11:40222 const MenuItem::OwnedList* all_items = manager->MenuItems(extension_key);
[email protected]0ea8fac2013-06-12 15:31:35223 if (!all_items || all_items->empty())
224 return false;
225
[email protected]f5fede02014-07-29 02:48:21226 *can_cross_incognito = util::CanCrossIncognito(*extension, browser_context_);
[email protected]fc103da2014-08-16 01:09:32227 *items = GetRelevantExtensionItems(*all_items, *can_cross_incognito);
[email protected]0ea8fac2013-06-12 15:31:35228
229 return true;
230}
231
[email protected]4f8a4d12012-09-28 19:23:09232MenuItem::List ContextMenuMatcher::GetRelevantExtensionItems(
avi5d5b7e92016-10-21 01:11:40233 const MenuItem::OwnedList& items,
[email protected]4f8a4d12012-09-28 19:23:09234 bool can_cross_incognito) {
235 MenuItem::List result;
avi5d5b7e92016-10-21 01:11:40236 for (auto i = items.begin(); i != items.end(); ++i) {
237 MenuItem* item = i->get();
[email protected]4f8a4d12012-09-28 19:23:09238
239 if (!filter_.Run(item))
240 continue;
241
[email protected]f5fede02014-07-29 02:48:21242 if (item->id().incognito == browser_context_->IsOffTheRecord() ||
[email protected]4f8a4d12012-09-28 19:23:09243 can_cross_incognito)
avi5d5b7e92016-10-21 01:11:40244 result.push_back(item);
[email protected]4f8a4d12012-09-28 19:23:09245 }
246 return result;
247}
248
249void ContextMenuMatcher::RecursivelyAppendExtensionItems(
250 const MenuItem::List& items,
251 bool can_cross_incognito,
[email protected]439f1e32013-12-09 20:09:09252 const base::string16& selection_text,
[email protected]4f8a4d12012-09-28 19:23:09253 ui::SimpleMenuModel* menu_model,
[email protected]69e1c12d2014-08-13 08:25:34254 int* index,
255 bool is_action_menu_top_level) {
[email protected]4f8a4d12012-09-28 19:23:09256 MenuItem::Type last_type = MenuItem::NORMAL;
257 int radio_group_id = 1;
catmullingsff2cdbc2017-08-22 22:07:02258 int num_visible_items = 0;
[email protected]4f8a4d12012-09-28 19:23:09259
avi5d5b7e92016-10-21 01:11:40260 for (auto i = items.begin(); i != items.end(); ++i) {
[email protected]4f8a4d12012-09-28 19:23:09261 MenuItem* item = *i;
262
263 // If last item was of type radio but the current one isn't, auto-insert
264 // a separator. The converse case is handled below.
265 if (last_type == MenuItem::RADIO &&
266 item->type() != MenuItem::RADIO) {
267 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
268 last_type = MenuItem::SEPARATOR;
269 }
270
[email protected]a146532b2014-07-30 11:20:09271 int menu_id = ConvertToExtensionsCustomCommandId(*index);
[email protected]69e1c12d2014-08-13 08:25:34272 // Action context menus have a limit for top level extension items to
273 // prevent control items from being pushed off the screen, since extension
274 // items will not be placed in a submenu.
[email protected]fc103da2014-08-16 01:09:32275 const int top_level_limit = api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT;
[email protected]69e1c12d2014-08-13 08:25:34276 if (menu_id >= extensions_context_custom_last ||
catmullingsff2cdbc2017-08-22 22:07:02277 (is_action_menu_top_level && num_visible_items >= top_level_limit))
[email protected]4f8a4d12012-09-28 19:23:09278 return;
[email protected]69e1c12d2014-08-13 08:25:34279
lazyboy413226da2015-05-14 22:18:20280 ++(*index);
catmullingsff2cdbc2017-08-22 22:07:02281 if (item->visible())
282 ++num_visible_items;
lazyboy413226da2015-05-14 22:18:20283
[email protected]4f8a4d12012-09-28 19:23:09284 extension_item_map_[menu_id] = item->id();
[email protected]439f1e32013-12-09 20:09:09285 base::string16 title = item->TitleWithReplacement(selection_text,
[email protected]4f8a4d12012-09-28 19:23:09286 kMaxExtensionItemTitleLength);
287 if (item->type() == MenuItem::NORMAL) {
288 MenuItem::List children =
289 GetRelevantExtensionItems(item->children(), can_cross_incognito);
290 if (children.empty()) {
291 menu_model->AddItem(menu_id, title);
292 } else {
293 ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
dchengc963c7142016-04-08 03:55:22294 extension_menu_models_.push_back(base::WrapUnique(submenu));
[email protected]4f8a4d12012-09-28 19:23:09295 menu_model->AddSubMenu(menu_id, title, submenu);
limasdf7e955d22015-12-15 05:17:39296 RecursivelyAppendExtensionItems(children, can_cross_incognito,
297 selection_text, submenu, index,
[email protected]69e1c12d2014-08-13 08:25:34298 false); // is_action_menu_top_level
[email protected]4f8a4d12012-09-28 19:23:09299 }
300 } else if (item->type() == MenuItem::CHECKBOX) {
301 menu_model->AddCheckItem(menu_id, title);
302 } else if (item->type() == MenuItem::RADIO) {
303 if (i != items.begin() &&
304 last_type != MenuItem::RADIO) {
305 radio_group_id++;
306
307 // Auto-append a separator if needed.
[email protected]00491c052013-02-08 10:53:25308 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09309 }
310
311 menu_model->AddRadioItem(menu_id, title, radio_group_id);
312 } else if (item->type() == MenuItem::SEPARATOR) {
[email protected]00491c052013-02-08 10:53:25313 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09314 }
315 last_type = item->type();
316 }
317}
318
319MenuItem* ContextMenuMatcher::GetExtensionMenuItem(int id) const {
[email protected]f5fede02014-07-29 02:48:21320 MenuManager* manager = MenuManager::Get(browser_context_);
[email protected]4f8a4d12012-09-28 19:23:09321 std::map<int, MenuItem::Id>::const_iterator i =
322 extension_item_map_.find(id);
323 if (i != extension_item_map_.end()) {
324 MenuItem* item = manager->GetItemById(i->second);
325 if (item)
326 return item;
327 }
328 return NULL;
329}
330
331void ContextMenuMatcher::SetExtensionIcon(const std::string& extension_id) {
[email protected]f5fede02014-07-29 02:48:21332 MenuManager* menu_manager = MenuManager::Get(browser_context_);
[email protected]4f8a4d12012-09-28 19:23:09333
334 int index = menu_model_->GetItemCount() - 1;
335 DCHECK_GE(index, 0);
336
estade32426e02016-12-18 01:26:17337 gfx::Image icon = menu_manager->GetIconForExtension(extension_id);
338 DCHECK_EQ(gfx::kFaviconSize, icon.Width());
339 DCHECK_EQ(gfx::kFaviconSize, icon.Height());
340 menu_model_->SetIcon(index, icon);
[email protected]4f8a4d12012-09-28 19:23:09341}
342
343} // namespace extensions