diff options
author | asargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-05-12 20:20:15 +0000 |
---|---|---|
committer | asargent@chromium.org <asargent@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2010-05-12 20:20:15 +0000 |
commit | 66dbfb2c72ec4d4884255777c52c7db55ae634d8 (patch) | |
tree | b6560104e68fb388df746f1e357b5ac0bdc627b4 | |
parent | 44af8775568b168c5288db91cfd429a86b40684f (diff) | |
download | chromium_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
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); + |