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 | |
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
17 files changed, 1484 insertions, 12 deletions
diff --git a/chrome/app/chrome_dll_resource.h b/chrome/app/chrome_dll_resource.h index 1d5caff..708eaa2 100644 --- a/chrome/app/chrome_dll_resource.h +++ b/chrome/app/chrome_dll_resource.h @@ -273,3 +273,8 @@ // Custom context menu entries #define IDC_CONTENT_CONTEXT_CUSTOM_FIRST 47000 #define IDC_CONTENT_CONTEXT_CUSTOM_LAST 48000 + +// Context menu items added by extensions. +#define IDC_EXTENSIONS_CONTEXT_CUSTOM_FIRST 49000 +#define IDC_EXTENSIONS_CONTEXT_CUSTOM_LAST 50000 + 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); }; diff --git a/chrome/chrome_browser.gypi b/chrome/chrome_browser.gypi index bebf2c8..56eaafa 100755 --- a/chrome/chrome_browser.gypi +++ b/chrome/chrome_browser.gypi @@ -920,6 +920,8 @@ 'browser/extensions/extension_browser_actions_api.h', 'browser/extensions/extension_clipboard_api.cc', 'browser/extensions/extension_clipboard_api.h', + 'browser/extensions/extension_context_menu_api.cc', + 'browser/extensions/extension_context_menu_api.h', 'browser/extensions/extension_context_menu_model.cc', 'browser/extensions/extension_context_menu_model.h', 'browser/extensions/extension_creator.cc', @@ -966,6 +968,8 @@ 'browser/extensions/extension_infobar_delegate.h', 'browser/extensions/extension_install_ui.cc', 'browser/extensions/extension_install_ui.h', + 'browser/extensions/extension_menu_manager.cc', + 'browser/extensions/extension_menu_manager.h', 'browser/extensions/extension_message_service.cc', 'browser/extensions/extension_message_service.h', 'browser/extensions/extension_metrics_module.cc', diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi index 5ba812b..69681d7 100644 --- a/chrome/chrome_tests.gypi +++ b/chrome/chrome_tests.gypi @@ -738,6 +738,7 @@ 'browser/download/save_package_unittest.cc', 'browser/encoding_menu_controller_unittest.cc', 'browser/extensions/convert_user_script_unittest.cc', + 'browser/extensions/extension_menu_manager_unittest.cc', 'browser/extensions/extension_messages_unittest.cc', 'browser/extensions/extension_process_manager_unittest.cc', 'browser/extensions/extension_ui_unittest.cc', diff --git a/chrome/common/extensions/api/extension_api.json b/chrome/common/extensions/api/extension_api.json index b354018..ca67e5a 100755 --- a/chrome/common/extensions/api/extension_api.json +++ b/chrome/common/extensions/api/extension_api.json @@ -2354,6 +2354,95 @@ "events": [] }, { + "namespace": "experimental.contextMenu", + "types": [], + "functions": [ + { + "name": "create", + "type": "function", + "description": "An API to add items to context menus.", + "parameters": [ + { + "type": "object", + "name": "createProperties", + "properties": { + "type": { + "type": "string", + "optional": true, + "description": "The type of menu item - one of 'NORMAL', 'CHECKBOX', 'RADIO', or 'SEPARATOR'. Defaults to 'NORMAL' if not specified." + }, + "title": { + "type": "string", + "optional": "true", + "description": "This must be specified unless type is 'SEPARATOR'." + }, + "checked": { + "type": "boolean", + "optional": true, + "description": " For items of type CHECKBOX or RADIO, should this be selected (RADIO) or checked (CHECKBOX)? Only one RADIO item can be selected at a time in a given group of RADIO items, with the last one to have checked == true winning." + }, + "contexts": { + "type": "array", + "items": {"type": "string"}, + "minItems": 1, + "optional": true, + "description": "List of contexts this menu item will appear in. Legal values are: 'ALL', 'PAGE', 'SELECTION', 'LINK', 'EDITABLE', 'IMAGE', 'VIDEO', and 'AUDIO'. Defaults to ['PAGE']." + }, + "enabledContexts": { + "type": "array", + "optional": true, + "items": {"type": "string"}, + "minItems": 1, + "description": "By default the values you pass for the contexts parameter make an item both shown and selectable in those contexts. If you want to limit the contexts where an item is selectable (i.e. not greyed out), you put the ones you want selectable in enabledContexts and any not listed will be shown but disabled. So, for example, if you wanted an item to appear for links and images but only be enabled for links, you would set 'contexts' : ['LINK', 'IMAGE'] and 'enabledContexts' : ['LINK']." + }, + "onclick": { + "type": "function", + "optional": true, + "description": "Function to be called back when your menu item or one of its children is clicked." + }, + "parentId": { + "type": "integer", + "optional": true, + "description": "The id of a parent menu item - this makes the item a child of a previously added item." + } + } + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [ + { + "name": "menuItemId", + "type": "integer", + "description": "The id of the newly created context menu item." + } + ] + } + ] + }, + { + "name": "remove", + "type": "function", + "description": "Remove a context menu item.", + "parameters": [ + { + "type": "integer", + "name": "menuItemId", + "description": "The id of the context menu item to remove." + }, + { + "type": "function", + "name": "callback", + "optional": true, + "parameters": [], + "description": "Called when the context menu has been removed." + } + ] + } + ] + }, + { "namespace": "experimental.metrics", "nodoc": true, "types": [ diff --git a/chrome/renderer/resources/extension_process_bindings.js b/chrome/renderer/resources/extension_process_bindings.js index e91fe27..7e803b5 100644 --- a/chrome/renderer/resources/extension_process_bindings.js +++ b/chrome/renderer/resources/extension_process_bindings.js @@ -80,6 +80,10 @@ var chrome = chrome || {}; }; } + if (request.customCallback) { + request.customCallback(name, request, response); + } + if (request.callback) { // Callbacks currently only support one callback argument. var callbackArgs = response ? [JSON.parse(response)] : []; @@ -165,8 +169,11 @@ var chrome = chrome || {}; } // Send an API request and optionally register a callback. - function sendRequest(functionName, args, argSchemas) { + function sendRequest(functionName, args, argSchemas, customCallback) { var request = prepareRequest(args, argSchemas); + if (customCallback) { + request.customCallback = customCallback; + } // JSON.stringify doesn't support a root object which is undefined. if (request.args === undefined) request.args = null; @@ -187,8 +194,8 @@ var chrome = chrome || {}; } var requestId = GetNextRequestId(); requests[requestId] = request; - return StartRequest(functionName, sargs, requestId, - request.callback ? true : false); + var hasCallback = (request.callback || customCallback) ? true : false; + return StartRequest(functionName, sargs, requestId, hasCallback); } // Send a special API request that is not JSON stringifiable, and optionally @@ -289,6 +296,25 @@ var chrome = chrome || {}; new chrome.Event("experimental.popup.onClosed." + renderViewId); } + function setupHiddenContextMenuEvent(extensionId) { + var eventName = "contextMenu/" + extensionId; + chromeHidden.contextMenuEvent = new chrome.Event(eventName); + chromeHidden.contextMenuHandlers = {}; + chromeHidden.contextMenuEvent.addListener(function() { + var menuItemId = arguments[0].menuItemId; + var onclick = chromeHidden.contextMenuHandlers[menuItemId]; + if (onclick) { + onclick.apply(onclick, arguments); + } + + var parentMenuItemId = arguments[0].parentMenuItemId; + var parentOnclick = chromeHidden.contextMenuHandlers[parentMenuItemId]; + if (parentOnclick) { + parentOnclick.apply(parentOnclick, arguments); + } + }); + } + chromeHidden.onLoad.addListener(function (extensionId) { chrome.initExtension(extensionId, false); @@ -341,7 +367,8 @@ var chrome = chrome || {}; retval = this.handleRequest.apply(this, arguments); } else { retval = sendRequest(this.name, arguments, - this.definition.parameters); + this.definition.parameters, + this.customCallback); } // Validate return value if defined - only in debug. @@ -549,6 +576,28 @@ var chrome = chrome || {}; details, this.name, this.definition.parameters, "page action"); }; + apiFunctions["experimental.contextMenu.create"].customCallback = + function(name, request, response) { + if (chrome.extension.lastError || !response) { + return; + } + + // Set up the onclick handler if we were passed one in the request. + if (request.args.onclick) { + var menuItemId = JSON.parse(response); + chromeHidden.contextMenuHandlers[menuItemId] = request.args.onclick; + } + }; + + apiFunctions["experimental.contextMenu.remove"].customCallback = + function(name, request, response) { + // Remove any onclick handler we had registered for this menu item. + if (request.args.length > 0) { + var menuItemId = request.args[0]; + delete chromeHidden.contextMenuHandlers[menuItemId]; + } + } + if (chrome.test) { chrome.test.getApiDefinitions = GetExtensionAPIDefinition; } @@ -557,6 +606,7 @@ var chrome = chrome || {}; setupPageActionEvents(extensionId); setupToolstripEvents(GetRenderViewId()); setupPopupEvents(GetRenderViewId()); + setupHiddenContextMenuEvent(extensionId); }); if (!chrome.experimental) diff --git a/chrome/renderer/resources/renderer_extension_bindings.js b/chrome/renderer/resources/renderer_extension_bindings.js index 17c09f1..5138e93 100644 --- a/chrome/renderer/resources/renderer_extension_bindings.js +++ b/chrome/renderer/resources/renderer_extension_bindings.js @@ -247,6 +247,7 @@ var chrome = chrome || {}; "experimental.accessibility", "experimental.bookmarkManager", "experimental.clipboard", + "experimental.contextMenu", "experimental.extension", "experimental.idle", "experimental.history", |