blob: fdda6a909bf94b1d5c7664b516a30326f2ef9491 [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.
Dave Schuylere83bf882017-08-22 18:50:0477 if (*index == 0 && menu_model_->GetItemCount())
78 menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:0979
80 // Extensions (other than platform apps) are only allowed one top-level slot
81 // (and it can't be a radio or checkbox item because we are going to put the
[email protected]69e1c12d2014-08-13 08:25:3482 // extension icon next to it), unless the context menu is an an action menu.
83 // Action menus do not include the extension action, and they only include
84 // items from one extension, so they are not placed within a submenu.
85 // Otherwise, we automatically push them into a submenu if there is more than
86 // one top-level item.
87 if (extension->is_platform_app() || is_action_menu) {
88 RecursivelyAppendExtensionItems(items,
89 can_cross_incognito,
90 selection_text,
91 menu_model_,
92 index,
93 is_action_menu);
[email protected]4f8a4d12012-09-28 19:23:0994 } else {
[email protected]a146532b2014-07-30 11:20:0995 int menu_id = ConvertToExtensionsCustomCommandId(*index);
96 (*index)++;
[email protected]439f1e32013-12-09 20:09:0997 base::string16 title;
[email protected]4f8a4d12012-09-28 19:23:0998 MenuItem::List submenu_items;
99
100 if (items.size() > 1 || items[0]->type() != MenuItem::NORMAL) {
[email protected]04338722013-12-24 23:18:05101 title = base::UTF8ToUTF16(extension->name());
[email protected]4f8a4d12012-09-28 19:23:09102 submenu_items = items;
103 } else {
104 MenuItem* item = items[0];
105 extension_item_map_[menu_id] = item->id();
106 title = item->TitleWithReplacement(selection_text,
107 kMaxExtensionItemTitleLength);
108 submenu_items = GetRelevantExtensionItems(item->children(),
109 can_cross_incognito);
110 }
111
112 // Now add our item(s) to the menu_model_.
113 if (submenu_items.empty()) {
114 menu_model_->AddItem(menu_id, title);
115 } else {
116 ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
dchengc963c7142016-04-08 03:55:22117 extension_menu_models_.push_back(base::WrapUnique(submenu));
[email protected]4f8a4d12012-09-28 19:23:09118 menu_model_->AddSubMenu(menu_id, title, submenu);
limasdf7e955d22015-12-15 05:17:39119 RecursivelyAppendExtensionItems(submenu_items, can_cross_incognito,
120 selection_text, submenu, index,
[email protected]69e1c12d2014-08-13 08:25:34121 false); // is_action_menu_top_level
[email protected]4f8a4d12012-09-28 19:23:09122 }
[email protected]69e1c12d2014-08-13 08:25:34123 if (!is_action_menu)
124 SetExtensionIcon(extension_key.extension_id);
[email protected]4f8a4d12012-09-28 19:23:09125 }
126}
127
128void ContextMenuMatcher::Clear() {
129 extension_item_map_.clear();
130 extension_menu_models_.clear();
131}
132
[email protected]0ea8fac2013-06-12 15:31:35133base::string16 ContextMenuMatcher::GetTopLevelContextMenuTitle(
[email protected]6f9d2c62014-03-10 12:12:05134 const MenuItem::ExtensionKey& extension_key,
[email protected]439f1e32013-12-09 20:09:09135 const base::string16& selection_text) {
[email protected]0ea8fac2013-06-12 15:31:35136 const Extension* extension = NULL;
137 MenuItem::List items;
138 bool can_cross_incognito;
[email protected]6f9d2c62014-03-10 12:12:05139 GetRelevantExtensionTopLevelItems(
[email protected]fc103da2014-08-16 01:09:32140 extension_key, &extension, &can_cross_incognito, &items);
[email protected]0ea8fac2013-06-12 15:31:35141
142 base::string16 title;
143
144 if (items.empty() ||
145 items.size() > 1 ||
146 items[0]->type() != MenuItem::NORMAL) {
[email protected]04338722013-12-24 23:18:05147 title = base::UTF8ToUTF16(extension->name());
[email protected]0ea8fac2013-06-12 15:31:35148 } else {
149 MenuItem* item = items[0];
150 title = item->TitleWithReplacement(
151 selection_text, kMaxExtensionItemTitleLength);
152 }
153 return title;
154}
155
[email protected]4f8a4d12012-09-28 19:23:09156bool ContextMenuMatcher::IsCommandIdChecked(int command_id) const {
157 MenuItem* item = GetExtensionMenuItem(command_id);
158 if (!item)
159 return false;
160 return item->checked();
161}
162
163bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const {
164 MenuItem* item = GetExtensionMenuItem(command_id);
165 if (!item)
166 return true;
167 return item->enabled();
168}
169
robcbe35ba2016-03-10 01:20:49170void ContextMenuMatcher::ExecuteCommand(
171 int command_id,
[email protected]4f8a4d12012-09-28 19:23:09172 content::WebContents* web_contents,
robcbe35ba2016-03-10 01:20:49173 content::RenderFrameHost* render_frame_host,
[email protected]4f8a4d12012-09-28 19:23:09174 const content::ContextMenuParams& params) {
[email protected]4f8a4d12012-09-28 19:23:09175 MenuItem* item = GetExtensionMenuItem(command_id);
176 if (!item)
177 return;
178
[email protected]f5fede02014-07-29 02:48:21179 MenuManager* manager = MenuManager::Get(browser_context_);
robcbe35ba2016-03-10 01:20:49180 manager->ExecuteCommand(browser_context_, web_contents, render_frame_host,
181 params, item->id());
[email protected]4f8a4d12012-09-28 19:23:09182}
183
[email protected]0ea8fac2013-06-12 15:31:35184bool ContextMenuMatcher::GetRelevantExtensionTopLevelItems(
[email protected]6f9d2c62014-03-10 12:12:05185 const MenuItem::ExtensionKey& extension_key,
[email protected]0ea8fac2013-06-12 15:31:35186 const Extension** extension,
187 bool* can_cross_incognito,
[email protected]fc103da2014-08-16 01:09:32188 MenuItem::List* items) {
189 *extension = ExtensionRegistry::Get(
190 browser_context_)->enabled_extensions().GetByID(
191 extension_key.extension_id);
[email protected]0ea8fac2013-06-12 15:31:35192 if (!*extension)
193 return false;
194
195 // Find matching items.
[email protected]f5fede02014-07-29 02:48:21196 MenuManager* manager = MenuManager::Get(browser_context_);
avi5d5b7e92016-10-21 01:11:40197 const MenuItem::OwnedList* all_items = manager->MenuItems(extension_key);
[email protected]0ea8fac2013-06-12 15:31:35198 if (!all_items || all_items->empty())
199 return false;
200
[email protected]f5fede02014-07-29 02:48:21201 *can_cross_incognito = util::CanCrossIncognito(*extension, browser_context_);
[email protected]fc103da2014-08-16 01:09:32202 *items = GetRelevantExtensionItems(*all_items, *can_cross_incognito);
[email protected]0ea8fac2013-06-12 15:31:35203
204 return true;
205}
206
[email protected]4f8a4d12012-09-28 19:23:09207MenuItem::List ContextMenuMatcher::GetRelevantExtensionItems(
avi5d5b7e92016-10-21 01:11:40208 const MenuItem::OwnedList& items,
[email protected]4f8a4d12012-09-28 19:23:09209 bool can_cross_incognito) {
210 MenuItem::List result;
avi5d5b7e92016-10-21 01:11:40211 for (auto i = items.begin(); i != items.end(); ++i) {
212 MenuItem* item = i->get();
[email protected]4f8a4d12012-09-28 19:23:09213
214 if (!filter_.Run(item))
215 continue;
216
[email protected]f5fede02014-07-29 02:48:21217 if (item->id().incognito == browser_context_->IsOffTheRecord() ||
[email protected]4f8a4d12012-09-28 19:23:09218 can_cross_incognito)
avi5d5b7e92016-10-21 01:11:40219 result.push_back(item);
[email protected]4f8a4d12012-09-28 19:23:09220 }
221 return result;
222}
223
224void ContextMenuMatcher::RecursivelyAppendExtensionItems(
225 const MenuItem::List& items,
226 bool can_cross_incognito,
[email protected]439f1e32013-12-09 20:09:09227 const base::string16& selection_text,
[email protected]4f8a4d12012-09-28 19:23:09228 ui::SimpleMenuModel* menu_model,
[email protected]69e1c12d2014-08-13 08:25:34229 int* index,
230 bool is_action_menu_top_level) {
[email protected]4f8a4d12012-09-28 19:23:09231 MenuItem::Type last_type = MenuItem::NORMAL;
232 int radio_group_id = 1;
Dave Schuylere83bf882017-08-22 18:50:04233 int num_items = 0;
[email protected]4f8a4d12012-09-28 19:23:09234
avi5d5b7e92016-10-21 01:11:40235 for (auto i = items.begin(); i != items.end(); ++i) {
[email protected]4f8a4d12012-09-28 19:23:09236 MenuItem* item = *i;
237
238 // If last item was of type radio but the current one isn't, auto-insert
239 // a separator. The converse case is handled below.
240 if (last_type == MenuItem::RADIO &&
241 item->type() != MenuItem::RADIO) {
242 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
243 last_type = MenuItem::SEPARATOR;
244 }
245
[email protected]a146532b2014-07-30 11:20:09246 int menu_id = ConvertToExtensionsCustomCommandId(*index);
[email protected]69e1c12d2014-08-13 08:25:34247 // Action context menus have a limit for top level extension items to
248 // prevent control items from being pushed off the screen, since extension
249 // items will not be placed in a submenu.
[email protected]fc103da2014-08-16 01:09:32250 const int top_level_limit = api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT;
[email protected]69e1c12d2014-08-13 08:25:34251 if (menu_id >= extensions_context_custom_last ||
Dave Schuylere83bf882017-08-22 18:50:04252 (is_action_menu_top_level && num_items >= top_level_limit))
[email protected]4f8a4d12012-09-28 19:23:09253 return;
[email protected]69e1c12d2014-08-13 08:25:34254
lazyboy413226da2015-05-14 22:18:20255 ++(*index);
Dave Schuylere83bf882017-08-22 18:50:04256 ++num_items;
lazyboy413226da2015-05-14 22:18:20257
[email protected]4f8a4d12012-09-28 19:23:09258 extension_item_map_[menu_id] = item->id();
[email protected]439f1e32013-12-09 20:09:09259 base::string16 title = item->TitleWithReplacement(selection_text,
[email protected]4f8a4d12012-09-28 19:23:09260 kMaxExtensionItemTitleLength);
261 if (item->type() == MenuItem::NORMAL) {
262 MenuItem::List children =
263 GetRelevantExtensionItems(item->children(), can_cross_incognito);
264 if (children.empty()) {
265 menu_model->AddItem(menu_id, title);
266 } else {
267 ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
dchengc963c7142016-04-08 03:55:22268 extension_menu_models_.push_back(base::WrapUnique(submenu));
[email protected]4f8a4d12012-09-28 19:23:09269 menu_model->AddSubMenu(menu_id, title, submenu);
limasdf7e955d22015-12-15 05:17:39270 RecursivelyAppendExtensionItems(children, can_cross_incognito,
271 selection_text, submenu, index,
[email protected]69e1c12d2014-08-13 08:25:34272 false); // is_action_menu_top_level
[email protected]4f8a4d12012-09-28 19:23:09273 }
274 } else if (item->type() == MenuItem::CHECKBOX) {
275 menu_model->AddCheckItem(menu_id, title);
276 } else if (item->type() == MenuItem::RADIO) {
277 if (i != items.begin() &&
278 last_type != MenuItem::RADIO) {
279 radio_group_id++;
280
281 // Auto-append a separator if needed.
[email protected]00491c052013-02-08 10:53:25282 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09283 }
284
285 menu_model->AddRadioItem(menu_id, title, radio_group_id);
286 } else if (item->type() == MenuItem::SEPARATOR) {
[email protected]00491c052013-02-08 10:53:25287 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09288 }
289 last_type = item->type();
290 }
291}
292
293MenuItem* ContextMenuMatcher::GetExtensionMenuItem(int id) const {
[email protected]f5fede02014-07-29 02:48:21294 MenuManager* manager = MenuManager::Get(browser_context_);
[email protected]4f8a4d12012-09-28 19:23:09295 std::map<int, MenuItem::Id>::const_iterator i =
296 extension_item_map_.find(id);
297 if (i != extension_item_map_.end()) {
298 MenuItem* item = manager->GetItemById(i->second);
299 if (item)
300 return item;
301 }
302 return NULL;
303}
304
305void ContextMenuMatcher::SetExtensionIcon(const std::string& extension_id) {
[email protected]f5fede02014-07-29 02:48:21306 MenuManager* menu_manager = MenuManager::Get(browser_context_);
[email protected]4f8a4d12012-09-28 19:23:09307
308 int index = menu_model_->GetItemCount() - 1;
309 DCHECK_GE(index, 0);
310
estade32426e02016-12-18 01:26:17311 gfx::Image icon = menu_manager->GetIconForExtension(extension_id);
312 DCHECK_EQ(gfx::kFaviconSize, icon.Width());
313 DCHECK_EQ(gfx::kFaviconSize, icon.Height());
314 menu_model_->SetIcon(index, icon);
[email protected]4f8a4d12012-09-28 19:23:09315}
316
317} // namespace extensions