summaryrefslogtreecommitdiffstats
path: root/chrome/browser
diff options
context:
space:
mode:
authorasargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-03-23 06:52:41 +0000
committerasargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-03-23 06:52:41 +0000
commit2e3b520ff4318342b027ea8aa303e5981ea9ce79 (patch)
treef2eba7cf4a7d6179033d9cc0f4bba5981984ec55 /chrome/browser
parentd11a34ba95bc98cf2c4b6caa2ce6ed469ae229ce (diff)
downloadchromium_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')
-rw-r--r--chrome/browser/extensions/extension_context_menu_api.cc149
-rw-r--r--chrome/browser/extensions/extension_context_menu_api.h22
-rw-r--r--chrome/browser/extensions/extension_function.h6
-rw-r--r--chrome/browser/extensions/extension_function_dispatcher.cc5
-rw-r--r--chrome/browser/extensions/extension_menu_manager.cc339
-rw-r--r--chrome/browser/extensions/extension_menu_manager.h221
-rw-r--r--chrome/browser/extensions/extension_menu_manager_unittest.cc295
-rw-r--r--chrome/browser/extensions/extension_message_service.h7
-rw-r--r--chrome/browser/extensions/extensions_service.h9
-rw-r--r--chrome/browser/tab_contents/render_view_context_menu.cc245
-rw-r--r--chrome/browser/tab_contents/render_view_context_menu.h40
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 &quota_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);
};