blob: 611ec7f61943a72eb6f7933fde02861c4a9e6124 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/bookmarks/bookmark_ui_operations_helper.h"
#include <cstddef>
#include <unordered_set>
#include "base/files/file_path.h"
#include "base/i18n/case_conversion.h"
#include "base/metrics/histogram_functions.h"
#include "base/notreached.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/bookmarks/bookmark_merged_surface_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/bookmarks/bookmark_drag_drop.h"
#include "chrome/browser/ui/bookmarks/bookmark_stats.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_node.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 "ui/base/clipboard/clipboard.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
using ::bookmarks::BookmarkModel;
using ::bookmarks::BookmarkNode;
using ::bookmarks::BookmarkNodeData;
using ::chrome::BookmarkReorderDropTarget;
using ::ui::mojom::DragOperation;
namespace {
// Returns the URL from the clipboard. If there is no URL an empty URL is
// returned.
GURL GetUrlFromClipboard(bool notify_if_restricted) {
std::u16string url_text;
ui::DataTransferEndpoint data_dst =
ui::DataTransferEndpoint(ui::EndpointType::kDefault,
{.notify_if_restricted = notify_if_restricted});
ui::Clipboard::GetForCurrentThread()->ReadText(
ui::ClipboardBuffer::kCopyPaste, &data_dst, &url_text);
return GURL(url_text);
}
// This traces node up to root, determines if it is a descendant of one of
// selected nodes.
bool HasAncestorInSelectedNodes(
BookmarkModel* model,
const std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>&
selected_nodes,
const BookmarkNode* node) {
std::unordered_set selected_nodes_set(selected_nodes.begin(),
selected_nodes.end());
const BookmarkNode* current_node = node;
while (current_node != nullptr) {
// Ancestor of `node` is already is the selected nodes. Copying the ancestor
// node, will include all descendants.
if (selected_nodes_set.find(current_node) != selected_nodes_set.end()) {
return true;
}
current_node = current_node->parent();
}
return false;
}
} // namespace
namespace internal {
BookmarkUIOperationsHelper::TargetParent::~TargetParent() = default;
BookmarkUIOperationsHelper::~BookmarkUIOperationsHelper() = default;
ui::mojom::DragOperation BookmarkUIOperationsHelper::DropBookmarks(
Profile* profile,
const bookmarks::BookmarkNodeData& data,
size_t index,
bool copy,
chrome::BookmarkReorderDropTarget target,
Browser* browser) {
CHECK(target_parent());
CHECK(!target_parent()->IsManaged());
if (!data.IsFromProfilePath(profile->GetPath())) {
RecordBookmarksAdded(profile);
RecordBookmarkDropped(data, target_parent()->IsPermanentNode(), false);
// Dropping a folder from different profile. Always accept.
AddNodesAsCopiesOfNodeData(data, index);
return DragOperation::kCopy;
}
const std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>
dragged_nodes = data.GetNodes(model(), profile->GetPath());
if (dragged_nodes.empty()) {
return DragOperation::kNone;
}
bool is_reorder =
!copy && target_parent()->IsDirectChild(dragged_nodes[0]->parent());
RecordBookmarkDropped(data, target_parent()->IsPermanentNode(), is_reorder);
if (is_reorder) {
base::UmaHistogramEnumeration("Bookmarks.ReorderDropTarget", target);
switch (target_parent()->GetType()) {
case bookmarks::BookmarkNode::URL:
NOTREACHED();
case bookmarks::BookmarkNode::FOLDER:
break;
case bookmarks::BookmarkNode::BOOKMARK_BAR:
base::UmaHistogramEnumeration(
"Bookmarks.ReorderDropTarget.InBookmarkBarNode", target);
break;
case bookmarks::BookmarkNode::OTHER_NODE:
base::UmaHistogramEnumeration(
"Bookmarks.ReorderDropTarget.InOtherBookmarkNode", target);
break;
case bookmarks::BookmarkNode::MOBILE:
base::UmaHistogramEnumeration(
"Bookmarks.ReorderDropTarget.InMobileBookmarkNode", target);
break;
}
}
// Drag from same profile. Copy or move nodes.
if (copy) {
AddNodesAsCopiesOfNodeData(data, index);
return DragOperation::kCopy;
}
MoveBookmarkNodeData(data, profile->GetPath(), index, browser);
return DragOperation::kMove;
}
// static
void BookmarkUIOperationsHelper::CopyToClipboard(
bookmarks::BookmarkModel* model,
const std::vector<
raw_ptr<const bookmarks::BookmarkNode, VectorExperimental>>& nodes,
bookmarks::metrics::BookmarkEditSource source,
bool is_off_the_record) {
CopyOrCutToClipboard(model, nodes, /*remove_nodes=*/false, source,
is_off_the_record);
}
// static
void BookmarkUIOperationsHelper::CutToClipboard(
bookmarks::BookmarkModel* model,
const std::vector<
raw_ptr<const bookmarks::BookmarkNode, VectorExperimental>>& nodes,
bookmarks::metrics::BookmarkEditSource source,
bool is_off_the_record) {
CopyOrCutToClipboard(model, nodes, /*remove_nodes=*/true, source,
is_off_the_record);
}
// static
void BookmarkUIOperationsHelper::CopyOrCutToClipboard(
bookmarks::BookmarkModel* model,
const std::vector<
raw_ptr<const bookmarks::BookmarkNode, VectorExperimental>>& nodes,
bool remove_nodes,
bookmarks::metrics::BookmarkEditSource source,
bool is_off_the_record) {
if (nodes.empty()) {
return;
}
// Create array of selected nodes with descendants filtered out.
std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> filtered_nodes;
for (const BookmarkNode* node : nodes) {
if (!HasAncestorInSelectedNodes(model, nodes, node->parent())) {
filtered_nodes.push_back(node);
}
}
CHECK(!filtered_nodes.empty());
BookmarkNodeData(filtered_nodes).WriteToClipboard(is_off_the_record);
if (remove_nodes) {
bookmarks::ScopedGroupBookmarkActions group_cut(model);
for (const bookmarks::BookmarkNode* node : filtered_nodes) {
model->Remove(node, source, FROM_HERE);
}
}
}
bool BookmarkUIOperationsHelper::CanPasteFromClipboard() const {
if (!target_parent() || target_parent()->IsManaged()) {
return false;
}
return BookmarkNodeData::ClipboardContainsBookmarks() ||
GetUrlFromClipboard(/*notify_if_restricted=*/false).is_valid();
}
void BookmarkUIOperationsHelper::PasteFromClipboard(size_t index) {
if (!target_parent()) {
return;
}
BookmarkNodeData bookmark_data;
if (!bookmark_data.ReadFromClipboard(ui::ClipboardBuffer::kCopyPaste)) {
GURL url = GetUrlFromClipboard(/*notify_if_restricted=*/true);
if (!url.is_valid()) {
return;
}
BookmarkNode node(/*id=*/0, base::Uuid::GenerateRandomV4(), url);
node.SetTitle(base::ASCIIToUTF16(url.spec()));
bookmark_data = BookmarkNodeData(&node);
}
CHECK_LE(index, target_parent()->GetChildrenCount());
if (bookmark_data.size() == 1 &&
model()->IsBookmarked(bookmark_data.elements[0].url)) {
MakeTitleUnique(bookmark_data.elements[0].url,
&bookmark_data.elements[0].title);
}
AddNodesAsCopiesOfNodeData(bookmark_data, index);
}
void BookmarkUIOperationsHelper::MakeTitleUnique(const GURL& url,
std::u16string* title) const {
const TargetParent* parent = target_parent();
CHECK(parent);
std::unordered_set<std::u16string> titles;
std::u16string original_title_lower = base::i18n::ToLower(*title);
const size_t children_size = parent->GetChildrenCount();
for (size_t i = 0; i < children_size; i++) {
const BookmarkNode* node = parent->GetNodeAtIndex(i);
if (node->is_url() && (url == node->url()) &&
base::StartsWith(base::i18n::ToLower(node->GetTitle()),
original_title_lower, base::CompareCase::SENSITIVE)) {
titles.insert(node->GetTitle());
}
}
if (titles.find(*title) == titles.end()) {
return;
}
for (size_t i = 0; i < titles.size(); i++) {
const std::u16string new_title(*title +
base::ASCIIToUTF16(base::StringPrintf(
" (%lu)", (unsigned long)(i + 1))));
if (titles.find(new_title) == titles.end()) {
*title = new_title;
return;
}
}
NOTREACHED();
}
} // namespace internal
// BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent:
// static
std::unique_ptr<BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent>
BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent::CreateTargetParent(
BookmarkModel* model,
const BookmarkNode* parent) {
if (!model || !parent) {
return nullptr;
}
return std::make_unique<TargetParent>(parent,
model->client()->IsNodeManaged(parent));
}
BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent::TargetParent(
const bookmarks::BookmarkNode* parent,
bool is_managed)
: parent_(parent), is_managed_(is_managed) {
CHECK(parent);
}
BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent::~TargetParent() =
default;
const bookmarks::BookmarkNode*
BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent::parent_node() const {
return parent_;
}
bool BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent::IsManaged()
const {
return is_managed_;
}
bool BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent::
IsPermanentNode() const {
return parent_->is_permanent_node();
}
bool BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent::IsDirectChild(
const bookmarks::BookmarkNode* node) const {
return node->parent() == parent_;
}
bookmarks::BookmarkNode::Type
BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent::GetType() const {
return parent_->type();
}
const bookmarks::BookmarkNode*
BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent::GetNodeAtIndex(
size_t index) const {
CHECK_LE(index, GetChildrenCount());
return parent_->children()[index].get();
}
size_t
BookmarkUIOperationsHelperNonMergedSurfaces::TargetParent::GetChildrenCount()
const {
return parent_->children().size();
}
// BookmarkUIOperationsHelperNonMergedSurfaces:
BookmarkUIOperationsHelperNonMergedSurfaces::
BookmarkUIOperationsHelperNonMergedSurfaces(
bookmarks::BookmarkModel* model,
const bookmarks::BookmarkNode* parent)
: model_(model),
target_parent_(TargetParent::CreateTargetParent(model, parent)) {
CHECK(model);
}
BookmarkUIOperationsHelperNonMergedSurfaces::
~BookmarkUIOperationsHelperNonMergedSurfaces() = default;
bookmarks::BookmarkModel* BookmarkUIOperationsHelperNonMergedSurfaces::model() {
return model_;
}
void BookmarkUIOperationsHelperNonMergedSurfaces::AddNodesAsCopiesOfNodeData(
const bookmarks::BookmarkNodeData& data,
size_t index_to_add_at) {
CHECK(parent_node());
bookmarks::ScopedGroupBookmarkActions group_drops(model_);
bookmarks::CloneBookmarkNode(model_, data.elements, parent_node(),
index_to_add_at,
/*reset_node_times=*/true);
}
void BookmarkUIOperationsHelperNonMergedSurfaces::MoveBookmarkNodeData(
const bookmarks::BookmarkNodeData& data,
const base::FilePath& profile_path,
size_t index_to_add_at,
Browser* browser) {
CHECK(parent_node());
const std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>
moved_nodes = data.GetNodes(model_, profile_path);
CHECK(!moved_nodes.empty());
for (const auto& moved_node : moved_nodes) {
CHECK(!model_->client()->IsNodeManaged(moved_node));
model_->Move(moved_node, parent_node(), index_to_add_at);
index_to_add_at = parent_node()->GetIndexOf(moved_node).value() + 1;
}
}
const internal::BookmarkUIOperationsHelper::TargetParent*
BookmarkUIOperationsHelperNonMergedSurfaces::target_parent() const {
if (!target_parent_) {
return nullptr;
}
return target_parent_.get();
}
const bookmarks::BookmarkNode*
BookmarkUIOperationsHelperNonMergedSurfaces::parent_node() const {
if (!target_parent_) {
return nullptr;
}
return target_parent_->parent_node();
}
// BookmarkUIOperationsHelperMergedSurfaces::TargetParent:
// static
std::unique_ptr<BookmarkUIOperationsHelperMergedSurfaces::TargetParent>
BookmarkUIOperationsHelperMergedSurfaces::TargetParent::CreateTargetParent(
BookmarkMergedSurfaceService* merged_surface_service,
const BookmarkParentFolder* parent) {
if (!merged_surface_service || !parent) {
return nullptr;
}
return std::make_unique<TargetParent>(merged_surface_service, parent);
}
BookmarkUIOperationsHelperMergedSurfaces::TargetParent::TargetParent(
BookmarkMergedSurfaceService* merged_surface_service,
const BookmarkParentFolder* parent)
: merged_surface_service_(merged_surface_service), parent_(parent) {
CHECK(parent);
}
BookmarkUIOperationsHelperMergedSurfaces::TargetParent::~TargetParent() =
default;
const BookmarkParentFolder*
BookmarkUIOperationsHelperMergedSurfaces::TargetParent::parent_folder() const {
return parent_;
}
bool BookmarkUIOperationsHelperMergedSurfaces::TargetParent::IsManaged() const {
return merged_surface_service_->IsParentFolderManaged(*parent_);
}
bool BookmarkUIOperationsHelperMergedSurfaces::TargetParent::IsPermanentNode()
const {
return !parent_->HoldsNonPermanentFolder();
}
bool BookmarkUIOperationsHelperMergedSurfaces::TargetParent::IsDirectChild(
const bookmarks::BookmarkNode* node) const {
return parent_->HasDirectChildNode(node);
}
bookmarks::BookmarkNode::Type
BookmarkUIOperationsHelperMergedSurfaces::TargetParent::GetType() const {
if (parent_->HoldsNonPermanentFolder()) {
return parent_->as_non_permanent_folder()->type();
}
switch (*parent_->as_permanent_folder()) {
case BookmarkParentFolder::PermanentFolderType::kBookmarkBarNode:
return bookmarks::BookmarkNode::Type::BOOKMARK_BAR;
case BookmarkParentFolder::PermanentFolderType::kOtherNode:
return bookmarks::BookmarkNode::Type::OTHER_NODE;
case BookmarkParentFolder::PermanentFolderType::kMobileNode:
return bookmarks::BookmarkNode::Type::MOBILE;
case BookmarkParentFolder::PermanentFolderType::kManagedNode:
// There is no specific type for managed nodes.
return bookmarks::BookmarkNode::Type::FOLDER;
}
NOTREACHED();
}
const bookmarks::BookmarkNode*
BookmarkUIOperationsHelperMergedSurfaces::TargetParent::GetNodeAtIndex(
size_t index) const {
CHECK(parent_);
CHECK_LE(index, GetChildrenCount());
return merged_surface_service_->GetNodeAtIndex(*parent_, index);
}
size_t
BookmarkUIOperationsHelperMergedSurfaces::TargetParent::GetChildrenCount()
const {
CHECK(parent_);
return merged_surface_service_->GetChildrenCount(*parent_);
}
// BookmarkUIOperationsHelperMergedSurfaces:
BookmarkUIOperationsHelperMergedSurfaces::
BookmarkUIOperationsHelperMergedSurfaces(
BookmarkMergedSurfaceService* merged_surface_service,
const BookmarkParentFolder* parent)
: merged_surface_service_(merged_surface_service),
target_parent_(
TargetParent::CreateTargetParent(merged_surface_service, parent)) {
CHECK(merged_surface_service);
}
BookmarkUIOperationsHelperMergedSurfaces::
~BookmarkUIOperationsHelperMergedSurfaces() = default;
const BookmarkNode*
BookmarkUIOperationsHelperMergedSurfaces::GetDefaultParentForNonMergedSurfaces()
const {
CHECK(parent_folder());
if (target_parent_->IsManaged()) {
return merged_surface_service_->GetParentForManagedNode(*parent_folder());
}
return merged_surface_service_->GetDefaultParentForNewNodes(*parent_folder());
}
bookmarks::BookmarkModel* BookmarkUIOperationsHelperMergedSurfaces::model() {
return merged_surface_service_->bookmark_model();
}
void BookmarkUIOperationsHelperMergedSurfaces::AddNodesAsCopiesOfNodeData(
const bookmarks::BookmarkNodeData& data,
size_t index_to_add_at) {
bookmarks::ScopedGroupBookmarkActions group_drops(model());
merged_surface_service_->AddNodesAsCopiesOfNodeData(
data.elements, *parent_folder(), index_to_add_at);
}
void BookmarkUIOperationsHelperMergedSurfaces::MoveBookmarkNodeData(
const bookmarks::BookmarkNodeData& data,
const base::FilePath& profile_path,
size_t index_to_add_at,
Browser* browser) {
CHECK_GE(data.size(), 1u);
const std::vector<raw_ptr<const BookmarkNode, VectorExperimental>>
moved_nodes = data.GetNodes(model(), profile_path);
CHECK(!moved_nodes.empty());
for (const auto& moved_node : moved_nodes) {
CHECK(!model()->client()->IsNodeManaged(moved_node));
merged_surface_service_->Move(moved_node, *parent_folder(), index_to_add_at,
browser);
index_to_add_at++;
}
}
const internal::BookmarkUIOperationsHelper::TargetParent*
BookmarkUIOperationsHelperMergedSurfaces::target_parent() const {
if (!target_parent_) {
return nullptr;
}
return target_parent_.get();
}
const BookmarkParentFolder*
BookmarkUIOperationsHelperMergedSurfaces::parent_folder() const {
if (!target_parent_) {
return nullptr;
}
return target_parent_->parent_folder();
}