blob: f93b03ad62e88891988a45bafbf4a3f76a157c7e [file] [log] [blame]
// Copyright 2013 The Chromium Authors
// 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 <stddef.h>
#include <stdint.h>
#include <cstdint>
#include <memory>
#include <utility>
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "components/bookmarks/browser/bookmark_node.h"
#include "components/bookmarks/browser/bookmark_node_data.h"
#include "components/bookmarks/browser/bookmark_undo_provider.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/bookmarks/browser/scoped_group_bookmark_actions.h"
#include "components/bookmarks/common/bookmark_metrics.h"
#include "components/strings/grit/components_strings.h"
#include "components/undo/undo_operation.h"
using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;
using bookmarks::BookmarkNodeData;
using bookmarks::BookmarkUndoProvider;
namespace {
// TODO(crbug.com/346918509): Remove this function once the migration of iOS to
// a single BookmarkModel instance is complete.
BookmarkModel* DetermineModelForNode(
const BookmarkNode* node,
const std::vector<raw_ptr<BookmarkModel>>& candidate_models) {
CHECK(node);
const BookmarkNode* root_node = node;
while (root_node->parent()) {
root_node = root_node->parent();
}
for (BookmarkModel* model : candidate_models) {
if (model->root_node() == root_node) {
return model;
}
}
NOTREACHED_NORETURN();
}
// BookmarkUndoOperation ------------------------------------------------------
// Base class for all bookmark related UndoOperations that facilitates access to
// the BookmarkUndoService.
class BookmarkUndoOperation : public UndoOperation {
public:
explicit BookmarkUndoOperation(BookmarkModel* bookmark_model)
: bookmark_model_(bookmark_model) {}
~BookmarkUndoOperation() override = default;
BookmarkModel* bookmark_model() { return bookmark_model_; }
private:
raw_ptr<BookmarkModel, DanglingUntriaged> bookmark_model_;
};
// BookmarkAddOperation -------------------------------------------------------
// Handles the undo of the insertion of a bookmark or folder.
class BookmarkAddOperation : public BookmarkUndoOperation {
public:
BookmarkAddOperation(BookmarkModel* bookmark_model,
const BookmarkNode* parent,
size_t index);
BookmarkAddOperation(const BookmarkAddOperation&) = delete;
BookmarkAddOperation& operator=(const BookmarkAddOperation&) = delete;
~BookmarkAddOperation() override = default;
// UndoOperation:
void Undo() override;
int GetUndoLabelId() const override;
int GetRedoLabelId() const override;
private:
int64_t parent_id_;
const size_t index_;
};
BookmarkAddOperation::BookmarkAddOperation(BookmarkModel* bookmark_model,
const BookmarkNode* parent,
size_t index)
: BookmarkUndoOperation(bookmark_model),
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->children()[index_].get(),
bookmarks::metrics::BookmarkEditSource::kUser, FROM_HERE);
}
int BookmarkAddOperation::GetUndoLabelId() const {
return IDS_BOOKMARK_BAR_UNDO_ADD;
}
int BookmarkAddOperation::GetRedoLabelId() const {
return IDS_BOOKMARK_BAR_REDO_DELETE;
}
// 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* model,
const BookmarkNode* parent,
size_t index,
std::unique_ptr<BookmarkNode> node);
BookmarkRemoveOperation(const BookmarkRemoveOperation&) = delete;
BookmarkRemoveOperation& operator=(const BookmarkRemoveOperation&) = delete;
~BookmarkRemoveOperation() override;
// UndoOperation:
void Undo() override;
int GetUndoLabelId() const override;
int GetRedoLabelId() const override;
private:
const int64_t parent_node_id_;
const size_t index_;
std::unique_ptr<BookmarkNode> node_;
};
BookmarkRemoveOperation::BookmarkRemoveOperation(
BookmarkModel* model,
const BookmarkNode* parent,
size_t index,
std::unique_ptr<BookmarkNode> node)
: BookmarkUndoOperation(model),
parent_node_id_(parent->id()),
index_(index),
node_(std::move(node)) {}
BookmarkRemoveOperation::~BookmarkRemoveOperation() = default;
void BookmarkRemoveOperation::Undo() {
DCHECK(node_);
const BookmarkNode* parent = bookmarks::GetBookmarkNodeByID(
bookmark_model(), parent_node_id_);
DCHECK(parent);
static_cast<BookmarkUndoProvider*>(bookmark_model())
->RestoreRemovedNode(parent, index_, std::move(node_));
}
int BookmarkRemoveOperation::GetUndoLabelId() const {
return IDS_BOOKMARK_BAR_UNDO_DELETE;
}
int BookmarkRemoveOperation::GetRedoLabelId() const {
return IDS_BOOKMARK_BAR_REDO_ADD;
}
// BookmarkEditOperation ------------------------------------------------------
// Handles the undo of the modification of a bookmark node.
class BookmarkEditOperation : public BookmarkUndoOperation {
public:
BookmarkEditOperation(BookmarkModel* bookmark_model,
const BookmarkNode* node);
BookmarkEditOperation(const BookmarkEditOperation&) = delete;
BookmarkEditOperation& operator=(const BookmarkEditOperation&) = delete;
~BookmarkEditOperation() override = default;
// UndoOperation:
void Undo() override;
int GetUndoLabelId() const override;
int GetRedoLabelId() const override;
private:
int64_t node_id_;
BookmarkNodeData original_bookmark_;
};
BookmarkEditOperation::BookmarkEditOperation(
BookmarkModel* bookmark_model,
const BookmarkNode* node)
: BookmarkUndoOperation(bookmark_model),
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,
bookmarks::metrics::BookmarkEditSource::kOther);
if (original_bookmark_.elements[0].is_url)
model->SetURL(node, original_bookmark_.elements[0].url,
bookmarks::metrics::BookmarkEditSource::kOther);
}
int BookmarkEditOperation::GetUndoLabelId() const {
return IDS_BOOKMARK_BAR_UNDO_EDIT;
}
int BookmarkEditOperation::GetRedoLabelId() const {
return IDS_BOOKMARK_BAR_REDO_EDIT;
}
// BookmarkMoveOperation ------------------------------------------------------
// Handles the undo of a bookmark being moved to a new location.
class BookmarkMoveOperation : public BookmarkUndoOperation {
public:
BookmarkMoveOperation(BookmarkModel* bookmark_model,
const BookmarkNode* old_parent,
size_t old_index,
const BookmarkNode* new_parent,
size_t new_index);
BookmarkMoveOperation(const BookmarkMoveOperation&) = delete;
BookmarkMoveOperation& operator=(const BookmarkMoveOperation&) = delete;
~BookmarkMoveOperation() override = default;
int GetUndoLabelId() const override;
int GetRedoLabelId() const override;
// UndoOperation:
void Undo() override;
private:
int64_t old_parent_id_;
int64_t new_parent_id_;
size_t old_index_;
size_t new_index_;
};
BookmarkMoveOperation::BookmarkMoveOperation(BookmarkModel* bookmark_model,
const BookmarkNode* old_parent,
size_t old_index,
const BookmarkNode* new_parent,
size_t new_index)
: BookmarkUndoOperation(bookmark_model),
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->children()[new_index_].get();
size_t 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;
}
// 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,
const BookmarkNode* parent);
BookmarkReorderOperation(const BookmarkReorderOperation&) = delete;
BookmarkReorderOperation& operator=(const BookmarkReorderOperation&) = delete;
~BookmarkReorderOperation() override;
// UndoOperation:
void Undo() override;
int GetUndoLabelId() const override;
int GetRedoLabelId() const override;
private:
int64_t parent_id_;
std::vector<int64_t> ordered_bookmarks_;
};
BookmarkReorderOperation::BookmarkReorderOperation(
BookmarkModel* bookmark_model,
const BookmarkNode* parent)
: BookmarkUndoOperation(bookmark_model),
parent_id_(parent->id()) {
ordered_bookmarks_.resize(parent->children().size());
base::ranges::transform(parent->children(), ordered_bookmarks_.begin(),
&BookmarkNode::id);
}
BookmarkReorderOperation::~BookmarkReorderOperation() = default;
void BookmarkReorderOperation::Undo() {
BookmarkModel* model = bookmark_model();
const BookmarkNode* parent =
bookmarks::GetBookmarkNodeByID(model, parent_id_);
DCHECK(parent);
std::vector<const BookmarkNode*> ordered_nodes;
for (int64_t ordered_bookmark : ordered_bookmarks_) {
ordered_nodes.push_back(
bookmarks::GetBookmarkNodeByID(model, ordered_bookmark));
}
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;
}
} // namespace
// BookmarkUndoService --------------------------------------------------------
BookmarkUndoService::BookmarkUndoService() = default;
BookmarkUndoService::~BookmarkUndoService() = default;
void BookmarkUndoService::StartObservingBookmarkModel(BookmarkModel* model) {
DCHECK(!scoped_observations_.IsObservingSource(model));
scoped_observations_.AddObservation(model);
observed_models_.insert(model);
}
void BookmarkUndoService::Shutdown() {
// After `RemoveAllObservations` call below - this instance won't be notified
// of `BookmarkModel` destruction. Undo operations keep a pointer to
// `BookmarkModel` - delete them to avoid dangling pointers.
undo_manager_.RemoveAllOperations();
scoped_observations_.RemoveAllObservations();
observed_models_.clear();
}
void BookmarkUndoService::BookmarkModelBeingDeleted() {
// Delete all undo operations to avoid dangling pointers to `BookmarkModel`
// that is getting destroyed. `BookmarkModel` is a `KeyedService`, so it is
// destroyed during shutdown along with other services - other `BookmarkModel`
// objects and `BookmarkUndoService` itself will be destroyed soon.
Shutdown();
}
void BookmarkUndoService::BookmarkNodeMoved(const BookmarkNode* old_parent,
size_t old_index,
const BookmarkNode* new_parent,
size_t new_index) {
BookmarkModel* model =
DetermineModelForNode(new_parent, scoped_observations_.sources());
CHECK_EQ(model,
DetermineModelForNode(old_parent, scoped_observations_.sources()));
std::unique_ptr<UndoOperation> op(new BookmarkMoveOperation(
model, old_parent, old_index, new_parent, new_index));
undo_manager()->AddUndoOperation(std::move(op));
}
void BookmarkUndoService::BookmarkNodeAdded(const BookmarkNode* parent,
size_t index,
bool added_by_user) {
BookmarkModel* model =
DetermineModelForNode(parent, scoped_observations_.sources());
std::unique_ptr<UndoOperation> op(
new BookmarkAddOperation(model, parent, index));
undo_manager()->AddUndoOperation(std::move(op));
}
void BookmarkUndoService::OnWillChangeBookmarkNode(const BookmarkNode* node) {
BookmarkModel* model =
DetermineModelForNode(node, scoped_observations_.sources());
std::unique_ptr<UndoOperation> op(new BookmarkEditOperation(model, node));
undo_manager()->AddUndoOperation(std::move(op));
}
void BookmarkUndoService::OnWillReorderBookmarkNode(const BookmarkNode* node) {
BookmarkModel* model =
DetermineModelForNode(node, scoped_observations_.sources());
std::unique_ptr<UndoOperation> op(new BookmarkReorderOperation(model, node));
undo_manager()->AddUndoOperation(std::move(op));
}
void BookmarkUndoService::GroupedBookmarkChangesBeginning() {
undo_manager()->StartGroupingActions();
}
void BookmarkUndoService::GroupedBookmarkChangesEnded() {
undo_manager()->EndGroupingActions();
}
void BookmarkUndoService::AddUndoEntryForRemovedNode(
BookmarkModel* model,
const BookmarkNode* parent,
size_t index,
std::unique_ptr<BookmarkNode> node) {
CHECK_EQ(model,
DetermineModelForNode(parent, scoped_observations_.sources()));
// `model` is guaranteed to outlive `BookmarkRemoveOperation`, since all undo
// operations are deleted whenever `BookmarkModelBeingDeleted` or `Shutdown`
// are invoked.
std::unique_ptr<UndoOperation> op(
new BookmarkRemoveOperation(model, parent, index, std::move(node)));
undo_manager()->AddUndoOperation(std::move(op));
}