summaryrefslogtreecommitdiffstats
path: root/components
diff options
context:
space:
mode:
authorsdefresne <sdefresne@chromium.org>2015-04-17 14:12:18 -0700
committerCommit bot <commit-bot@chromium.org>2015-04-17 21:14:10 +0000
commitc083d1f4de8537ee81131ddb5d388f2f686fac85 (patch)
tree91b5c0d53aa666aa08586d0827d2be4c36d240e5 /components
parent298d7eb01ab6f9c1f8781a31c2fddc13f9e4e4e2 (diff)
downloadchromium_src-c083d1f4de8537ee81131ddb5d388f2f686fac85.zip
chromium_src-c083d1f4de8537ee81131ddb5d388f2f686fac85.tar.gz
chromium_src-c083d1f4de8537ee81131ddb5d388f2f686fac85.tar.bz2
Move undo files into //components/undo
Create a new component //components/undo and move files from //chrome/browser/undo into the component. Add DEPS, OWNERS files and add dependencies on the new component into chrome/browser/BUILD.gn & chrome/chrome_browser.gypi. Files where moved using tools/git/move_source_files.py and the #include lines updated automatically by the script. BUG=476921 Review URL: https://codereview.chromium.org/1090993003 Cr-Commit-Position: refs/heads/master@{#325718}
Diffstat (limited to 'components')
-rw-r--r--components/BUILD.gn2
-rw-r--r--components/OWNERS3
-rw-r--r--components/components.gyp1
-rw-r--r--components/components_tests.gyp6
-rw-r--r--components/undo.gypi34
-rw-r--r--components/undo/BUILD.gn39
-rw-r--r--components/undo/DEPS17
-rw-r--r--components/undo/OWNERS5
-rw-r--r--components/undo/bookmark_renumber_observer.h20
-rw-r--r--components/undo/bookmark_undo_service.cc495
-rw-r--r--components/undo/bookmark_undo_service.h80
-rw-r--r--components/undo/bookmark_undo_service_test.cc421
-rw-r--r--components/undo/bookmark_undo_utils.cc24
-rw-r--r--components/undo/bookmark_undo_utils.h29
-rw-r--r--components/undo/undo_manager.cc224
-rw-r--r--components/undo/undo_manager.h134
-rw-r--r--components/undo/undo_manager_observer.h18
-rw-r--r--components/undo/undo_manager_test.cc265
-rw-r--r--components/undo/undo_operation.h26
19 files changed, 1843 insertions, 0 deletions
diff --git a/components/BUILD.gn b/components/BUILD.gn
index 2b58643..62c2946 100644
--- a/components/BUILD.gn
+++ b/components/BUILD.gn
@@ -95,6 +95,7 @@ group("all_components") {
"//components/translate/core/browser",
"//components/translate/core/common",
"//components/ui/zoom:ui_zoom",
+ "//components/undo",
"//components/update_client",
"//components/url_fixer",
"//components/url_matcher",
@@ -267,6 +268,7 @@ test("components_unittests") {
"//components/omnibox:unit_tests",
"//components/ownership:unit_tests",
"//components/packed_ct_ev_whitelist:unit_tests",
+ "//components/undo:unit_tests",
"//components/update_client:unit_tests",
"//components/variations:unit_tests",
"//components/web_resource:unit_tests",
diff --git a/components/OWNERS b/components/OWNERS
index 869139d..ef674a0 100644
--- a/components/OWNERS
+++ b/components/OWNERS
@@ -256,10 +256,13 @@ per-file translate_strings.grdp=toyoshim@chromium.org
per-file startup_metric_utils.gypi=jeremy@chromium.org
+per-file undo.gypi=sky@chromium.org
+per-file undo.gypi=tom.cassiotis@gmail.com
per-file undo_strings.grdp=sky@chromium.org
per-file undo_strings.grdp=tom.cassiotis@gmail.com
# For refactoring only during componentization of //chrome/browser/undo.
+per-file undo.gypi=sdefresne@chromium.org
per-file undo_strings.grdp=sdefresne@chromium.org
per-file user_manager.gypi=antrim@chromium.org
diff --git a/components/components.gyp b/components/components.gyp
index 67988d1..d6c5e0a8 100644
--- a/components/components.gyp
+++ b/components/components.gyp
@@ -65,6 +65,7 @@
'sync_driver.gypi',
'translate.gypi',
'ui_zoom.gypi',
+ 'undo.gypi',
'update_client.gypi',
'url_fixer.gypi',
'url_matcher.gypi',
diff --git a/components/components_tests.gyp b/components/components_tests.gyp
index ad976a6..39b57a2 100644
--- a/components/components_tests.gyp
+++ b/components/components_tests.gyp
@@ -162,6 +162,10 @@
'favicon/content/content_favicon_driver_unittest.cc',
'favicon/core/favicon_handler_unittest.cc',
],
+ 'undo_unittest_sources': [
+ 'undo/bookmark_undo_service_test.cc',
+ 'undo/undo_manager_test.cc',
+ ],
# Note: GN tests converted to here, need to do the rest.
'audio_modem_unittest_sources': [
@@ -649,6 +653,7 @@
'<@(suggestions_unittest_sources)',
'<@(sync_driver_unittest_sources)',
'<@(translate_unittest_sources)',
+ '<@(undo_unittest_sources)',
'<@(update_client_unittest_sources)',
'<@(url_fixer_unittest_sources)',
'<@(url_matcher_unittest_sources)',
@@ -759,6 +764,7 @@
'components.gyp:translate_core_common',
'components.gyp:translate_core_language_detection',
'components.gyp:ui_zoom',
+ 'components.gyp:undo_component',
'components.gyp:update_client',
'components.gyp:update_client_test_support',
'components.gyp:url_fixer',
diff --git a/components/undo.gypi b/components/undo.gypi
new file mode 100644
index 0000000..7039ace
--- /dev/null
+++ b/components/undo.gypi
@@ -0,0 +1,34 @@
+# Copyright 2015 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.
+
+{
+ 'targets': [
+ {
+ # GN version: //components/undo
+ 'target_name': 'undo_component',
+ 'type': 'static_library',
+ 'include_dirs': [
+ '..',
+ ],
+ 'dependencies': [
+ '../base/base.gyp:base',
+ '../ui/base/ui_base.gyp:ui_base',
+ 'bookmarks_browser',
+ 'components_strings.gyp:components_strings',
+ 'keyed_service_core',
+ ],
+ 'sources': [
+ 'undo/bookmark_renumber_observer.h',
+ 'undo/bookmark_undo_service.cc',
+ 'undo/bookmark_undo_service.h',
+ 'undo/bookmark_undo_utils.cc',
+ 'undo/bookmark_undo_utils.h',
+ 'undo/undo_manager.cc',
+ 'undo/undo_manager.h',
+ 'undo/undo_manager_observer.h',
+ 'undo/undo_operation.h',
+ ],
+ },
+ ],
+}
diff --git a/components/undo/BUILD.gn b/components/undo/BUILD.gn
new file mode 100644
index 0000000..c891cc0
--- /dev/null
+++ b/components/undo/BUILD.gn
@@ -0,0 +1,39 @@
+# Copyright 2015 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.
+
+static_library("undo") {
+ sources = [
+ "bookmark_renumber_observer.h",
+ "bookmark_undo_service.cc",
+ "bookmark_undo_service.h",
+ "bookmark_undo_utils.cc",
+ "bookmark_undo_utils.h",
+ "undo_manager.cc",
+ "undo_manager.h",
+ "undo_manager_observer.h",
+ "undo_operation.h",
+ ]
+
+ deps = [
+ "//base",
+ "//components/bookmarks/browser",
+ "//components/keyed_service/core",
+ "//components/strings",
+ "//ui/base",
+ ]
+}
+
+source_set("unit_tests") {
+ testonly = true
+ sources = [
+ "bookmark_undo_service_test.cc",
+ "undo_manager_test.cc",
+ ]
+
+ deps = [
+ "//components/bookmarks/test",
+ "//components/undo",
+ "//testing/gtest",
+ ]
+}
diff --git a/components/undo/DEPS b/components/undo/DEPS
new file mode 100644
index 0000000..a70c7a8
--- /dev/null
+++ b/components/undo/DEPS
@@ -0,0 +1,17 @@
+include_rules = [
+ # undo component is used on iOS and desktop and thus must not depends on
+ # system-specific directories.
+ "-content",
+ "-ios",
+
+ "+components/bookmarks/browser",
+ "+components/keyed_service/core",
+ "+grit/components_strings.h",
+ "+ui/base",
+]
+
+specific_include_rules = {
+ ".*_test\.cc": [
+ "+components/bookmarks/test",
+ ],
+}
diff --git a/components/undo/OWNERS b/components/undo/OWNERS
new file mode 100644
index 0000000..08fd84c
--- /dev/null
+++ b/components/undo/OWNERS
@@ -0,0 +1,5 @@
+sky@chromium.org
+tom.cassiotis@gmail.com
+
+# For refactoring only during componentization of //chrome/browser/undo.
+sdefresne@chromium.org
diff --git a/components/undo/bookmark_renumber_observer.h b/components/undo/bookmark_renumber_observer.h
new file mode 100644
index 0000000..4dc8df0
--- /dev/null
+++ b/components/undo/bookmark_renumber_observer.h
@@ -0,0 +1,20 @@
+// Copyright 2013 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 COMPONENTS_UNDO_BOOKMARK_RENUMBER_OBSERVER_H_
+#define COMPONENTS_UNDO_BOOKMARK_RENUMBER_OBSERVER_H_
+
+#include "base/basictypes.h"
+
+class BookmarkRenumberObserver {
+ public:
+ // Invoked when a bookmark id has been renumbered so that any
+ // BookmarkUndoOperations that refer to that bookmark can be updated.
+ virtual void OnBookmarkRenumbered(int64 old_id, int64 new_id) = 0;
+
+ protected:
+ virtual ~BookmarkRenumberObserver() {}
+};
+
+#endif // COMPONENTS_UNDO_BOOKMARK_RENUMBER_OBSERVER_H_
diff --git a/components/undo/bookmark_undo_service.cc b/components/undo/bookmark_undo_service.cc
new file mode 100644
index 0000000..695bacb
--- /dev/null
+++ b/components/undo/bookmark_undo_service.cc
@@ -0,0 +1,495 @@
+// Copyright 2013 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 "components/undo/bookmark_undo_service.h"
+
+#include "components/bookmarks/browser/bookmark_model.h"
+#include "components/bookmarks/browser/bookmark_node_data.h"
+#include "components/bookmarks/browser/bookmark_utils.h"
+#include "components/bookmarks/browser/scoped_group_bookmark_actions.h"
+#include "components/undo/bookmark_renumber_observer.h"
+#include "components/undo/undo_operation.h"
+#include "grit/components_strings.h"
+
+using bookmarks::BookmarkModel;
+using bookmarks::BookmarkNode;
+using bookmarks::BookmarkNodeData;
+
+namespace {
+
+// BookmarkUndoOperation ------------------------------------------------------
+
+// Base class for all bookmark related UndoOperations that facilitates access to
+// the BookmarkUndoService.
+class BookmarkUndoOperation : public UndoOperation,
+ public BookmarkRenumberObserver {
+ public:
+ BookmarkUndoOperation(BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer)
+ : bookmark_model_(bookmark_model),
+ undo_renumber_observer_(undo_renumber_observer) {}
+ ~BookmarkUndoOperation() override {}
+
+ BookmarkModel* bookmark_model() { return bookmark_model_; }
+
+ BookmarkRenumberObserver* undo_renumber_observer() {
+ return undo_renumber_observer_;
+ }
+
+ private:
+ BookmarkModel* bookmark_model_;
+ BookmarkRenumberObserver* undo_renumber_observer_;
+};
+
+// BookmarkAddOperation -------------------------------------------------------
+
+// Handles the undo of the insertion of a bookmark or folder.
+class BookmarkAddOperation : public BookmarkUndoOperation {
+ public:
+ BookmarkAddOperation(BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer,
+ const BookmarkNode* parent,
+ int index);
+ ~BookmarkAddOperation() override {}
+
+ // UndoOperation:
+ void Undo() override;
+ int GetUndoLabelId() const override;
+ int GetRedoLabelId() const override;
+
+ // BookmarkRenumberObserver:
+ void OnBookmarkRenumbered(int64 old_id, int64 new_id) override;
+
+ private:
+ int64 parent_id_;
+ const int index_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkAddOperation);
+};
+
+BookmarkAddOperation::BookmarkAddOperation(
+ BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer,
+ const BookmarkNode* parent,
+ int index)
+ : BookmarkUndoOperation(bookmark_model, undo_renumber_observer),
+ parent_id_(parent->id()),
+ index_(index) {
+}
+
+void BookmarkAddOperation::Undo() {
+ BookmarkModel* model = bookmark_model();
+ const BookmarkNode* parent =
+ bookmarks::GetBookmarkNodeByID(model, parent_id_);
+ DCHECK(parent);
+
+ model->Remove(parent, index_);
+}
+
+int BookmarkAddOperation::GetUndoLabelId() const {
+ return IDS_BOOKMARK_BAR_UNDO_ADD;
+}
+
+int BookmarkAddOperation::GetRedoLabelId() const {
+ return IDS_BOOKMARK_BAR_REDO_DELETE;
+}
+
+void BookmarkAddOperation::OnBookmarkRenumbered(int64 old_id, int64 new_id) {
+ if (parent_id_ == old_id)
+ parent_id_ = new_id;
+}
+
+// BookmarkRemoveOperation ----------------------------------------------------
+
+// Handles the undo of the deletion of a bookmark node. For a bookmark folder,
+// the information for all descendant bookmark nodes is maintained.
+//
+// The BookmarkModel allows only single bookmark node to be removed.
+class BookmarkRemoveOperation : public BookmarkUndoOperation {
+ public:
+ BookmarkRemoveOperation(BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer,
+ const BookmarkNode* parent,
+ int old_index,
+ const BookmarkNode* node);
+ ~BookmarkRemoveOperation() override {}
+
+ // UndoOperation:
+ void Undo() override;
+ int GetUndoLabelId() const override;
+ int GetRedoLabelId() const override;
+
+ // BookmarkRenumberObserver:
+ void OnBookmarkRenumbered(int64 old_id, int64 new_id) override;
+
+ private:
+ void UpdateBookmarkIds(const BookmarkNodeData::Element& element,
+ const BookmarkNode* parent,
+ int index_added_at);
+
+ int64 parent_id_;
+ const int old_index_;
+ BookmarkNodeData removed_node_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkRemoveOperation);
+};
+
+BookmarkRemoveOperation::BookmarkRemoveOperation(
+ BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer,
+ const BookmarkNode* parent,
+ int old_index,
+ const BookmarkNode* node)
+ : BookmarkUndoOperation(bookmark_model, undo_renumber_observer),
+ parent_id_(parent->id()),
+ old_index_(old_index),
+ removed_node_(node) {
+}
+
+void BookmarkRemoveOperation::Undo() {
+ DCHECK(removed_node_.is_valid());
+ BookmarkModel* model = bookmark_model();
+ const BookmarkNode* parent =
+ bookmarks::GetBookmarkNodeByID(model, parent_id_);
+ DCHECK(parent);
+
+ bookmarks::CloneBookmarkNode(
+ model, removed_node_.elements, parent, old_index_, false);
+ UpdateBookmarkIds(removed_node_.elements[0], parent, old_index_);
+}
+
+int BookmarkRemoveOperation::GetUndoLabelId() const {
+ return IDS_BOOKMARK_BAR_UNDO_DELETE;
+}
+
+int BookmarkRemoveOperation::GetRedoLabelId() const {
+ return IDS_BOOKMARK_BAR_REDO_ADD;
+}
+
+void BookmarkRemoveOperation::UpdateBookmarkIds(
+ const BookmarkNodeData::Element& element,
+ const BookmarkNode* parent,
+ int index_added_at) {
+ const BookmarkNode* node = parent->GetChild(index_added_at);
+ if (element.id() != node->id())
+ undo_renumber_observer()->OnBookmarkRenumbered(element.id(), node->id());
+ if (!element.is_url) {
+ for (int i = 0; i < static_cast<int>(element.children.size()); ++i)
+ UpdateBookmarkIds(element.children[i], node, 0);
+ }
+}
+
+void BookmarkRemoveOperation::OnBookmarkRenumbered(int64 old_id, int64 new_id) {
+ if (parent_id_ == old_id)
+ parent_id_ = new_id;
+}
+
+// BookmarkEditOperation ------------------------------------------------------
+
+// Handles the undo of the modification of a bookmark node.
+class BookmarkEditOperation : public BookmarkUndoOperation {
+ public:
+ BookmarkEditOperation(BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer,
+ const BookmarkNode* node);
+ ~BookmarkEditOperation() override {}
+
+ // UndoOperation:
+ void Undo() override;
+ int GetUndoLabelId() const override;
+ int GetRedoLabelId() const override;
+
+ // BookmarkRenumberObserver:
+ void OnBookmarkRenumbered(int64 old_id, int64 new_id) override;
+
+ private:
+ int64 node_id_;
+ BookmarkNodeData original_bookmark_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkEditOperation);
+};
+
+BookmarkEditOperation::BookmarkEditOperation(
+ BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer,
+ const BookmarkNode* node)
+ : BookmarkUndoOperation(bookmark_model, undo_renumber_observer),
+ node_id_(node->id()),
+ original_bookmark_(node) {
+}
+
+void BookmarkEditOperation::Undo() {
+ DCHECK(original_bookmark_.is_valid());
+ BookmarkModel* model = bookmark_model();
+ const BookmarkNode* node = bookmarks::GetBookmarkNodeByID(model, node_id_);
+ DCHECK(node);
+
+ model->SetTitle(node, original_bookmark_.elements[0].title);
+ if (original_bookmark_.elements[0].is_url)
+ model->SetURL(node, original_bookmark_.elements[0].url);
+}
+
+int BookmarkEditOperation::GetUndoLabelId() const {
+ return IDS_BOOKMARK_BAR_UNDO_EDIT;
+}
+
+int BookmarkEditOperation::GetRedoLabelId() const {
+ return IDS_BOOKMARK_BAR_REDO_EDIT;
+}
+
+void BookmarkEditOperation::OnBookmarkRenumbered(int64 old_id, int64 new_id) {
+ if (node_id_ == old_id)
+ node_id_ = new_id;
+}
+
+// BookmarkMoveOperation ------------------------------------------------------
+
+// Handles the undo of a bookmark being moved to a new location.
+class BookmarkMoveOperation : public BookmarkUndoOperation {
+ public:
+ BookmarkMoveOperation(BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index);
+ ~BookmarkMoveOperation() override {}
+ int GetUndoLabelId() const override;
+ int GetRedoLabelId() const override;
+
+ // UndoOperation:
+ void Undo() override;
+
+ // BookmarkRenumberObserver:
+ void OnBookmarkRenumbered(int64 old_id, int64 new_id) override;
+
+ private:
+ int64 old_parent_id_;
+ int64 new_parent_id_;
+ int old_index_;
+ int new_index_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkMoveOperation);
+};
+
+BookmarkMoveOperation::BookmarkMoveOperation(
+ BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index)
+ : BookmarkUndoOperation(bookmark_model, undo_renumber_observer),
+ old_parent_id_(old_parent->id()),
+ new_parent_id_(new_parent->id()),
+ old_index_(old_index),
+ new_index_(new_index) {
+}
+
+void BookmarkMoveOperation::Undo() {
+ BookmarkModel* model = bookmark_model();
+ const BookmarkNode* old_parent =
+ bookmarks::GetBookmarkNodeByID(model, old_parent_id_);
+ const BookmarkNode* new_parent =
+ bookmarks::GetBookmarkNodeByID(model, new_parent_id_);
+ DCHECK(old_parent);
+ DCHECK(new_parent);
+
+ const BookmarkNode* node = new_parent->GetChild(new_index_);
+ int destination_index = old_index_;
+
+ // If the bookmark was moved up within the same parent then the destination
+ // index needs to be incremented since the old index did not account for the
+ // moved bookmark.
+ if (old_parent == new_parent && new_index_ < old_index_)
+ ++destination_index;
+
+ model->Move(node, old_parent, destination_index);
+}
+
+int BookmarkMoveOperation::GetUndoLabelId() const {
+ return IDS_BOOKMARK_BAR_UNDO_MOVE;
+}
+
+int BookmarkMoveOperation::GetRedoLabelId() const {
+ return IDS_BOOKMARK_BAR_REDO_MOVE;
+}
+
+void BookmarkMoveOperation::OnBookmarkRenumbered(int64 old_id, int64 new_id) {
+ if (old_parent_id_ == old_id)
+ old_parent_id_ = new_id;
+ if (new_parent_id_ == old_id)
+ new_parent_id_ = new_id;
+}
+
+// BookmarkReorderOperation ---------------------------------------------------
+
+// Handle the undo of reordering of bookmarks that can happen as a result of
+// sorting a bookmark folder by name or the undo of that operation. The change
+// of order is not recursive so only the order of the immediate children of the
+// folder need to be restored.
+class BookmarkReorderOperation : public BookmarkUndoOperation {
+ public:
+ BookmarkReorderOperation(BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer,
+ const BookmarkNode* parent);
+ ~BookmarkReorderOperation() override;
+
+ // UndoOperation:
+ void Undo() override;
+ int GetUndoLabelId() const override;
+ int GetRedoLabelId() const override;
+
+ // BookmarkRenumberObserver:
+ void OnBookmarkRenumbered(int64 old_id, int64 new_id) override;
+
+ private:
+ int64 parent_id_;
+ std::vector<int64> ordered_bookmarks_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkReorderOperation);
+};
+
+BookmarkReorderOperation::BookmarkReorderOperation(
+ BookmarkModel* bookmark_model,
+ BookmarkRenumberObserver* undo_renumber_observer,
+ const BookmarkNode* parent)
+ : BookmarkUndoOperation(bookmark_model, undo_renumber_observer),
+ parent_id_(parent->id()) {
+ ordered_bookmarks_.resize(parent->child_count());
+ for (int i = 0; i < parent->child_count(); ++i)
+ ordered_bookmarks_[i] = parent->GetChild(i)->id();
+}
+
+BookmarkReorderOperation::~BookmarkReorderOperation() {
+}
+
+void BookmarkReorderOperation::Undo() {
+ BookmarkModel* model = bookmark_model();
+ const BookmarkNode* parent =
+ bookmarks::GetBookmarkNodeByID(model, parent_id_);
+ DCHECK(parent);
+
+ std::vector<const BookmarkNode*> ordered_nodes;
+ for (size_t i = 0; i < ordered_bookmarks_.size(); ++i) {
+ ordered_nodes.push_back(
+ bookmarks::GetBookmarkNodeByID(model, ordered_bookmarks_[i]));
+ }
+
+ model->ReorderChildren(parent, ordered_nodes);
+}
+
+int BookmarkReorderOperation::GetUndoLabelId() const {
+ return IDS_BOOKMARK_BAR_UNDO_REORDER;
+}
+
+int BookmarkReorderOperation::GetRedoLabelId() const {
+ return IDS_BOOKMARK_BAR_REDO_REORDER;
+}
+
+void BookmarkReorderOperation::OnBookmarkRenumbered(int64 old_id,
+ int64 new_id) {
+ if (parent_id_ == old_id)
+ parent_id_ = new_id;
+ for (size_t i = 0; i < ordered_bookmarks_.size(); ++i) {
+ if (ordered_bookmarks_[i] == old_id)
+ ordered_bookmarks_[i] = new_id;
+ }
+}
+
+} // namespace
+
+// BookmarkUndoService --------------------------------------------------------
+
+BookmarkUndoService::BookmarkUndoService() : scoped_observer_(this) {
+}
+
+BookmarkUndoService::~BookmarkUndoService() {
+}
+
+void BookmarkUndoService::Start(BookmarkModel* model) {
+ scoped_observer_.Add(model);
+}
+
+void BookmarkUndoService::Shutdown() {
+ scoped_observer_.RemoveAll();
+}
+
+void BookmarkUndoService::BookmarkModelLoaded(BookmarkModel* model,
+ bool ids_reassigned) {
+ undo_manager_.RemoveAllOperations();
+}
+
+void BookmarkUndoService::BookmarkModelBeingDeleted(BookmarkModel* model) {
+ undo_manager_.RemoveAllOperations();
+}
+
+void BookmarkUndoService::BookmarkNodeMoved(BookmarkModel* model,
+ const BookmarkNode* old_parent,
+ int old_index,
+ const BookmarkNode* new_parent,
+ int new_index) {
+ scoped_ptr<UndoOperation> op(new BookmarkMoveOperation(
+ model, this, old_parent, old_index, new_parent, new_index));
+ undo_manager()->AddUndoOperation(op.Pass());
+}
+
+void BookmarkUndoService::BookmarkNodeAdded(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int index) {
+ scoped_ptr<UndoOperation> op(
+ new BookmarkAddOperation(model, this, parent, index));
+ undo_manager()->AddUndoOperation(op.Pass());
+}
+
+void BookmarkUndoService::OnWillRemoveBookmarks(BookmarkModel* model,
+ const BookmarkNode* parent,
+ int old_index,
+ const BookmarkNode* node) {
+ scoped_ptr<UndoOperation> op(
+ new BookmarkRemoveOperation(model, this, parent, old_index, node));
+ undo_manager()->AddUndoOperation(op.Pass());
+}
+
+void BookmarkUndoService::OnWillRemoveAllUserBookmarks(BookmarkModel* model) {
+ bookmarks::ScopedGroupBookmarkActions merge_removes(model);
+ for (int i = 0; i < model->root_node()->child_count(); ++i) {
+ const BookmarkNode* permanent_node = model->root_node()->GetChild(i);
+ for (int j = permanent_node->child_count() - 1; j >= 0; --j) {
+ scoped_ptr<UndoOperation> op(new BookmarkRemoveOperation(
+ model, this, permanent_node, j, permanent_node->GetChild(j)));
+ undo_manager()->AddUndoOperation(op.Pass());
+ }
+ }
+}
+
+void BookmarkUndoService::OnWillChangeBookmarkNode(BookmarkModel* model,
+ const BookmarkNode* node) {
+ scoped_ptr<UndoOperation> op(new BookmarkEditOperation(model, this, node));
+ undo_manager()->AddUndoOperation(op.Pass());
+}
+
+void BookmarkUndoService::OnWillReorderBookmarkNode(BookmarkModel* model,
+ const BookmarkNode* node) {
+ scoped_ptr<UndoOperation> op(new BookmarkReorderOperation(model, this, node));
+ undo_manager()->AddUndoOperation(op.Pass());
+}
+
+void BookmarkUndoService::GroupedBookmarkChangesBeginning(
+ BookmarkModel* model) {
+ undo_manager()->StartGroupingActions();
+}
+
+void BookmarkUndoService::GroupedBookmarkChangesEnded(BookmarkModel* model) {
+ undo_manager()->EndGroupingActions();
+}
+
+void BookmarkUndoService::OnBookmarkRenumbered(int64 old_id, int64 new_id) {
+ std::vector<UndoOperation*> all_operations =
+ undo_manager()->GetAllUndoOperations();
+ for (UndoOperation* op : all_operations) {
+ static_cast<BookmarkUndoOperation*>(op)
+ ->OnBookmarkRenumbered(old_id, new_id);
+ }
+}
diff --git a/components/undo/bookmark_undo_service.h b/components/undo/bookmark_undo_service.h
new file mode 100644
index 0000000..8ae2eaf
--- /dev/null
+++ b/components/undo/bookmark_undo_service.h
@@ -0,0 +1,80 @@
+// Copyright 2013 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 COMPONENTS_UNDO_BOOKMARK_UNDO_SERVICE_H_
+#define COMPONENTS_UNDO_BOOKMARK_UNDO_SERVICE_H_
+
+#include <map>
+
+#include "base/scoped_observer.h"
+#include "components/bookmarks/browser/base_bookmark_model_observer.h"
+#include "components/bookmarks/browser/bookmark_node_data.h"
+#include "components/keyed_service/core/keyed_service.h"
+#include "components/undo/bookmark_renumber_observer.h"
+#include "components/undo/undo_manager.h"
+
+namespace bookmarks {
+class BookmarkModel;
+class BookmarkModelObserver;
+}
+
+// BookmarkUndoService --------------------------------------------------------
+
+// BookmarkUndoService is owned by the profile, and is responsible for observing
+// BookmarkModel changes in order to provide an undo for those changes.
+class BookmarkUndoService : public bookmarks::BaseBookmarkModelObserver,
+ public KeyedService,
+ public BookmarkRenumberObserver {
+ public:
+ BookmarkUndoService();
+ ~BookmarkUndoService() override;
+
+ // Starts the BookmarkUndoService and register it as a BookmarkModelObserver.
+ // Calling this method is optional, but the service will be inactive until it
+ // is called.
+ void Start(bookmarks::BookmarkModel* model);
+
+ UndoManager* undo_manager() { return &undo_manager_; }
+
+ // KeyedService:
+ void Shutdown() override;
+
+ private:
+ // bookmarks::BaseBookmarkModelObserver:
+ void BookmarkModelChanged() override {}
+ void BookmarkModelLoaded(bookmarks::BookmarkModel* model,
+ bool ids_reassigned) override;
+ void BookmarkModelBeingDeleted(bookmarks::BookmarkModel* model) override;
+ void BookmarkNodeMoved(bookmarks::BookmarkModel* model,
+ const bookmarks::BookmarkNode* old_parent,
+ int old_index,
+ const bookmarks::BookmarkNode* new_parent,
+ int new_index) override;
+ void BookmarkNodeAdded(bookmarks::BookmarkModel* model,
+ const bookmarks::BookmarkNode* parent,
+ int index) override;
+ void OnWillRemoveBookmarks(bookmarks::BookmarkModel* model,
+ const bookmarks::BookmarkNode* parent,
+ int old_index,
+ const bookmarks::BookmarkNode* node) override;
+ void OnWillRemoveAllUserBookmarks(bookmarks::BookmarkModel* model) override;
+ void OnWillChangeBookmarkNode(bookmarks::BookmarkModel* model,
+ const bookmarks::BookmarkNode* node) override;
+ void OnWillReorderBookmarkNode(bookmarks::BookmarkModel* model,
+ const bookmarks::BookmarkNode* node) override;
+ void GroupedBookmarkChangesBeginning(
+ bookmarks::BookmarkModel* model) override;
+ void GroupedBookmarkChangesEnded(bookmarks::BookmarkModel* model) override;
+
+ // BookmarkRenumberObserver:
+ void OnBookmarkRenumbered(int64 old_id, int64 new_id) override;
+
+ UndoManager undo_manager_;
+ ScopedObserver<bookmarks::BookmarkModel, bookmarks::BookmarkModelObserver>
+ scoped_observer_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkUndoService);
+};
+
+#endif // COMPONENTS_UNDO_BOOKMARK_UNDO_SERVICE_H_
diff --git a/components/undo/bookmark_undo_service_test.cc b/components/undo/bookmark_undo_service_test.cc
new file mode 100644
index 0000000..e97988a
--- /dev/null
+++ b/components/undo/bookmark_undo_service_test.cc
@@ -0,0 +1,421 @@
+// Copyright 2013 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 "components/undo/bookmark_undo_service.h"
+
+#include "base/strings/utf_string_conversions.h"
+#include "components/bookmarks/browser/bookmark_model.h"
+#include "components/bookmarks/test/bookmark_test_helpers.h"
+#include "components/bookmarks/test/test_bookmark_client.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using base::ASCIIToUTF16;
+using bookmarks::BookmarkModel;
+using bookmarks::BookmarkNode;
+
+namespace {
+
+class BookmarkUndoServiceTest : public testing::Test {
+ public:
+ BookmarkUndoServiceTest();
+
+ void SetUp() override;
+ void TearDown() override;
+
+ BookmarkModel* GetModel();
+ BookmarkUndoService* GetUndoService();
+
+ private:
+ scoped_ptr<bookmarks::TestBookmarkClient> test_bookmark_client_;
+ scoped_ptr<bookmarks::BookmarkModel> bookmark_model_;
+ scoped_ptr<BookmarkUndoService> bookmark_undo_service_;
+
+ DISALLOW_COPY_AND_ASSIGN(BookmarkUndoServiceTest);
+};
+
+BookmarkUndoServiceTest::BookmarkUndoServiceTest() {}
+
+void BookmarkUndoServiceTest::SetUp() {
+ DCHECK(!test_bookmark_client_);
+ DCHECK(!bookmark_model_);
+ DCHECK(!bookmark_undo_service_);
+ test_bookmark_client_.reset(new bookmarks::TestBookmarkClient);
+ bookmark_model_ = test_bookmark_client_->CreateModel();
+ bookmark_undo_service_.reset(new BookmarkUndoService);
+ bookmark_undo_service_->Start(bookmark_model_.get());
+ bookmarks::test::WaitForBookmarkModelToLoad(bookmark_model_.get());
+}
+
+BookmarkModel* BookmarkUndoServiceTest::GetModel() {
+ return bookmark_model_.get();
+}
+
+BookmarkUndoService* BookmarkUndoServiceTest::GetUndoService() {
+ return bookmark_undo_service_.get();
+}
+
+void BookmarkUndoServiceTest::TearDown() {
+ // Implement two-phase KeyedService shutdown for test KeyedServices.
+ bookmark_undo_service_->Shutdown();
+ bookmark_model_->Shutdown();
+ test_bookmark_client_->Shutdown();
+ bookmark_undo_service_.reset();
+ bookmark_model_.reset();
+ test_bookmark_client_.reset();
+}
+
+TEST_F(BookmarkUndoServiceTest, AddBookmark) {
+ BookmarkModel* model = GetModel();
+ BookmarkUndoService* undo_service = GetUndoService();
+
+ const BookmarkNode* parent = model->other_node();
+ model->AddURL(parent, 0, ASCIIToUTF16("foo"), GURL("http://www.bar.com"));
+
+ // Undo bookmark creation and test for no bookmarks.
+ undo_service->undo_manager()->Undo();
+ EXPECT_EQ(0, model->other_node()->child_count());
+
+ // Redo bookmark creation and ensure bookmark information is valid.
+ undo_service->undo_manager()->Redo();
+ const BookmarkNode* node = parent->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("foo"));
+ EXPECT_EQ(node->url(), GURL("http://www.bar.com"));
+}
+
+// Test that a bookmark removal action can be undone and redone.
+TEST_F(BookmarkUndoServiceTest, UndoBookmarkRemove) {
+ BookmarkModel* model = GetModel();
+ BookmarkUndoService* undo_service = GetUndoService();
+
+ const BookmarkNode* parent = model->other_node();
+ model->AddURL(parent, 0, ASCIIToUTF16("foo"), GURL("http://www.bar.com"));
+ model->Remove(parent, 0);
+
+ EXPECT_EQ(2U, undo_service->undo_manager()->undo_count());
+ EXPECT_EQ(0U, undo_service->undo_manager()->redo_count());
+
+ // Undo the deletion of the only bookmark and check the bookmark values.
+ undo_service->undo_manager()->Undo();
+ EXPECT_EQ(1, model->other_node()->child_count());
+ const BookmarkNode* node = parent->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("foo"));
+ EXPECT_EQ(node->url(), GURL("http://www.bar.com"));
+
+ EXPECT_EQ(1U, undo_service->undo_manager()->undo_count());
+ EXPECT_EQ(1U, undo_service->undo_manager()->redo_count());
+
+ // Redo the deletion and check that there are no bookmarks left.
+ undo_service->undo_manager()->Redo();
+ EXPECT_EQ(0, model->other_node()->child_count());
+
+ EXPECT_EQ(2U, undo_service->undo_manager()->undo_count());
+ EXPECT_EQ(0U, undo_service->undo_manager()->redo_count());
+}
+
+// Ensure the undo/redo works for editing of bookmark information grouped into
+// one action.
+TEST_F(BookmarkUndoServiceTest, UndoBookmarkGroupedAction) {
+ BookmarkModel* model = GetModel();
+ BookmarkUndoService* undo_service = GetUndoService();
+
+ const BookmarkNode* n1 = model->AddURL(model->other_node(),
+ 0,
+ ASCIIToUTF16("foo"),
+ GURL("http://www.foo.com"));
+ undo_service->undo_manager()->StartGroupingActions();
+ model->SetTitle(n1, ASCIIToUTF16("bar"));
+ model->SetURL(n1, GURL("http://www.bar.com"));
+ undo_service->undo_manager()->EndGroupingActions();
+
+ EXPECT_EQ(2U, undo_service->undo_manager()->undo_count());
+ EXPECT_EQ(0U, undo_service->undo_manager()->redo_count());
+
+ // Undo the modification of the bookmark and check for the original values.
+ undo_service->undo_manager()->Undo();
+ EXPECT_EQ(1, model->other_node()->child_count());
+ const BookmarkNode* node = model->other_node()->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("foo"));
+ EXPECT_EQ(node->url(), GURL("http://www.foo.com"));
+
+ // Redo the modifications and ensure the newer values are present.
+ undo_service->undo_manager()->Redo();
+ EXPECT_EQ(1, model->other_node()->child_count());
+ node = model->other_node()->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("bar"));
+ EXPECT_EQ(node->url(), GURL("http://www.bar.com"));
+
+ EXPECT_EQ(2U, undo_service->undo_manager()->undo_count());
+ EXPECT_EQ(0U, undo_service->undo_manager()->redo_count());
+}
+
+// Test moving bookmarks within a folder and between folders.
+TEST_F(BookmarkUndoServiceTest, UndoBookmarkMoveWithinFolder) {
+ BookmarkModel* model = GetModel();
+ BookmarkUndoService* undo_service = GetUndoService();
+
+ const BookmarkNode* n1 = model->AddURL(model->other_node(),
+ 0,
+ ASCIIToUTF16("foo"),
+ GURL("http://www.foo.com"));
+ const BookmarkNode* n2 = model->AddURL(model->other_node(),
+ 1,
+ ASCIIToUTF16("moo"),
+ GURL("http://www.moo.com"));
+ const BookmarkNode* n3 = model->AddURL(model->other_node(),
+ 2,
+ ASCIIToUTF16("bar"),
+ GURL("http://www.bar.com"));
+ model->Move(n1, model->other_node(), 3);
+
+ // Undo the move and check that the nodes are in order.
+ undo_service->undo_manager()->Undo();
+ EXPECT_EQ(model->other_node()->GetChild(0), n1);
+ EXPECT_EQ(model->other_node()->GetChild(1), n2);
+ EXPECT_EQ(model->other_node()->GetChild(2), n3);
+
+ // Redo the move and check that the first node is in the last position.
+ undo_service->undo_manager()->Redo();
+ EXPECT_EQ(model->other_node()->GetChild(0), n2);
+ EXPECT_EQ(model->other_node()->GetChild(1), n3);
+ EXPECT_EQ(model->other_node()->GetChild(2), n1);
+}
+
+// Test undo of a bookmark moved to a different folder.
+TEST_F(BookmarkUndoServiceTest, UndoBookmarkMoveToOtherFolder) {
+ BookmarkModel* model = GetModel();
+ BookmarkUndoService* undo_service = GetUndoService();
+
+ const BookmarkNode* n1 = model->AddURL(model->other_node(),
+ 0,
+ ASCIIToUTF16("foo"),
+ GURL("http://www.foo.com"));
+ const BookmarkNode* n2 = model->AddURL(model->other_node(),
+ 1,
+ ASCIIToUTF16("moo"),
+ GURL("http://www.moo.com"));
+ const BookmarkNode* n3 = model->AddURL(model->other_node(),
+ 2,
+ ASCIIToUTF16("bar"),
+ GURL("http://www.bar.com"));
+ const BookmarkNode* f1 =
+ model->AddFolder(model->other_node(), 3, ASCIIToUTF16("folder"));
+ model->Move(n3, f1, 0);
+
+ // Undo the move and check that the bookmark and folder are in place.
+ undo_service->undo_manager()->Undo();
+ ASSERT_EQ(4, model->other_node()->child_count());
+ EXPECT_EQ(model->other_node()->GetChild(0), n1);
+ EXPECT_EQ(model->other_node()->GetChild(1), n2);
+ EXPECT_EQ(model->other_node()->GetChild(2), n3);
+ EXPECT_EQ(model->other_node()->GetChild(3), f1);
+ EXPECT_EQ(0, f1->child_count());
+
+ // Redo the move back into the folder and check validity.
+ undo_service->undo_manager()->Redo();
+ ASSERT_EQ(3, model->other_node()->child_count());
+ EXPECT_EQ(model->other_node()->GetChild(0), n1);
+ EXPECT_EQ(model->other_node()->GetChild(1), n2);
+ EXPECT_EQ(model->other_node()->GetChild(2), f1);
+ ASSERT_EQ(1, f1->child_count());
+ EXPECT_EQ(f1->GetChild(0), n3);
+}
+
+// Tests the handling of multiple modifications that include renumbering of the
+// bookmark identifiers.
+TEST_F(BookmarkUndoServiceTest, UndoBookmarkRenameDelete) {
+ BookmarkModel* model = GetModel();
+ BookmarkUndoService* undo_service = GetUndoService();
+
+ const BookmarkNode* f1 = model->AddFolder(model->other_node(),
+ 0,
+ ASCIIToUTF16("folder"));
+ model->AddURL(f1, 0, ASCIIToUTF16("foo"), GURL("http://www.foo.com"));
+ model->SetTitle(f1, ASCIIToUTF16("Renamed"));
+ model->Remove(model->other_node(), 0);
+
+ // Undo the folder removal and ensure the folder and bookmark were restored.
+ undo_service->undo_manager()->Undo();
+ ASSERT_EQ(1, model->other_node()->child_count());
+ ASSERT_EQ(1, model->other_node()->GetChild(0)->child_count());
+ const BookmarkNode* node = model->other_node()->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("Renamed"));
+
+ node = model->other_node()->GetChild(0)->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("foo"));
+ EXPECT_EQ(node->url(), GURL("http://www.foo.com"));
+
+ // Undo the title change and ensure the folder was updated even though the
+ // id has changed.
+ undo_service->undo_manager()->Undo();
+ node = model->other_node()->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("folder"));
+
+ // Undo bookmark creation and test for removal of bookmark.
+ undo_service->undo_manager()->Undo();
+ ASSERT_EQ(0, model->other_node()->GetChild(0)->child_count());
+
+ // Undo folder creation and confirm the bookmark model is empty.
+ undo_service->undo_manager()->Undo();
+ ASSERT_EQ(0, model->other_node()->child_count());
+
+ // Redo all the actions and ensure the folder and bookmark are restored.
+ undo_service->undo_manager()->Redo(); // folder creation
+ undo_service->undo_manager()->Redo(); // bookmark creation
+ undo_service->undo_manager()->Redo(); // bookmark title change
+ ASSERT_EQ(1, model->other_node()->child_count());
+ ASSERT_EQ(1, model->other_node()->GetChild(0)->child_count());
+ node = model->other_node()->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("Renamed"));
+ node = model->other_node()->GetChild(0)->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("foo"));
+ EXPECT_EQ(node->url(), GURL("http://www.foo.com"));
+
+ undo_service->undo_manager()->Redo(); // folder deletion
+ EXPECT_EQ(0, model->other_node()->child_count());
+}
+
+// Test the undo of SortChildren and ReorderChildren.
+TEST_F(BookmarkUndoServiceTest, UndoBookmarkReorder) {
+ BookmarkModel* model = GetModel();
+ BookmarkUndoService* undo_service = GetUndoService();
+
+ const BookmarkNode* parent = model->other_node();
+ model->AddURL(parent, 0, ASCIIToUTF16("foo"), GURL("http://www.foo.com"));
+ model->AddURL(parent, 1, ASCIIToUTF16("moo"), GURL("http://www.moo.com"));
+ model->AddURL(parent, 2, ASCIIToUTF16("bar"), GURL("http://www.bar.com"));
+ model->SortChildren(parent);
+
+ // Test the undo of SortChildren.
+ undo_service->undo_manager()->Undo();
+ const BookmarkNode* node = parent->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("foo"));
+ EXPECT_EQ(node->url(), GURL("http://www.foo.com"));
+
+ node = parent->GetChild(1);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("moo"));
+ EXPECT_EQ(node->url(), GURL("http://www.moo.com"));
+
+ node = parent->GetChild(2);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("bar"));
+ EXPECT_EQ(node->url(), GURL("http://www.bar.com"));
+
+ // Test the redo of SortChildren.
+ undo_service->undo_manager()->Redo();
+ node = parent->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("bar"));
+ EXPECT_EQ(node->url(), GURL("http://www.bar.com"));
+
+ node = parent->GetChild(1);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("foo"));
+ EXPECT_EQ(node->url(), GURL("http://www.foo.com"));
+
+ node = parent->GetChild(2);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("moo"));
+ EXPECT_EQ(node->url(), GURL("http://www.moo.com"));
+
+}
+
+TEST_F(BookmarkUndoServiceTest, UndoBookmarkRemoveAll) {
+ BookmarkModel* model = GetModel();
+ BookmarkUndoService* undo_service = GetUndoService();
+
+ // Setup bookmarks in the Other Bookmarks and the Bookmark Bar.
+ const BookmarkNode* new_folder;
+ const BookmarkNode* parent = model->other_node();
+ model->AddURL(parent, 0, ASCIIToUTF16("foo"), GURL("http://www.google.com"));
+ new_folder= model->AddFolder(parent, 1, ASCIIToUTF16("folder"));
+ model->AddURL(new_folder, 0, ASCIIToUTF16("bar"), GURL("http://www.bar.com"));
+
+ parent = model->bookmark_bar_node();
+ model->AddURL(parent, 0, ASCIIToUTF16("a"), GURL("http://www.a.com"));
+ new_folder = model->AddFolder(parent, 1, ASCIIToUTF16("folder"));
+ model->AddURL(new_folder, 0, ASCIIToUTF16("b"), GURL("http://www.b.com"));
+
+ model->RemoveAllUserBookmarks();
+
+ // Test that the undo of RemoveAllUserBookmarks restores all folders and
+ // bookmarks.
+ undo_service->undo_manager()->Undo();
+
+ ASSERT_EQ(2, model->other_node()->child_count());
+ EXPECT_EQ(1, model->other_node()->GetChild(1)->child_count());
+ const BookmarkNode* node = model->other_node()->GetChild(1)->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("bar"));
+ EXPECT_EQ(node->url(), GURL("http://www.bar.com"));
+
+ ASSERT_EQ(2, model->bookmark_bar_node()->child_count());
+ EXPECT_EQ(1, model->bookmark_bar_node()->GetChild(1)->child_count());
+ node = model->bookmark_bar_node()->GetChild(1)->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("b"));
+ EXPECT_EQ(node->url(), GURL("http://www.b.com"));
+
+ // Test that the redo removes all folders and bookmarks.
+ undo_service->undo_manager()->Redo();
+ EXPECT_EQ(0, model->other_node()->child_count());
+ EXPECT_EQ(0, model->bookmark_bar_node()->child_count());
+}
+
+TEST_F(BookmarkUndoServiceTest, UndoRemoveFolderWithBookmarks) {
+ BookmarkModel* model = GetModel();
+ BookmarkUndoService* undo_service = GetUndoService();
+
+ // Setup bookmarks in the Other Bookmarks.
+ const BookmarkNode* new_folder;
+ const BookmarkNode* parent = model->other_node();
+ new_folder = model->AddFolder(parent, 0, ASCIIToUTF16("folder"));
+ model->AddURL(new_folder, 0, ASCIIToUTF16("bar"), GURL("http://www.bar.com"));
+
+ model->Remove(parent, 0);
+
+ // Test that the undo restores the bookmark and folder.
+ undo_service->undo_manager()->Undo();
+
+ ASSERT_EQ(1, model->other_node()->child_count());
+ new_folder = model->other_node()->GetChild(0);
+ EXPECT_EQ(1, new_folder->child_count());
+ const BookmarkNode* node = new_folder->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("bar"));
+ EXPECT_EQ(node->url(), GURL("http://www.bar.com"));
+
+ // Test that the redo restores the bookmark and folder.
+ undo_service->undo_manager()->Redo();
+
+ ASSERT_EQ(0, model->other_node()->child_count());
+
+ // Test that the undo after a redo restores the bookmark and folder.
+ undo_service->undo_manager()->Undo();
+
+ ASSERT_EQ(1, model->other_node()->child_count());
+ new_folder = model->other_node()->GetChild(0);
+ EXPECT_EQ(1, new_folder->child_count());
+ node = new_folder->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("bar"));
+ EXPECT_EQ(node->url(), GURL("http://www.bar.com"));
+}
+
+TEST_F(BookmarkUndoServiceTest, TestUpperLimit) {
+ BookmarkModel* model = GetModel();
+ BookmarkUndoService* undo_service = GetUndoService();
+
+ // This maximum is set in undo_manager.cc
+ const size_t kMaxUndoGroups = 100;
+
+ const BookmarkNode* parent = model->other_node();
+ model->AddURL(parent, 0, ASCIIToUTF16("foo"), GURL("http://www.foo.com"));
+ for (size_t i = 1; i < kMaxUndoGroups + 1; ++i)
+ model->AddURL(parent, i, ASCIIToUTF16("bar"), GURL("http://www.bar.com"));
+
+ EXPECT_EQ(kMaxUndoGroups, undo_service->undo_manager()->undo_count());
+
+ // Undo as many operations as possible.
+ while (undo_service->undo_manager()->undo_count())
+ undo_service->undo_manager()->Undo();
+
+ EXPECT_EQ(1, parent->child_count());
+ const BookmarkNode* node = model->other_node()->GetChild(0);
+ EXPECT_EQ(node->GetTitle(), ASCIIToUTF16("foo"));
+ EXPECT_EQ(node->url(), GURL("http://www.foo.com"));
+}
+
+} // namespace
diff --git a/components/undo/bookmark_undo_utils.cc b/components/undo/bookmark_undo_utils.cc
new file mode 100644
index 0000000..1539431
--- /dev/null
+++ b/components/undo/bookmark_undo_utils.cc
@@ -0,0 +1,24 @@
+// Copyright 2014 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 "components/undo/bookmark_undo_utils.h"
+
+#include "components/undo/bookmark_undo_service.h"
+#include "components/undo/undo_manager.h"
+
+// ScopedSuspendBookmarkUndo --------------------------------------------------
+
+ScopedSuspendBookmarkUndo::ScopedSuspendBookmarkUndo(
+ BookmarkUndoService* bookmark_undo_service)
+ : undo_manager_(bookmark_undo_service
+ ? bookmark_undo_service->undo_manager()
+ : nullptr) {
+ if (undo_manager_)
+ undo_manager_->SuspendUndoTracking();
+}
+
+ScopedSuspendBookmarkUndo::~ScopedSuspendBookmarkUndo() {
+ if (undo_manager_)
+ undo_manager_->ResumeUndoTracking();
+}
diff --git a/components/undo/bookmark_undo_utils.h b/components/undo/bookmark_undo_utils.h
new file mode 100644
index 0000000..987e2728
--- /dev/null
+++ b/components/undo/bookmark_undo_utils.h
@@ -0,0 +1,29 @@
+// Copyright 2014 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 COMPONENTS_UNDO_BOOKMARK_UNDO_UTILS_H_
+#define COMPONENTS_UNDO_BOOKMARK_UNDO_UTILS_H_
+
+#include "base/basictypes.h"
+
+class BookmarkUndoService;
+class UndoManager;
+
+// ScopedSuspendBookmarkUndo --------------------------------------------------
+
+// Scopes the suspension of the undo tracking for non-user initiated changes
+// such as those occuring during account synchronization.
+class ScopedSuspendBookmarkUndo {
+ public:
+ explicit ScopedSuspendBookmarkUndo(
+ BookmarkUndoService* bookmark_undo_service);
+ ~ScopedSuspendBookmarkUndo();
+
+ private:
+ UndoManager* undo_manager_;
+
+ DISALLOW_COPY_AND_ASSIGN(ScopedSuspendBookmarkUndo);
+};
+
+#endif // COMPONENTS_UNDO_BOOKMARK_UNDO_UTILS_H_
diff --git a/components/undo/undo_manager.cc b/components/undo/undo_manager.cc
new file mode 100644
index 0000000..28a8ab2
--- /dev/null
+++ b/components/undo/undo_manager.cc
@@ -0,0 +1,224 @@
+// Copyright 2013 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 "components/undo/undo_manager.h"
+
+#include "base/auto_reset.h"
+#include "base/logging.h"
+#include "components/undo/undo_manager_observer.h"
+#include "components/undo/undo_operation.h"
+#include "grit/components_strings.h"
+#include "ui/base/l10n/l10n_util.h"
+
+namespace {
+
+// Maximum number of changes that can be undone.
+const size_t kMaxUndoGroups = 100;
+
+} // namespace
+
+// UndoGroup ------------------------------------------------------------------
+
+UndoGroup::UndoGroup()
+ : undo_label_id_(IDS_BOOKMARK_BAR_UNDO),
+ redo_label_id_(IDS_BOOKMARK_BAR_REDO) {
+}
+
+UndoGroup::~UndoGroup() {
+}
+
+void UndoGroup::AddOperation(scoped_ptr<UndoOperation> operation) {
+ if (operations_.empty()) {
+ set_undo_label_id(operation->GetUndoLabelId());
+ set_redo_label_id(operation->GetRedoLabelId());
+ }
+ operations_.push_back(operation.release());
+}
+
+void UndoGroup::Undo() {
+ for (ScopedVector<UndoOperation>::reverse_iterator ri = operations_.rbegin();
+ ri != operations_.rend(); ++ri) {
+ (*ri)->Undo();
+ }
+}
+
+// UndoManager ----------------------------------------------------------------
+
+UndoManager::UndoManager()
+ : group_actions_count_(0),
+ undo_in_progress_action_(NULL),
+ undo_suspended_count_(0),
+ performing_undo_(false),
+ performing_redo_(false) {
+}
+
+UndoManager::~UndoManager() {
+ DCHECK_EQ(0, group_actions_count_);
+ DCHECK_EQ(0, undo_suspended_count_);
+ DCHECK(!performing_undo_);
+ DCHECK(!performing_redo_);
+}
+
+void UndoManager::Undo() {
+ Undo(&performing_undo_, &undo_actions_);
+}
+
+void UndoManager::Redo() {
+ Undo(&performing_redo_, &redo_actions_);
+}
+
+base::string16 UndoManager::GetUndoLabel() const {
+ return l10n_util::GetStringUTF16(
+ undo_actions_.empty() ? IDS_BOOKMARK_BAR_UNDO
+ : undo_actions_.back()->get_undo_label_id());
+}
+
+base::string16 UndoManager::GetRedoLabel() const {
+ return l10n_util::GetStringUTF16(
+ redo_actions_.empty() ? IDS_BOOKMARK_BAR_REDO
+ : redo_actions_.back()->get_redo_label_id());
+}
+
+void UndoManager::AddUndoOperation(scoped_ptr<UndoOperation> operation) {
+ if (IsUndoTrakingSuspended()) {
+ RemoveAllOperations();
+ operation.reset();
+ return;
+ }
+
+ if (group_actions_count_) {
+ pending_grouped_action_->AddOperation(operation.Pass());
+ } else {
+ UndoGroup* new_action = new UndoGroup();
+ new_action->AddOperation(operation.Pass());
+ AddUndoGroup(new_action);
+ }
+}
+
+void UndoManager::StartGroupingActions() {
+ if (!group_actions_count_)
+ pending_grouped_action_.reset(new UndoGroup());
+ ++group_actions_count_;
+}
+
+void UndoManager::EndGroupingActions() {
+ --group_actions_count_;
+ if (group_actions_count_ > 0)
+ return;
+
+ // Check that StartGroupingActions and EndGroupingActions are paired.
+ DCHECK_GE(group_actions_count_, 0);
+
+ bool is_user_action = !performing_undo_ && !performing_redo_;
+ if (!pending_grouped_action_->undo_operations().empty()) {
+ AddUndoGroup(pending_grouped_action_.release());
+ } else {
+ // No changes were executed since we started grouping actions, so the
+ // pending UndoGroup should be discarded.
+ pending_grouped_action_.reset();
+
+ // This situation is only expected when it is a user initiated action.
+ // Undo/Redo should have at least one operation performed.
+ DCHECK(is_user_action);
+ }
+}
+
+void UndoManager::SuspendUndoTracking() {
+ ++undo_suspended_count_;
+}
+
+void UndoManager::ResumeUndoTracking() {
+ DCHECK_GT(undo_suspended_count_, 0);
+ --undo_suspended_count_;
+}
+
+bool UndoManager::IsUndoTrakingSuspended() const {
+ return undo_suspended_count_ > 0;
+}
+
+std::vector<UndoOperation*> UndoManager::GetAllUndoOperations() const {
+ std::vector<UndoOperation*> result;
+ for (size_t i = 0; i < undo_actions_.size(); ++i) {
+ const std::vector<UndoOperation*>& operations =
+ undo_actions_[i]->undo_operations();
+ result.insert(result.end(), operations.begin(), operations.end());
+ }
+ for (size_t i = 0; i < redo_actions_.size(); ++i) {
+ const std::vector<UndoOperation*>& operations =
+ redo_actions_[i]->undo_operations();
+ result.insert(result.end(), operations.begin(), operations.end());
+ }
+ // Ensure that if an Undo is in progress the UndoOperations part of that
+ // UndoGroup are included in the returned set. This will ensure that any
+ // changes (such as renumbering) will be applied to any potentially
+ // unprocessed UndoOperations.
+ if (undo_in_progress_action_) {
+ const std::vector<UndoOperation*>& operations =
+ undo_in_progress_action_->undo_operations();
+ result.insert(result.end(), operations.begin(), operations.end());
+ }
+
+ return result;
+}
+
+void UndoManager::RemoveAllOperations() {
+ DCHECK(!group_actions_count_);
+ undo_actions_.clear();
+ redo_actions_.clear();
+
+ NotifyOnUndoManagerStateChange();
+}
+
+void UndoManager::AddObserver(UndoManagerObserver* observer) {
+ observers_.AddObserver(observer);
+}
+
+void UndoManager::RemoveObserver(UndoManagerObserver* observer) {
+ observers_.RemoveObserver(observer);
+}
+
+void UndoManager::Undo(bool* performing_indicator,
+ ScopedVector<UndoGroup>* active_undo_group) {
+ // Check that action grouping has been correctly ended.
+ DCHECK(!group_actions_count_);
+
+ if (active_undo_group->empty())
+ return;
+
+ base::AutoReset<bool> incoming_changes(performing_indicator, true);
+ scoped_ptr<UndoGroup> action(active_undo_group->back());
+ base::AutoReset<UndoGroup*> action_context(&undo_in_progress_action_,
+ action.get());
+ active_undo_group->weak_erase(
+ active_undo_group->begin() + active_undo_group->size() - 1);
+
+ StartGroupingActions();
+ action->Undo();
+ EndGroupingActions();
+
+ NotifyOnUndoManagerStateChange();
+}
+
+void UndoManager::NotifyOnUndoManagerStateChange() {
+ FOR_EACH_OBSERVER(
+ UndoManagerObserver, observers_, OnUndoManagerStateChange());
+}
+
+void UndoManager::AddUndoGroup(UndoGroup* new_undo_group) {
+ GetActiveUndoGroup()->push_back(new_undo_group);
+
+ // User actions invalidate any available redo actions.
+ if (is_user_action())
+ redo_actions_.clear();
+
+ // Limit the number of undo levels so the undo stack does not grow unbounded.
+ if (GetActiveUndoGroup()->size() > kMaxUndoGroups)
+ GetActiveUndoGroup()->erase(GetActiveUndoGroup()->begin());
+
+ NotifyOnUndoManagerStateChange();
+}
+
+ScopedVector<UndoGroup>* UndoManager::GetActiveUndoGroup() {
+ return performing_undo_ ? &redo_actions_ : &undo_actions_;
+}
diff --git a/components/undo/undo_manager.h b/components/undo/undo_manager.h
new file mode 100644
index 0000000..ebbe1b4
--- /dev/null
+++ b/components/undo/undo_manager.h
@@ -0,0 +1,134 @@
+// Copyright 2013 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 COMPONENTS_UNDO_UNDO_MANAGER_H_
+#define COMPONENTS_UNDO_UNDO_MANAGER_H_
+
+#include "base/basictypes.h"
+#include "base/memory/scoped_ptr.h"
+#include "base/memory/scoped_vector.h"
+#include "base/observer_list.h"
+#include "base/strings/string16.h"
+
+class UndoManagerObserver;
+class UndoOperation;
+
+// UndoGroup ------------------------------------------------------------------
+
+// UndoGroup represents a user action and stores all the operations that
+// make that action. Typically there is only one operation per UndoGroup.
+class UndoGroup {
+ public:
+ UndoGroup();
+ ~UndoGroup();
+
+ void AddOperation(scoped_ptr<UndoOperation> operation);
+ const std::vector<UndoOperation*>& undo_operations() {
+ return operations_.get();
+ }
+ void Undo();
+
+ // The resource string id describing the undo and redo action.
+ int get_undo_label_id() const { return undo_label_id_; }
+ void set_undo_label_id(int label_id) { undo_label_id_ = label_id; }
+
+ int get_redo_label_id() const { return redo_label_id_; }
+ void set_redo_label_id(int label_id) { redo_label_id_ = label_id; }
+
+ private:
+ ScopedVector<UndoOperation> operations_;
+
+ // The resource string id describing the undo and redo action.
+ int undo_label_id_;
+ int redo_label_id_;
+
+ DISALLOW_COPY_AND_ASSIGN(UndoGroup);
+};
+
+// UndoManager ----------------------------------------------------------------
+
+// Maintains user actions as a group of operations that store enough info to
+// undo and redo those operations.
+class UndoManager {
+ public:
+ UndoManager();
+ ~UndoManager();
+
+ // Perform an undo or redo operation.
+ void Undo();
+ void Redo();
+
+ size_t undo_count() const { return undo_actions_.size(); }
+ size_t redo_count() const { return redo_actions_.size(); }
+
+ base::string16 GetUndoLabel() const;
+ base::string16 GetRedoLabel() const;
+
+ void AddUndoOperation(scoped_ptr<UndoOperation> operation);
+
+ // Group multiple operations into one undoable action.
+ void StartGroupingActions();
+ void EndGroupingActions();
+
+ // Suspend undo tracking while processing non-user initiated changes such as
+ // profile synchonization.
+ void SuspendUndoTracking();
+ void ResumeUndoTracking();
+ bool IsUndoTrakingSuspended() const;
+
+ // Returns all UndoOperations that are awaiting Undo or Redo. Note that
+ // ownership of the UndoOperations is retained by UndoManager.
+ std::vector<UndoOperation*> GetAllUndoOperations() const;
+
+ // Remove all undo and redo operations. Note that grouping of actions and
+ // suspension of undo tracking states are left unchanged.
+ void RemoveAllOperations();
+
+ // Observers are notified when the internal state of this class changes.
+ void AddObserver(UndoManagerObserver* observer);
+ void RemoveObserver(UndoManagerObserver* observer);
+
+ private:
+ void Undo(bool* performing_indicator,
+ ScopedVector<UndoGroup>* active_undo_group);
+ bool is_user_action() const { return !performing_undo_ && !performing_redo_; }
+
+ // Notifies the observers that the undo manager's state has changed.
+ void NotifyOnUndoManagerStateChange();
+
+ // Handle the addition of |new_undo_group| to the active undo group container.
+ void AddUndoGroup(UndoGroup* new_undo_group);
+
+ // Returns the undo or redo UndoGroup container that should store the next
+ // change taking into account if an undo or redo is being executed.
+ ScopedVector<UndoGroup>* GetActiveUndoGroup();
+
+ // Containers of user actions ready for an undo or redo treated as a stack.
+ ScopedVector<UndoGroup> undo_actions_;
+ ScopedVector<UndoGroup> redo_actions_;
+
+ // The observers to notify when internal state changes.
+ ObserverList<UndoManagerObserver> observers_;
+
+ // Supports grouping operations into a single undo action.
+ int group_actions_count_;
+
+ // The container that is used when actions are grouped.
+ scoped_ptr<UndoGroup> pending_grouped_action_;
+
+ // The action that is in the process of being undone.
+ UndoGroup* undo_in_progress_action_;
+
+ // Supports the suspension of undo tracking.
+ int undo_suspended_count_;
+
+ // Set when executing Undo or Redo so that incoming changes are correctly
+ // processed.
+ bool performing_undo_;
+ bool performing_redo_;
+
+ DISALLOW_COPY_AND_ASSIGN(UndoManager);
+};
+
+#endif // COMPONENTS_UNDO_UNDO_MANAGER_H_
diff --git a/components/undo/undo_manager_observer.h b/components/undo/undo_manager_observer.h
new file mode 100644
index 0000000..1eecd66
--- /dev/null
+++ b/components/undo/undo_manager_observer.h
@@ -0,0 +1,18 @@
+// Copyright 2014 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 COMPONENTS_UNDO_UNDO_MANAGER_OBSERVER_H_
+#define COMPONENTS_UNDO_UNDO_MANAGER_OBSERVER_H_
+
+// Observer for the UndoManager.
+class UndoManagerObserver {
+ public:
+ // Invoked when the internal state of the UndoManager has changed.
+ virtual void OnUndoManagerStateChange() = 0;
+
+ protected:
+ virtual ~UndoManagerObserver() {}
+};
+
+#endif // COMPONENTS_UNDO_UNDO_MANAGER_OBSERVER_H_
diff --git a/components/undo/undo_manager_test.cc b/components/undo/undo_manager_test.cc
new file mode 100644
index 0000000..5053572
--- /dev/null
+++ b/components/undo/undo_manager_test.cc
@@ -0,0 +1,265 @@
+// Copyright 2013 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/auto_reset.h"
+#include "components/undo/undo_manager.h"
+#include "components/undo/undo_manager_observer.h"
+#include "components/undo/undo_operation.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace {
+
+class TestUndoOperation;
+
+// TestUndoService -------------------------------------------------------------
+
+class TestUndoService {
+ public:
+ TestUndoService();
+ ~TestUndoService();
+
+ void Redo();
+ void TriggerOperation();
+ void RecordUndoCall();
+
+ UndoManager undo_manager_;
+
+ bool performing_redo_;
+
+ int undo_operation_count_;
+ int redo_operation_count_;
+};
+
+// TestUndoOperation -----------------------------------------------------------
+
+class TestUndoOperation : public UndoOperation {
+ public:
+ explicit TestUndoOperation(TestUndoService* undo_service);
+ ~TestUndoOperation() override;
+
+ // UndoOperation:
+ void Undo() override;
+ int GetUndoLabelId() const override;
+ int GetRedoLabelId() const override;
+
+ private:
+ TestUndoService* undo_service_;
+
+ DISALLOW_COPY_AND_ASSIGN(TestUndoOperation);
+};
+
+TestUndoOperation::TestUndoOperation(TestUndoService* undo_service)
+ : undo_service_(undo_service) {
+}
+
+TestUndoOperation::~TestUndoOperation() {
+}
+
+void TestUndoOperation::Undo() {
+ undo_service_->TriggerOperation();
+ undo_service_->RecordUndoCall();
+}
+
+int TestUndoOperation::GetUndoLabelId() const {
+ return 0;
+}
+
+int TestUndoOperation::GetRedoLabelId() const {
+ return 0;
+}
+
+// TestUndoService -------------------------------------------------------------
+
+TestUndoService::TestUndoService() : performing_redo_(false),
+ undo_operation_count_(0),
+ redo_operation_count_(0) {
+}
+
+TestUndoService::~TestUndoService() {
+}
+
+void TestUndoService::Redo() {
+ base::AutoReset<bool> incoming_changes(&performing_redo_, true);
+ undo_manager_.Redo();
+}
+
+void TestUndoService::TriggerOperation() {
+ undo_manager_.AddUndoOperation(make_scoped_ptr(new TestUndoOperation(this)));
+}
+
+void TestUndoService::RecordUndoCall() {
+ if (performing_redo_)
+ ++redo_operation_count_;
+ else
+ ++undo_operation_count_;
+}
+
+// TestObserver ----------------------------------------------------------------
+
+class TestObserver : public UndoManagerObserver {
+ public:
+ TestObserver() : state_change_count_(0) {}
+ // Returns the number of state change callbacks
+ int state_change_count() { return state_change_count_; }
+
+ void OnUndoManagerStateChange() override { ++state_change_count_; }
+
+ private:
+ int state_change_count_;
+
+ DISALLOW_COPY_AND_ASSIGN(TestObserver);
+};
+
+// Tests -----------------------------------------------------------------------
+
+TEST(UndoServiceTest, AddUndoActions) {
+ TestUndoService undo_service;
+
+ undo_service.TriggerOperation();
+ undo_service.TriggerOperation();
+ EXPECT_EQ(2U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());
+}
+
+TEST(UndoServiceTest, UndoMultipleActions) {
+ TestUndoService undo_service;
+
+ undo_service.TriggerOperation();
+ undo_service.TriggerOperation();
+
+ undo_service.undo_manager_.Undo();
+ EXPECT_EQ(1U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(1U, undo_service.undo_manager_.redo_count());
+
+ undo_service.undo_manager_.Undo();
+ EXPECT_EQ(0U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(2U, undo_service.undo_manager_.redo_count());
+
+ EXPECT_EQ(2, undo_service.undo_operation_count_);
+ EXPECT_EQ(0, undo_service.redo_operation_count_);
+}
+
+TEST(UndoServiceTest, RedoAction) {
+ TestUndoService undo_service;
+
+ undo_service.TriggerOperation();
+
+ undo_service.undo_manager_.Undo();
+ EXPECT_EQ(0U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(1U, undo_service.undo_manager_.redo_count());
+
+ undo_service.Redo();
+ EXPECT_EQ(1U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());
+
+ EXPECT_EQ(1, undo_service.undo_operation_count_);
+ EXPECT_EQ(1, undo_service.redo_operation_count_);
+}
+
+TEST(UndoServiceTest, GroupActions) {
+ TestUndoService undo_service;
+
+ // Add two operations in a single action.
+ undo_service.undo_manager_.StartGroupingActions();
+ undo_service.TriggerOperation();
+ undo_service.TriggerOperation();
+ undo_service.undo_manager_.EndGroupingActions();
+
+ // Check that only one action is created.
+ EXPECT_EQ(1U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());
+
+ undo_service.undo_manager_.Undo();
+ EXPECT_EQ(0U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(1U, undo_service.undo_manager_.redo_count());
+
+ undo_service.Redo();
+ EXPECT_EQ(1U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());
+
+ // Check that both operations were called in Undo and Redo.
+ EXPECT_EQ(2, undo_service.undo_operation_count_);
+ EXPECT_EQ(2, undo_service.redo_operation_count_);
+}
+
+TEST(UndoServiceTest, SuspendUndoTracking) {
+ TestUndoService undo_service;
+
+ undo_service.undo_manager_.SuspendUndoTracking();
+ EXPECT_TRUE(undo_service.undo_manager_.IsUndoTrakingSuspended());
+
+ undo_service.TriggerOperation();
+
+ undo_service.undo_manager_.ResumeUndoTracking();
+ EXPECT_FALSE(undo_service.undo_manager_.IsUndoTrakingSuspended());
+
+ EXPECT_EQ(0U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());
+}
+
+TEST(UndoServiceTest, RedoEmptyAfterNewAction) {
+ TestUndoService undo_service;
+
+ undo_service.TriggerOperation();
+ undo_service.undo_manager_.Undo();
+ EXPECT_EQ(0U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(1U, undo_service.undo_manager_.redo_count());
+
+ undo_service.TriggerOperation();
+ EXPECT_EQ(1U, undo_service.undo_manager_.undo_count());
+ EXPECT_EQ(0U, undo_service.undo_manager_.redo_count());
+}
+
+TEST(UndoServiceTest, GetAllUndoOperations) {
+ TestUndoService undo_service;
+
+ undo_service.TriggerOperation();
+
+ undo_service.undo_manager_.StartGroupingActions();
+ undo_service.TriggerOperation();
+ undo_service.TriggerOperation();
+ undo_service.undo_manager_.EndGroupingActions();
+
+ undo_service.TriggerOperation();
+
+ undo_service.undo_manager_.Undo();
+ ASSERT_EQ(2U, undo_service.undo_manager_.undo_count());
+ ASSERT_EQ(1U, undo_service.undo_manager_.redo_count());
+
+ std::vector<UndoOperation*> all_operations =
+ undo_service.undo_manager_.GetAllUndoOperations();
+ EXPECT_EQ(4U, all_operations.size());
+}
+
+TEST(UndoServiceTest, ObserverCallbacks) {
+ TestObserver observer;
+ TestUndoService undo_service;
+ undo_service.undo_manager_.AddObserver(&observer);
+ EXPECT_EQ(0, observer.state_change_count());
+
+ undo_service.TriggerOperation();
+ EXPECT_EQ(1, observer.state_change_count());
+
+ undo_service.undo_manager_.StartGroupingActions();
+ undo_service.TriggerOperation();
+ undo_service.TriggerOperation();
+ undo_service.undo_manager_.EndGroupingActions();
+ EXPECT_EQ(2, observer.state_change_count());
+
+ // There should be at least 1 observer callback for undo.
+ undo_service.undo_manager_.Undo();
+ int callback_count_after_undo = observer.state_change_count();
+ EXPECT_GT(callback_count_after_undo, 2);
+
+ // There should be at least 1 observer callback for redo.
+ undo_service.undo_manager_.Redo();
+ int callback_count_after_redo = observer.state_change_count();
+ EXPECT_GT(callback_count_after_redo, callback_count_after_undo);
+
+ undo_service.undo_manager_.RemoveObserver(&observer);
+ undo_service.undo_manager_.Undo();
+ EXPECT_EQ(callback_count_after_redo, observer.state_change_count());
+}
+
+} // namespace
diff --git a/components/undo/undo_operation.h b/components/undo/undo_operation.h
new file mode 100644
index 0000000..b78f103
--- /dev/null
+++ b/components/undo/undo_operation.h
@@ -0,0 +1,26 @@
+// Copyright 2013 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 COMPONENTS_UNDO_UNDO_OPERATION_H_
+#define COMPONENTS_UNDO_UNDO_OPERATION_H_
+
+// Base class for all undo operations.
+class UndoOperation {
+ public:
+ virtual ~UndoOperation() {}
+
+ virtual void Undo() = 0;
+
+ // Returns the resource string id describing the undo/redo of this operation
+ // for use as labels in the UI.
+ // Note: The labels describe the original user action, this may result in
+ // the meaning of the redo label being reversed. For example, an
+ // UndoOperation representing a deletion would have been created in order to
+ // redo an addition by the user. In this case, the redo label string for the
+ // UndoOperation of delete would be "Redo add".
+ virtual int GetUndoLabelId() const = 0;
+ virtual int GetRedoLabelId() const = 0;
+};
+
+#endif // COMPONENTS_UNDO_UNDO_OPERATION_H_