blob: 916ca78a6b090db8f8dd9876fcd64e5a10c3487b [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/clipboard/clipboard_history_resource_manager.h"
#include <string>
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/clipboard/clipboard_history_url_title_fetcher.h"
#include "ash/constants/ash_features.h"
#include "ash/display/display_util.h"
#include "ash/public/cpp/clipboard_image_model_factory.h"
#include "ash/public/cpp/window_tree_host_lookup.h"
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_util.h"
#include "chromeos/crosapi/mojom/clipboard_history.mojom.h"
#include "chromeos/ui/clipboard_history/clipboard_history_util.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/display/screen.h"
namespace ash {
// ClipboardHistoryResourceManager ---------------------------------------------
ClipboardHistoryResourceManager::ClipboardHistoryResourceManager(
ClipboardHistory* clipboard_history)
: clipboard_history_(clipboard_history) {
clipboard_history_->AddObserver(this);
}
ClipboardHistoryResourceManager::~ClipboardHistoryResourceManager() {
clipboard_history_->RemoveObserver(this);
if (ClipboardImageModelFactory::Get())
ClipboardImageModelFactory::Get()->OnShutdown();
}
void ClipboardHistoryResourceManager::AddObserver(Observer* observer) const {
observers_.AddObserver(observer);
}
void ClipboardHistoryResourceManager::RemoveObserver(Observer* observer) const {
observers_.RemoveObserver(observer);
}
ClipboardHistoryResourceManager::ImageModelRequest::ImageModelRequest() =
default;
ClipboardHistoryResourceManager::ImageModelRequest::ImageModelRequest(
const ImageModelRequest& other) = default;
ClipboardHistoryResourceManager::ImageModelRequest&
ClipboardHistoryResourceManager::ImageModelRequest::operator=(
const ImageModelRequest&) = default;
ClipboardHistoryResourceManager::ImageModelRequest::~ImageModelRequest() =
default;
void ClipboardHistoryResourceManager::MaybeQueryUrlTitle(
const ClipboardHistoryItem& item) {
// `url_title_fetcher` may be null in tests.
if (auto* const url_title_fetcher = ClipboardHistoryUrlTitleFetcher::Get();
url_title_fetcher &&
chromeos::clipboard_history::IsUrl(item.display_text())) {
url_title_fetcher->QueryHistory(
GURL(item.display_text()),
base::BindOnce(&ClipboardHistoryResourceManager::OnHistoryQueryComplete,
weak_factory_.GetWeakPtr(), item.id()));
}
}
void ClipboardHistoryResourceManager::OnHistoryQueryComplete(
const base::UnguessableToken& item_id,
std::optional<std::u16string> maybe_title) {
auto& items = clipboard_history_->GetItems();
auto item = base::ranges::find(items, item_id, &ClipboardHistoryItem::id);
if (item == items.end()) {
return;
}
if (maybe_title) {
base::TrimWhitespace(*maybe_title, base::TRIM_ALL, &(*maybe_title));
if (maybe_title->empty()) {
// If the retrieved title was empty or consisted of only whitespace, the
// item has nothing to display as secondary text.
maybe_title.reset();
}
}
item->set_secondary_display_text(maybe_title);
}
void ClipboardHistoryResourceManager::SetOrRequestHtmlPreview(
const ClipboardHistoryItem& item) {
auto& items = clipboard_history_->GetItems();
// See if we have an `existing` item that will render the same as `item`.
auto it = base::ranges::find_if(items, [&](const auto& existing) {
return &existing != &item &&
existing.display_format() ==
crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml &&
existing.data().markup_data() == item.data().markup_data();
});
// If no existing item will render the same as `item`, create a new request to
// render an HTML preview for `item`. Note that the image model factory may
// or may not start rendering immediately depending on its activation status.
if (it == items.end()) {
base::UnguessableToken id = base::UnguessableToken::Create();
ImageModelRequest image_model_request;
image_model_request.id = id;
image_model_request.clipboard_history_item_ids.push_back(item.id());
image_model_requests_.push_back(std::move(image_model_request));
// `image_model_factory` can be nullptr in tests.
auto* image_model_factory = ClipboardImageModelFactory::Get();
if (!image_model_factory) {
return;
}
// `text_input_client` can be nullptr in tests.
const auto* text_input_client =
ash::GetWindowTreeHostForDisplay(
display::Screen::GetScreen()->GetPrimaryDisplay().id())
->GetInputMethod()
->GetTextInputClient();
const gfx::Rect bounding_box =
text_input_client ? text_input_client->GetSelectionBoundingBox()
: gfx::Rect();
image_model_factory->Render(
id, item.data().markup_data(),
IsRectContainedByAnyDisplay(bounding_box) ? bounding_box.size()
: gfx::Size(),
base::BindOnce(&ClipboardHistoryResourceManager::OnImageModelRendered,
weak_factory_.GetWeakPtr(), id));
return;
}
// If there is an existing item that will render the same as `item`, check
// whether the existing item's preview has rendered.
auto image_model_request = GetImageModelRequestForItem(*it);
if (image_model_request != image_model_requests_.end()) {
// If rendering is still in progress, just note that `item` will need to
// hear about the result as well.
image_model_request->clipboard_history_item_ids.push_back(item.id());
} else {
// If rendering has finished, set `item` to have the same preview.
auto mutable_item =
base::ranges::find(items, item.id(), &ClipboardHistoryItem::id);
DCHECK(mutable_item != items.end());
const auto& existing_preview = it->display_image();
DCHECK(existing_preview.has_value());
mutable_item->SetDisplayImage(existing_preview.value());
}
}
void ClipboardHistoryResourceManager::OnImageModelRendered(
const base::UnguessableToken& id,
ui::ImageModel image_model) {
auto image_model_request = base::ranges::find(
image_model_requests_, id,
&ClipboardHistoryResourceManager::ImageModelRequest::id);
if (image_model_request == image_model_requests_.end()) {
return;
}
// Set the HTML preview for each item attached to `id`'s request.
for (auto& item : clipboard_history_->GetItems()) {
if (!base::Contains(image_model_request->clipboard_history_item_ids,
item.id())) {
continue;
}
DCHECK(item.display_image().has_value());
if (item.display_image().value() != image_model) {
item.SetDisplayImage(image_model);
}
}
for (auto& observer : observers_) {
observer.OnCachedImageModelUpdated(
image_model_request->clipboard_history_item_ids);
}
image_model_requests_.erase(image_model_request);
}
void ClipboardHistoryResourceManager::CancelUnfinishedRequests() {
for (const auto& image_model_request : image_model_requests_) {
ClipboardImageModelFactory::Get()->CancelRequest(image_model_request.id);
}
}
std::vector<ClipboardHistoryResourceManager::ImageModelRequest>::iterator
ClipboardHistoryResourceManager::GetImageModelRequestForItem(
const ClipboardHistoryItem& item) {
return base::ranges::find_if(
image_model_requests_, [&](const auto& image_model_request) {
return base::Contains(image_model_request.clipboard_history_item_ids,
item.id());
});
}
void ClipboardHistoryResourceManager::OnClipboardHistoryItemAdded(
const ClipboardHistoryItem& item,
bool is_duplicate) {
if (item.display_format() ==
crosapi::mojom::ClipboardHistoryDisplayFormat::kText &&
features::IsClipboardHistoryUrlTitlesEnabled()) {
// An item being re-copied might need its URL title changed based on updates
// to the user's browsing history.
MaybeQueryUrlTitle(item);
} else if (item.display_format() ==
crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml &&
!is_duplicate) {
// If an item is being copied for the first time, we begin rendering its
// HTML preview as soon as possible.
SetOrRequestHtmlPreview(item);
}
}
void ClipboardHistoryResourceManager::OnClipboardHistoryItemRemoved(
const ClipboardHistoryItem& item) {
// For items that will not be represented by their rendered HTML, do nothing.
if (item.display_format() !=
crosapi::mojom::ClipboardHistoryDisplayFormat::kHtml) {
return;
}
// If the item's image model request has already finished, there is nothing
// more to do.
auto image_model_request = GetImageModelRequestForItem(item);
if (image_model_request == image_model_requests_.end()) {
return;
}
// If `item` was attached to a pending request, make sure it is not updated
// when rendering finishes.
base::Erase(image_model_request->clipboard_history_item_ids, item.id());
if (image_model_request->clipboard_history_item_ids.empty()) {
// If no more items are waiting on the image model, cancel the request.
ClipboardImageModelFactory::Get()->CancelRequest(image_model_request->id);
image_model_requests_.erase(image_model_request);
}
}
void ClipboardHistoryResourceManager::OnClipboardHistoryCleared() {
CancelUnfinishedRequests();
image_model_requests_.clear();
}
} // namespace ash