1 // 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.
5 #include "chrome/browser/extensions/context_menu_matcher.h"
7 #include "base/strings/utf_string_conversions.h"
8 #include "chrome/app/chrome_command_ids.h"
9 #include "chrome/browser/extensions/extension_util.h"
10 #include "chrome/common/extensions/api/context_menus.h"
11 #include "content/public/browser/browser_context.h"
12 #include "content/public/common/context_menu_params.h"
13 #include "extensions/browser/extension_registry.h"
14 #include "ui/gfx/favicon_size.h"
15 #include "ui/gfx/image/image.h"
17 namespace extensions
{
21 // The range of command IDs reserved for extension's custom menus.
22 // TODO(oshima): These values will be injected by embedders.
23 int extensions_context_custom_first
= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST
;
24 int extensions_context_custom_last
= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST
;
29 const size_t ContextMenuMatcher::kMaxExtensionItemTitleLength
= 75;
32 int ContextMenuMatcher::ConvertToExtensionsCustomCommandId(int id
) {
33 return extensions_context_custom_first
+ id
;
37 bool ContextMenuMatcher::IsExtensionsCustomCommandId(int id
) {
38 return id
>= extensions_context_custom_first
&&
39 id
<= extensions_context_custom_last
;
42 ContextMenuMatcher::ContextMenuMatcher(
43 content::BrowserContext
* browser_context
,
44 ui::SimpleMenuModel::Delegate
* delegate
,
45 ui::SimpleMenuModel
* menu_model
,
46 const base::Callback
<bool(const MenuItem
*)>& filter
)
47 : browser_context_(browser_context
),
48 menu_model_(menu_model
),
53 void ContextMenuMatcher::AppendExtensionItems(
54 const MenuItem::ExtensionKey
& extension_key
,
55 const base::string16
& selection_text
,
57 bool is_action_menu
) {
60 extensions_context_custom_last
- extensions_context_custom_first
;
61 if (*index
>= max_index
)
64 const Extension
* extension
= NULL
;
66 bool can_cross_incognito
;
67 if (!GetRelevantExtensionTopLevelItems(
68 extension_key
, &extension
, &can_cross_incognito
, &items
))
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.
76 if (*index
== 0 && menu_model_
->GetItemCount())
77 menu_model_
->AddSeparator(ui::NORMAL_SEPARATOR
);
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
81 // 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
,
94 int menu_id
= ConvertToExtensionsCustomCommandId(*index
);
97 MenuItem::List submenu_items
;
99 if (items
.size() > 1 || items
[0]->type() != MenuItem::NORMAL
) {
100 title
= base::UTF8ToUTF16(extension
->name());
101 submenu_items
= items
;
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
);
111 // Now add our item(s) to the menu_model_.
112 if (submenu_items
.empty()) {
113 menu_model_
->AddItem(menu_id
, title
);
115 ui::SimpleMenuModel
* submenu
= new ui::SimpleMenuModel(delegate_
);
116 extension_menu_models_
.push_back(submenu
);
117 menu_model_
->AddSubMenu(menu_id
, title
, submenu
);
118 RecursivelyAppendExtensionItems(submenu_items
,
123 false); // is_action_menu_top_level
126 SetExtensionIcon(extension_key
.extension_id
);
130 void ContextMenuMatcher::Clear() {
131 extension_item_map_
.clear();
132 extension_menu_models_
.clear();
135 base::string16
ContextMenuMatcher::GetTopLevelContextMenuTitle(
136 const MenuItem::ExtensionKey
& extension_key
,
137 const base::string16
& selection_text
) {
138 const Extension
* extension
= NULL
;
139 MenuItem::List items
;
140 bool can_cross_incognito
;
141 GetRelevantExtensionTopLevelItems(
142 extension_key
, &extension
, &can_cross_incognito
, &items
);
144 base::string16 title
;
148 items
[0]->type() != MenuItem::NORMAL
) {
149 title
= base::UTF8ToUTF16(extension
->name());
151 MenuItem
* item
= items
[0];
152 title
= item
->TitleWithReplacement(
153 selection_text
, kMaxExtensionItemTitleLength
);
158 bool ContextMenuMatcher::IsCommandIdChecked(int command_id
) const {
159 MenuItem
* item
= GetExtensionMenuItem(command_id
);
162 return item
->checked();
165 bool ContextMenuMatcher::IsCommandIdEnabled(int command_id
) const {
166 MenuItem
* item
= GetExtensionMenuItem(command_id
);
169 return item
->enabled();
172 void ContextMenuMatcher::ExecuteCommand(int command_id
,
173 content::WebContents
* web_contents
,
174 const content::ContextMenuParams
& params
) {
175 MenuItem
* item
= GetExtensionMenuItem(command_id
);
179 MenuManager
* manager
= MenuManager::Get(browser_context_
);
180 manager
->ExecuteCommand(browser_context_
, web_contents
, params
, item
->id());
183 bool ContextMenuMatcher::GetRelevantExtensionTopLevelItems(
184 const MenuItem::ExtensionKey
& extension_key
,
185 const Extension
** extension
,
186 bool* can_cross_incognito
,
187 MenuItem::List
* items
) {
188 *extension
= ExtensionRegistry::Get(
189 browser_context_
)->enabled_extensions().GetByID(
190 extension_key
.extension_id
);
194 // Find matching items.
195 MenuManager
* manager
= MenuManager::Get(browser_context_
);
196 const MenuItem::List
* all_items
= manager
->MenuItems(extension_key
);
197 if (!all_items
|| all_items
->empty())
200 *can_cross_incognito
= util::CanCrossIncognito(*extension
, browser_context_
);
201 *items
= GetRelevantExtensionItems(*all_items
, *can_cross_incognito
);
206 MenuItem::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
;
214 if (!filter_
.Run(item
))
217 if (item
->id().incognito
== browser_context_
->IsOffTheRecord() ||
219 result
.push_back(*i
);
224 void ContextMenuMatcher::RecursivelyAppendExtensionItems(
225 const MenuItem::List
& items
,
226 bool can_cross_incognito
,
227 const base::string16
& selection_text
,
228 ui::SimpleMenuModel
* menu_model
,
230 bool is_action_menu_top_level
) {
231 MenuItem::Type last_type
= MenuItem::NORMAL
;
232 int radio_group_id
= 1;
235 for (MenuItem::List::const_iterator i
= items
.begin();
236 i
!= items
.end(); ++i
) {
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
;
247 int menu_id
= ConvertToExtensionsCustomCommandId(*index
);
248 // Action context menus have a limit for top level extension items to
249 // prevent control items from being pushed off the screen, since extension
250 // items will not be placed in a submenu.
251 const int top_level_limit
= api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT
;
252 if (menu_id
>= extensions_context_custom_last
||
253 (is_action_menu_top_level
&& num_items
>= top_level_limit
))
259 extension_item_map_
[menu_id
] = item
->id();
260 base::string16 title
= item
->TitleWithReplacement(selection_text
,
261 kMaxExtensionItemTitleLength
);
262 if (item
->type() == MenuItem::NORMAL
) {
263 MenuItem::List children
=
264 GetRelevantExtensionItems(item
->children(), can_cross_incognito
);
265 if (children
.empty()) {
266 menu_model
->AddItem(menu_id
, title
);
268 ui::SimpleMenuModel
* submenu
= new ui::SimpleMenuModel(delegate_
);
269 extension_menu_models_
.push_back(submenu
);
270 menu_model
->AddSubMenu(menu_id
, title
, submenu
);
271 RecursivelyAppendExtensionItems(children
,
276 false); // is_action_menu_top_level
278 } else if (item
->type() == MenuItem::CHECKBOX
) {
279 menu_model
->AddCheckItem(menu_id
, title
);
280 } else if (item
->type() == MenuItem::RADIO
) {
281 if (i
!= items
.begin() &&
282 last_type
!= MenuItem::RADIO
) {
285 // Auto-append a separator if needed.
286 menu_model
->AddSeparator(ui::NORMAL_SEPARATOR
);
289 menu_model
->AddRadioItem(menu_id
, title
, radio_group_id
);
290 } else if (item
->type() == MenuItem::SEPARATOR
) {
291 menu_model
->AddSeparator(ui::NORMAL_SEPARATOR
);
293 last_type
= item
->type();
297 MenuItem
* ContextMenuMatcher::GetExtensionMenuItem(int id
) const {
298 MenuManager
* manager
= MenuManager::Get(browser_context_
);
299 std::map
<int, MenuItem::Id
>::const_iterator i
=
300 extension_item_map_
.find(id
);
301 if (i
!= extension_item_map_
.end()) {
302 MenuItem
* item
= manager
->GetItemById(i
->second
);
309 void ContextMenuMatcher::SetExtensionIcon(const std::string
& extension_id
) {
310 MenuManager
* menu_manager
= MenuManager::Get(browser_context_
);
312 int index
= menu_model_
->GetItemCount() - 1;
315 const SkBitmap
& icon
= menu_manager
->GetIconForExtension(extension_id
);
316 DCHECK(icon
.width() == gfx::kFaviconSize
);
317 DCHECK(icon
.height() == gfx::kFaviconSize
);
319 menu_model_
->SetIcon(index
, gfx::Image::CreateFrom1xBitmap(icon
));
322 } // namespace extensions