blob: 81cb9712a037365e008541abf347eb4338ace606 [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
[email protected]112158af2013-06-07 23:46:187#include "base/strings/utf_string_conversions.h"
[email protected]4f8a4d12012-09-28 19:23:098#include "chrome/app/chrome_command_ids.h"
[email protected]a7ff4b72013-10-17 20:56:029#include "chrome/browser/extensions/extension_util.h"
[email protected]fc103da2014-08-16 01:09:3210#include "chrome/common/extensions/api/context_menus.h"
[email protected]f5fede02014-07-29 02:48:2111#include "content/public/browser/browser_context.h"
[email protected]4f8a4d12012-09-28 19:23:0912#include "content/public/common/context_menu_params.h"
[email protected]fc103da2014-08-16 01:09:3213#include "extensions/browser/extension_registry.h"
[email protected]4f8a4d12012-09-28 19:23:0914#include "ui/gfx/favicon_size.h"
[email protected]f34efa22013-03-05 19:14:2315#include "ui/gfx/image/image.h"
[email protected]4f8a4d12012-09-28 19:23:0916
17namespace extensions {
18
[email protected]a146532b2014-07-30 11:20:0919namespace {
20
21// The range of command IDs reserved for extension's custom menus.
22// TODO(oshima): These values will be injected by embedders.
23int extensions_context_custom_first = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST;
24int extensions_context_custom_last = IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST;
25
26} // namespace
27
[email protected]4f8a4d12012-09-28 19:23:0928// static
29const size_t ContextMenuMatcher::kMaxExtensionItemTitleLength = 75;
30
[email protected]a146532b2014-07-30 11:20:0931// static
32int ContextMenuMatcher::ConvertToExtensionsCustomCommandId(int id) {
33 return extensions_context_custom_first + id;
34}
35
36// static
37bool ContextMenuMatcher::IsExtensionsCustomCommandId(int id) {
38 return id >= extensions_context_custom_first &&
39 id <= extensions_context_custom_last;
40}
41
[email protected]4f8a4d12012-09-28 19:23:0942ContextMenuMatcher::ContextMenuMatcher(
[email protected]f5fede02014-07-29 02:48:2143 content::BrowserContext* browser_context,
[email protected]4f8a4d12012-09-28 19:23:0944 ui::SimpleMenuModel::Delegate* delegate,
45 ui::SimpleMenuModel* menu_model,
46 const base::Callback<bool(const MenuItem*)>& filter)
[email protected]f5fede02014-07-29 02:48:2147 : browser_context_(browser_context),
48 menu_model_(menu_model),
49 delegate_(delegate),
[email protected]4f8a4d12012-09-28 19:23:0950 filter_(filter) {
51}
52
[email protected]439f1e32013-12-09 20:09:0953void ContextMenuMatcher::AppendExtensionItems(
[email protected]6f9d2c62014-03-10 12:12:0554 const MenuItem::ExtensionKey& extension_key,
[email protected]439f1e32013-12-09 20:09:0955 const base::string16& selection_text,
[email protected]69e1c12d2014-08-13 08:25:3456 int* index,
57 bool is_action_menu) {
[email protected]4f8a4d12012-09-28 19:23:0958 DCHECK_GE(*index, 0);
59 int max_index =
[email protected]a146532b2014-07-30 11:20:0960 extensions_context_custom_last - extensions_context_custom_first;
[email protected]0ea8fac2013-06-12 15:31:3561 if (*index >= max_index)
[email protected]4f8a4d12012-09-28 19:23:0962 return;
63
[email protected]0ea8fac2013-06-12 15:31:3564 const Extension* extension = NULL;
65 MenuItem::List items;
66 bool can_cross_incognito;
[email protected]6f9d2c62014-03-10 12:12:0567 if (!GetRelevantExtensionTopLevelItems(
[email protected]fc103da2014-08-16 01:09:3268 extension_key, &extension, &can_cross_incognito, &items))
[email protected]4f8a4d12012-09-28 19:23:0969 return;
[email protected]4f8a4d12012-09-28 19:23:0970
71 if (items.empty())
72 return;
73
74 // If this is the first extension-provided menu item, and there are other
75 // items in the menu, and the last item is not a separator add a separator.
[email protected]00491c052013-02-08 10:53:2576 if (*index == 0 && menu_model_->GetItemCount())
[email protected]4f8a4d12012-09-28 19:23:0977 menu_model_->AddSeparator(ui::NORMAL_SEPARATOR);
78
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) {
87 RecursivelyAppendExtensionItems(items,
88 can_cross_incognito,
89 selection_text,
90 menu_model_,
91 index,
92 is_action_menu);
[email protected]4f8a4d12012-09-28 19:23:0993 } else {
[email protected]a146532b2014-07-30 11:20:0994 int menu_id = ConvertToExtensionsCustomCommandId(*index);
95 (*index)++;
[email protected]439f1e32013-12-09 20:09:0996 base::string16 title;
[email protected]4f8a4d12012-09-28 19:23:0997 MenuItem::List submenu_items;
98
99 if (items.size() > 1 || items[0]->type() != MenuItem::NORMAL) {
[email protected]04338722013-12-24 23:18:05100 title = base::UTF8ToUTF16(extension->name());
[email protected]4f8a4d12012-09-28 19:23:09101 submenu_items = items;
102 } else {
103 MenuItem* item = items[0];
104 extension_item_map_[menu_id] = item->id();
105 title = item->TitleWithReplacement(selection_text,
106 kMaxExtensionItemTitleLength);
107 submenu_items = GetRelevantExtensionItems(item->children(),
108 can_cross_incognito);
109 }
110
111 // Now add our item(s) to the menu_model_.
112 if (submenu_items.empty()) {
113 menu_model_->AddItem(menu_id, title);
114 } else {
115 ui::SimpleMenuModel* submenu = new ui::SimpleMenuModel(delegate_);
116 extension_menu_models_.push_back(submenu);
117 menu_model_->AddSubMenu(menu_id, title, submenu);
[email protected]69e1c12d2014-08-13 08:25:34118 RecursivelyAppendExtensionItems(submenu_items,
119 can_cross_incognito,
120 selection_text,
121 submenu,
122 index,
123 false); // is_action_menu_top_level
[email protected]4f8a4d12012-09-28 19:23:09124 }
[email protected]69e1c12d2014-08-13 08:25:34125 if (!is_action_menu)
126 SetExtensionIcon(extension_key.extension_id);
[email protected]4f8a4d12012-09-28 19:23:09127 }
128}
129
130void ContextMenuMatcher::Clear() {
131 extension_item_map_.clear();
132 extension_menu_models_.clear();
133}
134
[email protected]0ea8fac2013-06-12 15:31:35135base::string16 ContextMenuMatcher::GetTopLevelContextMenuTitle(
[email protected]6f9d2c62014-03-10 12:12:05136 const MenuItem::ExtensionKey& extension_key,
[email protected]439f1e32013-12-09 20:09:09137 const base::string16& selection_text) {
[email protected]0ea8fac2013-06-12 15:31:35138 const Extension* extension = NULL;
139 MenuItem::List items;
140 bool can_cross_incognito;
[email protected]6f9d2c62014-03-10 12:12:05141 GetRelevantExtensionTopLevelItems(
[email protected]fc103da2014-08-16 01:09:32142 extension_key, &extension, &can_cross_incognito, &items);
[email protected]0ea8fac2013-06-12 15:31:35143
144 base::string16 title;
145
146 if (items.empty() ||
147 items.size() > 1 ||
148 items[0]->type() != MenuItem::NORMAL) {
[email protected]04338722013-12-24 23:18:05149 title = base::UTF8ToUTF16(extension->name());
[email protected]0ea8fac2013-06-12 15:31:35150 } else {
151 MenuItem* item = items[0];
152 title = item->TitleWithReplacement(
153 selection_text, kMaxExtensionItemTitleLength);
154 }
155 return title;
156}
157
[email protected]4f8a4d12012-09-28 19:23:09158bool ContextMenuMatcher::IsCommandIdChecked(int command_id) const {
159 MenuItem* item = GetExtensionMenuItem(command_id);
160 if (!item)
161 return false;
162 return item->checked();
163}
164
165bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const {
166 MenuItem* item = GetExtensionMenuItem(command_id);
167 if (!item)
168 return true;
169 return item->enabled();
170}
171
172void ContextMenuMatcher::ExecuteCommand(int command_id,
173 content::WebContents* web_contents,
174 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_);
180 manager->ExecuteCommand(browser_context_, web_contents, params, item->id());
[email protected]4f8a4d12012-09-28 19:23:09181}
182
[email protected]0ea8fac2013-06-12 15:31:35183bool ContextMenuMatcher::GetRelevantExtensionTopLevelItems(
[email protected]6f9d2c62014-03-10 12:12:05184 const MenuItem::ExtensionKey& extension_key,
[email protected]0ea8fac2013-06-12 15:31:35185 const Extension** extension,
186 bool* can_cross_incognito,
[email protected]fc103da2014-08-16 01:09:32187 MenuItem::List* items) {
188 *extension = ExtensionRegistry::Get(
189 browser_context_)->enabled_extensions().GetByID(
190 extension_key.extension_id);
[email protected]0ea8fac2013-06-12 15:31:35191 if (!*extension)
192 return false;
193
194 // Find matching items.
[email protected]f5fede02014-07-29 02:48:21195 MenuManager* manager = MenuManager::Get(browser_context_);
[email protected]6f9d2c62014-03-10 12:12:05196 const MenuItem::List* all_items = manager->MenuItems(extension_key);
[email protected]0ea8fac2013-06-12 15:31:35197 if (!all_items || all_items->empty())
198 return false;
199
[email protected]f5fede02014-07-29 02:48:21200 *can_cross_incognito = util::CanCrossIncognito(*extension, browser_context_);
[email protected]fc103da2014-08-16 01:09:32201 *items = GetRelevantExtensionItems(*all_items, *can_cross_incognito);
[email protected]0ea8fac2013-06-12 15:31:35202
203 return true;
204}
205
[email protected]4f8a4d12012-09-28 19:23:09206MenuItem::List ContextMenuMatcher::GetRelevantExtensionItems(
207 const MenuItem::List& items,
208 bool can_cross_incognito) {
209 MenuItem::List result;
210 for (MenuItem::List::const_iterator i = items.begin();
211 i != items.end(); ++i) {
212 const MenuItem* item = *i;
213
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)
219 result.push_back(*i);
220 }
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;
[email protected]69e1c12d2014-08-13 08:25:34233 int num_items = 0;
[email protected]4f8a4d12012-09-28 19:23:09234
235 for (MenuItem::List::const_iterator i = items.begin();
236 i != items.end(); ++i) {
237 MenuItem* item = *i;
238
239 // If last item was of type radio but the current one isn't, auto-insert
240 // a separator. The converse case is handled below.
241 if (last_type == MenuItem::RADIO &&
242 item->type() != MenuItem::RADIO) {
243 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
244 last_type = MenuItem::SEPARATOR;
245 }
246
[email protected]a146532b2014-07-30 11:20:09247 int menu_id = ConvertToExtensionsCustomCommandId(*index);
[email protected]69e1c12d2014-08-13 08:25:34248 ++(*index);
249 ++num_items;
250 // Action context menus have a limit for top level extension items to
251 // prevent control items from being pushed off the screen, since extension
252 // items will not be placed in a submenu.
[email protected]fc103da2014-08-16 01:09:32253 const int top_level_limit = api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT;
[email protected]69e1c12d2014-08-13 08:25:34254 if (menu_id >= extensions_context_custom_last ||
[email protected]fc103da2014-08-16 01:09:32255 (is_action_menu_top_level && num_items >= top_level_limit))
[email protected]4f8a4d12012-09-28 19:23:09256 return;
[email protected]69e1c12d2014-08-13 08:25:34257
[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_);
268 extension_menu_models_.push_back(submenu);
269 menu_model->AddSubMenu(menu_id, title, submenu);
[email protected]69e1c12d2014-08-13 08:25:34270 RecursivelyAppendExtensionItems(children,
271 can_cross_incognito,
272 selection_text,
273 submenu,
274 index,
275 false); // is_action_menu_top_level
[email protected]4f8a4d12012-09-28 19:23:09276 }
277 } else if (item->type() == MenuItem::CHECKBOX) {
278 menu_model->AddCheckItem(menu_id, title);
279 } else if (item->type() == MenuItem::RADIO) {
280 if (i != items.begin() &&
281 last_type != MenuItem::RADIO) {
282 radio_group_id++;
283
284 // Auto-append a separator if needed.
[email protected]00491c052013-02-08 10:53:25285 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09286 }
287
288 menu_model->AddRadioItem(menu_id, title, radio_group_id);
289 } else if (item->type() == MenuItem::SEPARATOR) {
[email protected]00491c052013-02-08 10:53:25290 menu_model->AddSeparator(ui::NORMAL_SEPARATOR);
[email protected]4f8a4d12012-09-28 19:23:09291 }
292 last_type = item->type();
293 }
294}
295
296MenuItem* ContextMenuMatcher::GetExtensionMenuItem(int id) const {
[email protected]f5fede02014-07-29 02:48:21297 MenuManager* manager = MenuManager::Get(browser_context_);
[email protected]4f8a4d12012-09-28 19:23:09298 std::map<int, MenuItem::Id>::const_iterator i =
299 extension_item_map_.find(id);
300 if (i != extension_item_map_.end()) {
301 MenuItem* item = manager->GetItemById(i->second);
302 if (item)
303 return item;
304 }
305 return NULL;
306}
307
308void ContextMenuMatcher::SetExtensionIcon(const std::string& extension_id) {
[email protected]f5fede02014-07-29 02:48:21309 MenuManager* menu_manager = MenuManager::Get(browser_context_);
[email protected]4f8a4d12012-09-28 19:23:09310
311 int index = menu_model_->GetItemCount() - 1;
312 DCHECK_GE(index, 0);
313
314 const SkBitmap& icon = menu_manager->GetIconForExtension(extension_id);
315 DCHECK(icon.width() == gfx::kFaviconSize);
316 DCHECK(icon.height() == gfx::kFaviconSize);
317
[email protected]32e7a9b2013-01-23 23:00:19318 menu_model_->SetIcon(index, gfx::Image::CreateFrom1xBitmap(icon));
[email protected]4f8a4d12012-09-28 19:23:09319}
320
321} // namespace extensions