Commit f58dffe3 authored by Alex Newcomer's avatar Alex Newcomer Committed by Commit Bot

cros: Rendering HTML for ClipboardHistory initial CL

see go/render-html-multipaste for the design doc.

This is the first CL to render HTML in multipaste.

New objects:
ClipboardHistoryResourceManager
[stub]ClipboardHistoryImageModelFactory/Impl
 - The next CL will include implementation of FactoryImpl.

Changes:
 - ClipboardHistory now has observers which are notified on
   things added/removed from history.
 - ClipboardData is wrapped by ClipboardHistoryItem
   - This has an ID to identify different ClipboardHistoryItems
     quickly.
 - ClipboardHistoryHelper -> ClipboardHistoryResourceManager.
   - CHResourceManager serves as a cache for ImageModels which will
     be rendered by ClipboardImageModelFactory.

Bug: 1117230
Change-Id: Ia73d284eb3481525bc04bb04622801eda3c7a8d8
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2363023
Commit-Queue: Alex Newcomer <newcomer@chromium.org>
Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Reviewed-by: default avatarDavid Black <dmblack@google.com>
Cr-Commit-Position: refs/heads/master@{#800337}
parent 9c284879
......@@ -276,10 +276,12 @@ component("ash") {
"clipboard/clipboard_history.h",
"clipboard/clipboard_history_controller.cc",
"clipboard/clipboard_history_controller.h",
"clipboard/clipboard_history_helper.cc",
"clipboard/clipboard_history_helper.h",
"clipboard/clipboard_history_item.cc",
"clipboard/clipboard_history_item.h",
"clipboard/clipboard_history_menu_model_adapter.cc",
"clipboard/clipboard_history_menu_model_adapter.h",
"clipboard/clipboard_history_resource_manager.cc",
"clipboard/clipboard_history_resource_manager.h",
"dbus/ash_dbus_services.cc",
"dbus/ash_dbus_services.h",
"dbus/display_service_provider.cc",
......@@ -1816,7 +1818,7 @@ test("ash_unittests") {
"autoclick/autoclick_drag_event_rewriter_unittest.cc",
"autoclick/autoclick_unittest.cc",
"capture_mode/capture_mode_unittests.cc",
"clipboard/clipboard_history_helper_unittest.cc",
"clipboard/clipboard_history_resource_manager_unittest.cc",
"clipboard/clipboard_history_unittest.cc",
"dbus/gesture_properties_service_provider_unittest.cc",
"dbus/url_handler_service_provider_unittest.cc",
......
......@@ -4,7 +4,6 @@
#include "ash/clipboard/clipboard_history.h"
#include "ash/clipboard/clipboard_history_controller.h"
#include "base/stl_util.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "ui/base/clipboard/clipboard_monitor.h"
......@@ -35,12 +34,22 @@ ClipboardHistory::~ClipboardHistory() {
ui::ClipboardMonitor::GetInstance()->RemoveObserver(this);
}
const std::list<ui::ClipboardData>& ClipboardHistory::GetItems() const {
void ClipboardHistory::AddObserver(Observer* observer) const {
observers_.AddObserver(observer);
}
void ClipboardHistory::RemoveObserver(Observer* observer) const {
observers_.RemoveObserver(observer);
}
const std::list<ClipboardHistoryItem>& ClipboardHistory::GetItems() const {
return history_list_;
}
void ClipboardHistory::Clear() {
history_list_ = std::list<ui::ClipboardData>();
history_list_ = std::list<ClipboardHistoryItem>();
for (auto& observer : observers_)
observer.OnClipboardHistoryCleared();
}
bool ClipboardHistory::IsEmpty() const {
......@@ -75,9 +84,16 @@ void ClipboardHistory::OnClipboardDataChanged() {
}
void ClipboardHistory::CommitData(ui::ClipboardData data) {
history_list_.push_front(std::move(data));
if (history_list_.size() > kMaxClipboardItemsShared)
history_list_.emplace_front(std::move(data));
for (auto& observer : observers_)
observer.OnClipboardHistoryItemAdded(history_list_.front());
if (history_list_.size() > kMaxClipboardItemsShared) {
auto removed = std::move(history_list_.back());
history_list_.pop_back();
for (auto& observer : observers_)
observer.OnClipboardHistoryItemRemoved(removed);
}
}
void ClipboardHistory::Pause() {
......
......@@ -9,20 +9,30 @@
#include <map>
#include "ash/ash_export.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "base/component_export.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/clipboard/clipboard_observer.h"
namespace ui {
class ClipboardData;
} // namespace ui
namespace ash {
// Keeps track of the last few things saved in the clipboard.
class ASH_EXPORT ClipboardHistory : public ui::ClipboardObserver {
public:
class ASH_EXPORT Observer : public base::CheckedObserver {
public:
// Called when a ClipboardHistoryItem has been added.
virtual void OnClipboardHistoryItemAdded(const ClipboardHistoryItem& item) {
}
// Called when a ClipboardHistoryItem has been removed.
virtual void OnClipboardHistoryItemRemoved(
const ClipboardHistoryItem& item) {}
// Called when ClipboardHistory is Clear()-ed.
virtual void OnClipboardHistoryCleared() {}
};
// Prevents clipboard history from being recorded within its scope. If
// anything is copied within its scope, history will not be recorded.
class ASH_EXPORT ScopedPause {
......@@ -41,9 +51,12 @@ class ASH_EXPORT ClipboardHistory : public ui::ClipboardObserver {
ClipboardHistory& operator=(const ClipboardHistory&) = delete;
~ClipboardHistory() override;
void AddObserver(Observer* observer) const;
void RemoveObserver(Observer* observer) const;
// Returns the list of most recent items. The returned list is sorted by
// recency.
const std::list<ui::ClipboardData>& GetItems() const;
const std::list<ClipboardHistoryItem>& GetItems() const;
// Deletes clipboard history. Does not modify content stored in the clipboard.
void Clear();
......@@ -66,7 +79,11 @@ class ASH_EXPORT ClipboardHistory : public ui::ClipboardObserver {
// The history of data copied to the Clipboard. Items of the list are sorted
// by recency.
std::list<ui::ClipboardData> history_list_;
std::list<ClipboardHistoryItem> history_list_;
// Mutable to allow adding/removing from |observers_| through a const
// ClipboardHistory.
mutable base::ObserverList<Observer> observers_;
// Factory to create WeakPtrs used to debounce calls to CommitData().
base::WeakPtrFactory<ClipboardHistory> commit_data_weak_factory_{this};
......
......@@ -6,8 +6,8 @@
#include "ash/accelerators/accelerator_controller_impl.h"
#include "ash/clipboard/clipboard_history.h"
#include "ash/clipboard/clipboard_history_helper.h"
#include "ash/clipboard/clipboard_history_menu_model_adapter.h"
#include "ash/clipboard/clipboard_history_resource_manager.h"
#include "ash/public/cpp/window_tree_host_lookup.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
......@@ -37,32 +37,6 @@ namespace ash {
namespace {
// TODO(dmblack): Move to clipboard_history_helper.
ui::ImageModel GetImageModelForClipboardData(const ui::ClipboardData& item) {
if (item.format() & static_cast<int>(ui::ClipboardInternalFormat::kBitmap)) {
// TODO(newcomer): Show a smaller version of the bitmap.
return ui::ImageModel();
}
if (item.format() & static_cast<int>(ui::ClipboardInternalFormat::kWeb))
return ui::ImageModel::FromVectorIcon(ash::kWebSmartPasteIcon);
if (item.format() &
static_cast<int>(ui::ClipboardInternalFormat::kBookmark)) {
return ui::ImageModel::FromVectorIcon(ash::kWebBookmarkIcon);
}
if (item.format() & static_cast<int>(ui::ClipboardInternalFormat::kHtml))
return ui::ImageModel::FromVectorIcon(ash::kHtmlIcon);
if (item.format() & static_cast<int>(ui::ClipboardInternalFormat::kRtf))
return ui::ImageModel::FromVectorIcon(ash::kRtfIcon);
if (item.format() & static_cast<int>(ui::ClipboardInternalFormat::kText))
return ui::ImageModel::FromVectorIcon(ash::kTextIcon);
if (item.format() & static_cast<int>(ui::ClipboardInternalFormat::kCustom)) {
// TODO(crbug/1108901): Handle file manager case.
// TODO(crbug/1108902): Handle fallback case.
return ui::ImageModel();
}
return ui::ImageModel();
}
ui::ClipboardNonBacked* GetClipboard() {
auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
DCHECK(clipboard);
......@@ -133,6 +107,8 @@ class ClipboardHistoryController::MenuDelegate
ClipboardHistoryController::ClipboardHistoryController()
: clipboard_history_(std::make_unique<ClipboardHistory>()),
resource_manager_(std::make_unique<ClipboardHistoryResourceManager>(
clipboard_history_.get())),
accelerator_target_(std::make_unique<AcceleratorTarget>(this)),
menu_delegate_(std::make_unique<MenuDelegate>(this)) {}
......@@ -171,7 +147,7 @@ void ClipboardHistoryController::ShowMenu() {
return;
clipboard_items_ =
std::vector<ui::ClipboardData>(clipboard_history_->GetItems().begin(),
std::vector<ClipboardHistoryItem>(clipboard_history_->GetItems().begin(),
clipboard_history_->GetItems().end());
std::unique_ptr<ui::SimpleMenuModel> menu_model =
......@@ -181,8 +157,8 @@ void ClipboardHistoryController::ShowMenu() {
IDS_CLIPBOARD_MENU_CLIPBOARD));
int index = 0;
for (const auto& item : clipboard_items_) {
menu_model->AddItemWithIcon(index++, clipboard::helper::GetLabel(item),
GetImageModelForClipboardData(item));
menu_model->AddItemWithIcon(index++, resource_manager_->GetLabel(item),
resource_manager_->GetImageModel(item));
}
menu_model->AddSeparator(ui::MenuSeparatorType::NORMAL_SEPARATOR);
menu_model->AddItemWithIcon(
......@@ -213,14 +189,14 @@ void ClipboardHistoryController::MenuOptionSelected(int index,
// If necessary, replace the clipboard's |original_data| temporarily so that
// we can paste the selected history item.
const bool shift_key_pressed = event_flags & ui::EF_SHIFT_DOWN;
if (shift_key_pressed || *it != *clipboard->GetClipboardData()) {
if (shift_key_pressed || it->data() != *clipboard->GetClipboardData()) {
std::unique_ptr<ui::ClipboardData> temp_data;
if (shift_key_pressed) {
// When the shift key is pressed, we only paste plain text.
temp_data = std::make_unique<ui::ClipboardData>();
temp_data->set_text(it->text());
temp_data->set_text(it->data().text());
} else {
temp_data = std::make_unique<ui::ClipboardData>(*it);
temp_data = std::make_unique<ui::ClipboardData>(it->data());
}
// Pause clipboard history when manipulating the clipboard for a paste.
ClipboardHistory::ScopedPause scoped_pause(clipboard_history_.get());
......
......@@ -9,20 +9,18 @@
#include <vector>
#include "ash/ash_export.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "base/memory/weak_ptr.h"
namespace gfx {
class Rect;
} // namespace gfx
namespace ui {
class ClipboardData;
} // namespace ui
namespace ash {
class ClipboardHistory;
class ClipboardHistoryMenuModelAdapter;
class ClipboardHistoryResourceManager;
// Shows a menu with the last few things saved in the clipboard when the
// keyboard shortcut is pressed.
......@@ -45,6 +43,12 @@ class ASH_EXPORT ClipboardHistoryController {
// Returns the history which tracks what is being copied to the clipboard.
const ClipboardHistory* history() const { return clipboard_history_.get(); }
// Returns the resource manager which gets labels and images for items copied
// to the clipboard.
const ClipboardHistoryResourceManager* resource_manager() const {
return resource_manager_.get();
}
private:
class AcceleratorTarget;
class MenuDelegate;
......@@ -60,12 +64,14 @@ class ASH_EXPORT ClipboardHistoryController {
std::unique_ptr<ClipboardHistoryMenuModelAdapter> context_menu_;
// Used to keep track of what is being copied to the clipboard.
std::unique_ptr<ClipboardHistory> clipboard_history_;
// Manages resources for clipboard history.
std::unique_ptr<ClipboardHistoryResourceManager> resource_manager_;
// Detects the search+v key combo.
std::unique_ptr<AcceleratorTarget> accelerator_target_;
// Handles events on the contextual menu.
std::unique_ptr<MenuDelegate> menu_delegate_;
// The items we show in the contextual menu. Saved so we can paste them later.
std::vector<ui::ClipboardData> clipboard_items_;
std::vector<ClipboardHistoryItem> clipboard_items_;
base::WeakPtrFactory<ClipboardHistoryController> weak_ptr_factory_{this};
};
......
// Copyright 2020 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 "ash/clipboard/clipboard_history_helper.h"
#include <string>
#include <vector>
#include "base/notreached.h"
#include "base/strings/escape.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/strings/grit/ui_strings.h"
namespace ash {
namespace clipboard {
namespace helper {
namespace {
constexpr char kFileSystemSourcesType[] = "fs/sources";
// Private ---------------------------------------------------------------------
// Returns true if |data| contains the specified |format|.
bool ContainsFormat(const ui::ClipboardData& data,
ui::ClipboardInternalFormat format) {
return data.format() & static_cast<int>(format);
}
// Returns the localized string for the specified |resource_id|.
base::string16 GetLocalizedString(int resource_id) {
return ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
resource_id);
}
// TODO(crbug/1108902): Handle fallback case.
// Returns the label to display for the custom data contained within |data|.
base::string16 GetLabelForCustomData(const ui::ClipboardData& data) {
DCHECK(ContainsFormat(data, ui::ClipboardInternalFormat::kCustom));
// Attempt to read file system sources in the custom data.
base::string16 sources;
ui::ReadCustomDataForType(
data.custom_data_data().c_str(), data.custom_data_data().size(),
base::UTF8ToUTF16(kFileSystemSourcesType), &sources);
if (sources.empty())
return base::UTF8ToUTF16("<Custom Data>");
// Split sources into a list.
std::vector<base::StringPiece16> source_list =
base::SplitStringPiece(sources, base::UTF8ToUTF16("\n"),
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
// Strip path information, so all that's left are file names.
for (auto it = source_list.begin(); it != source_list.end(); ++it)
*it = it->substr(it->find_last_of(base::UTF8ToUTF16("/")) + 1);
// Join file names, unescaping encoded character sequences for display. This
// ensures that "My%20File.txt" will display as "My File.txt".
return base::UTF8ToUTF16(base::UnescapeURLComponent(
base::UTF16ToUTF8(base::JoinString(source_list, base::UTF8ToUTF16(", "))),
base::UnescapeRule::SPACES));
}
} // namespace
// Public ----------------------------------------------------------------------
base::string16 GetLabel(const ui::ClipboardData& data) {
if (ContainsFormat(data, ui::ClipboardInternalFormat::kBitmap))
return GetLocalizedString(IDS_CLIPBOARD_MENU_IMAGE);
if (ContainsFormat(data, ui::ClipboardInternalFormat::kText))
return base::UTF8ToUTF16(data.text());
if (ContainsFormat(data, ui::ClipboardInternalFormat::kHtml))
return base::UTF8ToUTF16(data.markup_data());
if (ContainsFormat(data, ui::ClipboardInternalFormat::kRtf))
return GetLocalizedString(IDS_CLIPBOARD_MENU_RTF_CONTENT);
if (ContainsFormat(data, ui::ClipboardInternalFormat::kBookmark))
return base::UTF8ToUTF16(data.bookmark_title());
if (ContainsFormat(data, ui::ClipboardInternalFormat::kWeb))
return GetLocalizedString(IDS_CLIPBOARD_MENU_WEB_SMART_PASTE);
if (ContainsFormat(data, ui::ClipboardInternalFormat::kCustom))
return GetLabelForCustomData(data);
NOTREACHED();
return base::string16();
}
} // namespace helper
} // namespace clipboard
} // namespace ash
// Copyright 2020 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 ASH_CLIPBOARD_CLIPBOARD_HISTORY_HELPER_H_
#define ASH_CLIPBOARD_CLIPBOARD_HISTORY_HELPER_H_
#include "ash/ash_export.h"
#include "base/strings/string16.h"
namespace ui {
class ClipboardData;
} // namespace ui
namespace ash {
namespace clipboard {
namespace helper {
// Returns the label to display for the specified clipboard |data|.
ASH_EXPORT base::string16 GetLabel(const ui::ClipboardData& data);
} // namespace helper
} // namespace clipboard
} // namespace ash
#endif // ASH_CLIPBOARD_CLIPBOARD_HISTORY_HELPER_H_
// Copyright 2020 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 "ash/clipboard/clipboard_history_helper.h"
#include <string>
#include <unordered_map>
#include "base/optional.h"
#include "base/pickle.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/icu_test_util.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/clipboard/clipboard_format_type.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/gfx/image/image_unittest_util.h"
namespace ash {
namespace clipboard {
namespace helper {
namespace {
// ClipboardDataBuilder --------------------------------------------------------
class ClipboardDataBuilder {
public:
ClipboardDataBuilder() = default;
ClipboardDataBuilder(const ClipboardDataBuilder&) = delete;
ClipboardDataBuilder& operator=(const ClipboardDataBuilder&) = delete;
~ClipboardDataBuilder() = default;
ui::ClipboardData Build() const {
ui::ClipboardData data;
if (text_.has_value())
data.set_text(text_.value());
if (markup_.has_value())
data.set_markup_data(markup_.value());
if (rtf_.has_value())
data.SetRTFData(rtf_.value());
if (bookmark_title_.has_value())
data.set_bookmark_title(bookmark_title_.value());
if (bitmap_.has_value())
data.SetBitmapData(bitmap_.value());
if (custom_format_.has_value() && custom_data_.has_value())
data.SetCustomData(custom_format_.value(), custom_data_.value());
if (web_smart_paste_.has_value())
data.set_web_smart_paste(web_smart_paste_.value());
return data;
}
ClipboardDataBuilder& SetText(const std::string& text) {
text_ = text;
return *this;
}
ClipboardDataBuilder& ClearText() {
text_ = base::nullopt;
return *this;
}
ClipboardDataBuilder& SetMarkup(const std::string& markup) {
markup_ = markup;
return *this;
}
ClipboardDataBuilder& ClearMarkup() {
markup_ = base::nullopt;
return *this;
}
ClipboardDataBuilder& SetRtf(const std::string& rtf) {
rtf_ = rtf;
return *this;
}
ClipboardDataBuilder& ClearRtf() {
rtf_ = base::nullopt;
return *this;
}
ClipboardDataBuilder& SetBookmarkTitle(const std::string& bookmark_title) {
bookmark_title_ = bookmark_title;
return *this;
}
ClipboardDataBuilder& ClearBookmarkTitle() {
bookmark_title_ = base::nullopt;
return *this;
}
ClipboardDataBuilder& SetBitmap(const SkBitmap& bitmap) {
bitmap_ = bitmap;
return *this;
}
ClipboardDataBuilder& ClearBitmap() {
bitmap_ = base::nullopt;
return *this;
}
ClipboardDataBuilder& SetCustomData(const std::string& custom_format,
const std::string& custom_data) {
custom_format_ = custom_format;
custom_data_ = custom_data;
return *this;
}
ClipboardDataBuilder& ClearCustomData() {
custom_format_ = base::nullopt;
custom_data_ = base::nullopt;
return *this;
}
ClipboardDataBuilder& SetFileSystemData(
std::initializer_list<std::string>&& source_list) {
constexpr char kFileSystemSourcesType[] = "fs/sources";
base::Pickle custom_data;
ui::WriteCustomDataToPickle(
std::unordered_map<base::string16, base::string16>(
{{base::UTF8ToUTF16(kFileSystemSourcesType),
base::UTF8ToUTF16(base::JoinString(source_list, "\n"))}}),
&custom_data);
return SetCustomData(
ui::ClipboardFormatType::GetWebCustomDataType().GetName(),
std::string(static_cast<const char*>(custom_data.data()),
custom_data.size()));
}
ClipboardDataBuilder& SetWebSmartPaste(bool web_smart_paste) {
web_smart_paste_ = web_smart_paste;
return *this;
}
ClipboardDataBuilder& ClearWebSmartPaste() {
web_smart_paste_ = base::nullopt;
return *this;
}
private:
base::Optional<std::string> text_;
base::Optional<std::string> markup_;
base::Optional<std::string> rtf_;
base::Optional<std::string> bookmark_title_;
base::Optional<SkBitmap> bitmap_;
base::Optional<std::string> custom_format_;
base::Optional<std::string> custom_data_;
base::Optional<bool> web_smart_paste_;
};
} // namespace
// Tests -----------------------------------------------------------------------
using ClipboardHistoryHelperTest = testing::Test;
TEST_F(ClipboardHistoryHelperTest, GetLabel) {
base::test::ScopedRestoreICUDefaultLocale locale("en_US");
// Populate a builder with all the data formats that we expect to handle.
ClipboardDataBuilder builder;
builder.SetText("Text")
.SetMarkup("Markup")
.SetRtf("Rtf")
.SetBookmarkTitle("Bookmark Title")
.SetBitmap(gfx::test::CreateBitmap(10, 10))
.SetCustomData("Custom Format", "Custom Data")
.SetWebSmartPaste(true);
// Bitmap data always take precedence.
EXPECT_EQ(GetLabel(builder.Build()), base::UTF8ToUTF16("Image"));
builder.ClearBitmap();
// In the absence of bitmap data, text data takes precedence.
EXPECT_EQ(GetLabel(builder.Build()), base::UTF8ToUTF16("Text"));
builder.ClearText();
// In the absence of text data, HTML data takes precedence.
EXPECT_EQ(GetLabel(builder.Build()), base::UTF8ToUTF16("Markup"));
builder.ClearMarkup();
// In the absence of HTML data, RTF data takes precedence.
EXPECT_EQ(GetLabel(builder.Build()), base::UTF8ToUTF16("RTF Content"));
builder.ClearRtf();
// In the absence of RTF data, bookmark data takes precedence.
EXPECT_EQ(GetLabel(builder.Build()), base::UTF8ToUTF16("Bookmark Title"));
builder.ClearBookmarkTitle();
// In the absence of bookmark data, web smart paste data takes precedence.
EXPECT_EQ(GetLabel(builder.Build()),
base::UTF8ToUTF16("Web Smart Paste Content"));
builder.ClearWebSmartPaste();
// In the absence of web smart paste data, custom data takes precedence.
EXPECT_EQ(GetLabel(builder.Build()), base::UTF8ToUTF16("<Custom Data>"));
builder.SetFileSystemData(
{"/path/to/My%20File.txt", "/path/to/My%20Other%20File.txt"});
// We specially treat custom file system data to show a list of file names.
EXPECT_EQ(GetLabel(builder.Build()),
base::UTF8ToUTF16("My File.txt, My Other File.txt"));
}
} // namespace helper
} // namespace clipboard
} // namespace ash
// Copyright 2020 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 "ash/clipboard/clipboard_history_item.h"
namespace ash {
ClipboardHistoryItem::ClipboardHistoryItem(ui::ClipboardData data)
: id_(base::UnguessableToken::Create()), data_(std::move(data)) {}
ClipboardHistoryItem::ClipboardHistoryItem(const ClipboardHistoryItem&) =
default;
ClipboardHistoryItem::ClipboardHistoryItem(ClipboardHistoryItem&&) = default;
ClipboardHistoryItem::~ClipboardHistoryItem() = default;
} // namespace ash
// Copyright 2020 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 ASH_CLIPBOARD_CLIPBOARD_HISTORY_ITEM_H_
#define ASH_CLIPBOARD_CLIPBOARD_HISTORY_ITEM_H_
#include "ash/ash_export.h"
#include "base/unguessable_token.h"
#include "ui/base/clipboard/clipboard_data.h"
namespace ash {
// Wraps ClipboardData with a unique identifier.
class ASH_EXPORT ClipboardHistoryItem {
public:
explicit ClipboardHistoryItem(ui::ClipboardData data);
ClipboardHistoryItem(const ClipboardHistoryItem&);
ClipboardHistoryItem(ClipboardHistoryItem&&);
// Copy/move assignment operators are deleted to be consistent with
// ui::ClipboardData and ui::ClipboardDataEndpoint.
ClipboardHistoryItem& operator=(const ClipboardHistoryItem&) = delete;
ClipboardHistoryItem& operator=(ClipboardHistoryItem&&) = delete;
~ClipboardHistoryItem();
const base::UnguessableToken& id() const { return id_; }
const ui::ClipboardData& data() const { return data_; }
private:
// Unique identifier.
base::UnguessableToken id_;
ui::ClipboardData data_;
};
} // namespace ash
#endif // ASH_CLIPBOARD_CLIPBOARD_HISTORY_ITEM_H_
// Copyright 2020 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 "ash/clipboard/clipboard_history_resource_manager.h"
#include <string>
#include "ash/public/cpp/clipboard_image_model_factory.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "base/bind.h"
#include "base/notreached.h"
#include "base/stl_util.h"
#include "base/strings/escape.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/strings/grit/ui_strings.h"
namespace ash {
namespace {
constexpr char kFileSystemSourcesType[] = "fs/sources";
// Helpers ---------------------------------------------------------------------
// Returns true if |data| contains the specified |format|.
bool ContainsFormat(const ui::ClipboardData& data,
ui::ClipboardInternalFormat format) {
return data.format() & static_cast<int>(format);
}
// Returns the localized string for the specified |resource_id|.
base::string16 GetLocalizedString(int resource_id) {
return ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
resource_id);
}
// TODO(crbug/1108902): Handle fallback case.
// Returns the label to display for the custom data contained within |data|.
base::string16 GetLabelForCustomData(const ui::ClipboardData& data) {
DCHECK(ContainsFormat(data, ui::ClipboardInternalFormat::kCustom));
// Attempt to read file system sources in the custom data.
base::string16 sources;
ui::ReadCustomDataForType(
data.custom_data_data().c_str(), data.custom_data_data().size(),
base::UTF8ToUTF16(kFileSystemSourcesType), &sources);
if (sources.empty()) {
// TODO(https://crbug.com/1119931): Move this to a grd file to make sure it
// is internationalized.
return base::UTF8ToUTF16("<Custom Data>");
}
// Split sources into a list.
std::vector<base::StringPiece16> source_list =
base::SplitStringPiece(sources, base::UTF8ToUTF16("\n"),
base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
// Strip path information, so all that's left are file names.
for (auto it = source_list.begin(); it != source_list.end(); ++it)
*it = it->substr(it->find_last_of(base::UTF8ToUTF16("/")) + 1);
// Join file names, unescaping encoded character sequences for display. This
// ensures that "My%20File.txt" will display as "My File.txt".
return base::UTF8ToUTF16(base::UnescapeURLComponent(
base::UTF16ToUTF8(base::JoinString(source_list, base::UTF8ToUTF16(", "))),
base::UnescapeRule::SPACES));
}
} // namespace
// ClipboardHistoryResourceManager ---------------------------------------------
ClipboardHistoryResourceManager::ClipboardHistoryResourceManager(
const ClipboardHistory* clipboard_history)
: clipboard_history_(clipboard_history) {
clipboard_history_->AddObserver(this);
}
ClipboardHistoryResourceManager::~ClipboardHistoryResourceManager() {
clipboard_history_->RemoveObserver(this);
CancelUnfinishedRequests();
}
ui::ImageModel ClipboardHistoryResourceManager::GetImageModel(
const ClipboardHistoryItem& item) const {
// Use a cached image model when possible.
auto cached_image_model = FindCachedImageModelForItem(item);
if (cached_image_model != cached_image_models_.end())
return cached_image_model->image_model;
// TODO(newcomer): Show a smaller version of the bitmap.
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kBitmap))
return ui::ImageModel();
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kWeb))
return ui::ImageModel::FromVectorIcon(ash::kWebSmartPasteIcon);
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kBookmark))
return ui::ImageModel::FromVectorIcon(ash::kWebBookmarkIcon);
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kHtml))
return ui::ImageModel::FromVectorIcon(ash::kHtmlIcon);
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kRtf))
return ui::ImageModel::FromVectorIcon(ash::kRtfIcon);
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kText))
return ui::ImageModel::FromVectorIcon(ash::kTextIcon);
// TODO(crbug/1108901): Handle file manager case.
// TODO(crbug/1108902): Handle fallback case.
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kCustom))
return ui::ImageModel();
NOTREACHED();
return ui::ImageModel();
}
base::string16 ClipboardHistoryResourceManager::GetLabel(
const ClipboardHistoryItem& item) const {
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kBitmap))
return GetLocalizedString(IDS_CLIPBOARD_MENU_IMAGE);
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kText))
return base::UTF8ToUTF16(item.data().text());
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kHtml))
return base::UTF8ToUTF16(item.data().markup_data());
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kRtf))
return GetLocalizedString(IDS_CLIPBOARD_MENU_RTF_CONTENT);
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kBookmark))
return base::UTF8ToUTF16(item.data().bookmark_title());
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kWeb))
return GetLocalizedString(IDS_CLIPBOARD_MENU_WEB_SMART_PASTE);
if (ContainsFormat(item.data(), ui::ClipboardInternalFormat::kCustom))
return GetLabelForCustomData(item.data());
NOTREACHED();
return base::string16();
}
ClipboardHistoryResourceManager::CachedImageModel::CachedImageModel() {}
ClipboardHistoryResourceManager::CachedImageModel::CachedImageModel(
const CachedImageModel& other) = default;
ClipboardHistoryResourceManager::CachedImageModel&
ClipboardHistoryResourceManager::CachedImageModel::operator=(
const CachedImageModel&) = default;
ClipboardHistoryResourceManager::CachedImageModel::~CachedImageModel() =
default;
void ClipboardHistoryResourceManager::CacheImageModel(
const base::UnguessableToken& id,
ui::ImageModel image_model) {
auto cached_image_model = base::ConstCastIterator(
cached_image_models_, FindCachedImageModelForId(id));
if (cached_image_model != cached_image_models_.end())
cached_image_model->image_model = std::move(image_model);
}
std::vector<ClipboardHistoryResourceManager::CachedImageModel>::const_iterator
ClipboardHistoryResourceManager::FindCachedImageModelForId(
const base::UnguessableToken& id) const {
return std::find_if(cached_image_models_.cbegin(),
cached_image_models_.cend(),
[&](const auto& cached_image_model) {
return cached_image_model.id == id;
});
}
std::vector<ClipboardHistoryResourceManager::CachedImageModel>::const_iterator
ClipboardHistoryResourceManager::FindCachedImageModelForItem(
const ClipboardHistoryItem& item) const {
return std::find_if(
cached_image_models_.cbegin(), cached_image_models_.cend(),
[&](const auto& cached_image_model) {
return base::Contains(cached_image_model.clipboard_history_item_ids,
item.id());
});
}
void ClipboardHistoryResourceManager::CancelUnfinishedRequests() {
for (const auto& cached_image_model : cached_image_models_) {
if (cached_image_model.image_model.IsEmpty())
ClipboardImageModelFactory::Get()->CancelRequest(cached_image_model.id);
}
}
void ClipboardHistoryResourceManager::OnClipboardHistoryItemAdded(
const ClipboardHistoryItem& item) {
// For items that will be represented by their rendered HTML, we need to do
// some prep work to pre-render and cache an image model.
if (!item.data().bitmap().isNull() || item.data().markup_data().empty())
return;
const auto& items = clipboard_history_->GetItems();
// See if we have an |existing| item that will render the same as |item|.
auto it = std::find_if(items.begin(), items.end(), [&](const auto& existing) {
return &existing != &item && existing.data().bitmap().isNull() &&
existing.data().markup_data() == item.data().markup_data();
});
// If we don't have an existing image model in the cache, create one and
// instruct ClipboardImageModelFactory to render it. Note that the factory may
// or may not start rendering immediately depending on its activation status.
if (it == items.end()) {
base::UnguessableToken id = base::UnguessableToken::Create();
CachedImageModel cached_image_model;
cached_image_model.id = id;
cached_image_model.clipboard_history_item_ids.push_back(item.id());
cached_image_models_.push_back(std::move(cached_image_model));
ClipboardImageModelFactory::Get()->Render(
id, item.data().markup_data(),
base::BindOnce(&ClipboardHistoryResourceManager::CacheImageModel,
weak_factory_.GetWeakPtr(), id));
return;
}
// If we do have an existing model, we need only to update its usages.
auto cached_image_model = base::ConstCastIterator(
cached_image_models_, FindCachedImageModelForItem(*it));
DCHECK(cached_image_model != cached_image_models_.end());
cached_image_model->clipboard_history_item_ids.push_back(item.id());
}
void ClipboardHistoryResourceManager::OnClipboardHistoryItemRemoved(
const ClipboardHistoryItem& item) {
// For items that will not be represented by their rendered HTML, do nothing.
if (!item.data().bitmap().isNull() || item.data().markup_data().empty())
return;
// We should have an image model in the cache.
auto cached_image_model = base::ConstCastIterator(
cached_image_models_, FindCachedImageModelForItem(item));
DCHECK(cached_image_model != cached_image_models_.end());
// Update usages.
base::Erase(cached_image_model->clipboard_history_item_ids, item.id());
if (!cached_image_model->clipboard_history_item_ids.empty())
return;
// If the ImageModel was never rendered, cancel the request.
if (cached_image_model->image_model.IsEmpty())
ClipboardImageModelFactory::Get()->CancelRequest(cached_image_model->id);
// If the cached image model is no longer in use, it can be erased.
cached_image_models_.erase(cached_image_model);
}
void ClipboardHistoryResourceManager::OnClipboardHistoryCleared() {
CancelUnfinishedRequests();
cached_image_models_ = std::vector<CachedImageModel>();
}
} // namespace ash
// Copyright 2020 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 ASH_CLIPBOARD_CLIPBOARD_HISTORY_RESOURCE_MANAGER_H_
#define ASH_CLIPBOARD_CLIPBOARD_HISTORY_RESOURCE_MANAGER_H_
#include <vector>
#include "ash/ash_export.h"
#include "ash/clipboard/clipboard_history.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "base/strings/string16.h"
#include "base/unguessable_token.h"
#include "ui/base/models/image_model.h"
namespace ash {
class ASH_EXPORT ClipboardHistoryResourceManager
: public ClipboardHistory::Observer {
public:
explicit ClipboardHistoryResourceManager(
const ClipboardHistory* clipboard_history);
ClipboardHistoryResourceManager(const ClipboardHistoryResourceManager&) =
delete;
ClipboardHistoryResourceManager& operator=(
const ClipboardHistoryResourceManager&) = delete;
~ClipboardHistoryResourceManager() override;
// Returns the image to display for the specified clipboard history |item|.
ui::ImageModel GetImageModel(const ClipboardHistoryItem& item) const;
// Returns the label to display for the specified clipboard history |item|.
base::string16 GetLabel(const ClipboardHistoryItem& item) const;
private:
struct CachedImageModel {
CachedImageModel();
CachedImageModel(const CachedImageModel&);
CachedImageModel& operator=(const CachedImageModel&);
~CachedImageModel();
// Unique identifier.
base::UnguessableToken id;
// ImageModel that was created by ClipboardImageModelFactory.
ui::ImageModel image_model;
// ClipboardHistoryItem id's which utilize this CachedImageModel.
std::vector<base::UnguessableToken> clipboard_history_item_ids;
};
// Caches the specified |image_model| with the specified |id|.
void CacheImageModel(const base::UnguessableToken& id,
ui::ImageModel image_model);
// Finds the cached image model associated with the specified |id|.
std::vector<ClipboardHistoryResourceManager::CachedImageModel>::const_iterator
FindCachedImageModelForId(const base::UnguessableToken& id) const;
// Finds the cached image model associated with the specified |item|.
std::vector<ClipboardHistoryResourceManager::CachedImageModel>::const_iterator
FindCachedImageModelForItem(const ClipboardHistoryItem& item) const;
// Cancels all unfinished requests.
void CancelUnfinishedRequests();
// ClipboardHistory::Observer:
void OnClipboardHistoryItemAdded(const ClipboardHistoryItem& item) override;
void OnClipboardHistoryItemRemoved(const ClipboardHistoryItem& item) override;
void OnClipboardHistoryCleared() override;
// Owned by ClipboardHistoryController.
const ClipboardHistory* const clipboard_history_;
std::vector<CachedImageModel> cached_image_models_;
base::WeakPtrFactory<ClipboardHistoryResourceManager> weak_factory_{this};
};
} // namespace ash
#endif // ASH_CLIPBOARD_CLIPBOARD_HISTORY_RESOURCE_MANAGER_H_
// Copyright 2020 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 "ash/clipboard/clipboard_history_resource_manager.h"
#include <string>
#include <unordered_map>
#include "ash/clipboard/clipboard_history.h"
#include "ash/clipboard/clipboard_history_controller.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/public/cpp/clipboard_image_model_factory.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/callback.h"
#include "base/optional.h"
#include "base/pickle.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/icu_test_util.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/constants/chromeos_features.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/clipboard/clipboard_format_type.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
namespace ash {
namespace {
void FlushMessageLoop() {
base::RunLoop run_loop;
base::SequencedTaskRunnerHandle::Get()->PostTask(FROM_HERE,
run_loop.QuitClosure());
run_loop.Run();
}
SkBitmap GetRandomBitmap() {
SkColor color = rand() % 0xFFFFFF + 1;
SkBitmap bitmap;
bitmap.allocN32Pixels(24, 24);
bitmap.eraseARGB(255, SkColorGetR(color), SkColorGetG(color),
SkColorGetB(color));
return bitmap;
}
ui::ImageModel GetRandomImageModel() {
return ui::ImageModel::FromImageSkia(
gfx::ImageSkia::CreateFrom1xBitmap(GetRandomBitmap()));
}
// ClipboardHistoryItemBuilder ------------------------------------------------
class ClipboardHistoryItemBuilder {
public:
ClipboardHistoryItemBuilder() = default;
ClipboardHistoryItemBuilder(const ClipboardHistoryItemBuilder&) = delete;
ClipboardHistoryItemBuilder& operator=(const ClipboardHistoryItemBuilder&) =
delete;
~ClipboardHistoryItemBuilder() = default;
ClipboardHistoryItem Build() const {
ui::ClipboardData data;
if (text_.has_value())
data.set_text(text_.value());
if (markup_.has_value())
data.set_markup_data(markup_.value());
if (rtf_.has_value())
data.SetRTFData(rtf_.value());
if (bookmark_title_.has_value())
data.set_bookmark_title(bookmark_title_.value());
if (bitmap_.has_value())
data.SetBitmapData(bitmap_.value());
if (custom_format_.has_value() && custom_data_.has_value())
data.SetCustomData(custom_format_.value(), custom_data_.value());
if (web_smart_paste_.has_value())
data.set_web_smart_paste(web_smart_paste_.value());
return ClipboardHistoryItem(std::move(data));
}
ClipboardHistoryItemBuilder& SetText(const std::string& text) {
text_ = text;
return *this;
}
ClipboardHistoryItemBuilder& ClearText() {
text_ = base::nullopt;
return *this;
}
ClipboardHistoryItemBuilder& SetMarkup(const std::string& markup) {
markup_ = markup;
return *this;
}
ClipboardHistoryItemBuilder& ClearMarkup() {
markup_ = base::nullopt;
return *this;
}
ClipboardHistoryItemBuilder& SetRtf(const std::string& rtf) {
rtf_ = rtf;
return *this;
}
ClipboardHistoryItemBuilder& ClearRtf() {
rtf_ = base::nullopt;
return *this;
}
ClipboardHistoryItemBuilder& SetBookmarkTitle(
const std::string& bookmark_title) {
bookmark_title_ = bookmark_title;
return *this;
}
ClipboardHistoryItemBuilder& ClearBookmarkTitle() {
bookmark_title_ = base::nullopt;
return *this;
}
ClipboardHistoryItemBuilder& SetBitmap(const SkBitmap& bitmap) {
bitmap_ = bitmap;
return *this;
}
ClipboardHistoryItemBuilder& ClearBitmap() {
bitmap_ = base::nullopt;
return *this;
}
ClipboardHistoryItemBuilder& SetCustomData(const std::string& custom_format,
const std::string& custom_data) {
custom_format_ = custom_format;
custom_data_ = custom_data;
return *this;
}
ClipboardHistoryItemBuilder& ClearCustomData() {
custom_format_ = base::nullopt;
custom_data_ = base::nullopt;
return *this;
}
ClipboardHistoryItemBuilder& SetFileSystemData(
std::initializer_list<std::string>&& source_list) {
constexpr char kFileSystemSourcesType[] = "fs/sources";
base::Pickle custom_data;
ui::WriteCustomDataToPickle(
std::unordered_map<base::string16, base::string16>(
{{base::UTF8ToUTF16(kFileSystemSourcesType),
base::UTF8ToUTF16(base::JoinString(source_list, "\n"))}}),
&custom_data);
return SetCustomData(
ui::ClipboardFormatType::GetWebCustomDataType().GetName(),
std::string(static_cast<const char*>(custom_data.data()),
custom_data.size()));
}
ClipboardHistoryItemBuilder& SetWebSmartPaste(bool web_smart_paste) {
web_smart_paste_ = web_smart_paste;
return *this;
}
ClipboardHistoryItemBuilder& ClearWebSmartPaste() {
web_smart_paste_ = base::nullopt;
return *this;
}
private:
base::Optional<std::string> text_;
base::Optional<std::string> markup_;
base::Optional<std::string> rtf_;
base::Optional<std::string> bookmark_title_;
base::Optional<SkBitmap> bitmap_;
base::Optional<std::string> custom_format_;
base::Optional<std::string> custom_data_;
base::Optional<bool> web_smart_paste_;
};
} // namespace
// Tests -----------------------------------------------------------------------
class MockClipboardImageModelFactory : public ClipboardImageModelFactory {
public:
MockClipboardImageModelFactory() = default;
MockClipboardImageModelFactory(const MockClipboardImageModelFactory&) =
delete;
MockClipboardImageModelFactory& operator=(
const MockClipboardImageModelFactory&) = delete;
~MockClipboardImageModelFactory() override = default;
MOCK_METHOD(void,
Render,
(const base::UnguessableToken&,
const std::string&,
ImageModelCallback),
(override));
MOCK_METHOD(void, CancelRequest, (const base::UnguessableToken&), (override));
MOCK_METHOD(void, Activate, (), (override));
MOCK_METHOD(void, Deactivate, (), (override));
};
class ClipboardHistoryResourceManagerTest : public AshTestBase {
public:
ClipboardHistoryResourceManagerTest() = default;
ClipboardHistoryResourceManagerTest(
const ClipboardHistoryResourceManagerTest&) = delete;
ClipboardHistoryResourceManagerTest& operator=(
const ClipboardHistoryResourceManagerTest&) = delete;
~ClipboardHistoryResourceManagerTest() override = default;
void SetUp() override {
scoped_feature_list_.InitAndEnableFeature(
chromeos::features::kClipboardHistory);
AshTestBase::SetUp();
clipboard_history_ =
Shell::Get()->clipboard_history_controller()->history();
resource_manager_ =
Shell::Get()->clipboard_history_controller()->resource_manager();
mock_image_factory_ =
std::make_unique<testing::StrictMock<MockClipboardImageModelFactory>>();
}
const ClipboardHistory* clipboard_history() const {
return clipboard_history_;
}
const ClipboardHistoryResourceManager* resource_manager() {
return resource_manager_;
}
MockClipboardImageModelFactory* mock_image_factory() {
return mock_image_factory_.get();
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
const ClipboardHistory* clipboard_history_;
const ClipboardHistoryResourceManager* resource_manager_;
std::unique_ptr<MockClipboardImageModelFactory> mock_image_factory_;
};
TEST_F(ClipboardHistoryResourceManagerTest, GetLabel) {
base::test::ScopedRestoreICUDefaultLocale locale("en_US");
// Populate a builder with all the data formats that we expect to handle.
ClipboardHistoryItemBuilder builder;
builder.SetText("Text")
.SetMarkup("Markup")
.SetRtf("Rtf")
.SetBookmarkTitle("Bookmark Title")
.SetBitmap(gfx::test::CreateBitmap(10, 10))
.SetCustomData("Custom Format", "Custom Data")
.SetWebSmartPaste(true);
// Bitmap data always take precedence.
EXPECT_EQ(resource_manager()->GetLabel(builder.Build()),
base::UTF8ToUTF16("Image"));
builder.ClearBitmap();
// In the absence of bitmap data, text data takes precedence.
EXPECT_EQ(resource_manager()->GetLabel(builder.Build()),
base::UTF8ToUTF16("Text"));
builder.ClearText();
// In the absence of text data, HTML data takes precedence.
EXPECT_EQ(resource_manager()->GetLabel(builder.Build()),
base::UTF8ToUTF16("Markup"));
builder.ClearMarkup();
// In the absence of HTML data, RTF data takes precedence.
EXPECT_EQ(resource_manager()->GetLabel(builder.Build()),
base::UTF8ToUTF16("RTF Content"));
builder.ClearRtf();
// In the absence of RTF data, bookmark data takes precedence.
EXPECT_EQ(resource_manager()->GetLabel(builder.Build()),
base::UTF8ToUTF16("Bookmark Title"));
builder.ClearBookmarkTitle();
// In the absence of bookmark data, web smart paste data takes precedence.
EXPECT_EQ(resource_manager()->GetLabel(builder.Build()),
base::UTF8ToUTF16("Web Smart Paste Content"));
builder.ClearWebSmartPaste();
// In the absence of web smart paste data, custom data takes precedence.
EXPECT_EQ(resource_manager()->GetLabel(builder.Build()),
base::UTF8ToUTF16("<Custom Data>"));
builder.SetFileSystemData(
{"/path/to/My%20File.txt", "/path/to/My%20Other%20File.txt"});
// We specially treat custom file system data to show a list of file names.
EXPECT_EQ(resource_manager()->GetLabel(builder.Build()),
base::UTF8ToUTF16("My File.txt, My Other File.txt"));
}
// Tests that Render is called once when an eligible item is added
// to ClipboardHistory.
TEST_F(ClipboardHistoryResourceManagerTest, BasicCachedImageModel) {
ui::ImageModel expected_image_model = GetRandomImageModel();
ON_CALL(*mock_image_factory(), Render)
.WillByDefault(testing::WithArg<2>(
[&](ClipboardImageModelFactory::ImageModelCallback callback) {
std::move(callback).Run(expected_image_model);
}));
EXPECT_CALL(*mock_image_factory(), CancelRequest).Times(0);
EXPECT_CALL(*mock_image_factory(), Render).Times(1);
// Write a basic ClipboardData which is eligible to render HTML.
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteHTML(base::UTF8ToUTF16("test"), "source_url");
}
FlushMessageLoop();
EXPECT_EQ(expected_image_model, resource_manager()->GetImageModel(
clipboard_history()->GetItems().front()));
}
// Tests that copying duplicate HTML to the buffer results in only one render
// request, and that that request is canceled once when the item is forgotten.
TEST_F(ClipboardHistoryResourceManagerTest, DuplicateHTML) {
// Write two duplicate ClipboardDatas. Two things should be in clipboard
// history, but they should share a CachedImageModel.
ui::ImageModel expected_image_model = GetRandomImageModel();
ON_CALL(*mock_image_factory(), Render)
.WillByDefault(testing::WithArg<2>(
[&](ClipboardImageModelFactory::ImageModelCallback callback) {
std::move(callback).Run(expected_image_model);
}));
EXPECT_CALL(*mock_image_factory(), CancelRequest).Times(0);
EXPECT_CALL(*mock_image_factory(), Render).Times(1);
for (int i = 0; i < 2; ++i) {
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteHTML(base::UTF8ToUTF16("test"), "source_url");
}
FlushMessageLoop();
}
auto items = clipboard_history()->GetItems();
EXPECT_EQ(2u, items.size());
for (const auto& item : items)
EXPECT_EQ(expected_image_model, resource_manager()->GetImageModel(item));
}
// Tests that two different eligible ClipboardData copied results in two calls
// to Render and Cancel.
TEST_F(ClipboardHistoryResourceManagerTest, DifferentHTML) {
// Write two ClipboardData with different HTML.
ui::ImageModel first_expected_image_model = GetRandomImageModel();
ui::ImageModel second_expected_image_model = GetRandomImageModel();
std::deque<ui::ImageModel> expected_image_models{first_expected_image_model,
second_expected_image_model};
ON_CALL(*mock_image_factory(), Render)
.WillByDefault(testing::WithArg<2>(
[&](ClipboardImageModelFactory::ImageModelCallback callback) {
std::move(callback).Run(expected_image_models.front());
expected_image_models.pop_front();
}));
EXPECT_CALL(*mock_image_factory(), Render).Times(2);
EXPECT_CALL(*mock_image_factory(), CancelRequest).Times(0);
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteHTML(base::UTF8ToUTF16("test"), "source_url");
}
FlushMessageLoop();
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteHTML(base::UTF8ToUTF16("different"), "source_url");
}
FlushMessageLoop();
std::list<ClipboardHistoryItem> items = clipboard_history()->GetItems();
EXPECT_EQ(2u, items.size());
EXPECT_EQ(second_expected_image_model,
resource_manager()->GetImageModel(items.front()));
items.pop_front();
EXPECT_EQ(first_expected_image_model,
resource_manager()->GetImageModel(items.front()));
}
// Tests that items that are ineligible for CachedImageModels (items with image
// representations, or no markup) do not request Render.
TEST_F(ClipboardHistoryResourceManagerTest, IneligibleItem) {
// Write a ClipboardData with an image, no CachedImageModel should be created.
EXPECT_CALL(*mock_image_factory(), Render).Times(0);
EXPECT_CALL(*mock_image_factory(), CancelRequest).Times(0);
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteHTML(base::UTF8ToUTF16("test"), "source_url");
scw.WriteImage(GetRandomBitmap());
}
FlushMessageLoop();
EXPECT_EQ(1u, clipboard_history()->GetItems().size());
// Write a ClipboardData with no markup and no image. No CachedImageModel
// should be created.
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteText(base::UTF8ToUTF16("test"));
scw.WriteRTF("rtf");
scw.WriteBookmark(base::UTF8ToUTF16("bookmark_title"), "test_url");
}
FlushMessageLoop();
EXPECT_EQ(2u, clipboard_history()->GetItems().size());
}
// Tests that incomplete requests are canceled when the item corresponding with
// the request is forgotten by ClipboardHistory.
TEST_F(ClipboardHistoryResourceManagerTest, IncompleteRequestCanceled) {
EXPECT_CALL(*mock_image_factory(), Render).Times(1);
EXPECT_CALL(*mock_image_factory(), CancelRequest).Times(1);
// Because we do not provide an ON_CALL for MockClipboardImageModelFactory,
// Render will do nothing. This simulates an incomplete request from the
// perspective of ClipboardHistoryResourceManager.
{
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteHTML(base::UTF8ToUTF16("test"), "source_url");
}
FlushMessageLoop();
}
} // namespace ash
......@@ -8,6 +8,7 @@
#include <unordered_map>
#include "ash/clipboard/clipboard_history_controller.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/shell.h"
#include "ash/test/ash_test_base.h"
#include "base/strings/utf_string_conversions.h"
......@@ -37,7 +38,7 @@ class ClipboardHistoryTest : public AshTestBase {
Shell::Get()->clipboard_history_controller()->history());
}
const std::list<ui::ClipboardData>& GetClipboardHistoryData() {
const std::list<ClipboardHistoryItem>& GetClipboardHistoryItems() {
return clipboard_history_->GetItems();
}
......@@ -62,13 +63,13 @@ class ClipboardHistoryTest : public AshTestBase {
}
void EnsureTextHistory(const std::vector<base::string16>& expected_strings) {
const std::list<ui::ClipboardData>& datas = GetClipboardHistoryData();
EXPECT_EQ(expected_strings.size(), datas.size());
const std::list<ClipboardHistoryItem>& items = GetClipboardHistoryItems();
EXPECT_EQ(expected_strings.size(), items.size());
int expected_strings_index = 0;
for (auto& data : datas) {
for (const auto& item : items) {
EXPECT_EQ(expected_strings[expected_strings_index++],
base::UTF8ToUTF16(data.text()));
base::UTF8ToUTF16(item.data().text()));
}
}
......@@ -84,13 +85,13 @@ class ClipboardHistoryTest : public AshTestBase {
}
base::RunLoop().RunUntilIdle();
}
const std::list<ui::ClipboardData>& datas = GetClipboardHistoryData();
EXPECT_EQ(expected_bitmaps.size(), datas.size());
const std::list<ClipboardHistoryItem>& items = GetClipboardHistoryItems();
EXPECT_EQ(expected_bitmaps.size(), items.size());
int expected_bitmaps_index = 0;
for (auto& data : datas) {
for (const auto& item : items) {
EXPECT_TRUE(gfx::BitmapsAreEqual(
expected_bitmaps[expected_bitmaps_index++], data.bitmap()));
expected_bitmaps[expected_bitmaps_index++], item.data().bitmap()));
}
}
......@@ -110,12 +111,12 @@ class ClipboardHistoryTest : public AshTestBase {
}
base::RunLoop().RunUntilIdle();
const std::list<ui::ClipboardData> datas = GetClipboardHistoryData();
EXPECT_EQ(1u, datas.size());
const std::list<ClipboardHistoryItem> items = GetClipboardHistoryItems();
EXPECT_EQ(1u, items.size());
std::unordered_map<base::string16, base::string16> actual_data;
ui::ReadCustomDataIntoMap(datas.front().custom_data_data().c_str(),
datas.front().custom_data_data().size(),
ui::ReadCustomDataIntoMap(items.front().data().custom_data_data().c_str(),
items.front().data().custom_data_data().size(),
&actual_data);
EXPECT_EQ(expected_data, actual_data);
......
......@@ -112,6 +112,8 @@ component("cpp") {
"caption_buttons/snap_controller.h",
"cast_config_controller.cc",
"cast_config_controller.h",
"clipboard_image_model_factory.cc",
"clipboard_image_model_factory.h",
"default_frame_header.cc",
"default_frame_header.h",
"default_scale_factor_retriever.cc",
......
// Copyright 2020 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 "ash/public/cpp/clipboard_image_model_factory.h"
namespace ash {
namespace {
ClipboardImageModelFactory* g_instance = nullptr;
} // namespace
ClipboardImageModelFactory::ClipboardImageModelFactory() {
DCHECK_EQ(nullptr, g_instance);
g_instance = this;
}
ClipboardImageModelFactory::~ClipboardImageModelFactory() {
DCHECK_EQ(g_instance, this);
g_instance = nullptr;
}
// static
ClipboardImageModelFactory* ClipboardImageModelFactory::Get() {
return g_instance;
}
} // namespace ash
\ No newline at end of file
// Copyright 2020 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 ASH_PUBLIC_CPP_CLIPBOARD_IMAGE_MODEL_FACTORY_H_
#define ASH_PUBLIC_CPP_CLIPBOARD_IMAGE_MODEL_FACTORY_H_
#include <string>
#include "ash/public/cpp/ash_public_export.h"
#include "base/callback.h"
#include "base/unguessable_token.h"
#include "ui/base/models/image_model.h"
namespace ui {
class ImageModel;
} // namespace ui
namespace ash {
// A factory implemented in the Browser with the primary profile which is
// responsible for creating ui::ImageModels out of html strings from
// ClipboardHistory to work around dependency restrictions in ash. This will not
// create ImageModels until it is activated.
class ASH_PUBLIC_EXPORT ClipboardImageModelFactory {
public:
// Returns the singleton factory instance.
static ClipboardImageModelFactory* Get();
using ImageModelCallback = base::OnceCallback<void(ui::ImageModel)>;
// Asynchronously renders |html_markup|, identified by |id|. Later the
// rendered html will be returned via |callback|, which is called as long as
// the request is not canceled via CancelRequest. If the request times out, an
// empty ImageModel will be passed through |callback|.
virtual void Render(const base::UnguessableToken& id,
const std::string& html_markup,
ImageModelCallback callback) = 0;
// Called to stop rendering which was requested with |id|.
virtual void CancelRequest(const base::UnguessableToken& id) = 0;
// Until Activate() is called, ClipboardImageModelFactory is in an inactive
// state and all rendering requests will be queued until activated.
virtual void Activate() = 0;
// Called after Activate() to pause rendering requests.
virtual void Deactivate() = 0;
protected:
ClipboardImageModelFactory();
virtual ~ClipboardImageModelFactory();
};
} // namespace ash
#endif // ASH_PUBLIC_CPP_CLIPBOARD_IMAGE_MODEL_FACTORY_H_
......@@ -29,7 +29,9 @@
#include "chrome/browser/component_updater/sth_set_component_remover.h"
#include "chrome/browser/google/google_brand_chromeos.h"
#include "chrome/browser/net/nss_context.h"
#include "chrome/browser/ui/ash/clipboard_image_model_factory_impl.h"
#include "chrome/common/pref_names.h"
#include "chromeos/constants/chromeos_features.h"
#include "chromeos/network/network_cert_loader.h"
#include "chromeos/tpm/install_attributes.h"
#include "components/prefs/pref_service.h"
......@@ -200,6 +202,11 @@ void UserSessionInitializer::InitializePrimaryProfileServices(
if (crostini_manager)
crostini_manager->MaybeUpdateCrostini();
if (chromeos::features::IsClipboardHistoryEnabled()) {
clipboard_image_model_factory_impl_ =
std::make_unique<ClipboardImageModelFactoryImpl>(profile);
}
g_browser_process->platform_part()->InitializePrimaryProfileServices(profile);
}
......
......@@ -5,9 +5,12 @@
#ifndef CHROME_BROWSER_CHROMEOS_LOGIN_SESSION_USER_SESSION_INITIALIZER_H_
#define CHROME_BROWSER_CHROMEOS_LOGIN_SESSION_USER_SESSION_INITIALIZER_H_
#include <memory>
#include "components/session_manager/core/session_manager_observer.h"
#include "components/user_manager/user.h"
class ClipboardImageModelFactoryImpl;
class Profile;
namespace user_manager {
......@@ -72,6 +75,10 @@ class UserSessionInitializer : public session_manager::SessionManagerObserver {
base::OnceClosure init_rlz_impl_closure_for_testing_;
// Clipboard html image generator for the primary user.
std::unique_ptr<ClipboardImageModelFactoryImpl>
clipboard_image_model_factory_impl_;
base::WeakPtrFactory<UserSessionInitializer> weak_factory_{this};
};
......
......@@ -1862,6 +1862,8 @@ static_library("ui") {
"ash/chrome_screenshot_grabber_test_observer.h",
"ash/chrome_shell_delegate.cc",
"ash/chrome_shell_delegate.h",
"ash/clipboard_image_model_factory_impl.cc",
"ash/clipboard_image_model_factory_impl.h",
"ash/clipboard_util.cc",
"ash/clipboard_util.h",
"ash/holding_space/holding_space_keyed_service.cc",
......
......@@ -7,6 +7,7 @@
#include "ash/clipboard/clipboard_history.h"
#include "ash/clipboard/clipboard_history_controller.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/shell.h"
#include "base/test/bind_test_util.h"
#include "base/test/scoped_feature_list.h"
......@@ -75,7 +76,7 @@ ash::ClipboardHistoryController* GetClipboardHistoryController() {
return ash::Shell::Get()->clipboard_history_controller();
}
const std::list<ui::ClipboardData>& GetClipboardData() {
const std::list<ash::ClipboardHistoryItem>& GetClipboardItems() {
return GetClipboardHistoryController()->history()->GetItems();
}
......@@ -142,31 +143,31 @@ class ClipboardHistoryWithMultiProfileBrowserTest
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
VerifyClipboardHistoryAcrossMultiUser) {
LoginUser(account_id1_);
EXPECT_TRUE(GetClipboardData().empty());
EXPECT_TRUE(GetClipboardItems().empty());
// Store text when the user1 is active.
const std::string copypaste_data1("user1_text1");
SetClipboardText(copypaste_data1);
{
const std::list<ui::ClipboardData>& data = GetClipboardData();
EXPECT_EQ(1u, data.size());
EXPECT_EQ(copypaste_data1, data.front().text());
const std::list<ash::ClipboardHistoryItem>& items = GetClipboardItems();
EXPECT_EQ(1u, items.size());
EXPECT_EQ(copypaste_data1, items.front().data().text());
}
// Log in as the user2. The clipboard history should be non-empty.
chromeos::UserAddingScreen::Get()->Start();
AddUser(account_id2_);
EXPECT_FALSE(GetClipboardData().empty());
EXPECT_FALSE(GetClipboardItems().empty());
// Store text when the user2 is active.
const std::string copypaste_data2("user2_text1");
SetClipboardText(copypaste_data2);
{
const std::list<ui::ClipboardData>& data = GetClipboardData();
EXPECT_EQ(2u, data.size());
EXPECT_EQ(copypaste_data2, data.front().text());
const std::list<ash::ClipboardHistoryItem>& items = GetClipboardItems();
EXPECT_EQ(2u, items.size());
EXPECT_EQ(copypaste_data2, items.front().data().text());
}
// Switch to the user1.
......@@ -177,19 +178,19 @@ IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
SetClipboardText(copypaste_data3);
{
const std::list<ui::ClipboardData>& data = GetClipboardData();
EXPECT_EQ(3u, data.size());
const std::list<ash::ClipboardHistoryItem>& items = GetClipboardItems();
EXPECT_EQ(3u, items.size());
// Note that items in |data| follow the time ordering. The most recent item
// is always the first one.
auto it = data.begin();
EXPECT_EQ(copypaste_data3, it->text());
auto it = items.begin();
EXPECT_EQ(copypaste_data3, it->data().text());
std::advance(it, 1u);
EXPECT_EQ(copypaste_data2, it->text());
EXPECT_EQ(copypaste_data2, it->data().text());
std::advance(it, 1u);
EXPECT_EQ(copypaste_data1, it->text());
EXPECT_EQ(copypaste_data1, it->data().text());
}
}
......@@ -203,7 +204,7 @@ IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
CloseAllBrowsers();
// No clipboard data. So the clipboard history menu should not show.
ASSERT_TRUE(GetClipboardData().empty());
ASSERT_TRUE(GetClipboardItems().empty());
ShowContextMenuViaAccelerator();
EXPECT_FALSE(GetClipboardHistoryController()->IsMenuShowing());
......
// Copyright 2020 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 "chrome/browser/ui/ash/clipboard_image_model_factory_impl.h"
#include "chrome/browser/profiles/profile.h"
ClipboardImageModelFactoryImpl::ClipboardImageModelFactoryImpl(
Profile* primary_profile)
: primary_profile_(primary_profile) {
DCHECK(primary_profile_);
}
ClipboardImageModelFactoryImpl::~ClipboardImageModelFactoryImpl() = default;
void ClipboardImageModelFactoryImpl::Render(const base::UnguessableToken& id,
const std::string& html_markup,
ImageModelCallback callback) {
std::move(callback).Run(ui::ImageModel());
}
void ClipboardImageModelFactoryImpl::CancelRequest(
const base::UnguessableToken& id) {}
void ClipboardImageModelFactoryImpl::Activate() {}
void ClipboardImageModelFactoryImpl::Deactivate() {}
// Copyright 2020 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 CHROME_BROWSER_UI_ASH_CLIPBOARD_IMAGE_MODEL_FACTORY_IMPL_H_
#define CHROME_BROWSER_UI_ASH_CLIPBOARD_IMAGE_MODEL_FACTORY_IMPL_H_
#include <string>
#include "ash/public/cpp/clipboard_image_model_factory.h"
#include "base/unguessable_token.h"
class Profile;
// Implements the singleton ClipboardImageModelFactory.
class ClipboardImageModelFactoryImpl : public ash::ClipboardImageModelFactory {
public:
explicit ClipboardImageModelFactoryImpl(Profile* primary_profile);
ClipboardImageModelFactoryImpl(ClipboardImageModelFactoryImpl&) = delete;
ClipboardImageModelFactoryImpl& operator=(ClipboardImageModelFactoryImpl&) =
delete;
~ClipboardImageModelFactoryImpl() override;
private:
// ash::ClipboardImageModelFactory:
void Render(const base::UnguessableToken& id,
const std::string& html_markup,
ImageModelCallback callback) override;
void CancelRequest(const base::UnguessableToken& id) override;
void Activate() override;
void Deactivate() override;
// The primary profile, used instead of the active profile to create the
// WebContents that renders html.
Profile* const primary_profile_;
};
#endif // CHROME_BROWSER_UI_ASH_CLIPBOARD_IMAGE_MODEL_FACTORY_IMPL_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment