diff options
author | asargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-03-23 06:52:41 +0000 |
---|---|---|
committer | asargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-03-23 06:52:41 +0000 |
commit | 2e3b520ff4318342b027ea8aa303e5981ea9ce79 (patch) | |
tree | f2eba7cf4a7d6179033d9cc0f4bba5981984ec55 /chrome/browser | |
parent | d11a34ba95bc98cf2c4b6caa2ce6ed469ae229ce (diff) | |
download | chromium_src-2e3b520ff4318342b027ea8aa303e5981ea9ce79.zip chromium_src-2e3b520ff4318342b027ea8aa303e5981ea9ce79.tar.gz chromium_src-2e3b520ff4318342b027ea8aa303e5981ea9ce79.tar.bz2 |
Initial version of an experimental Extensions Context Menu API.
The proposal for the API is documented at:
http://dev.chromium.org/developers/design-documents/extensions/context-menu-api
Notable limitations in this initial implementation:
-No reliable way to get at the unique, actual node clicked on in contexts like
IMAGE/LINK/etc. We'll need to fix this in the long run - see the API proposal
page for some notes on this.
-No update or deleteAll methods ; the only way you can change items is to delete
by id and re-add.
-We aren't yet matching the UI goal of having the extension items at the
top level include the extension icon on the left. This will require a
refactoring of RenderViewContextMenu to steal some of the code from the
bookmarks bar context menus, which can display favicons.
-The only kind of parent->child menu that currently works is if you have
a single top-level parent, and only one level of children. (This is because
of how RenderViewContextMenu currently works)
-No browser tests that the menu items actually get drawn (will wait on those
until the above mentioned refactor is complete), or API tests (the API may
change a bit based on feedback, at which point I'll write more tests).
-Unit tests need to cover some more cases.
BUG=32363
TEST=Should be able to create context menus with this API.
Review URL: http://codereview.chromium.org/1042003
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@42321 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome/browser')
11 files changed, 1330 insertions, 8 deletions
diff --git a/chrome/browser/extensions/extension_context_menu_api.cc b/chrome/browser/extensions/extension_context_menu_api.cc new file mode 100644 index 0000000..ef004f5 --- /dev/null +++ b/chrome/browser/extensions/extension_context_menu_api.cc @@ -0,0 +1,149 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/extensions/extension_context_menu_api.h" + +#include "chrome/browser/extensions/extension_menu_manager.h" +#include "chrome/browser/extensions/extensions_service.h" +#include "chrome/browser/profile.h" + +const wchar_t kEnabledContextsKey[] = L"enabledContexts"; +const wchar_t kCheckedKey[] = L"checked"; +const wchar_t kContextsKey[] = L"contexts"; +const wchar_t kParentIdKey[] = L"parentId"; +const wchar_t kTitleKey[] = L"title"; +const wchar_t kTypeKey[] = L"type"; + +const char kTitleNeededError[] = + "All menu items except for separators must have a title"; +const char kCheckedError[] = + "Only items with type RADIO or CHECKBOX can be checked"; +const char kParentsMustBeNormalError[] = + "Parent items must have type NORMAL"; + +bool ParseContexts(const DictionaryValue* properties, + const std::wstring& key, + ExtensionMenuItem::ContextList* result) { + ListValue* list = NULL; + if (!properties->GetList(key, &list)) { + return true; + } + ExtensionMenuItem::ContextList tmp_result; + + std::string value; + for (size_t i = 0; i < list->GetSize(); i++) { + if (!list->GetString(i, &value)) + return false; + + if (value == "ALL") + tmp_result.Add(ExtensionMenuItem::ALL); + else if (value == "PAGE") + tmp_result.Add(ExtensionMenuItem::PAGE); + else if (value == "SELECTION") + tmp_result.Add(ExtensionMenuItem::SELECTION); + else if (value == "LINK") + tmp_result.Add(ExtensionMenuItem::LINK); + else if (value == "EDITABLE") + tmp_result.Add(ExtensionMenuItem::EDITABLE); + else if (value == "IMAGE") + tmp_result.Add(ExtensionMenuItem::IMAGE); + else if (value == "VIDEO") + tmp_result.Add(ExtensionMenuItem::VIDEO); + else if (value == "AUDIO") + tmp_result.Add(ExtensionMenuItem::AUDIO); + else + return false; + } + *result = tmp_result; + return true; +} + +bool CreateContextMenuFunction::RunImpl() { + EXTENSION_FUNCTION_VALIDATE(args_->IsType(Value::TYPE_DICTIONARY)); + const DictionaryValue* properties = args_as_dictionary(); + std::string title; + if (properties->HasKey(kTitleKey)) + EXTENSION_FUNCTION_VALIDATE(properties->GetString(kTitleKey, &title)); + + ExtensionMenuManager* menu_manager = + profile()->GetExtensionsService()->menu_manager(); + + ExtensionMenuItem::ContextList contexts(ExtensionMenuItem::PAGE); + if (!ParseContexts(properties, kContextsKey, &contexts)) + EXTENSION_FUNCTION_ERROR("Invalid value for " + WideToASCII(kContextsKey)); + + ExtensionMenuItem::ContextList enabled_contexts = contexts; + if (!ParseContexts(properties, kEnabledContextsKey, &enabled_contexts)) + EXTENSION_FUNCTION_ERROR("Invalid value for " + + WideToASCII(kEnabledContextsKey)); + + ExtensionMenuItem::Type type = ExtensionMenuItem::NORMAL; + if (properties->HasKey(kTypeKey)) { + std::string type_string; + EXTENSION_FUNCTION_VALIDATE(properties->GetString(kTypeKey, &type_string)); + if (type_string == "CHECKBOX") + type = ExtensionMenuItem::CHECKBOX; + else if (type_string == "RADIO") + type = ExtensionMenuItem::RADIO; + else if (type_string == "SEPARATOR") + type = ExtensionMenuItem::SEPARATOR; + else if (type_string != "NORMAL") + EXTENSION_FUNCTION_ERROR("Invalid type string '" + type_string + "'"); + } + + if (title.empty() && type != ExtensionMenuItem::SEPARATOR) + EXTENSION_FUNCTION_ERROR(kTitleNeededError); + + bool checked = false; + if (properties->HasKey(kCheckedKey)) { + EXTENSION_FUNCTION_VALIDATE(properties->GetBoolean(kCheckedKey, &checked)); + if (checked && type != ExtensionMenuItem::CHECKBOX && + type != ExtensionMenuItem::RADIO) + EXTENSION_FUNCTION_ERROR(kCheckedError); + } + + scoped_ptr<ExtensionMenuItem> item( + new ExtensionMenuItem(extension_id(), title, checked, type, contexts, + enabled_contexts)); + + int id = 0; + if (properties->HasKey(kParentIdKey)) { + int parent_id = 0; + EXTENSION_FUNCTION_VALIDATE(properties->GetInteger(kParentIdKey, + &parent_id)); + ExtensionMenuItem* parent = menu_manager->GetItemById(parent_id); + if (!parent) + EXTENSION_FUNCTION_ERROR("Cannot find menu item with id " + + IntToString(parent_id)); + if (parent->type() != ExtensionMenuItem::NORMAL) + EXTENSION_FUNCTION_ERROR(kParentsMustBeNormalError); + + id = menu_manager->AddChildItem(parent_id, item.release()); + } else { + id = menu_manager->AddContextItem(item.release()); + } + EXTENSION_FUNCTION_VALIDATE(id > 0); + + if (has_callback()) + result_.reset(Value::CreateIntegerValue(id)); + + return true; +} + +bool RemoveContextMenuFunction::RunImpl() { + EXTENSION_FUNCTION_VALIDATE(args_->IsType(Value::TYPE_INTEGER)); + int id = 0; + EXTENSION_FUNCTION_VALIDATE(args_->GetAsInteger(&id)); + ExtensionsService* service = profile()->GetExtensionsService(); + ExtensionMenuManager* manager = service->menu_manager(); + + ExtensionMenuItem* item = manager->GetItemById(id); + // Ensure one extension can't remove another's menu items. + if (!item || item->extension_id() != extension_id()) + EXTENSION_FUNCTION_ERROR( + StringPrintf("no menu item with id %d is registered", id)); + + EXTENSION_FUNCTION_VALIDATE(manager->RemoveContextMenuItem(id)); + return true; +} diff --git a/chrome/browser/extensions/extension_context_menu_api.h b/chrome/browser/extensions/extension_context_menu_api.h new file mode 100644 index 0000000..71a0353 --- /dev/null +++ b/chrome/browser/extensions/extension_context_menu_api.h @@ -0,0 +1,22 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_CONTEXT_MENU_API_H__ +#define CHROME_BROWSER_EXTENSIONS_EXTENSION_CONTEXT_MENU_API_H__ + +#include "chrome/browser/extensions/extension_function.h" + +class CreateContextMenuFunction : public SyncExtensionFunction { + ~CreateContextMenuFunction() {} + virtual bool RunImpl(); + DECLARE_EXTENSION_FUNCTION_NAME("experimental.contextMenu.create") +}; + +class RemoveContextMenuFunction : public SyncExtensionFunction { + ~RemoveContextMenuFunction() {} + virtual bool RunImpl(); + DECLARE_EXTENSION_FUNCTION_NAME("experimental.contextMenu.remove") +}; + +#endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_CONTEXT_MENU_API_H__ diff --git a/chrome/browser/extensions/extension_function.h b/chrome/browser/extensions/extension_function.h index 06f69ab..75e2553a 100644 --- a/chrome/browser/extensions/extension_function.h +++ b/chrome/browser/extensions/extension_function.h @@ -24,6 +24,12 @@ class QuotaLimitHeuristic; } \ } while (0) +#define EXTENSION_FUNCTION_ERROR(error) do { \ + error_ = error; \ + bad_message_ = true; \ + return false; \ + } while (0) + #define DECLARE_EXTENSION_FUNCTION_NAME(name) \ public: static const char* function_name() { return name; } diff --git a/chrome/browser/extensions/extension_function_dispatcher.cc b/chrome/browser/extensions/extension_function_dispatcher.cc index 30350db..546205c 100644 --- a/chrome/browser/extensions/extension_function_dispatcher.cc +++ b/chrome/browser/extensions/extension_function_dispatcher.cc @@ -16,6 +16,7 @@ #include "chrome/browser/extensions/extension_bookmarks_module_constants.h" #include "chrome/browser/extensions/extension_browser_actions_api.h" #include "chrome/browser/extensions/extension_clipboard_api.h" +#include "chrome/browser/extensions/extension_context_menu_api.h" #include "chrome/browser/extensions/extension_dom_ui.h" #include "chrome/browser/extensions/extension_function.h" #include "chrome/browser/extensions/extension_history_api.h" @@ -205,6 +206,10 @@ void FactoryRegistry::ResetFunctions() { RegisterFunction<ExecuteCopyClipboardFunction>(); RegisterFunction<ExecuteCutClipboardFunction>(); RegisterFunction<ExecutePasteClipboardFunction>(); + + // Context Menus. + RegisterFunction<CreateContextMenuFunction>(); + RegisterFunction<RemoveContextMenuFunction>(); } void FactoryRegistry::GetAllNames(std::vector<std::string>* names) { diff --git a/chrome/browser/extensions/extension_menu_manager.cc b/chrome/browser/extensions/extension_menu_manager.cc new file mode 100644 index 0000000..809085e --- /dev/null +++ b/chrome/browser/extensions/extension_menu_manager.cc @@ -0,0 +1,339 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "chrome/browser/extensions/extension_menu_manager.h" + +#include "base/logging.h" +#include "base/string_util.h" +#include "base/utf_string_conversions.h" +#include "base/values.h" +#include "base/json/json_writer.h" +#include "chrome/app/chrome_dll_resource.h" +#include "chrome/browser/extensions/extension_message_service.h" +#include "chrome/browser/extensions/extension_tabs_module.h" +#include "chrome/browser/profile.h" +#include "chrome/common/extensions/extension.h" +#include "webkit/glue/context_menu.h" + +ExtensionMenuItem::ExtensionMenuItem(const std::string& extension_id, + std::string title, + bool checked, Type type, + const ContextList& contexts, + const ContextList& enabled_contexts) + : extension_id_(extension_id), + title_(title), + id_(0), + type_(type), + checked_(checked), + contexts_(contexts), + enabled_contexts_(enabled_contexts), + parent_id_(0) {} + +ExtensionMenuItem::~ExtensionMenuItem() {} + +ExtensionMenuItem* ExtensionMenuItem::ChildAt(int index) const { + if (index < 0 || static_cast<size_t>(index) >= children_.size()) + return NULL; + return children_[index].get(); +} + +bool ExtensionMenuItem::RemoveChild(int child_id) { + for (List::iterator i = children_.begin(); i != children_.end(); ++i) { + if ((*i)->id() == child_id) { + children_.erase(i); + return true; + } else if ((*i)->RemoveChild(child_id)) { + return true; + } + } + return false; +} + +string16 ExtensionMenuItem::TitleWithReplacement( + const string16& selection) const { + string16 result = UTF8ToUTF16(title_); + // TODO(asargent) - Change this to properly handle %% escaping so you can + // put "%s" in titles that won't get substituted. + ReplaceSubstringsAfterOffset(&result, 0, ASCIIToUTF16("%s"), selection); + return result; +} + +bool ExtensionMenuItem::SetChecked(bool checked) { + if (type_ != CHECKBOX && type_ != RADIO) + return false; + checked_ = checked; + return true; +} + +void ExtensionMenuItem::AddChild(ExtensionMenuItem* item) { + item->parent_id_ = id_; + children_.push_back(linked_ptr<ExtensionMenuItem>(item)); +} + +ExtensionMenuManager::ExtensionMenuManager() : next_item_id_(1) { + registrar_.Add(this, NotificationType::EXTENSION_UNLOADED, + NotificationService::AllSources()); +} + +ExtensionMenuManager::~ExtensionMenuManager() {} + +std::set<std::string> ExtensionMenuManager::ExtensionIds() { + std::set<std::string> id_set; + for (MenuItemMap::const_iterator i = context_items_.begin(); + i != context_items_.end(); ++i) { + id_set.insert(i->first); + } + return id_set; +} + +std::vector<const ExtensionMenuItem*> ExtensionMenuManager::MenuItems( + const std::string& extension_id) { + std::vector<const ExtensionMenuItem*> result; + + MenuItemMap::iterator i = context_items_.find(extension_id); + if (i != context_items_.end()) { + ExtensionMenuItem::List& list = i->second; + ExtensionMenuItem::List::iterator j; + for (j = list.begin(); j != list.end(); ++j) { + result.push_back(j->get()); + } + } + + return result; +} + +int ExtensionMenuManager::AddContextItem(ExtensionMenuItem* item) { + const std::string& extension_id = item->extension_id(); + // The item must have a non-empty extension id. + if (extension_id.empty()) + return 0; + + DCHECK_EQ(0, item->id()); + item->set_id(next_item_id_++); + + context_items_[extension_id].push_back(linked_ptr<ExtensionMenuItem>(item)); + items_by_id_[item->id()] = item; + + if (item->type() == ExtensionMenuItem::RADIO && item->checked()) + RadioItemSelected(item); + + return item->id(); +} + +int ExtensionMenuManager::AddChildItem(int parent_id, + ExtensionMenuItem* child) { + ExtensionMenuItem* parent = GetItemById(parent_id); + if (!parent || parent->type() != ExtensionMenuItem::NORMAL || + parent->extension_id() != child->extension_id()) + return 0; + child->set_id(next_item_id_++); + parent->AddChild(child); + items_by_id_[child->id()] = child; + return child->id(); +} + +bool ExtensionMenuManager::RemoveContextMenuItem(int id) { + if (items_by_id_.find(id) == items_by_id_.end()) + return false; + + MenuItemMap::iterator i; + for (i = context_items_.begin(); i != context_items_.end(); ++i) { + ExtensionMenuItem::List& list = i->second; + ExtensionMenuItem::List::iterator j; + for (j = list.begin(); j < list.end(); ++j) { + // See if the current item is a match, or if one of its children was. + if ((*j)->id() == id) { + list.erase(j); + items_by_id_.erase(id); + return true; + } else if ((*j)->RemoveChild(id)) { + items_by_id_.erase(id); + return true; + } + } + } + NOTREACHED(); // The check at the very top should prevent getting here. + return false; +} + +ExtensionMenuItem* ExtensionMenuManager::GetItemById(int id) { + std::map<int, ExtensionMenuItem*>::const_iterator i = items_by_id_.find(id); + if (i != items_by_id_.end()) + return i->second; + else + return NULL; +} + +void ExtensionMenuManager::RadioItemSelected(ExtensionMenuItem* item) { + // If this is a child item, we need to get a handle to the list from its + // parent. Otherwise get a handle to the top-level list. + ExtensionMenuItem::List* list = NULL; + if (item->parent_id()) { + ExtensionMenuItem* parent = GetItemById(item->parent_id()); + if (!parent) { + NOTREACHED(); + return; + } + list = parent->children(); + } else { + if (context_items_.find(item->extension_id()) == context_items_.end()) { + NOTREACHED(); + return; + } + list = &context_items_[item->extension_id()]; + } + + // Find where |item| is in the list. + ExtensionMenuItem::List::iterator item_location; + for (item_location = list->begin(); item_location != list->end(); + ++item_location) { + if (item_location->get() == item) + break; + } + if (item_location == list->end()) { + NOTREACHED(); // We should have found the item. + return; + } + + // Iterate backwards from |item| and uncheck any adjacent radio items. + ExtensionMenuItem::List::iterator i; + if (item_location != list->begin()) { + i = item_location; + do { + --i; + if ((*i)->type() != ExtensionMenuItem::RADIO) + break; + (*i)->SetChecked(false); + } while (i != list->begin()); + } + + // Now iterate forwards from |item| and uncheck any adjacent radio items. + for (i = item_location + 1; i != list->end(); ++i) { + if ((*i)->type() != ExtensionMenuItem::RADIO) + break; + (*i)->SetChecked(false); + } +} + +static void AddURLProperty(DictionaryValue* dictionary, + const std::wstring& key, const GURL& url) { + if (!url.is_empty()) + dictionary->SetString(key, url.possibly_invalid_spec()); +} + +void ExtensionMenuManager::GetItemAndIndex(int id, ExtensionMenuItem** item, + size_t* index) { + for (MenuItemMap::const_iterator i = context_items_.begin(); + i != context_items_.end(); ++i) { + const ExtensionMenuItem::List& list = i->second; + for (size_t tmp_index = 0; tmp_index < list.size(); tmp_index++) { + if (list[tmp_index]->id() == id) { + if (item) + *item = list[tmp_index].get(); + if (index) + *index = tmp_index; + return; + } + } + } + if (item) + *item = NULL; + if (index) + *index = 0; +} + + +void ExtensionMenuManager::ExecuteCommand(Profile* profile, + TabContents* tab_contents, + const ContextMenuParams& params, + int menuItemId) { + ExtensionMessageService* service = profile->GetExtensionMessageService(); + if (!service) + return; + + ExtensionMenuItem* item = GetItemById(menuItemId); + if (!item) + return; + + if (item->type() == ExtensionMenuItem::RADIO) + RadioItemSelected(item); + + ListValue args; + + DictionaryValue* properties = new DictionaryValue(); + properties->SetInteger(L"menuItemId", item->id()); + if (item->parent_id()) + properties->SetInteger(L"parentMenuItemId", item->parent_id()); + + switch (params.media_type) { + case WebKit::WebContextMenuData::MediaTypeImage: + properties->SetString(L"mediaType", "IMAGE"); + break; + case WebKit::WebContextMenuData::MediaTypeVideo: + properties->SetString(L"mediaType", "VIDEO"); + break; + case WebKit::WebContextMenuData::MediaTypeAudio: + properties->SetString(L"mediaType", "AUDIO"); + break; + default: {} // Do nothing. + } + + AddURLProperty(properties, L"linkUrl", params.unfiltered_link_url); + AddURLProperty(properties, L"srcUrl", params.src_url); + AddURLProperty(properties, L"mainFrameUrl", params.page_url); + AddURLProperty(properties, L"frameUrl", params.frame_url); + + if (params.selection_text.length() > 0) + properties->SetString(L"selectionText", params.selection_text); + + properties->SetBoolean(L"editable", params.is_editable); + + args.Append(properties); + + // Add the tab info to the argument list. + if (tab_contents) { + args.Append(ExtensionTabUtil::CreateTabValue(tab_contents)); + } else { + args.Append(new DictionaryValue()); + } + + if (item->type() == ExtensionMenuItem::CHECKBOX || + item->type() == ExtensionMenuItem::RADIO) { + bool was_checked = item->checked(); + properties->SetBoolean(L"wasChecked", was_checked); + + // RADIO items always get set to true when you click on them, but CHECKBOX + // items get their state toggled. + bool checked = + (item->type() == ExtensionMenuItem::RADIO) ? true : !was_checked; + + item->SetChecked(checked); + properties->SetBoolean(L"checked", item->checked()); + } + + std::string json_args; + base::JSONWriter::Write(&args, false, &json_args); + std::string event_name = "contextMenu/" + item->extension_id(); + service->DispatchEventToRenderers(event_name, json_args, + profile->IsOffTheRecord()); +} + +void ExtensionMenuManager::Observe(NotificationType type, + const NotificationSource& source, + const NotificationDetails& details) { + // Remove menu items for disabled/uninstalled extensions. + if (type != NotificationType::EXTENSION_UNLOADED) { + NOTREACHED(); + return; + } + Extension* extension = Details<Extension>(details).ptr(); + MenuItemMap::iterator i = context_items_.find(extension->id()); + if (i != context_items_.end()) { + const ExtensionMenuItem::List& list = i->second; + ExtensionMenuItem::List::const_iterator j; + for (j = list.begin(); j != list.end(); ++j) + items_by_id_.erase((*j)->id()); + context_items_.erase(i); + } +} diff --git a/chrome/browser/extensions/extension_menu_manager.h b/chrome/browser/extensions/extension_menu_manager.h new file mode 100644 index 0000000..2e927b7f --- /dev/null +++ b/chrome/browser/extensions/extension_menu_manager.h @@ -0,0 +1,221 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_EXTENSIONS_EXTENSION_MENU_MANAGER_H_ +#define CHROME_BROWSER_EXTENSIONS_EXTENSION_MENU_MANAGER_H_ + +#include <map> +#include <set> +#include <string> +#include <vector> + +#include "base/basictypes.h" +#include "base/linked_ptr.h" +#include "base/stl_util-inl.h" +#include "base/string16.h" +#include "chrome/common/notification_observer.h" +#include "chrome/common/notification_registrar.h" + +struct ContextMenuParams; + +class ExtensionMessageService; +class Profile; +class TabContents; + +// Represents a menu item added by an extension. +class ExtensionMenuItem { + public: + // A list of owned ExtensionMenuItem's. + typedef std::vector<linked_ptr<ExtensionMenuItem> > List; + + // For context menus, these are the contexts where an item can appear and + // potentially be enabled. + enum Context { + ALL = 1, + PAGE = 2, + SELECTION = 4, + LINK = 8, + EDITABLE = 16, + IMAGE = 32, + VIDEO = 64, + AUDIO = 128, + }; + + // An item can be only one of these types. + enum Type { + NORMAL, + CHECKBOX, + RADIO, + SEPARATOR + }; + + // A list of Contexts for an item (where it should be shown/enabled). + class ContextList { + public: + ContextList() : value_(0) {} + explicit ContextList(Context context) : value_(context) {} + ContextList(const ContextList& other) : value_(other.value_) {} + + void operator=(const ContextList& other) { + value_ = other.value_; + } + + bool Contains(Context context) const { + return (value_ & context) > 0; + } + + void Add(Context context) { + value_ |= context; + } + + private: + uint32 value_; // A bitmask of Context values. + }; + + ExtensionMenuItem(const std::string& extension_id, std::string title, + bool checked, Type type, const ContextList& contexts, + const ContextList& enabled_contexts); + virtual ~ExtensionMenuItem(); + + // Simple accessor methods. + const std::string& extension_id() const { return extension_id_; } + const std::string& title() const { return title_; } + int id() const { return id_; } + int parent_id() const { return parent_id_; } + int child_count() const { return children_.size(); } + ContextList contexts() const { return contexts_; } + ContextList enabled_contexts() const { return enabled_contexts_; } + Type type() const { return type_; } + bool checked() const { return checked_; } + + // Returns the child at the given index, or NULL. + ExtensionMenuItem* ChildAt(int index) const; + + // Returns the title with any instances of %s replaced by |selection|. + string16 TitleWithReplacement(const string16& selection) const; + + protected: + friend class ExtensionMenuManager; + + // This is protected because the ExtensionMenuManager is in charge of + // assigning unique ids. + virtual void set_id(int id) { + id_ = id; + } + + // Provides direct access to the children of this item. + List* children() { return &children_; } + + // Set the checked state to |checked|. Returns true if successful. + bool SetChecked(bool checked); + + // Takes ownership of |item| and sets its parent_id_. + void AddChild(ExtensionMenuItem* item); + + // Removes child menu item with the given id, returning true if the item was + // found and removed, or false otherwise. + bool RemoveChild(int child_id); + + private: + // The extension that added this item. + std::string extension_id_; + + // What gets shown in the menu for this item. + std::string title_; + + // A unique id for this item. The value 0 means "not yet assigned". + int id_; + + Type type_; + + // This should only be true for items of type CHECKBOX or RADIO. + bool checked_; + + // In what contexts should the item be shown? + ContextList contexts_; + + // In what contexts should the item be enabled (i.e. not greyed out). This + // should be a subset of contexts_. + ContextList enabled_contexts_; + + // If this item is a child of another item, the unique id of its parent. If + // this is a top-level item with no parent, this will be 0. + int parent_id_; + + // Any children this item may have. + List children_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionMenuItem); +}; + +// This class keeps track of menu items added by extensions. +class ExtensionMenuManager : public NotificationObserver { + public: + ExtensionMenuManager(); + virtual ~ExtensionMenuManager(); + + // Returns the ids of extensions which have menu items registered. + std::set<std::string> ExtensionIds(); + + // Returns a list of all the *top-level* menu items (added via AddContextItem) + // for the given extension id, *not* including child items (added via + // AddChildItem); although those can be reached via the top-level items' + // ChildAt function. A view can then decide how to display these, including + // whether to put them into a submenu if there are more than 1. + std::vector<const ExtensionMenuItem*> MenuItems( + const std::string& extension_id); + + // Takes ownership of |item|. Returns the id assigned to the item. Has the + // side-effect of incrementing the next_item_id_ member. + int AddContextItem(ExtensionMenuItem* item); + + // Add an item as a child of another item which has been previously added, and + // takes ownership of |item|. Returns the id assigned to the item, or 0 on + // error. Has the side-effect of incrementing the next_item_id_ member. + int AddChildItem(int parent_id, ExtensionMenuItem* child); + + // Removes a context menu item with the given id (whether it is a top-level + // item or a child of some other item), returning true if the item was found + // and removed or false otherwise. + bool RemoveContextMenuItem(int id); + + // Returns the item with the given |id| or NULL. + ExtensionMenuItem* GetItemById(int id); + + // Called when a menu item is clicked on by the user. + void ExecuteCommand(Profile* profile, TabContents* tab_contents, + const ContextMenuParams& params, + int menuItemId); + + // Implements the NotificationObserver interface. + virtual void Observe(NotificationType type, const NotificationSource& source, + const NotificationDetails& details); + + private: + // This is a helper function which takes care of de-selecting any other radio + // items in the same group (i.e. that are adjacent in the list). + void RadioItemSelected(ExtensionMenuItem* item); + + // If an item with |id| is found, |item| will be set to point to it and + // |index| will be set to its index within the containing list. + void GetItemAndIndex(int id, ExtensionMenuItem** item, size_t* index); + + // We keep items organized by mapping an extension id to a list of items. + typedef std::map<std::string, ExtensionMenuItem::List> MenuItemMap; + MenuItemMap context_items_; + + // This lets us make lookup by id fast. It maps id to ExtensionMenuItem* for + // all items the menu manager knows about, including all children of top-level + // items. + std::map<int, ExtensionMenuItem*> items_by_id_; + + // The id we will assign to the next item that gets added. + int next_item_id_; + + NotificationRegistrar registrar_; + + DISALLOW_COPY_AND_ASSIGN(ExtensionMenuManager); +}; + +#endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_MENU_MANAGER_H_ diff --git a/chrome/browser/extensions/extension_menu_manager_unittest.cc b/chrome/browser/extensions/extension_menu_manager_unittest.cc new file mode 100644 index 0000000..e81fc04 --- /dev/null +++ b/chrome/browser/extensions/extension_menu_manager_unittest.cc @@ -0,0 +1,295 @@ +// Copyright (c) 2010 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <vector> + +#include "base/json/json_reader.h" +#include "base/path_service.h" +#include "base/scoped_temp_dir.h" +#include "base/values.h" +#include "chrome/browser/extensions/extension_menu_manager.h" +#include "chrome/browser/extensions/extension_message_service.h" +#include "chrome/common/chrome_paths.h" +#include "chrome/common/extensions/extension.h" +#include "chrome/common/extensions/extension_constants.h" +#include "chrome/common/notification_service.h" +#include "chrome/test/testing_profile.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "webkit/glue/context_menu.h" + +using testing::_; +using testing::AtLeast; +using testing::Return; +using testing::SaveArg; + +// Base class for tests. +class ExtensionMenuManagerTest : public testing::Test { + public: + ExtensionMenuManagerTest() {} + ~ExtensionMenuManagerTest() {} + + // Returns a test item with some default values you can override if you want + // to by passing in |properties| (currently just extension_id). Caller owns + // the return value and is responsible for freeing it. + static ExtensionMenuItem* CreateTestItem(DictionaryValue* properties) { + std::string extension_id = "0123456789"; // A default dummy value. + if (properties && properties->HasKey(L"extension_id")) + EXPECT_TRUE(properties->GetString(L"extension_id", &extension_id)); + + ExtensionMenuItem::Type type = ExtensionMenuItem::NORMAL; + ExtensionMenuItem::ContextList contexts(ExtensionMenuItem::ALL); + ExtensionMenuItem::ContextList enabled_contexts = contexts; + std::string title = "test"; + + return new ExtensionMenuItem(extension_id, title, false, type, contexts, + enabled_contexts); + } + + protected: + ExtensionMenuManager manager_; + + private: + DISALLOW_COPY_AND_ASSIGN(ExtensionMenuManagerTest); +}; + +// Tests adding, getting, and removing items. +TEST_F(ExtensionMenuManagerTest, AddGetRemoveItems) { + // Add a new item, make sure you can get it back. + ExtensionMenuItem* item1 = CreateTestItem(NULL); + ASSERT_TRUE(item1 != NULL); + int id1 = manager_.AddContextItem(item1); // Ownership transferred. + ASSERT_GT(id1, 0); + ASSERT_EQ(item1, manager_.GetItemById(id1)); + std::vector<const ExtensionMenuItem*> items = + manager_.MenuItems(item1->extension_id()); + ASSERT_EQ(1u, items.size()); + ASSERT_EQ(item1, items[0]); + + // Add a second item, make sure it comes back too. + ExtensionMenuItem* item2 = CreateTestItem(NULL); + int id2 = manager_.AddContextItem(item2); // Ownership transferred. + ASSERT_GT(id2, 0); + ASSERT_NE(id1, id2); + ASSERT_EQ(item2, manager_.GetItemById(id2)); + items = manager_.MenuItems(item2->extension_id()); + ASSERT_EQ(2u, items.size()); + ASSERT_EQ(item1, items[0]); + ASSERT_EQ(item2, items[1]); + + // Try adding item 3, then removing it. + ExtensionMenuItem* item3 = CreateTestItem(NULL); + std::string extension_id = item3->extension_id(); + int id3 = manager_.AddContextItem(item3); // Ownership transferred. + ASSERT_GT(id3, 0); + ASSERT_EQ(item3, manager_.GetItemById(id3)); + ASSERT_EQ(3u, manager_.MenuItems(extension_id).size()); + ASSERT_TRUE(manager_.RemoveContextMenuItem(id3)); + ASSERT_EQ(NULL, manager_.GetItemById(id3)); + ASSERT_EQ(2u, manager_.MenuItems(extension_id).size()); + + // Make sure removing a non-existent item returns false. + ASSERT_FALSE(manager_.RemoveContextMenuItem(5)); +} + +// Test adding/removing child items. +TEST_F(ExtensionMenuManagerTest, ChildFunctions) { + DictionaryValue properties; + properties.SetString(L"extension_id", "1111"); + ExtensionMenuItem* item1 = CreateTestItem(&properties); + + properties.SetString(L"extension_id", "2222"); + ExtensionMenuItem* item2 = CreateTestItem(&properties); + ExtensionMenuItem* item2_child = CreateTestItem(&properties); + ExtensionMenuItem* item2_grandchild = CreateTestItem(&properties); + + // This third item we expect to fail inserting, so we use a scoped_ptr to make + // sure it gets deleted. + properties.SetString(L"extension_id", "3333"); + scoped_ptr<ExtensionMenuItem> item3(CreateTestItem(&properties)); + + // Add in the first two items. + int id1 = manager_.AddContextItem(item1); // Ownership transferred. + int id2 = manager_.AddContextItem(item2); // Ownership transferred. + + ASSERT_NE(id1, id2); + + // Try adding item3 as a child of item2 - this should fail because item3 has + // a different extension id. + ASSERT_EQ(0, manager_.AddChildItem(id2, item3.get())); + + // Add item2_child as a child of item2. + int id2_child = manager_.AddChildItem(id2, item2_child); + ASSERT_GT(id2_child, 0); + ASSERT_EQ(1, item2->child_count()); + ASSERT_EQ(0, item1->child_count()); + ASSERT_EQ(item2_child, manager_.GetItemById(id2_child)); + + ASSERT_EQ(1u, manager_.MenuItems(item1->extension_id()).size()); + ASSERT_EQ(item1, manager_.MenuItems(item1->extension_id()).at(0)); + + // Add item2_grandchild as a child of item2_child, then remove it. + int id2_grandchild = manager_.AddChildItem(id2_child, item2_grandchild); + ASSERT_GT(id2_grandchild, 0); + ASSERT_EQ(1, item2->child_count()); + ASSERT_EQ(1, item2_child->child_count()); + ASSERT_TRUE(manager_.RemoveContextMenuItem(id2_grandchild)); + + // We should only get 1 thing back when asking for item2's extension id, since + // it has a child item. + ASSERT_EQ(1u, manager_.MenuItems(item2->extension_id()).size()); + ASSERT_EQ(item2, manager_.MenuItems(item2->extension_id()).at(0)); + + // Remove child2_item. + ASSERT_TRUE(manager_.RemoveContextMenuItem(id2_child)); + ASSERT_EQ(1u, manager_.MenuItems(item2->extension_id()).size()); + ASSERT_EQ(item2, manager_.MenuItems(item2->extension_id()).at(0)); + ASSERT_EQ(0, item2->child_count()); +} + +// Tests that we properly remove an extension's menu item when that extension is +// unloaded. +TEST_F(ExtensionMenuManagerTest, ExtensionUnloadRemovesMenuItems) { + ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + + NotificationService* notifier = NotificationService::current(); + ASSERT_TRUE(notifier != NULL); + + // Create a test extension. + DictionaryValue extension_properties; + extension_properties.SetString(extension_manifest_keys::kVersion, "1"); + extension_properties.SetString(extension_manifest_keys::kName, "Test"); + Extension extension(temp_dir.path().AppendASCII("extension")); + std::string errors; + ASSERT_TRUE(extension.InitFromValue(extension_properties, + false, // No public key required. + &errors)) << errors; + + // Create an ExtensionMenuItem and put it into the manager. + DictionaryValue item_properties; + item_properties.SetString(L"extension_id", extension.id()); + ExtensionMenuItem* item1 = CreateTestItem(&item_properties); + ASSERT_EQ(extension.id(), item1->extension_id()); + int id1 = manager_.AddContextItem(item1); // Ownership transferred. + ASSERT_GT(id1, 0); + ASSERT_EQ(1u, manager_.MenuItems(extension.id()).size()); + + // Create a menu item with a different extension id and add it to the manager. + std::string alternate_extension_id = "0000"; + item_properties.SetString(L"extension_id", alternate_extension_id); + ExtensionMenuItem* item2 = CreateTestItem(&item_properties); + ASSERT_NE(item1->extension_id(), item2->extension_id()); + int id2 = manager_.AddContextItem(item2); // Ownership transferred. + ASSERT_GT(id2, 0); + + // Notify that the extension was unloaded, and make sure the right item is + // gone. + notifier->Notify(NotificationType::EXTENSION_UNLOADED, + Source<Profile>(NULL), + Details<Extension>(&extension)); + ASSERT_EQ(0u, manager_.MenuItems(extension.id()).size()); + ASSERT_EQ(1u, manager_.MenuItems(alternate_extension_id).size()); + ASSERT_TRUE(manager_.GetItemById(id1) == NULL); + ASSERT_TRUE(manager_.GetItemById(id2) != NULL); +} + +// A mock message service for tests of ExtensionMenuManager::ExecuteCommand. +class MockExtensionMessageService : public ExtensionMessageService { + public: + explicit MockExtensionMessageService(Profile* profile) : + ExtensionMessageService(profile) {} + + MOCK_METHOD3(DispatchEventToRenderers, void(const std::string& event_name, + const std::string& event_args, + bool has_incognito_data)); + + private: + DISALLOW_COPY_AND_ASSIGN(MockExtensionMessageService); +}; + +// A mock profile for tests of ExtensionMenuManager::ExecuteCommand. +class MockTestingProfile : public TestingProfile { + public: + MockTestingProfile() {} + MOCK_METHOD0(GetExtensionMessageService, ExtensionMessageService*()); + MOCK_METHOD0(IsOffTheRecord, bool()); + + private: + DISALLOW_COPY_AND_ASSIGN(MockTestingProfile); +}; + +TEST_F(ExtensionMenuManagerTest, ExecuteCommand) { + MessageLoopForUI message_loop; + ChromeThread ui_thread(ChromeThread::UI, &message_loop); + + MockTestingProfile profile; + + scoped_refptr<MockExtensionMessageService> mock_message_service = + new MockExtensionMessageService(&profile); + + ContextMenuParams params; + params.media_type = WebKit::WebContextMenuData::MediaTypeImage; + params.src_url = GURL("http://foo.bar/image.png"); + params.page_url = GURL("http://foo.bar"); + params.selection_text = L"Hello World"; + params.is_editable = false; + + ExtensionMenuItem* item = CreateTestItem(NULL); + int id = manager_.AddContextItem(item); // Ownership transferred. + ASSERT_GT(id, 0); + + EXPECT_CALL(profile, GetExtensionMessageService()) + .Times(1) + .WillOnce(Return(mock_message_service.get())); + + EXPECT_CALL(profile, IsOffTheRecord()) + .Times(AtLeast(1)) + .WillRepeatedly(Return(false)); + + // Use the magic of googlemock to save a parameter to our mock's + // DispatchEventToRenderers method into event_args. + std::string event_args; + std::string expected_event_name = "contextMenu/" + item->extension_id(); + EXPECT_CALL(*mock_message_service.get(), + DispatchEventToRenderers(expected_event_name, _, + profile.IsOffTheRecord())) + .Times(1) + .WillOnce(SaveArg<1>(&event_args)); + + manager_.ExecuteCommand(&profile, NULL /* tab_contents */, params, id); + + // Parse the json event_args, which should turn into a 2-element list where + // the first element is a dictionary we want to inspect for the correct + // values. + scoped_ptr<Value> result(base::JSONReader::Read(event_args, true)); + Value* value = result.get(); + ASSERT_TRUE(result.get() != NULL); + ASSERT_EQ(Value::TYPE_LIST, value->GetType()); + ListValue* list = static_cast<ListValue*>(value); + ASSERT_EQ(2u, list->GetSize()); + + DictionaryValue* info; + ASSERT_TRUE(list->GetDictionary(0, &info)); + + int tmp_id = 0; + ASSERT_TRUE(info->GetInteger(L"menuItemId", &tmp_id)); + ASSERT_EQ(id, tmp_id); + + std::string tmp; + ASSERT_TRUE(info->GetString(L"mediaType", &tmp)); + ASSERT_EQ("IMAGE", tmp); + ASSERT_TRUE(info->GetString(L"srcUrl", &tmp)); + ASSERT_EQ(params.src_url.spec(), tmp); + ASSERT_TRUE(info->GetString(L"mainFrameUrl", &tmp)); + ASSERT_EQ(params.page_url.spec(), tmp); + + std::wstring wide_tmp; + ASSERT_TRUE(info->GetString(L"selectionText", &wide_tmp)); + ASSERT_EQ(params.selection_text, wide_tmp); + + bool bool_tmp = true; + ASSERT_TRUE(info->GetBoolean(L"editable", &bool_tmp)); + ASSERT_EQ(params.is_editable, bool_tmp); +} diff --git a/chrome/browser/extensions/extension_message_service.h b/chrome/browser/extensions/extension_message_service.h index 6b8ade6..7d7540e 100644 --- a/chrome/browser/extensions/extension_message_service.h +++ b/chrome/browser/extensions/extension_message_service.h @@ -1,4 +1,4 @@ -// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -81,7 +81,7 @@ class ExtensionMessageService void PostMessageFromRenderer(int port_id, const std::string& message); // Send an event to every registered extension renderer. - void DispatchEventToRenderers( + virtual void DispatchEventToRenderers( const std::string& event_name, const std::string& event_args, bool has_incognito_data); @@ -132,11 +132,12 @@ class ExtensionMessageService private: friend class ChromeThread; friend class DeleteTask<ExtensionMessageService>; + friend class MockExtensionMessageService; // A map of channel ID to its channel object. typedef std::map<int, MessageChannel*> MessageChannelMap; - ~ExtensionMessageService(); + virtual ~ExtensionMessageService(); // Allocates a pair of port ids. // NOTE: this can be called from any thread. diff --git a/chrome/browser/extensions/extensions_service.h b/chrome/browser/extensions/extensions_service.h index ff832aa..aef46b7 100644 --- a/chrome/browser/extensions/extensions_service.h +++ b/chrome/browser/extensions/extensions_service.h @@ -1,4 +1,4 @@ -// Copyright (c) 2009 The Chromium Authors. All rights reserved. +// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -19,6 +19,7 @@ #include "base/tuple.h" #include "base/values.h" #include "chrome/browser/chrome_thread.h" +#include "chrome/browser/extensions/extension_menu_manager.h" #include "chrome/browser/extensions/extension_prefs.h" #include "chrome/browser/extensions/extension_process_manager.h" #include "chrome/browser/extensions/extension_toolbar_model.h" @@ -272,6 +273,9 @@ class ExtensionsService ExtensionsQuotaService* quota_service() { return "a_service_; } + // Access to menu items added by extensions. + ExtensionMenuManager* menu_manager() { return &menu_manager_; } + // Notify the frontend that there was an error loading an extension. // This method is public because ExtensionsServiceBackend can post to here. void ReportExtensionLoadError(const FilePath& extension_path, @@ -356,6 +360,9 @@ class ExtensionsService NotificationRegistrar registrar_; + // Keeps track of menu items added by extensions. + ExtensionMenuManager menu_manager_; + // List of registered component extensions (see Extension::Location). typedef std::vector<ComponentExtensionInfo> RegisteredComponentExtensions; RegisteredComponentExtensions component_extension_manifests_; diff --git a/chrome/browser/tab_contents/render_view_context_menu.cc b/chrome/browser/tab_contents/render_view_context_menu.cc index dfcbcb3..4359dc5 100644 --- a/chrome/browser/tab_contents/render_view_context_menu.cc +++ b/chrome/browser/tab_contents/render_view_context_menu.cc @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include <functional> + #include "chrome/browser/tab_contents/render_view_context_menu.h" #include "app/clipboard/clipboard.h" @@ -14,6 +16,8 @@ #include "chrome/browser/debugger/devtools_manager.h" #include "chrome/browser/debugger/devtools_window.h" #include "chrome/browser/download/download_manager.h" +#include "chrome/browser/extensions/extension_menu_manager.h" +#include "chrome/browser/extensions/extensions_service.h" #include "chrome/browser/fonts_languages_window.h" #include "chrome/browser/metrics/user_metrics.h" #include "chrome/browser/net/browser_url_util.h" @@ -66,6 +70,190 @@ void RenderViewContextMenu::Init() { DoInit(); } +static bool ExtensionContextMatch(ContextMenuParams params, + ExtensionMenuItem::ContextList contexts) { + bool has_link = !params.link_url.is_empty(); + bool has_selection = !params.selection_text.empty(); + + if (contexts.Contains(ExtensionMenuItem::ALL) || + (has_selection && contexts.Contains(ExtensionMenuItem::SELECTION)) || + (has_link && contexts.Contains(ExtensionMenuItem::LINK)) || + (params.is_editable && contexts.Contains(ExtensionMenuItem::EDITABLE))) { + return true; + } + + switch (params.media_type) { + case WebContextMenuData::MediaTypeImage: + return contexts.Contains(ExtensionMenuItem::IMAGE); + + case WebContextMenuData::MediaTypeVideo: + return contexts.Contains(ExtensionMenuItem::VIDEO); + + case WebContextMenuData::MediaTypeAudio: + return contexts.Contains(ExtensionMenuItem::AUDIO); + + default: + break; + } + + // PAGE is the least specific context, so we only examine that if none of the + // other contexts apply. + if (!has_link && !has_selection && !params.is_editable && + params.media_type == WebContextMenuData::MediaTypeNone && + contexts.Contains(ExtensionMenuItem::PAGE)) + return true; + + return false; +} + +void RenderViewContextMenu::GetItemsForExtension( + const std::string& extension_id, + std::vector<const ExtensionMenuItem*>* items) { + ExtensionsService* service = profile_->GetExtensionsService(); + + // Get the set of possible items, and iterate to find which ones are + // applicable. + std::vector<const ExtensionMenuItem*> potential_items = + service->menu_manager()->MenuItems(extension_id); + + std::vector<const ExtensionMenuItem*>::const_iterator i; + for (i = potential_items.begin(); i != potential_items.end(); ++i) { + const ExtensionMenuItem* item = *i; + if (ExtensionContextMatch(params_, item->contexts())) + items->push_back(item); + } +} + +bool RenderViewContextMenu::MaybeStartExtensionSubMenu( + const string16& selection_text, const std::string& extension_name, + std::vector<const ExtensionMenuItem*>* items, int* index) { + if (items->size() == 0 || + (items->size() == 1 && items->at(0)->child_count() == 0)) + return false; + + int menu_id = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST + (*index)++; + string16 title; + const ExtensionMenuItem* first_item = items->at(0); + if (first_item->child_count() > 0) { + title = first_item->TitleWithReplacement(selection_text); + extension_item_map_[menu_id] = first_item->id(); + } else { + title = UTF8ToUTF16(extension_name); + } + StartSubMenu(menu_id, title); + + // If we have 1 parent item with a submenu of children, pull the + // parent out of |items| and put the children in. + if (items->size() == 1 && first_item->child_count() > 0) { + const ExtensionMenuItem* parent = first_item; + items->clear(); + for (int j = 0; j < parent->child_count(); j++) { + const ExtensionMenuItem* child = parent->ChildAt(j); + if (ExtensionContextMatch(params_, child->contexts())) + items->push_back(child); + } + } + + return true; +} + +void RenderViewContextMenu::AppendExtensionItems( + const std::string& extension_id, int* index) { + Extension* extension = + profile_->GetExtensionsService()->GetExtensionById(extension_id, false); + DCHECK_GE(*index, 0); + int max_index = + IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST - IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST; + if (!extension || *index >= max_index) + return; + + std::vector<const ExtensionMenuItem*> items; + GetItemsForExtension(extension_id, &items); + if (items.empty()) + return; + + string16 selection_text = PrintableSelectionText(); + + // If this is the first extension-provided menu item, add a separator. + if (*index == 0) + AppendSeparator(); + + bool submenu_started = MaybeStartExtensionSubMenu( + selection_text, extension->name(), &items, index); + + ExtensionMenuItem::Type last_type = ExtensionMenuItem::NORMAL; + for (std::vector<const ExtensionMenuItem*>::iterator i = items.begin(); + i != items.end(); ++i) { + const ExtensionMenuItem* item = *i; + if (item->type() == ExtensionMenuItem::SEPARATOR) { + // We don't want the case of an extension with one top-level item that is + // just a separator, so make sure this is inside a submenu. + if (submenu_started) { + AppendSeparator(); + last_type = ExtensionMenuItem::SEPARATOR; + } + continue; + } + + // Auto-prepend a separator, if needed, to group radio items together. + if (item->type() != ExtensionMenuItem::RADIO && + item->type() != ExtensionMenuItem::SEPARATOR && + last_type == ExtensionMenuItem::RADIO) { + AppendSeparator(); + } + + int menu_id = IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST + (*index)++; + if (menu_id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) + return; + extension_item_map_[menu_id] = item->id(); + string16 title = item->TitleWithReplacement(selection_text); + if (item->type() == ExtensionMenuItem::NORMAL) { + AppendMenuItem(menu_id, title); + } else if (item->type() == ExtensionMenuItem::CHECKBOX) { + AppendCheckboxMenuItem(menu_id, title); + } else if (item->type() == ExtensionMenuItem::RADIO) { + // Auto-append a separator if needed to group radio items together. + if (*index > 0 && last_type != ExtensionMenuItem::RADIO && + last_type != ExtensionMenuItem::SEPARATOR) + AppendSeparator(); + + AppendRadioMenuItem(menu_id, title); + } else { + NOTREACHED(); + } + last_type = item->type(); + } + + if (submenu_started) + FinishSubMenu(); +} + +void RenderViewContextMenu::AppendAllExtensionItems() { + extension_item_map_.clear(); + ExtensionsService* service = profile_->GetExtensionsService(); + ExtensionMenuManager* menu_manager = service->menu_manager(); + + // Get a list of extension id's that have context menu items, and sort it by + // the extension's name. + std::set<std::string> ids = menu_manager->ExtensionIds(); + std::vector<std::pair<std::string, std::string> > sorted_ids; + for (std::set<std::string>::iterator i = ids.begin(); i != ids.end(); ++i) { + Extension* extension = service->GetExtensionById(*i, false); + if (extension) + sorted_ids.push_back( + std::pair<std::string, std::string>(extension->name(), *i)); + } + // TODO(asargent) - See if this works properly for i18n names (bug 32363). + std::sort(sorted_ids.begin(), sorted_ids.end()); + + int index = 0; + std::vector<std::pair<std::string, std::string> >::const_iterator i; + for (i = sorted_ids.begin(); + i != sorted_ids.end(); ++i) { + AppendExtensionItems(i->second, &index); + } +} + void RenderViewContextMenu::InitMenu() { bool has_link = !params_.link_url.is_empty(); bool has_selection = !params_.selection_text.empty(); @@ -123,6 +311,9 @@ void RenderViewContextMenu::InitMenu() { if (has_selection) AppendSearchProvider(); + if (!is_devtools) + AppendAllExtensionItems(); + // In the DevTools popup menu, "developer items" is normally the only section, // so omit the separator there. if (!is_devtools) @@ -247,8 +438,7 @@ void RenderViewContextMenu::AppendSearchProvider() { if (!selection_navigation_url_.is_valid()) return; - string16 printable_selection_text( - WideToUTF16(l10n_util::TruncateString(params_.selection_text, 50))); + string16 printable_selection_text = PrintableSelectionText(); // Escape "&" as "&&". for (size_t i = printable_selection_text.find('&'); i != string16::npos; i = printable_selection_text.find('&', i + 2)) @@ -356,12 +546,24 @@ void RenderViewContextMenu::AppendEditableItems() { l10n_util::GetStringUTF16(IDS_CONTENT_CONTEXT_WRITING_DIRECTION_RTL)); FinishSubMenu(); -#endif // OS_MACOSX +#endif // OS_MACOSX AppendSeparator(); AppendMenuItem(IDS_CONTENT_CONTEXT_SELECTALL); } +ExtensionMenuItem* RenderViewContextMenu::GetExtensionMenuItem(int id) const { + ExtensionMenuManager* manager = + profile_->GetExtensionsService()->menu_manager(); + std::map<int, int>::const_iterator i = extension_item_map_.find(id); + if (i != extension_item_map_.end()) { + ExtensionMenuItem* item = manager->GetItemById(i->second); + if (item) + return item; + } + return NULL; +} + // Menu delegate functions ----------------------------------------------------- bool RenderViewContextMenu::IsItemCommandEnabled(int id) const { @@ -383,6 +585,16 @@ bool RenderViewContextMenu::IsItemCommandEnabled(int id) const { return false; } + // Extension items. + if (id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && + id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) { + ExtensionMenuItem* item = GetExtensionMenuItem(id); + if (item) + return ExtensionContextMatch(params_, item->enabled_contexts()); + else + return false; + } + switch (id) { case IDS_CONTENT_CONTEXT_BACK: return source_tab_contents_->controller().CanGoBack(); @@ -572,6 +784,15 @@ bool RenderViewContextMenu::ItemIsChecked(int id) const { WebContextMenuData::MediaLoop) != 0; } + if (id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && + id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) { + ExtensionMenuItem* item = GetExtensionMenuItem(id); + if (item) + return item->checked(); + else + return false; + } + #if defined(OS_MACOSX) if (id == IDC_WRITING_DIRECTION_DEFAULT) return params_.writing_direction_default & @@ -626,6 +847,20 @@ void RenderViewContextMenu::ExecuteItemCommand(int id) { return; } + // Process extension menu items. + if (id >= IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST && + id <= IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST) { + ExtensionMenuManager* manager = + profile_->GetExtensionsService()->menu_manager(); + std::map<int, int>::const_iterator i = extension_item_map_.find(id); + if (i != extension_item_map_.end()) { + manager->ExecuteCommand(profile_, source_tab_contents_, params_, + i->second); + } + return; + } + + switch (id) { case IDS_CONTENT_CONTEXT_OPENLINKNEWTAB: OpenURL(params_.link_url, @@ -942,6 +1177,10 @@ bool RenderViewContextMenu::IsDevCommandEnabled(int id) const { return true; } +string16 RenderViewContextMenu::PrintableSelectionText() { + return WideToUTF16(l10n_util::TruncateString(params_.selection_text, 50)); +} + // Controller functions -------------------------------------------------------- void RenderViewContextMenu::OpenURL( diff --git a/chrome/browser/tab_contents/render_view_context_menu.h b/chrome/browser/tab_contents/render_view_context_menu.h index c06af95..aa90ca9 100644 --- a/chrome/browser/tab_contents/render_view_context_menu.h +++ b/chrome/browser/tab_contents/render_view_context_menu.h @@ -1,15 +1,20 @@ -// Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. +// Copyright (c) 2010 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #ifndef CHROME_BROWSER_TAB_CONTENTS_RENDER_VIEW_CONTEXT_MENU_H_ #define CHROME_BROWSER_TAB_CONTENTS_RENDER_VIEW_CONTEXT_MENU_H_ +#include <map> +#include <string> +#include <vector> + #include "base/string16.h" #include "chrome/common/page_transition_types.h" #include "webkit/glue/context_menu.h" #include "webkit/glue/window_open_disposition.h" +class ExtensionMenuItem; class Profile; class TabContents; @@ -89,6 +94,28 @@ class RenderViewContextMenu { void AppendCopyItem(); void AppendEditableItems(); void AppendSearchProvider(); + void AppendAllExtensionItems(); + + // When extensions have more than 1 top-level item or a single parent item + // with children, we will start a sub menu. In the case of 1 parent with + // children, we will remove the parent from |items| and insert the children + // into it. The |index| parameter is incremented if we start a submenu. This + // returns true if a submenu was started. If we had multiple top-level items + // that needed to be pushed into a submenu, we'll use |extension_name| as the + // title. + bool MaybeStartExtensionSubMenu(const string16& selection_text, + const std::string& extension_name, + std::vector<const ExtensionMenuItem*>* items, + int* index); + + // Fills in |items| with matching items for extension with |extension_id|. + void GetItemsForExtension(const std::string& extension_id, + std::vector<const ExtensionMenuItem*>* items); + + // This is a helper function to append items for one particular extension. + // The |index| parameter is used for assigning id's, and is incremented for + // each item actually added. + void AppendExtensionItems(const std::string& extension_id, int* index); // Opens the specified URL string in a new tab. If |in_current_window| is // false, a new window is created to hold the new tab. @@ -110,10 +137,21 @@ class RenderViewContextMenu { bool IsDevCommandEnabled(int id) const; + // Returns a (possibly truncated) version of the current selection text + // suitable or putting in the title of a menu item. + string16 PrintableSelectionText(); + + // Attempts to get an ExtensionMenuItem given the id of a context menu item. + ExtensionMenuItem* GetExtensionMenuItem(int id) const; + // The destination URL to use if the user tries to search for or navigate to // a text selection. GURL selection_navigation_url_; + // Maps the id from a context menu item to the ExtensionMenuItem's internal + // id. + std::map<int, int> extension_item_map_; + DISALLOW_COPY_AND_ASSIGN(RenderViewContextMenu); }; |