extension_menu_manager.cc revision 21d179b334e59e9a3bfcaed4c4430bef1bc5759d
1// Copyright (c) 2010 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/extension_menu_manager.h" 6 7#include <algorithm> 8 9#include "app/l10n_util.h" 10#include "base/logging.h" 11#include "base/stl_util-inl.h" 12#include "base/string_util.h" 13#include "base/utf_string_conversions.h" 14#include "base/values.h" 15#include "base/json/json_writer.h" 16#include "chrome/browser/extensions/extension_event_router.h" 17#include "chrome/browser/extensions/extension_tabs_module.h" 18#include "chrome/browser/profiles/profile.h" 19#include "chrome/common/extensions/extension.h" 20#include "chrome/common/notification_service.h" 21#include "gfx/favicon_size.h" 22#include "webkit/glue/context_menu.h" 23 24ExtensionMenuItem::ExtensionMenuItem(const Id& id, 25 std::string title, 26 bool checked, 27 Type type, 28 const ContextList& contexts) 29 : id_(id), 30 title_(title), 31 type_(type), 32 checked_(checked), 33 contexts_(contexts), 34 parent_id_(0) { 35} 36 37ExtensionMenuItem::~ExtensionMenuItem() { 38 STLDeleteElements(&children_); 39} 40 41ExtensionMenuItem* ExtensionMenuItem::ReleaseChild(const Id& child_id, 42 bool recursive) { 43 for (List::iterator i = children_.begin(); i != children_.end(); ++i) { 44 ExtensionMenuItem* child = NULL; 45 if ((*i)->id() == child_id) { 46 child = *i; 47 children_.erase(i); 48 return child; 49 } else if (recursive) { 50 child = (*i)->ReleaseChild(child_id, recursive); 51 if (child) 52 return child; 53 } 54 } 55 return NULL; 56} 57 58std::set<ExtensionMenuItem::Id> ExtensionMenuItem::RemoveAllDescendants() { 59 std::set<Id> result; 60 for (List::iterator i = children_.begin(); i != children_.end(); ++i) { 61 ExtensionMenuItem* child = *i; 62 result.insert(child->id()); 63 std::set<Id> removed = child->RemoveAllDescendants(); 64 result.insert(removed.begin(), removed.end()); 65 } 66 STLDeleteElements(&children_); 67 return result; 68} 69 70string16 ExtensionMenuItem::TitleWithReplacement( 71 const string16& selection, size_t max_length) const { 72 string16 result = UTF8ToUTF16(title_); 73 // TODO(asargent) - Change this to properly handle %% escaping so you can 74 // put "%s" in titles that won't get substituted. 75 ReplaceSubstringsAfterOffset(&result, 0, ASCIIToUTF16("%s"), selection); 76 77 if (result.length() > max_length) 78 result = l10n_util::TruncateString(result, max_length); 79 return result; 80} 81 82bool ExtensionMenuItem::SetChecked(bool checked) { 83 if (type_ != CHECKBOX && type_ != RADIO) 84 return false; 85 checked_ = checked; 86 return true; 87} 88 89void ExtensionMenuItem::AddChild(ExtensionMenuItem* item) { 90 item->parent_id_.reset(new Id(id_)); 91 children_.push_back(item); 92} 93 94const int ExtensionMenuManager::kAllowedSchemes = 95 URLPattern::SCHEME_HTTP | URLPattern::SCHEME_HTTPS; 96 97ExtensionMenuManager::ExtensionMenuManager() { 98 registrar_.Add(this, NotificationType::EXTENSION_UNLOADED, 99 NotificationService::AllSources()); 100} 101 102ExtensionMenuManager::~ExtensionMenuManager() { 103 MenuItemMap::iterator i; 104 for (i = context_items_.begin(); i != context_items_.end(); ++i) { 105 STLDeleteElements(&(i->second)); 106 } 107} 108 109std::set<std::string> ExtensionMenuManager::ExtensionIds() { 110 std::set<std::string> id_set; 111 for (MenuItemMap::const_iterator i = context_items_.begin(); 112 i != context_items_.end(); ++i) { 113 id_set.insert(i->first); 114 } 115 return id_set; 116} 117 118const ExtensionMenuItem::List* ExtensionMenuManager::MenuItems( 119 const std::string& extension_id) { 120 MenuItemMap::iterator i = context_items_.find(extension_id); 121 if (i != context_items_.end()) { 122 return &(i->second); 123 } 124 return NULL; 125} 126 127bool ExtensionMenuManager::AddContextItem(const Extension* extension, 128 ExtensionMenuItem* item) { 129 const std::string& extension_id = item->extension_id(); 130 // The item must have a non-empty extension id, and not have already been 131 // added. 132 if (extension_id.empty() || ContainsKey(items_by_id_, item->id())) 133 return false; 134 135 DCHECK_EQ(extension->id(), extension_id); 136 137 bool first_item = !ContainsKey(context_items_, extension_id); 138 context_items_[extension_id].push_back(item); 139 items_by_id_[item->id()] = item; 140 141 if (item->type() == ExtensionMenuItem::RADIO && item->checked()) 142 RadioItemSelected(item); 143 144 // If this is the first item for this extension, start loading its icon. 145 if (first_item) 146 icon_manager_.LoadIcon(extension); 147 148 return true; 149} 150 151bool ExtensionMenuManager::AddChildItem(const ExtensionMenuItem::Id& parent_id, 152 ExtensionMenuItem* child) { 153 ExtensionMenuItem* parent = GetItemById(parent_id); 154 if (!parent || parent->type() != ExtensionMenuItem::NORMAL || 155 parent->extension_id() != child->extension_id() || 156 ContainsKey(items_by_id_, child->id())) 157 return false; 158 parent->AddChild(child); 159 items_by_id_[child->id()] = child; 160 return true; 161} 162 163bool ExtensionMenuManager::DescendantOf( 164 ExtensionMenuItem* item, 165 const ExtensionMenuItem::Id& ancestor_id) { 166 // Work our way up the tree until we find the ancestor or NULL. 167 ExtensionMenuItem::Id* id = item->parent_id(); 168 while (id != NULL) { 169 DCHECK(*id != item->id()); // Catch circular graphs. 170 if (*id == ancestor_id) 171 return true; 172 ExtensionMenuItem* next = GetItemById(*id); 173 if (!next) { 174 NOTREACHED(); 175 return false; 176 } 177 id = next->parent_id(); 178 } 179 return false; 180} 181 182bool ExtensionMenuManager::ChangeParent( 183 const ExtensionMenuItem::Id& child_id, 184 const ExtensionMenuItem::Id* parent_id) { 185 ExtensionMenuItem* child = GetItemById(child_id); 186 ExtensionMenuItem* new_parent = parent_id ? GetItemById(*parent_id) : NULL; 187 if ((parent_id && (child_id == *parent_id)) || !child || 188 (!new_parent && parent_id != NULL) || 189 (new_parent && (DescendantOf(new_parent, child_id) || 190 child->extension_id() != new_parent->extension_id()))) 191 return false; 192 193 ExtensionMenuItem::Id* old_parent_id = child->parent_id(); 194 if (old_parent_id != NULL) { 195 ExtensionMenuItem* old_parent = GetItemById(*old_parent_id); 196 if (!old_parent) { 197 NOTREACHED(); 198 return false; 199 } 200 ExtensionMenuItem* taken = 201 old_parent->ReleaseChild(child_id, false /* non-recursive search*/); 202 DCHECK(taken == child); 203 } else { 204 // This is a top-level item, so we need to pull it out of our list of 205 // top-level items. 206 MenuItemMap::iterator i = context_items_.find(child->extension_id()); 207 if (i == context_items_.end()) { 208 NOTREACHED(); 209 return false; 210 } 211 ExtensionMenuItem::List& list = i->second; 212 ExtensionMenuItem::List::iterator j = std::find(list.begin(), list.end(), 213 child); 214 if (j == list.end()) { 215 NOTREACHED(); 216 return false; 217 } 218 list.erase(j); 219 } 220 221 if (new_parent) { 222 new_parent->AddChild(child); 223 } else { 224 context_items_[child->extension_id()].push_back(child); 225 child->parent_id_.reset(NULL); 226 } 227 return true; 228} 229 230bool ExtensionMenuManager::RemoveContextMenuItem( 231 const ExtensionMenuItem::Id& id) { 232 if (!ContainsKey(items_by_id_, id)) 233 return false; 234 235 std::string extension_id = GetItemById(id)->extension_id(); 236 MenuItemMap::iterator i = context_items_.find(extension_id); 237 if (i == context_items_.end()) { 238 NOTREACHED(); 239 return false; 240 } 241 242 bool result = false; 243 std::set<ExtensionMenuItem::Id> items_removed; 244 ExtensionMenuItem::List& list = i->second; 245 ExtensionMenuItem::List::iterator j; 246 for (j = list.begin(); j < list.end(); ++j) { 247 // See if the current top-level item is a match. 248 if ((*j)->id() == id) { 249 items_removed = (*j)->RemoveAllDescendants(); 250 items_removed.insert(id); 251 delete *j; 252 list.erase(j); 253 result = true; 254 break; 255 } else { 256 // See if the item to remove was found as a descendant of the current 257 // top-level item. 258 ExtensionMenuItem* child = (*j)->ReleaseChild(id, true /* recursive */); 259 if (child) { 260 items_removed = child->RemoveAllDescendants(); 261 items_removed.insert(id); 262 delete child; 263 result = true; 264 break; 265 } 266 } 267 } 268 DCHECK(result); // The check at the very top should have prevented this. 269 270 // Clear entries from the items_by_id_ map. 271 std::set<ExtensionMenuItem::Id>::iterator removed_iter; 272 for (removed_iter = items_removed.begin(); 273 removed_iter != items_removed.end(); 274 ++removed_iter) { 275 items_by_id_.erase(*removed_iter); 276 } 277 278 if (list.empty()) { 279 context_items_.erase(extension_id); 280 icon_manager_.RemoveIcon(extension_id); 281 } 282 283 return result; 284} 285 286void ExtensionMenuManager::RemoveAllContextItems(std::string extension_id) { 287 ExtensionMenuItem::List::iterator i; 288 for (i = context_items_[extension_id].begin(); 289 i != context_items_[extension_id].end(); ++i) { 290 ExtensionMenuItem* item = *i; 291 items_by_id_.erase(item->id()); 292 293 // Remove descendants from this item and erase them from the lookup cache. 294 std::set<ExtensionMenuItem::Id> removed_ids = item->RemoveAllDescendants(); 295 std::set<ExtensionMenuItem::Id>::const_iterator j; 296 for (j = removed_ids.begin(); j != removed_ids.end(); ++j) { 297 items_by_id_.erase(*j); 298 } 299 } 300 STLDeleteElements(&context_items_[extension_id]); 301 context_items_.erase(extension_id); 302 icon_manager_.RemoveIcon(extension_id); 303} 304 305ExtensionMenuItem* ExtensionMenuManager::GetItemById( 306 const ExtensionMenuItem::Id& id) const { 307 std::map<ExtensionMenuItem::Id, ExtensionMenuItem*>::const_iterator i = 308 items_by_id_.find(id); 309 if (i != items_by_id_.end()) 310 return i->second; 311 else 312 return NULL; 313} 314 315void ExtensionMenuManager::RadioItemSelected(ExtensionMenuItem* item) { 316 // If this is a child item, we need to get a handle to the list from its 317 // parent. Otherwise get a handle to the top-level list. 318 const ExtensionMenuItem::List* list = NULL; 319 if (item->parent_id()) { 320 ExtensionMenuItem* parent = GetItemById(*item->parent_id()); 321 if (!parent) { 322 NOTREACHED(); 323 return; 324 } 325 list = &(parent->children()); 326 } else { 327 if (context_items_.find(item->extension_id()) == context_items_.end()) { 328 NOTREACHED(); 329 return; 330 } 331 list = &context_items_[item->extension_id()]; 332 } 333 334 // Find where |item| is in the list. 335 ExtensionMenuItem::List::const_iterator item_location; 336 for (item_location = list->begin(); item_location != list->end(); 337 ++item_location) { 338 if (*item_location == item) 339 break; 340 } 341 if (item_location == list->end()) { 342 NOTREACHED(); // We should have found the item. 343 return; 344 } 345 346 // Iterate backwards from |item| and uncheck any adjacent radio items. 347 ExtensionMenuItem::List::const_iterator i; 348 if (item_location != list->begin()) { 349 i = item_location; 350 do { 351 --i; 352 if ((*i)->type() != ExtensionMenuItem::RADIO) 353 break; 354 (*i)->SetChecked(false); 355 } while (i != list->begin()); 356 } 357 358 // Now iterate forwards from |item| and uncheck any adjacent radio items. 359 for (i = item_location + 1; i != list->end(); ++i) { 360 if ((*i)->type() != ExtensionMenuItem::RADIO) 361 break; 362 (*i)->SetChecked(false); 363 } 364} 365 366static void AddURLProperty(DictionaryValue* dictionary, 367 const std::string& key, const GURL& url) { 368 if (!url.is_empty()) 369 dictionary->SetString(key, url.possibly_invalid_spec()); 370} 371 372void ExtensionMenuManager::ExecuteCommand( 373 Profile* profile, 374 TabContents* tab_contents, 375 const ContextMenuParams& params, 376 const ExtensionMenuItem::Id& menuItemId) { 377 ExtensionEventRouter* event_router = profile->GetExtensionEventRouter(); 378 if (!event_router) 379 return; 380 381 ExtensionMenuItem* item = GetItemById(menuItemId); 382 if (!item) 383 return; 384 385 if (item->type() == ExtensionMenuItem::RADIO) 386 RadioItemSelected(item); 387 388 ListValue args; 389 390 DictionaryValue* properties = new DictionaryValue(); 391 properties->SetInteger("menuItemId", item->id().uid); 392 if (item->parent_id()) 393 properties->SetInteger("parentMenuItemId", item->parent_id()->uid); 394 395 switch (params.media_type) { 396 case WebKit::WebContextMenuData::MediaTypeImage: 397 properties->SetString("mediaType", "image"); 398 break; 399 case WebKit::WebContextMenuData::MediaTypeVideo: 400 properties->SetString("mediaType", "video"); 401 break; 402 case WebKit::WebContextMenuData::MediaTypeAudio: 403 properties->SetString("mediaType", "audio"); 404 break; 405 default: {} // Do nothing. 406 } 407 408 AddURLProperty(properties, "linkUrl", params.unfiltered_link_url); 409 AddURLProperty(properties, "srcUrl", params.src_url); 410 AddURLProperty(properties, "pageUrl", params.page_url); 411 AddURLProperty(properties, "frameUrl", params.frame_url); 412 413 if (params.selection_text.length() > 0) 414 properties->SetString("selectionText", params.selection_text); 415 416 properties->SetBoolean("editable", params.is_editable); 417 418 args.Append(properties); 419 420 // Add the tab info to the argument list. 421 if (tab_contents) { 422 args.Append(ExtensionTabUtil::CreateTabValue(tab_contents)); 423 } else { 424 args.Append(new DictionaryValue()); 425 } 426 427 if (item->type() == ExtensionMenuItem::CHECKBOX || 428 item->type() == ExtensionMenuItem::RADIO) { 429 bool was_checked = item->checked(); 430 properties->SetBoolean("wasChecked", was_checked); 431 432 // RADIO items always get set to true when you click on them, but CHECKBOX 433 // items get their state toggled. 434 bool checked = 435 (item->type() == ExtensionMenuItem::RADIO) ? true : !was_checked; 436 437 item->SetChecked(checked); 438 properties->SetBoolean("checked", item->checked()); 439 } 440 441 std::string json_args; 442 base::JSONWriter::Write(&args, false, &json_args); 443 std::string event_name = "contextMenus"; 444 event_router->DispatchEventToExtension( 445 item->extension_id(), event_name, json_args, profile, GURL()); 446} 447 448void ExtensionMenuManager::Observe(NotificationType type, 449 const NotificationSource& source, 450 const NotificationDetails& details) { 451 // Remove menu items for disabled/uninstalled extensions. 452 if (type != NotificationType::EXTENSION_UNLOADED) { 453 NOTREACHED(); 454 return; 455 } 456 const Extension* extension = 457 Details<UnloadedExtensionInfo>(details)->extension; 458 if (ContainsKey(context_items_, extension->id())) { 459 RemoveAllContextItems(extension->id()); 460 } 461} 462 463const SkBitmap& ExtensionMenuManager::GetIconForExtension( 464 const std::string& extension_id) { 465 return icon_manager_.GetIcon(extension_id); 466} 467 468// static 469bool ExtensionMenuManager::HasAllowedScheme(const GURL& url) { 470 URLPattern pattern(kAllowedSchemes); 471 return pattern.SetScheme(url.scheme()); 472} 473 474ExtensionMenuItem::Id::Id() 475 : profile(NULL), uid(0) { 476} 477 478ExtensionMenuItem::Id::Id(Profile* profile, std::string extension_id, int uid) 479 : profile(profile), extension_id(extension_id), uid(uid) { 480} 481 482ExtensionMenuItem::Id::~Id() { 483} 484 485bool ExtensionMenuItem::Id::operator==(const Id& other) const { 486 return (profile == other.profile && 487 extension_id == other.extension_id && 488 uid == other.uid); 489} 490 491bool ExtensionMenuItem::Id::operator!=(const Id& other) const { 492 return !(*this == other); 493} 494 495bool ExtensionMenuItem::Id::operator<(const Id& other) const { 496 if (profile < other.profile) 497 return true; 498 if (profile == other.profile) { 499 if (extension_id < other.extension_id) 500 return true; 501 if (extension_id == other.extension_id) 502 return uid < other.uid; 503 } 504 return false; 505} 506