1 // Copyright 2020 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "ash/clipboard/clipboard_history_resource_manager.h"
6
7 #include <string>
8
9 #include "ash/clipboard/clipboard_history_util.h"
10 #include "ash/public/cpp/clipboard_image_model_factory.h"
11 #include "ash/resources/vector_icons/vector_icons.h"
12 #include "base/bind.h"
13 #include "base/notreached.h"
14 #include "base/stl_util.h"
15 #include "base/strings/escape.h"
16 #include "base/strings/string_split.h"
17 #include "base/strings/string_util.h"
18 #include "base/strings/utf_string_conversions.h"
19 #include "ui/base/clipboard/clipboard_data.h"
20 #include "ui/base/clipboard/custom_data_helper.h"
21 #include "ui/base/resource/resource_bundle.h"
22 #include "ui/gfx/canvas.h"
23 #include "ui/gfx/color_palette.h"
24 #include "ui/gfx/image/canvas_image_source.h"
25 #include "ui/gfx/paint_vector_icon.h"
26 #include "ui/strings/grit/ui_strings.h"
27
28 namespace ash {
29
30 namespace {
31
32 constexpr int kPlaceholderImageEdgePadding = 5;
33 constexpr int kPlaceholderImageWidth = 234;
34 constexpr int kPlaceholderImageHeight = 74;
35 constexpr int kPlaceholderImageOutlineCornerRadius = 8;
36 constexpr int kPlaceholderImageSVGSize = 32;
37
38 // Used to draw the UnrenderedHTMLPlaceholderImage, which is shown while HTML is
39 // rendering. Drawn in order to turn the square and single colored SVG into a
40 // multicolored rectangle image.
41 class UnrenderedHTMLPlaceholderImage : public gfx::CanvasImageSource {
42 public:
UnrenderedHTMLPlaceholderImage()43 UnrenderedHTMLPlaceholderImage()
44 : gfx::CanvasImageSource(
45 gfx::Size(kPlaceholderImageWidth, kPlaceholderImageHeight)) {}
46 UnrenderedHTMLPlaceholderImage(const UnrenderedHTMLPlaceholderImage&) =
47 delete;
48 UnrenderedHTMLPlaceholderImage& operator=(
49 const UnrenderedHTMLPlaceholderImage&) = delete;
50 ~UnrenderedHTMLPlaceholderImage() override = default;
51
52 // gfx::CanvasImageSource:
Draw(gfx::Canvas * canvas)53 void Draw(gfx::Canvas* canvas) override {
54 cc::PaintFlags flags;
55 flags.setStyle(cc::PaintFlags::kFill_Style);
56 flags.setAntiAlias(true);
57 flags.setColor(gfx::kGoogleGrey100);
58 canvas->DrawRoundRect(
59 {kPlaceholderImageEdgePadding, kPlaceholderImageEdgePadding,
60 kPlaceholderImageWidth - 2 * kPlaceholderImageEdgePadding,
61 kPlaceholderImageHeight - 2 * kPlaceholderImageEdgePadding},
62 kPlaceholderImageOutlineCornerRadius, flags);
63
64 flags = cc::PaintFlags();
65 flags.setStyle(cc::PaintFlags::kFill_Style);
66 flags.setAntiAlias(true);
67 const gfx::ImageSkia center_image =
68 gfx::CreateVectorIcon(kUnrenderedHtmlPlaceholderIcon,
69 kPlaceholderImageSVGSize, gfx::kGoogleGrey600);
70 canvas->DrawImageInt(
71 center_image, (size().width() - center_image.size().width()) / 2,
72 (size().height() - center_image.size().height()) / 2, flags);
73 }
74 };
75
76 // Helpers ---------------------------------------------------------------------
77
78 // Returns the localized string for the specified |resource_id|.
GetLocalizedString(int resource_id)79 base::string16 GetLocalizedString(int resource_id) {
80 return ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
81 resource_id);
82 }
83
84 // Returns the label to display for the custom data contained within |data|.
GetLabelForCustomData(const ui::ClipboardData & data)85 base::string16 GetLabelForCustomData(const ui::ClipboardData& data) {
86 // Currently the only supported type of custom data is file system data. This
87 // code should not be reached if `data` does not contain file system data.
88 base::string16 sources = ClipboardHistoryUtil::GetFileSystemSources(data);
89 if (sources.empty()) {
90 NOTREACHED();
91 return base::string16();
92 }
93
94 // Split sources into a list.
95 std::vector<base::StringPiece16> source_list =
96 base::SplitStringPiece(sources, base::UTF8ToUTF16("\n"),
97 base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
98
99 // Strip path information, so all that's left are file names.
100 for (auto it = source_list.begin(); it != source_list.end(); ++it)
101 *it = it->substr(it->find_last_of(base::UTF8ToUTF16("/")) + 1);
102
103 // Join file names, unescaping encoded character sequences for display. This
104 // ensures that "My%20File.txt" will display as "My File.txt".
105 return base::UTF8ToUTF16(base::UnescapeURLComponent(
106 base::UTF16ToUTF8(base::JoinString(source_list, base::UTF8ToUTF16(", "))),
107 base::UnescapeRule::SPACES));
108 }
109
110 } // namespace
111
112 // ClipboardHistoryResourceManager ---------------------------------------------
113
ClipboardHistoryResourceManager(const ClipboardHistory * clipboard_history)114 ClipboardHistoryResourceManager::ClipboardHistoryResourceManager(
115 const ClipboardHistory* clipboard_history)
116 : clipboard_history_(clipboard_history),
117 placeholder_image_model_(
118 ui::ImageModel::FromImageSkia(gfx::CanvasImageSource::MakeImageSkia<
119 UnrenderedHTMLPlaceholderImage>())) {
120 clipboard_history_->AddObserver(this);
121 }
122
~ClipboardHistoryResourceManager()123 ClipboardHistoryResourceManager::~ClipboardHistoryResourceManager() {
124 clipboard_history_->RemoveObserver(this);
125 if (ClipboardImageModelFactory::Get())
126 ClipboardImageModelFactory::Get()->OnShutdown();
127 }
128
GetImageModel(const ClipboardHistoryItem & item) const129 ui::ImageModel ClipboardHistoryResourceManager::GetImageModel(
130 const ClipboardHistoryItem& item) const {
131 // Use a cached image model when possible.
132 auto cached_image_model = FindCachedImageModelForItem(item);
133 if (cached_image_model == cached_image_models_.end() ||
134 cached_image_model->image_model.IsEmpty()) {
135 return placeholder_image_model_;
136 }
137 return cached_image_model->image_model;
138 }
139
GetLabel(const ClipboardHistoryItem & item) const140 base::string16 ClipboardHistoryResourceManager::GetLabel(
141 const ClipboardHistoryItem& item) const {
142 const ui::ClipboardData& data = item.data();
143 switch (ClipboardHistoryUtil::CalculateMainFormat(data).value()) {
144 case ui::ClipboardInternalFormat::kBitmap:
145 return GetLocalizedString(IDS_CLIPBOARD_MENU_IMAGE);
146 case ui::ClipboardInternalFormat::kText:
147 return base::UTF8ToUTF16(data.text());
148 case ui::ClipboardInternalFormat::kHtml:
149 return base::UTF8ToUTF16(data.markup_data());
150 case ui::ClipboardInternalFormat::kSvg:
151 return base::UTF8ToUTF16(data.svg_data());
152 case ui::ClipboardInternalFormat::kRtf:
153 return GetLocalizedString(IDS_CLIPBOARD_MENU_RTF_CONTENT);
154 case ui::ClipboardInternalFormat::kBookmark:
155 return base::UTF8ToUTF16(data.bookmark_title());
156 case ui::ClipboardInternalFormat::kWeb:
157 return GetLocalizedString(IDS_CLIPBOARD_MENU_WEB_SMART_PASTE);
158 case ui::ClipboardInternalFormat::kCustom:
159 return GetLabelForCustomData(data);
160 }
161 }
162
AddObserver(Observer * observer) const163 void ClipboardHistoryResourceManager::AddObserver(Observer* observer) const {
164 observers_.AddObserver(observer);
165 }
166
RemoveObserver(Observer * observer) const167 void ClipboardHistoryResourceManager::RemoveObserver(Observer* observer) const {
168 observers_.RemoveObserver(observer);
169 }
170
171 ClipboardHistoryResourceManager::CachedImageModel::CachedImageModel() = default;
172
173 ClipboardHistoryResourceManager::CachedImageModel::CachedImageModel(
174 const CachedImageModel& other) = default;
175
176 ClipboardHistoryResourceManager::CachedImageModel&
177 ClipboardHistoryResourceManager::CachedImageModel::operator=(
178 const CachedImageModel&) = default;
179
180 ClipboardHistoryResourceManager::CachedImageModel::~CachedImageModel() =
181 default;
182
CacheImageModel(const base::UnguessableToken & id,ui::ImageModel image_model)183 void ClipboardHistoryResourceManager::CacheImageModel(
184 const base::UnguessableToken& id,
185 ui::ImageModel image_model) {
186 auto cached_image_model = base::ConstCastIterator(
187 cached_image_models_, FindCachedImageModelForId(id));
188 if (cached_image_model == cached_image_models_.end())
189 return;
190
191 cached_image_model->image_model = std::move(image_model);
192
193 for (auto& observer : observers_) {
194 observer.OnCachedImageModelUpdated(
195 cached_image_model->clipboard_history_item_ids);
196 }
197 }
198
199 std::vector<ClipboardHistoryResourceManager::CachedImageModel>::const_iterator
FindCachedImageModelForId(const base::UnguessableToken & id) const200 ClipboardHistoryResourceManager::FindCachedImageModelForId(
201 const base::UnguessableToken& id) const {
202 return std::find_if(cached_image_models_.cbegin(),
203 cached_image_models_.cend(),
204 [&](const auto& cached_image_model) {
205 return cached_image_model.id == id;
206 });
207 }
208
209 std::vector<ClipboardHistoryResourceManager::CachedImageModel>::const_iterator
FindCachedImageModelForItem(const ClipboardHistoryItem & item) const210 ClipboardHistoryResourceManager::FindCachedImageModelForItem(
211 const ClipboardHistoryItem& item) const {
212 return std::find_if(
213 cached_image_models_.cbegin(), cached_image_models_.cend(),
214 [&](const auto& cached_image_model) {
215 return base::Contains(cached_image_model.clipboard_history_item_ids,
216 item.id());
217 });
218 }
219
CancelUnfinishedRequests()220 void ClipboardHistoryResourceManager::CancelUnfinishedRequests() {
221 for (const auto& cached_image_model : cached_image_models_) {
222 if (cached_image_model.image_model.IsEmpty())
223 ClipboardImageModelFactory::Get()->CancelRequest(cached_image_model.id);
224 }
225 }
226
OnClipboardHistoryItemAdded(const ClipboardHistoryItem & item)227 void ClipboardHistoryResourceManager::OnClipboardHistoryItemAdded(
228 const ClipboardHistoryItem& item) {
229 // For items that will be represented by their rendered HTML, we need to do
230 // some prep work to pre-render and cache an image model.
231 if (ClipboardHistoryUtil::CalculateMainFormat(item.data()) !=
232 ui::ClipboardInternalFormat::kHtml) {
233 return;
234 }
235
236 const auto& items = clipboard_history_->GetItems();
237
238 // See if we have an |existing| item that will render the same as |item|.
239 auto it = std::find_if(items.begin(), items.end(), [&](const auto& existing) {
240 return &existing != &item && existing.data().bitmap().isNull() &&
241 existing.data().markup_data() == item.data().markup_data();
242 });
243
244 // If we don't have an existing image model in the cache, create one and
245 // instruct ClipboardImageModelFactory to render it. Note that the factory may
246 // or may not start rendering immediately depending on its activation status.
247 if (it == items.end()) {
248 base::UnguessableToken id = base::UnguessableToken::Create();
249 CachedImageModel cached_image_model;
250 cached_image_model.id = id;
251 cached_image_model.clipboard_history_item_ids.push_back(item.id());
252 cached_image_models_.push_back(std::move(cached_image_model));
253
254 ClipboardImageModelFactory::Get()->Render(
255 id, item.data().markup_data(),
256 base::BindOnce(&ClipboardHistoryResourceManager::CacheImageModel,
257 weak_factory_.GetWeakPtr(), id));
258 return;
259 }
260 // If we do have an existing model, we need only to update its usages.
261 auto cached_image_model = base::ConstCastIterator(
262 cached_image_models_, FindCachedImageModelForItem(*it));
263 DCHECK(cached_image_model != cached_image_models_.end());
264 cached_image_model->clipboard_history_item_ids.push_back(item.id());
265 }
266
OnClipboardHistoryItemRemoved(const ClipboardHistoryItem & item)267 void ClipboardHistoryResourceManager::OnClipboardHistoryItemRemoved(
268 const ClipboardHistoryItem& item) {
269 // For items that will not be represented by their rendered HTML, do nothing.
270 if (ClipboardHistoryUtil::CalculateMainFormat(item.data()) !=
271 ui::ClipboardInternalFormat::kHtml) {
272 return;
273 }
274
275 // We should have an image model in the cache.
276 auto cached_image_model = base::ConstCastIterator(
277 cached_image_models_, FindCachedImageModelForItem(item));
278
279 DCHECK(cached_image_model != cached_image_models_.end());
280
281 // Update usages.
282 base::Erase(cached_image_model->clipboard_history_item_ids, item.id());
283 if (!cached_image_model->clipboard_history_item_ids.empty())
284 return;
285
286 // If the ImageModel was never rendered, cancel the request.
287 if (cached_image_model->image_model.IsEmpty())
288 ClipboardImageModelFactory::Get()->CancelRequest(cached_image_model->id);
289
290 // If the cached image model is no longer in use, it can be erased.
291 cached_image_models_.erase(cached_image_model);
292 }
293
OnClipboardHistoryCleared()294 void ClipboardHistoryResourceManager::OnClipboardHistoryCleared() {
295 CancelUnfinishedRequests();
296 cached_image_models_ = std::vector<CachedImageModel>();
297 }
298
299 } // namespace ash
300