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. 4 5 #include "chrome/browser/extensions/context_menu_matcher.h" 6 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" 16 17 namespace extensions { 18 19 namespace { 20 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; 25 26 } // namespace 27 28 // static 29 const size_t ContextMenuMatcher::kMaxExtensionItemTitleLength = 75; 30 31 // static 32 int ContextMenuMatcher::ConvertToExtensionsCustomCommandId(int id) { 33 return extensions_context_custom_first + id; 34 } 35 36 // static 37 bool ContextMenuMatcher::IsExtensionsCustomCommandId(int id) { 38 return id >= extensions_context_custom_first && 39 id <= extensions_context_custom_last; 40 } 41 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), 49 delegate_(delegate), 50 filter_(filter) { 51 } 52 53 void ContextMenuMatcher::AppendExtensionItems( 54 const MenuItem::ExtensionKey& extension_key, 55 const base::string16& selection_text, 56 int* index, 57 bool is_action_menu) { 58 DCHECK_GE(*index, 0); 59 int max_index = 60 extensions_context_custom_last - extensions_context_custom_first; 61 if (*index >= max_index) 62 return; 63 64 const Extension* extension = NULL; 65 MenuItem::List items; 66 bool can_cross_incognito; 67 if (!GetRelevantExtensionTopLevelItems( 68 extension_key, &extension, &can_cross_incognito, &items)) 69 return; 70 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. 76 if (*index == 0 && menu_model_->GetItemCount()) 77 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 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, 88 can_cross_incognito, 89 selection_text, 90 menu_model_, 91 index, 92 is_action_menu); 93 } else { 94 int menu_id = ConvertToExtensionsCustomCommandId(*index); 95 (*index)++; 96 base::string16 title; 97 MenuItem::List submenu_items; 98 99 if (items.size() > 1 || items[0]->type() != MenuItem::NORMAL) { 100 title = base::UTF8ToUTF16(extension->name()); 101 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); 118 RecursivelyAppendExtensionItems(submenu_items, 119 can_cross_incognito, 120 selection_text, 121 submenu, 122 index, 123 false); // is_action_menu_top_level 124 } 125 if (!is_action_menu) 126 SetExtensionIcon(extension_key.extension_id); 127 } 128 } 129 130 void ContextMenuMatcher::Clear() { 131 extension_item_map_.clear(); 132 extension_menu_models_.clear(); 133 } 134 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); 143 144 base::string16 title; 145 146 if (items.empty() || 147 items.size() > 1 || 148 items[0]->type() != MenuItem::NORMAL) { 149 title = base::UTF8ToUTF16(extension->name()); 150 } else { 151 MenuItem* item = items[0]; 152 title = item->TitleWithReplacement( 153 selection_text, kMaxExtensionItemTitleLength); 154 } 155 return title; 156 } 157 158 bool ContextMenuMatcher::IsCommandIdChecked(int command_id) const { 159 MenuItem* item = GetExtensionMenuItem(command_id); 160 if (!item) 161 return false; 162 return item->checked(); 163 } 164 165 bool ContextMenuMatcher::IsCommandIdEnabled(int command_id) const { 166 MenuItem* item = GetExtensionMenuItem(command_id); 167 if (!item) 168 return true; 169 return item->enabled(); 170 } 171 172 void ContextMenuMatcher::ExecuteCommand(int command_id, 173 content::WebContents* web_contents, 174 const content::ContextMenuParams& params) { 175 MenuItem* item = GetExtensionMenuItem(command_id); 176 if (!item) 177 return; 178 179 MenuManager* manager = MenuManager::Get(browser_context_); 180 manager->ExecuteCommand(browser_context_, web_contents, params, item->id()); 181 } 182 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); 191 if (!*extension) 192 return false; 193 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()) 198 return false; 199 200 *can_cross_incognito = util::CanCrossIncognito(*extension, browser_context_); 201 *items = GetRelevantExtensionItems(*all_items, *can_cross_incognito); 202 203 return true; 204 } 205 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; 213 214 if (!filter_.Run(item)) 215 continue; 216 217 if (item->id().incognito == browser_context_->IsOffTheRecord() || 218 can_cross_incognito) 219 result.push_back(*i); 220 } 221 return result; 222 } 223 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, 229 int* index, 230 bool is_action_menu_top_level) { 231 MenuItem::Type last_type = MenuItem::NORMAL; 232 int radio_group_id = 1; 233 int num_items = 0; 234 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 247 int menu_id = ConvertToExtensionsCustomCommandId(*index); 248 ++(*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. 253 const int top_level_limit = api::context_menus::ACTION_MENU_TOP_LEVEL_LIMIT; 254 if (menu_id >= extensions_context_custom_last || 255 (is_action_menu_top_level && num_items >= top_level_limit)) 256 return; 257 258 extension_item_map_[menu_id] = item->id(); 259 base::string16 title = item->TitleWithReplacement(selection_text, 260 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); 270 RecursivelyAppendExtensionItems(children, 271 can_cross_incognito, 272 selection_text, 273 submenu, 274 index, 275 false); // is_action_menu_top_level 276 } 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. 285 menu_model->AddSeparator(ui::NORMAL_SEPARATOR); 286 } 287 288 menu_model->AddRadioItem(menu_id, title, radio_group_id); 289 } else if (item->type() == MenuItem::SEPARATOR) { 290 menu_model->AddSeparator(ui::NORMAL_SEPARATOR); 291 } 292 last_type = item->type(); 293 } 294 } 295 296 MenuItem* ContextMenuMatcher::GetExtensionMenuItem(int id) const { 297 MenuManager* manager = MenuManager::Get(browser_context_); 298 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 308 void ContextMenuMatcher::SetExtensionIcon(const std::string& extension_id) { 309 MenuManager* menu_manager = MenuManager::Get(browser_context_); 310 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 318 menu_model_->SetIcon(index, gfx::Image::CreateFrom1xBitmap(icon)); 319 } 320 321 } // namespace extensions 322