summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorasargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-05-12 20:20:15 +0000
committerasargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98>2010-05-12 20:20:15 +0000
commit66dbfb2c72ec4d4884255777c52c7db55ae634d8 (patch)
treeb6560104e68fb388df746f1e357b5ac0bdc627b4
parent44af8775568b168c5288db91cfd429a86b40684f (diff)
downloadchromium_src-66dbfb2c72ec4d4884255777c52c7db55ae634d8.zip
chromium_src-66dbfb2c72ec4d4884255777c52c7db55ae634d8.tar.gz
chromium_src-66dbfb2c72ec4d4884255777c52c7db55ae634d8.tar.bz2
Add update and removeAll functions to extensions context menu API
BUG=39505 TEST=Should be able to add a bunch of context menu items and then change or remove them using update and removeAll respectively. Review URL: http://codereview.chromium.org/1736028 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@47060 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r--chrome/browser/extensions/extension_context_menu_api.cc260
-rw-r--r--chrome/browser/extensions/extension_context_menu_api.h52
-rw-r--r--chrome/browser/extensions/extension_context_menu_apitest.cc16
-rw-r--r--chrome/browser/extensions/extension_function_dispatcher.cc2
-rw-r--r--chrome/browser/extensions/extension_menu_manager.cc158
-rw-r--r--chrome/browser/extensions/extension_menu_manager.h42
-rw-r--r--chrome/browser/extensions/extension_menu_manager_unittest.cc118
-rw-r--r--chrome/chrome_tests.gypi1
-rw-r--r--chrome/common/extensions/api/extension_api.json83
-rw-r--r--chrome/renderer/resources/extension_apitest.js26
-rw-r--r--chrome/test/data/extensions/api_test/context_menus/manifest.json7
-rw-r--r--chrome/test/data/extensions/api_test/context_menus/test.html1
-rw-r--r--chrome/test/data/extensions/api_test/context_menus/test.js74
13 files changed, 743 insertions, 97 deletions
diff --git a/chrome/browser/extensions/extension_context_menu_api.cc b/chrome/browser/extensions/extension_context_menu_api.cc
index ef004f5..08afa51 100644
--- a/chrome/browser/extensions/extension_context_menu_api.cc
+++ b/chrome/browser/extensions/extension_context_menu_api.cc
@@ -4,7 +4,7 @@
#include "chrome/browser/extensions/extension_context_menu_api.h"
-#include "chrome/browser/extensions/extension_menu_manager.h"
+#include "base/values.h"
#include "chrome/browser/extensions/extensions_service.h"
#include "chrome/browser/profile.h"
@@ -22,11 +22,12 @@ const char kCheckedError[] =
const char kParentsMustBeNormalError[] =
"Parent items must have type NORMAL";
-bool ParseContexts(const DictionaryValue* properties,
- const std::wstring& key,
- ExtensionMenuItem::ContextList* result) {
+bool ExtensionContextMenuFunction::ParseContexts(
+ const DictionaryValue& properties,
+ const wchar_t* key,
+ ExtensionMenuItem::ContextList* result) {
ListValue* list = NULL;
- if (!properties->GetList(key, &list)) {
+ if (!properties.GetList(key, &list)) {
return true;
}
ExtensionMenuItem::ContextList tmp_result;
@@ -36,73 +37,137 @@ bool ParseContexts(const DictionaryValue* properties,
if (!list->GetString(i, &value))
return false;
- if (value == "ALL")
+ if (value == "ALL") {
tmp_result.Add(ExtensionMenuItem::ALL);
- else if (value == "PAGE")
+ } else if (value == "PAGE") {
tmp_result.Add(ExtensionMenuItem::PAGE);
- else if (value == "SELECTION")
+ } else if (value == "SELECTION") {
tmp_result.Add(ExtensionMenuItem::SELECTION);
- else if (value == "LINK")
+ } else if (value == "LINK") {
tmp_result.Add(ExtensionMenuItem::LINK);
- else if (value == "EDITABLE")
+ } else if (value == "EDITABLE") {
tmp_result.Add(ExtensionMenuItem::EDITABLE);
- else if (value == "IMAGE")
+ } else if (value == "IMAGE") {
tmp_result.Add(ExtensionMenuItem::IMAGE);
- else if (value == "VIDEO")
+ } else if (value == "VIDEO") {
tmp_result.Add(ExtensionMenuItem::VIDEO);
- else if (value == "AUDIO")
+ } else if (value == "AUDIO") {
tmp_result.Add(ExtensionMenuItem::AUDIO);
- else
+ } else {
+ error_ = "Invalid value for " + WideToASCII(key);
return false;
+ }
}
*result = tmp_result;
return true;
}
+bool ExtensionContextMenuFunction::ParseType(
+ const DictionaryValue& properties,
+ const ExtensionMenuItem::Type& default_value,
+ ExtensionMenuItem::Type* result) {
+ DCHECK(result);
+ if (!properties.HasKey(kTypeKey)) {
+ *result = default_value;
+ return true;
+ }
+
+ std::string type_string;
+ if (!properties.GetString(kTypeKey, &type_string))
+ return false;
+
+ if (type_string == "NORMAL") {
+ *result = ExtensionMenuItem::NORMAL;
+ } else if (type_string == "CHECKBOX") {
+ *result = ExtensionMenuItem::CHECKBOX;
+ } else if (type_string == "RADIO") {
+ *result = ExtensionMenuItem::RADIO;
+ } else if (type_string == "SEPARATOR") {
+ *result = ExtensionMenuItem::SEPARATOR;
+ } else {
+ error_ = "Invalid type string '" + type_string + "'";
+ return false;
+ }
+ return true;
+}
+
+bool ExtensionContextMenuFunction::ParseChecked(
+ ExtensionMenuItem::Type type,
+ const DictionaryValue& properties,
+ bool default_value,
+ bool* checked) {
+ if (!properties.HasKey(kCheckedKey)) {
+ *checked = default_value;
+ return true;
+ }
+ if (!properties.GetBoolean(kCheckedKey, checked))
+ return false;
+ if (checked && type != ExtensionMenuItem::CHECKBOX &&
+ type != ExtensionMenuItem::RADIO) {
+ error_ = "Only CHECKBOX and RADIO type items can be checked";
+ return false;
+ }
+ return true;
+}
+
+bool ExtensionContextMenuFunction::GetParent(
+ const DictionaryValue& properties,
+ const ExtensionMenuManager& manager,
+ ExtensionMenuItem** result) {
+ if (!properties.HasKey(kParentIdKey))
+ return true;
+ int parent_id = 0;
+ if (properties.HasKey(kParentIdKey) &&
+ !properties.GetInteger(kParentIdKey, &parent_id))
+ return false;
+
+ ExtensionMenuItem* parent = manager.GetItemById(parent_id);
+ if (!parent) {
+ error_ = "Cannot find menu item with id " + IntToString(parent_id);
+ return false;
+ }
+ if (parent->type() != ExtensionMenuItem::NORMAL) {
+ error_ = kParentsMustBeNormalError;
+ return false;
+ }
+ *result = parent;
+ return true;
+}
+
bool CreateContextMenuFunction::RunImpl() {
EXTENSION_FUNCTION_VALIDATE(args_->IsType(Value::TYPE_DICTIONARY));
const DictionaryValue* properties = args_as_dictionary();
+ EXTENSION_FUNCTION_VALIDATE(properties != NULL);
+
std::string title;
- if (properties->HasKey(kTitleKey))
- EXTENSION_FUNCTION_VALIDATE(properties->GetString(kTitleKey, &title));
+ if (properties->HasKey(kTitleKey) &&
+ !properties->GetString(kTitleKey, &title))
+ return false;
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));
+ if (!ParseContexts(*properties, kContextsKey, &contexts))
+ return false;
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 (!ParseContexts(*properties, kEnabledContextsKey, &enabled_contexts))
+ return false;
- if (title.empty() && type != ExtensionMenuItem::SEPARATOR)
- EXTENSION_FUNCTION_ERROR(kTitleNeededError);
+ ExtensionMenuItem::Type type;
+ if (!ParseType(*properties, ExtensionMenuItem::NORMAL, &type))
+ return false;
- 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);
+ if (title.empty() && type != ExtensionMenuItem::SEPARATOR) {
+ error_ = kTitleNeededError;
+ return false;
}
+ bool checked;
+ if (!ParseChecked(type, *properties, false, &checked))
+ return false;
+
scoped_ptr<ExtensionMenuItem> item(
new ExtensionMenuItem(extension_id(), title, checked, type, contexts,
enabled_contexts));
@@ -110,20 +175,24 @@ bool CreateContextMenuFunction::RunImpl() {
int id = 0;
if (properties->HasKey(kParentIdKey)) {
int parent_id = 0;
- EXTENSION_FUNCTION_VALIDATE(properties->GetInteger(kParentIdKey,
- &parent_id));
+ if (properties->GetInteger(kParentIdKey, &parent_id))
+ return false;
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);
-
+ if (!parent) {
+ error_ = "Cannot find menu item with id " + IntToString(parent_id);
+ return false;
+ }
+ if (parent->type() != ExtensionMenuItem::NORMAL) {
+ error_ = kParentsMustBeNormalError;
+ return false;
+ }
id = menu_manager->AddChildItem(parent_id, item.release());
} else {
id = menu_manager->AddContextItem(item.release());
}
- EXTENSION_FUNCTION_VALIDATE(id > 0);
+
+ if (id <= 0)
+ return false;
if (has_callback())
result_.reset(Value::CreateIntegerValue(id));
@@ -131,6 +200,78 @@ bool CreateContextMenuFunction::RunImpl() {
return true;
}
+bool UpdateContextMenuFunction::RunImpl() {
+ EXTENSION_FUNCTION_VALIDATE(args_->IsType(Value::TYPE_LIST));
+ const ListValue* args = args_as_list();
+ int item_id = 0;
+ EXTENSION_FUNCTION_VALIDATE(args->GetInteger(0, &item_id));
+
+ ExtensionsService* service = profile()->GetExtensionsService();
+ ExtensionMenuManager* manager = service->menu_manager();
+ ExtensionMenuItem* item = manager->GetItemById(item_id);
+ if (!item || item->extension_id() != extension_id()) {
+ error_ = "Invalid menu item id: " + IntToString(item_id);
+ return false;
+ }
+
+ DictionaryValue *properties = NULL;
+ EXTENSION_FUNCTION_VALIDATE(args->GetDictionary(1, &properties));
+ EXTENSION_FUNCTION_VALIDATE(properties != NULL);
+
+ ExtensionMenuManager* menu_manager =
+ profile()->GetExtensionsService()->menu_manager();
+
+ // Type.
+ ExtensionMenuItem::Type type;
+ if (!ParseType(*properties, item->type(), &type))
+ return false;
+ if (type != item->type())
+ item->set_type(type);
+
+ // Title.
+ std::string title;
+ if (properties->HasKey(kTitleKey) &&
+ !properties->GetString(kTitleKey, &title))
+ return false;
+ if (title.empty() && type != ExtensionMenuItem::SEPARATOR) {
+ error_ = kTitleNeededError;
+ return false;
+ }
+ item->set_title(title);
+
+ // Checked state.
+ bool checked;
+ if (!ParseChecked(item->type(), *properties, item->checked(), &checked))
+ return false;
+ if (checked != item->checked()) {
+ if (!item->SetChecked(checked))
+ return false;
+ }
+
+ // Contexts.
+ ExtensionMenuItem::ContextList contexts(item->contexts());
+ if (!ParseContexts(*properties, kContextsKey, &contexts))
+ return false;
+ if (contexts != item->contexts())
+ item->set_contexts(contexts);
+
+ // Enabled contexts.
+ ExtensionMenuItem::ContextList enabled_contexts(item->contexts());
+ if (!ParseContexts(*properties, kEnabledContextsKey, &enabled_contexts))
+ return false;
+ if (enabled_contexts != item->enabled_contexts())
+ item->set_enabled_contexts(enabled_contexts);
+
+ // Parent id.
+ ExtensionMenuItem* parent = NULL;
+ if (!GetParent(*properties, *menu_manager, &parent))
+ return false;
+ if (parent && !menu_manager->ChangeParent(item->id(), parent->id()))
+ return false;
+
+ return true;
+}
+
bool RemoveContextMenuFunction::RunImpl() {
EXTENSION_FUNCTION_VALIDATE(args_->IsType(Value::TYPE_INTEGER));
int id = 0;
@@ -140,10 +281,17 @@ bool RemoveContextMenuFunction::RunImpl() {
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));
+ if (!item || item->extension_id() != extension_id()) {
+ error_ = StringPrintf("no menu item with id %d is registered", id);
+ return false;
+ }
- EXTENSION_FUNCTION_VALIDATE(manager->RemoveContextMenuItem(id));
+ return manager->RemoveContextMenuItem(id);
+}
+
+bool RemoveAllContextMenusFunction::RunImpl() {
+ ExtensionsService* service = profile()->GetExtensionsService();
+ ExtensionMenuManager* manager = service->menu_manager();
+ manager->RemoveAllContextItems(extension_id());
return true;
}
diff --git a/chrome/browser/extensions/extension_context_menu_api.h b/chrome/browser/extensions/extension_context_menu_api.h
index 71a0353..c9c3380 100644
--- a/chrome/browser/extensions/extension_context_menu_api.h
+++ b/chrome/browser/extensions/extension_context_menu_api.h
@@ -6,17 +6,65 @@
#define CHROME_BROWSER_EXTENSIONS_EXTENSION_CONTEXT_MENU_API_H__
#include "chrome/browser/extensions/extension_function.h"
+#include "chrome/browser/extensions/extension_menu_manager.h"
-class CreateContextMenuFunction : public SyncExtensionFunction {
+class ExtensionContextMenuFunction : public SyncExtensionFunction {
+ public:
+ ~ExtensionContextMenuFunction() {}
+
+ protected:
+ // Helper function to read and parse a list of menu item contexts.
+ bool ParseContexts(const DictionaryValue& properties,
+ const wchar_t* key,
+ ExtensionMenuItem::ContextList* result);
+
+ // Looks in properties for the "type" key, and reads the value in |result|. On
+ // error, returns false and puts an error message into error_. If the key is
+ // not present, |result| is set to |default_value| and the return value is
+ // true.
+ bool ParseType(const DictionaryValue& properties,
+ const ExtensionMenuItem::Type& default_value,
+ ExtensionMenuItem::Type* result);
+
+ // Helper to read and parse the "checked" property.
+ bool ParseChecked(ExtensionMenuItem::Type type,
+ const DictionaryValue& properties,
+ bool default_value,
+ bool* checked);
+
+ // If the parentId key was specified in properties, this will try looking up
+ // an ExtensionMenuItem with that id and set it into |result|. Returns false
+ // on error, with an explanation written into error_. Note that if the
+ // parentId key is not in properties, this will return true and leave |result|
+ // unset. Also, it is considered an error if the item found has a type other
+ // than NORMAL.
+ bool GetParent(const DictionaryValue& properties,
+ const ExtensionMenuManager& manager,
+ ExtensionMenuItem** result);
+};
+
+class CreateContextMenuFunction : public ExtensionContextMenuFunction {
~CreateContextMenuFunction() {}
virtual bool RunImpl();
DECLARE_EXTENSION_FUNCTION_NAME("experimental.contextMenu.create")
};
-class RemoveContextMenuFunction : public SyncExtensionFunction {
+class UpdateContextMenuFunction : public ExtensionContextMenuFunction {
+ ~UpdateContextMenuFunction() {}
+ virtual bool RunImpl();
+ DECLARE_EXTENSION_FUNCTION_NAME("experimental.contextMenu.update")
+};
+
+class RemoveContextMenuFunction : public ExtensionContextMenuFunction {
~RemoveContextMenuFunction() {}
virtual bool RunImpl();
DECLARE_EXTENSION_FUNCTION_NAME("experimental.contextMenu.remove")
};
+class RemoveAllContextMenusFunction : public ExtensionContextMenuFunction {
+ ~RemoveAllContextMenusFunction() {}
+ virtual bool RunImpl();
+ DECLARE_EXTENSION_FUNCTION_NAME("experimental.contextMenu.removeAll")
+};
+
#endif // CHROME_BROWSER_EXTENSIONS_EXTENSION_CONTEXT_MENU_API_H__
diff --git a/chrome/browser/extensions/extension_context_menu_apitest.cc b/chrome/browser/extensions/extension_context_menu_apitest.cc
new file mode 100644
index 0000000..ac8b114
--- /dev/null
+++ b/chrome/browser/extensions/extension_context_menu_apitest.cc
@@ -0,0 +1,16 @@
+// 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 "base/command_line.h"
+#include "chrome/browser/extensions/extension_apitest.h"
+#include "chrome/common/chrome_switches.h"
+
+IN_PROC_BROWSER_TEST_F(ExtensionApiTest, ContextMenus) {
+ // TODO(asargent): Remove when context menu API is no longer experimental
+ // (http://crbug.com/39508).
+ CommandLine::ForCurrentProcess()->AppendSwitch(
+ switches::kEnableExperimentalExtensionApis);
+
+ ASSERT_TRUE(RunExtensionTest("context_menus")) << message_;
+}
diff --git a/chrome/browser/extensions/extension_function_dispatcher.cc b/chrome/browser/extensions/extension_function_dispatcher.cc
index 37080c8..6bfbbbb 100644
--- a/chrome/browser/extensions/extension_function_dispatcher.cc
+++ b/chrome/browser/extensions/extension_function_dispatcher.cc
@@ -215,7 +215,9 @@ void FactoryRegistry::ResetFunctions() {
// Context Menus.
RegisterFunction<CreateContextMenuFunction>();
+ RegisterFunction<UpdateContextMenuFunction>();
RegisterFunction<RemoveContextMenuFunction>();
+ RegisterFunction<RemoveAllContextMenusFunction>();
}
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
index bbf9909..057cec1 100644
--- a/chrome/browser/extensions/extension_menu_manager.cc
+++ b/chrome/browser/extensions/extension_menu_manager.cc
@@ -4,6 +4,8 @@
#include "chrome/browser/extensions/extension_menu_manager.h"
+#include <algorithm>
+
#include "base/logging.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
@@ -39,15 +41,42 @@ ExtensionMenuItem* ExtensionMenuItem::ChildAt(int index) const {
}
bool ExtensionMenuItem::RemoveChild(int child_id) {
+ ExtensionMenuItem* child = ReleaseChild(child_id, true);
+ if (child) {
+ delete child;
+ return true;
+ } else {
+ return false;
+ }
+}
+
+ExtensionMenuItem* ExtensionMenuItem::ReleaseChild(int child_id,
+ bool recursive) {
for (List::iterator i = children_.begin(); i != children_.end(); ++i) {
+ ExtensionMenuItem* child = NULL;
if ((*i)->id() == child_id) {
+ child = i->release();
children_.erase(i);
- return true;
- } else if ((*i)->RemoveChild(child_id)) {
- return true;
+ return child;
+ } else if (recursive) {
+ child = (*i)->ReleaseChild(child_id, recursive);
+ if (child)
+ return child;
}
}
- return false;
+ return NULL;
+}
+
+std::set<int> ExtensionMenuItem::RemoveAllDescendants() {
+ std::set<int> result;
+ for (List::iterator i = children_.begin(); i != children_.end(); ++i) {
+ ExtensionMenuItem* child = i->get();
+ result.insert(child->id());
+ std::set<int> removed = child->RemoveAllDescendants();
+ result.insert(removed.begin(), removed.end());
+ }
+ children_.clear();
+ return result;
}
string16 ExtensionMenuItem::TitleWithReplacement(
@@ -133,31 +162,120 @@ int ExtensionMenuManager::AddChildItem(int parent_id,
return child->id();
}
-bool ExtensionMenuManager::RemoveContextMenuItem(int id) {
- if (items_by_id_.find(id) == items_by_id_.end())
+bool ExtensionMenuManager::DescendantOf(ExtensionMenuItem* item,
+ int ancestor_id) {
+ DCHECK_GT(ancestor_id, 0);
+
+ // Work our way up the tree until we find the ancestor or 0.
+ int id = item->parent_id();
+ while (id > 0) {
+ DCHECK(id != item->id()); // Catch circular graphs.
+ if (id == ancestor_id)
+ return true;
+ ExtensionMenuItem* next = GetItemById(id);
+ if (!next) {
+ NOTREACHED();
+ return false;
+ }
+ id = next->parent_id();
+ }
+ return false;
+}
+
+bool ExtensionMenuManager::ChangeParent(int child_id, int parent_id) {
+ ExtensionMenuItem* child = GetItemById(child_id);
+ ExtensionMenuItem* new_parent = GetItemById(parent_id);
+ if (child_id == parent_id || !child || (!new_parent && parent_id > 0) ||
+ (new_parent && (DescendantOf(new_parent, child_id) ||
+ child->extension_id() != new_parent->extension_id())))
return false;
- MenuItemMap::iterator i;
- for (i = context_items_.begin(); i != context_items_.end(); ++i) {
+ int old_parent_id = child->parent_id();
+ if (old_parent_id > 0) {
+ ExtensionMenuItem* old_parent = GetItemById(old_parent_id);
+ if (!old_parent) {
+ NOTREACHED();
+ return false;
+ }
+ ExtensionMenuItem* taken =
+ old_parent->ReleaseChild(child_id, false /* non-recursive search*/);
+ DCHECK(taken == child);
+ } else {
+ // This is a top-level item, so we need to pull it out of our list of
+ // top-level items.
+ DCHECK_EQ(0, old_parent_id);
+ MenuItemMap::iterator i = context_items_.find(child->extension_id());
+ if (i == context_items_.end()) {
+ NOTREACHED();
+ return false;
+ }
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;
- }
+ ExtensionMenuItem::List::iterator j = std::find(list.begin(), list.end(),
+ child);
+ if (j == list.end()) {
+ NOTREACHED();
+ return false;
+ }
+ j->release();
+ list.erase(j);
+ }
+
+ if (new_parent) {
+ new_parent->AddChild(child);
+ } else {
+ context_items_[child->extension_id()].push_back(
+ linked_ptr<ExtensionMenuItem>(child));
+ child->parent_id_ = 0;
+ }
+ return true;
+}
+
+bool ExtensionMenuManager::RemoveContextMenuItem(int id) {
+ if (!ContainsKey(items_by_id_, id))
+ return false;
+
+ std::string extension_id = GetItemById(id)->extension_id();
+ MenuItemMap::iterator i = context_items_.find(extension_id);
+ if (i == context_items_.end()) {
+ NOTREACHED();
+ return false;
+ }
+
+ 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) {
+void ExtensionMenuManager::RemoveAllContextItems(std::string extension_id) {
+ ExtensionMenuItem::List::iterator i;
+ for (i = context_items_[extension_id].begin();
+ i != context_items_[extension_id].end(); ++i) {
+ ExtensionMenuItem* item = i->get();
+ items_by_id_.erase(item->id());
+
+ // Remove descendants from this item and erase them from the lookup cache.
+ std::set<int> removed_ids = item->RemoveAllDescendants();
+ for (std::set<int>::const_iterator j = removed_ids.begin();
+ j != removed_ids.end(); ++j) {
+ items_by_id_.erase(*j);
+ }
+ }
+ context_items_.erase(extension_id);
+}
+
+ExtensionMenuItem* ExtensionMenuManager::GetItemById(int id) const {
std::map<int, ExtensionMenuItem*>::const_iterator i = items_by_id_.find(id);
if (i != items_by_id_.end())
return i->second;
diff --git a/chrome/browser/extensions/extension_menu_manager.h b/chrome/browser/extensions/extension_menu_manager.h
index 2e927b7f..771bf19 100644
--- a/chrome/browser/extensions/extension_menu_manager.h
+++ b/chrome/browser/extensions/extension_menu_manager.h
@@ -61,6 +61,14 @@ class ExtensionMenuItem {
value_ = other.value_;
}
+ bool operator==(const ContextList& other) const {
+ return value_ == other.value_;
+ }
+
+ bool operator!=(const ContextList& other) const {
+ return !(*this == other);
+ }
+
bool Contains(Context context) const {
return (value_ & context) > 0;
}
@@ -89,12 +97,21 @@ class ExtensionMenuItem {
Type type() const { return type_; }
bool checked() const { return checked_; }
+ // Simple mutator methods.
+ void set_title(std::string new_title) { title_ = new_title; }
+ void set_contexts(ContextList contexts) { contexts_ = contexts; }
+ void set_enabled_contexts(ContextList contexts) { contexts_ = contexts; }
+ void set_type(Type type) { type_ = type; }
+
// 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;
+ // Set the checked state to |checked|. Returns true if successful.
+ bool SetChecked(bool checked);
+
protected:
friend class ExtensionMenuManager;
@@ -107,9 +124,6 @@ class ExtensionMenuItem {
// 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);
@@ -117,6 +131,14 @@ class ExtensionMenuItem {
// found and removed, or false otherwise.
bool RemoveChild(int child_id);
+ // Takes the child item from this parent. The item is returned and the caller
+ // then owns the pointer.
+ ExtensionMenuItem* ReleaseChild(int child_id, bool recursive);
+
+ // Recursively removes all descendant items (children, grandchildren, etc.),
+ // returning the ids of the removed items.
+ std::set<int> RemoveAllDescendants();
+
private:
// The extension that added this item.
std::string extension_id_;
@@ -175,13 +197,22 @@ class ExtensionMenuManager : public NotificationObserver {
// error. Has the side-effect of incrementing the next_item_id_ member.
int AddChildItem(int parent_id, ExtensionMenuItem* child);
+ // Makes existing item with |child_id| a child of the item with |parent_id|.
+ // If the child item was already a child of another parent, this will remove
+ // it from that parent first. It is an error to try and move an item to be a
+ // child of one of its own descendants.
+ bool ChangeParent(int child_id, int parent_id);
+
// 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);
+ // Removes all items for the given extension id.
+ void RemoveAllContextItems(std::string extension_id);
+
// Returns the item with the given |id| or NULL.
- ExtensionMenuItem* GetItemById(int id);
+ ExtensionMenuItem* GetItemById(int id) const;
// Called when a menu item is clicked on by the user.
void ExecuteCommand(Profile* profile, TabContents* tab_contents,
@@ -201,6 +232,9 @@ class ExtensionMenuManager : public NotificationObserver {
// |index| will be set to its index within the containing list.
void GetItemAndIndex(int id, ExtensionMenuItem** item, size_t* index);
+ // Returns true if item is a descendant of an item with id |ancestor_id|.
+ bool DescendantOf(ExtensionMenuItem* item, int ancestor_id);
+
// 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_;
diff --git a/chrome/browser/extensions/extension_menu_manager_unittest.cc b/chrome/browser/extensions/extension_menu_manager_unittest.cc
index bdc282e..fce804a 100644
--- a/chrome/browser/extensions/extension_menu_manager_unittest.cc
+++ b/chrome/browser/extensions/extension_menu_manager_unittest.cc
@@ -148,6 +148,88 @@ TEST_F(ExtensionMenuManagerTest, ChildFunctions) {
ASSERT_EQ(0, item2->child_count());
}
+// Tests changing parents.
+TEST_F(ExtensionMenuManagerTest, ChangeParent) {
+ // First create two items and add them both to the manager.
+ ExtensionMenuItem* item1 = CreateTestItem(NULL);
+ ExtensionMenuItem* item2 = CreateTestItem(NULL);
+
+ int id1 = manager_.AddContextItem(item1);
+ ASSERT_GT(id1, 0);
+ int id2 = manager_.AddContextItem(item2);
+ ASSERT_GT(id2, 0);
+
+ std::vector<const ExtensionMenuItem*> items =
+ manager_.MenuItems(item1->extension_id());
+ ASSERT_EQ(2u, items.size());
+ ASSERT_EQ(item1, items.at(0));
+ ASSERT_EQ(item2, items.at(1));
+
+ // Now create a third item, initially add it as a child of item1, then move
+ // it to be a child of item2.
+ ExtensionMenuItem* item3 = CreateTestItem(NULL);
+
+ int id3 = manager_.AddChildItem(id1, item3);
+ ASSERT_GT(id3, 0);
+ ASSERT_EQ(1, item1->child_count());
+ ASSERT_EQ(item3, item1->ChildAt(0));
+
+ ASSERT_TRUE(manager_.ChangeParent(id3, id2));
+ ASSERT_EQ(0, item1->child_count());
+ ASSERT_EQ(1, item2->child_count());
+ ASSERT_EQ(item3, item2->ChildAt(0));
+
+ // Move item2 to be a child of item1.
+ ASSERT_TRUE(manager_.ChangeParent(id2, id1));
+ ASSERT_EQ(1, item1->child_count());
+ ASSERT_EQ(item2, item1->ChildAt(0));
+ ASSERT_EQ(1, item2->child_count());
+ ASSERT_EQ(item3, item2->ChildAt(0));
+
+ // Since item2 was a top-level item but is no longer, we should only have 1
+ // top-level item.
+ items = manager_.MenuItems(item1->extension_id());
+ ASSERT_EQ(1u, items.size());
+ ASSERT_EQ(item1, items.at(0));
+
+ // Move item3 back to being a child of item1, so it's now a sibling of item2.
+ ASSERT_TRUE(manager_.ChangeParent(id3, id1));
+ ASSERT_EQ(2, item1->child_count());
+ ASSERT_EQ(item2, item1->ChildAt(0));
+ ASSERT_EQ(item3, item1->ChildAt(1));
+
+ // Try switching item3 to be the parent of item1 - this should fail.
+ ASSERT_FALSE(manager_.ChangeParent(id1, id3));
+ ASSERT_EQ(0, item3->child_count());
+ ASSERT_EQ(2, item1->child_count());
+ ASSERT_EQ(item2, item1->ChildAt(0));
+ ASSERT_EQ(item3, item1->ChildAt(1));
+ items = manager_.MenuItems(item1->extension_id());
+ ASSERT_EQ(1u, items.size());
+ ASSERT_EQ(item1, items.at(0));
+
+ // Move item2 to be a top-level item.
+ ASSERT_TRUE(manager_.ChangeParent(id2, 0));
+ items = manager_.MenuItems(item1->extension_id());
+ ASSERT_EQ(2u, items.size());
+ ASSERT_EQ(item1, items.at(0));
+ ASSERT_EQ(item2, items.at(1));
+ ASSERT_EQ(1, item1->child_count());
+ ASSERT_EQ(item3, item1->ChildAt(0));
+
+ // Make sure you can't move a node to be a child of another extension's item.
+ DictionaryValue properties;
+ properties.SetString(L"extension_id", "4444");
+ ExtensionMenuItem* item4 = CreateTestItem(&properties);
+ int id4 = manager_.AddContextItem(item4);
+ ASSERT_GT(id4, 0);
+ ASSERT_FALSE(manager_.ChangeParent(id4, id1));
+ ASSERT_FALSE(manager_.ChangeParent(id1, id4));
+
+ // Make sure you can't make an item be it's own parent.
+ ASSERT_FALSE(manager_.ChangeParent(id1, id1));
+}
+
// Tests that we properly remove an extension's menu item when that extension is
// unloaded.
TEST_F(ExtensionMenuManagerTest, ExtensionUnloadRemovesMenuItems) {
@@ -221,6 +303,42 @@ class MockTestingProfile : public TestingProfile {
DISALLOW_COPY_AND_ASSIGN(MockTestingProfile);
};
+// Tests the RemoveAll functionality.
+TEST_F(ExtensionMenuManagerTest, RemoveAll) {
+ // Try removing all items for an extension id that doesn't have any items.
+ manager_.RemoveAllContextItems("CCCC");
+
+ // Add 2 top-level and one child item for extension id AAAA.
+ DictionaryValue properties;
+ properties.SetString(L"extension_id", "AAAA");
+ ExtensionMenuItem* item1 = CreateTestItem(&properties);
+ ExtensionMenuItem* item2 = CreateTestItem(&properties);
+ ExtensionMenuItem* item3 = CreateTestItem(&properties);
+ int id1 = manager_.AddContextItem(item1);
+ int id2 = manager_.AddContextItem(item2);
+ EXPECT_GT(id1, 0);
+ EXPECT_GT(id2, 0);
+ int id3 = manager_.AddChildItem(id1, item3);
+ EXPECT_GT(id3, 0);
+
+ // Add one top-level item for extension id BBBB.
+ properties.SetString(L"extension_id", "BBBB");
+ ExtensionMenuItem* item4 = CreateTestItem(&properties);
+ manager_.AddContextItem(item4);
+
+ EXPECT_EQ(2u, manager_.MenuItems("AAAA").size());
+ EXPECT_EQ(1u, manager_.MenuItems("BBBB").size());
+
+ // Remove the BBBB item.
+ manager_.RemoveAllContextItems("BBBB");
+ EXPECT_EQ(2u, manager_.MenuItems("AAAA").size());
+ EXPECT_EQ(0u, manager_.MenuItems("BBBB").size());
+
+ // Remove the AAAA items.
+ manager_.RemoveAllContextItems("AAAA");
+ EXPECT_EQ(0u, manager_.MenuItems("AAAA").size());
+}
+
TEST_F(ExtensionMenuManagerTest, ExecuteCommand) {
MessageLoopForUI message_loop;
ChromeThread ui_thread(ChromeThread::UI, &message_loop);
diff --git a/chrome/chrome_tests.gypi b/chrome/chrome_tests.gypi
index 59c0059..c2792ae 100644
--- a/chrome/chrome_tests.gypi
+++ b/chrome/chrome_tests.gypi
@@ -1332,6 +1332,7 @@
'browser/extensions/extension_browsertest.h',
'browser/extensions/extension_browsertests_misc.cc',
'browser/extensions/extension_clipboard_apitest.cc',
+ 'browser/extensions/extension_context_menu_apitest.cc',
'browser/extensions/extension_crash_recovery_browsertest.cc',
'browser/extensions/extension_geolocation_apitest.cc',
'browser/extensions/extension_get_views_apitest.cc',
diff --git a/chrome/common/extensions/api/extension_api.json b/chrome/common/extensions/api/extension_api.json
index 99e07f9..794248a 100644
--- a/chrome/common/extensions/api/extension_api.json
+++ b/chrome/common/extensions/api/extension_api.json
@@ -1503,7 +1503,7 @@
"type": "function",
"description": "Shows an infobar in the specified tab. The infobar will be closed automatically when the tab navigates. Use window.close() to close the infobar before then.",
"parameters": [
- {
+ {
"name": "details",
"type": "object",
"properties": {
@@ -2496,11 +2496,11 @@
{
"name": "create",
"type": "function",
- "description": "An API to add items to context menus.",
+ "description": "An API to add items to context menus.",
"parameters": [
{
"type": "object",
- "name": "createProperties",
+ "name": "createProperties",
"properties": {
"type": {
"type": "string",
@@ -2535,7 +2535,7 @@
"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,
@@ -2558,6 +2558,65 @@
]
},
{
+ "name": "update",
+ "type": "function",
+ "description": "Update a previously created context menu item.",
+ "parameters": [
+ {
+ "type": "integer",
+ "name": "id",
+ "description": "The id of the item to update."
+ },
+ {
+ "type": "object",
+ "name": "updateProperties",
+ "description": "The properties to update. Accepts the same values as the create function.",
+ "properties": {
+ "type": {
+ "type": "string",
+ "optional": true
+ },
+ "title": {
+ "type": "string",
+ "optional": "true"
+ },
+ "checked": {
+ "type": "boolean",
+ "optional": true
+ },
+ "contexts": {
+ "type": "array",
+ "items": {"type": "string"},
+ "minItems": 1,
+ "optional": true
+ },
+ "enabledContexts": {
+ "type": "array",
+ "optional": true,
+ "items": {"type": "string"},
+ "minItems": 1
+ },
+ "onclick": {
+ "type": "function",
+ "optional": true
+ },
+ "parentId": {
+ "type": "integer",
+ "optional": true,
+ "description": "Note: you cannot change an item to be a child of one of it's own descendants."
+ }
+ }
+ },
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": [],
+ "description": "Called when the context menu has been updated."
+ }
+ ]
+ },
+ {
"name": "remove",
"type": "function",
"description": "Remove a context menu item.",
@@ -2575,8 +2634,22 @@
"description": "Called when the context menu has been removed."
}
]
+ },
+ {
+ "name": "removeAll",
+ "type": "function",
+ "description": "Remove all context menu items added by this extension.",
+ "parameters": [
+ {
+ "type": "function",
+ "name": "callback",
+ "optional": true,
+ "parameters": [],
+ "description": "Called when removal is complete."
+ }
+ ]
}
- ]
+ ]
},
{
"namespace": "experimental.metrics",
diff --git a/chrome/renderer/resources/extension_apitest.js b/chrome/renderer/resources/extension_apitest.js
index de3b150..d587f72 100644
--- a/chrome/renderer/resources/extension_apitest.js
+++ b/chrome/renderer/resources/extension_apitest.js
@@ -19,9 +19,15 @@ var chrome = chrome || {};
throw "completed";
}
+ // Helper function to get around the fact that function names in javascript
+ // are read-only, and you can't assign one to anonymous functions.
+ function testName(test) {
+ return test.name || test.generatedName;
+ }
+
chrome.test.fail = function(message) {
if (completed) throw "completed";
- chrome.test.log("( FAILED ) " + currentTest.name);
+ chrome.test.log("( FAILED ) " + testName(currentTest));
var stack = "(no stack available)";
try {
@@ -38,7 +44,7 @@ var chrome = chrome || {};
message = "FAIL (no message)";
}
message += "\n" + stack;
- console.log("[FAIL] " + currentTest.name + ": " + message);
+ console.log("[FAIL] " + testName(currentTest) + ": " + message);
chrome.test.notifyFail(message);
complete();
};
@@ -53,7 +59,7 @@ var chrome = chrome || {};
var pendingCallbacks = 0;
- chrome.test.callbackAdded = function () {
+ chrome.test.callbackAdded = function() {
pendingCallbacks++;
return function() {
@@ -73,18 +79,18 @@ var chrome = chrome || {};
return;
}
try {
- chrome.test.log("( RUN ) " + currentTest.name);
+ chrome.test.log("( RUN ) " + testName(currentTest));
currentTest.call();
} catch (e) {
var message = e.stack || "(no stack available)";
- console.log("[FAIL] " + currentTest.name + ": " + message);
+ console.log("[FAIL] " + testName(currentTest) + ": " + message);
chrome.test.notifyFail(message);
complete();
}
};
chrome.test.succeed = function() {
- console.log("[SUCCESS] " + currentTest.name);
+ console.log("[SUCCESS] " + testName(currentTest));
chrome.test.log("( SUCCESS )");
// Use setTimeout here to allow previous test contexts to be
// eligible for garbage collection.
@@ -106,11 +112,11 @@ var chrome = chrome || {};
chrome.test.assertEq = function(expected, actual) {
if (expected != actual) {
- chrome.test.fail("API Test Error in " + currentTest.name +
+ chrome.test.fail("API Test Error in " + testName(currentTest) +
"\nActual: " + actual + "\nExpected: " + expected);
}
if (typeof(expected) != typeof(actual)) {
- chrome.test.fail("API Test Error in " + currentTest.name +
+ chrome.test.fail("API Test Error in " + testName(currentTest) +
" (type mismatch)\nActual Type: " + typeof(actual) +
"\nExpected Type:" + typeof(expected));
}
@@ -134,9 +140,9 @@ var chrome = chrome || {};
stack = e.stack.toString();
}
var msg = "Exception during execution of callback in " +
- currentTest.name;
+ testName(currentTest);
msg += "\n" + stack;
- console.log("[FAIL] " + currentTest.name + ": " + msg);
+ console.log("[FAIL] " + testName(currentTest) + ": " + msg);
chrome.test.notifyFail(msg);
complete();
}
diff --git a/chrome/test/data/extensions/api_test/context_menus/manifest.json b/chrome/test/data/extensions/api_test/context_menus/manifest.json
new file mode 100644
index 0000000..e678526
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/context_menus/manifest.json
@@ -0,0 +1,7 @@
+{
+ "name": "chrome.extension.contextMenu",
+ "version": "0.1",
+ "description": "end-to-end browser test for chrome.contextMenu API",
+ "background_page": "test.html",
+ "permissions": ["experimental"]
+}
diff --git a/chrome/test/data/extensions/api_test/context_menus/test.html b/chrome/test/data/extensions/api_test/context_menus/test.html
new file mode 100644
index 0000000..46f4d74
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/context_menus/test.html
@@ -0,0 +1 @@
+<script src="test.js"></script>
diff --git a/chrome/test/data/extensions/api_test/context_menus/test.js b/chrome/test/data/extensions/api_test/context_menus/test.js
new file mode 100644
index 0000000..9dbbcbd
--- /dev/null
+++ b/chrome/test/data/extensions/api_test/context_menus/test.js
@@ -0,0 +1,74 @@
+
+if (!chrome.contextMenu) {
+ chrome.contextMenu = chrome.experimental.contextMenu;
+}
+
+var assertNoLastError = chrome.test.assertNoLastError;
+
+var tests = [
+ function simple() {
+ chrome.contextMenu.create({"title":"1"}, chrome.test.callbackPass());
+ },
+
+ function no_properties() {
+ chrome.contextMenu.create({}, function(id) {
+ chrome.test.assertTrue(chrome.extension.lastError != null);
+ chrome.test.succeed();
+ });
+ },
+
+ function remove() {
+ chrome.contextMenu.create({"title":"1"}, function(id) {
+ assertNoLastError();
+ chrome.contextMenu.remove(id, chrome.test.callbackPass());
+ });
+ },
+
+ function update() {
+ chrome.contextMenu.create({"title":"update test"}, function(id) {
+ assertNoLastError();
+ chrome.contextMenu.update(id, {"title": "test2"},
+ chrome.test.callbackPass());
+ });
+ },
+
+ function removeAll() {
+ chrome.contextMenu.create({"title":"1"}, function(id) {
+ assertNoLastError();
+ chrome.contextMenu.create({"title":"2"}, function(id2) {
+ assertNoLastError();
+ chrome.contextMenu.removeAll(chrome.test.callbackPass());
+ });
+ });
+ }
+];
+
+
+// Add tests for creating menu item with various types and contexts.
+var types = ["CHECKBOX", "RADIO", "SEPARATOR"];
+var contexts = ["ALL", "PAGE", "SELECTION", "LINK", "EDITABLE", "IMAGE",
+ "VIDEO", "AUDIO"];
+function makeCreateTest(type, contexts) {
+ var result = function() {
+ var title = type;
+ if (contexts && contexts.length > 0) {
+ title += " " + contexts.join(",");
+ }
+ var properties = {"title": title, "type": type};
+
+ chrome.contextMenu.create(properties, chrome.test.callbackPass());
+ };
+ result.generatedName = "create_" + type +
+ (contexts ? "-" + contexts.join(",") : "");
+ return result;
+}
+
+for (var i in types) {
+ tests.push(makeCreateTest(types[i]));
+}
+for (var i in contexts) {
+ tests.push(makeCreateTest("NORMAL", [ contexts[i] ]));
+}
+
+chrome.test.runTests(tests);
+