blob: 5f53e3351807102407774d8ef9f55188f96f1d00 [file] [log] [blame]
// 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_controller_impl.h"
#include <map>
#include <memory>
#include <set>
#include <vector>
#include "ash/accelerators/accelerator_controller_impl.h"
#include "ash/clipboard/clipboard_history_menu_model_adapter.h"
#include "ash/clipboard/clipboard_history_resource_manager.h"
#include "ash/clipboard/clipboard_history_util.h"
#include "ash/clipboard/clipboard_nudge_constants.h"
#include "ash/clipboard/clipboard_nudge_controller.h"
#include "ash/clipboard/scoped_clipboard_history_pause_impl.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/style/scoped_light_mode_as_default.h"
#include "ash/public/cpp/window_tree_host_lookup.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/wm/window_util.h"
#include "base/barrier_closure.h"
#include "base/bind.h"
#include "base/callback_forward.h"
#include "base/json/values_util.h"
#include "base/location.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/one_shot_event.h"
#include "base/ranges/algorithm.h"
#include "base/strings/utf_string_conversions.h"
#include "base/synchronization/lock.h"
#include "base/task/bind_post_task.h"
#include "base/task/thread_pool.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/unguessable_token.h"
#include "base/values.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/accelerators/accelerator.h"
#include "ui/base/clipboard/clipboard_constants.h"
#include "ui/base/clipboard/clipboard_data.h"
#include "ui/base/clipboard/clipboard_non_backed.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/models/image_model.h"
#include "ui/base/models/menu_separator_types.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/base/webui/web_ui_util.h"
#include "ui/events/event.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/keyboard_codes_posix.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/controls/menu/menu_controller.h"
namespace ash {
namespace {
constexpr char kImageDataKey[] = "imageData";
constexpr char kTextDataKey[] = "textData";
constexpr char kFormatDataKey[] = "displayFormat";
constexpr char kPngFormat[] = "png";
constexpr char kHtmlFormat[] = "html";
constexpr char kTextFormat[] = "text";
constexpr char kFileFormat[] = "file";
ui::ClipboardNonBacked* GetClipboard() {
auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
DCHECK(clipboard);
return clipboard;
}
// Encodes `bitmap` and maps the corresponding ClipboardHistoryItem ID, `id, to
// the resulting PNG in `encoded_pngs`. This function should run on a background
// thread.
void EncodeBitmapToPNG(
base::OnceClosure barrier_callback,
std::map<base::UnguessableToken, std::vector<uint8_t>>* const encoded_pngs,
base::UnguessableToken id,
SkBitmap bitmap) {
auto png = ui::ClipboardData::EncodeBitmapData(bitmap);
// Don't acquire the lock until after the image encoding has finished.
static base::NoDestructor<base::Lock> map_lock;
base::AutoLock lock(*map_lock);
encoded_pngs->emplace(id, std::move(png));
std::move(barrier_callback).Run();
}
using ClipboardHistoryPasteType =
ash::ClipboardHistoryControllerImpl::ClipboardHistoryPasteType;
bool IsPlainTextPaste(ClipboardHistoryPasteType paste_type) {
switch (paste_type) {
case ClipboardHistoryPasteType::kPlainTextAccelerator:
case ClipboardHistoryPasteType::kPlainTextKeystroke:
case ClipboardHistoryPasteType::kPlainTextMouse:
case ClipboardHistoryPasteType::kPlainTextTouch:
case ClipboardHistoryPasteType::kPlainTextVirtualKeyboard:
return true;
case ClipboardHistoryPasteType::kRichTextAccelerator:
case ClipboardHistoryPasteType::kRichTextKeystroke:
case ClipboardHistoryPasteType::kRichTextMouse:
case ClipboardHistoryPasteType::kRichTextTouch:
case ClipboardHistoryPasteType::kRichTextVirtualKeyboard:
return false;
}
}
} // namespace
// ClipboardHistoryControllerImpl::AcceleratorTarget ---------------------------
class ClipboardHistoryControllerImpl::AcceleratorTarget
: public ui::AcceleratorTarget {
public:
explicit AcceleratorTarget(ClipboardHistoryControllerImpl* controller)
: controller_(controller),
delete_selected_(ui::Accelerator(
/*key_code=*/ui::VKEY_BACK,
/*modifiers=*/ui::EF_NONE,
/*key_state=*/ui::Accelerator::KeyState::PRESSED)),
tab_navigation_(ui::Accelerator(
/*key_code=*/ui::VKEY_TAB,
/*modifiers=*/ui::EF_NONE,
/*key_state=*/ui::Accelerator::KeyState::PRESSED)),
shift_tab_navigation_(ui::Accelerator(
/*key_code=*/ui::VKEY_TAB,
/*modifiers=*/ui::EF_SHIFT_DOWN,
/*key_state=*/ui::Accelerator::KeyState::PRESSED)) {}
AcceleratorTarget(const AcceleratorTarget&) = delete;
AcceleratorTarget& operator=(const AcceleratorTarget&) = delete;
~AcceleratorTarget() override = default;
void OnMenuShown() {
Shell::Get()->accelerator_controller()->Register(
{delete_selected_, tab_navigation_, shift_tab_navigation_},
/*accelerator_target=*/this);
}
void OnMenuClosed() {
Shell::Get()->accelerator_controller()->Unregister(
delete_selected_, /*accelerator_target=*/this);
Shell::Get()->accelerator_controller()->Unregister(
tab_navigation_, /*accelerator_target=*/this);
Shell::Get()->accelerator_controller()->Unregister(
shift_tab_navigation_, /*accelerator_target=*/this);
}
private:
// ui::AcceleratorTarget:
bool AcceleratorPressed(const ui::Accelerator& accelerator) override {
if (accelerator == delete_selected_) {
HandleDeleteSelected(accelerator.modifiers());
} else if (accelerator == tab_navigation_) {
HandleTab();
} else if (accelerator == shift_tab_navigation_) {
HandleShiftTab();
} else {
NOTREACHED();
return false;
}
return true;
}
bool CanHandleAccelerators() const override {
return controller_->IsMenuShowing() || controller_->CanShowMenu();
}
void HandleDeleteSelected(int event_flags) {
DCHECK(controller_->IsMenuShowing());
controller_->DeleteSelectedMenuItemIfAny();
}
void HandleTab() {
DCHECK(controller_->IsMenuShowing());
controller_->AdvancePseudoFocus(/*reverse=*/false);
}
void HandleShiftTab() {
DCHECK(controller_->IsMenuShowing());
controller_->AdvancePseudoFocus(/*reverse=*/true);
}
// The controller responsible for showing the Clipboard History menu.
ClipboardHistoryControllerImpl* const controller_;
// The accelerator to delete the selected menu item. It is only registered
// while the menu is showing.
const ui::Accelerator delete_selected_;
// Move the pseudo focus forward.
const ui::Accelerator tab_navigation_;
// Moves the pseudo focus backward.
const ui::Accelerator shift_tab_navigation_;
};
// ClipboardHistoryControllerImpl::MenuDelegate --------------------------------
class ClipboardHistoryControllerImpl::MenuDelegate
: public ui::SimpleMenuModel::Delegate {
public:
explicit MenuDelegate(ClipboardHistoryControllerImpl* controller)
: controller_(controller) {}
MenuDelegate(const MenuDelegate&) = delete;
MenuDelegate& operator=(const MenuDelegate&) = delete;
// ui::SimpleMenuModel::Delegate:
void ExecuteCommand(int command_id, int event_flags) override {
controller_->ExecuteCommand(command_id, event_flags);
}
private:
// The controller responsible for showing the Clipboard History menu.
ClipboardHistoryControllerImpl* const controller_;
};
// ClipboardHistoryControllerImpl ----------------------------------------------
ClipboardHistoryControllerImpl::ClipboardHistoryControllerImpl()
: 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)),
nudge_controller_(
std::make_unique<ClipboardNudgeController>(clipboard_history_.get(),
this)) {
clipboard_history_->AddObserver(this);
resource_manager_->AddObserver(this);
}
ClipboardHistoryControllerImpl::~ClipboardHistoryControllerImpl() {
resource_manager_->RemoveObserver(this);
clipboard_history_->RemoveObserver(this);
}
void ClipboardHistoryControllerImpl::Shutdown() {
nudge_controller_.reset();
}
void ClipboardHistoryControllerImpl::AddObserver(
ClipboardHistoryController::Observer* observer) const {
observers_.AddObserver(observer);
}
void ClipboardHistoryControllerImpl::RemoveObserver(
ClipboardHistoryController::Observer* observer) const {
observers_.RemoveObserver(observer);
}
bool ClipboardHistoryControllerImpl::IsMenuShowing() const {
return context_menu_ && context_menu_->IsRunning();
}
void ClipboardHistoryControllerImpl::ToggleMenuShownByAccelerator() {
if (IsMenuShowing()) {
// Before hiding the menu, paste the selected menu item, or the first item
// if none is selected.
PasteMenuItemData(context_menu_->GetSelectedMenuItemCommand().value_or(
ClipboardHistoryUtil::kFirstItemCommandId),
ClipboardHistoryPasteType::kRichTextAccelerator);
return;
}
if (ClipboardHistoryUtil::IsEnabledInCurrentMode() && IsEmpty()) {
nudge_controller_->ShowNudge(ClipboardNudgeType::kZeroStateNudge);
return;
}
ShowMenu(CalculateAnchorRect(), ui::MENU_SOURCE_KEYBOARD,
crosapi::mojom::ClipboardHistoryControllerShowSource::kAccelerator);
}
void ClipboardHistoryControllerImpl::ShowMenu(
const gfx::Rect& anchor_rect,
ui::MenuSourceType source_type,
crosapi::mojom::ClipboardHistoryControllerShowSource show_source) {
if (IsMenuShowing() || !CanShowMenu())
return;
// Close the running context menu if any before showing the clipboard history
// menu. Because the clipboard history menu should not be nested.
auto* active_menu_instance = views::MenuController::GetActiveInstance();
if (active_menu_instance)
active_menu_instance->Cancel(views::MenuController::ExitType::kAll);
context_menu_ = ClipboardHistoryMenuModelAdapter::Create(
menu_delegate_.get(),
base::BindRepeating(&ClipboardHistoryControllerImpl::OnMenuClosed,
base::Unretained(this)),
clipboard_history_.get(), resource_manager_.get());
context_menu_->Run(anchor_rect, source_type);
DCHECK(IsMenuShowing());
accelerator_target_->OnMenuShown();
base::UmaHistogramEnumeration("Ash.ClipboardHistory.ContextMenu.ShowMenu",
show_source);
// The first menu item should be selected as default after the clipboard
// history menu shows. Note that the menu item is selected asynchronously
// to avoid the interference from synthesized mouse events.
menu_task_timer_.Start(
FROM_HERE, base::TimeDelta(),
base::BindOnce(
[](const base::WeakPtr<ClipboardHistoryControllerImpl>&
controller_weak_ptr) {
if (!controller_weak_ptr)
return;
controller_weak_ptr->context_menu_->SelectMenuItemWithCommandId(
ClipboardHistoryUtil::kFirstItemCommandId);
if (controller_weak_ptr->initial_item_selected_callback_for_test_) {
controller_weak_ptr->initial_item_selected_callback_for_test_
.Run();
}
},
weak_ptr_factory_.GetWeakPtr()));
for (auto& observer : observers_)
observer.OnClipboardHistoryMenuShown(show_source);
}
gfx::Rect ClipboardHistoryControllerImpl::GetMenuBoundsInScreenForTest() const {
return context_menu_->GetMenuBoundsInScreenForTest();
}
void ClipboardHistoryControllerImpl::GetHistoryValuesForTest(
GetHistoryValuesCallback callback) const {
GetHistoryValues(/*item_id_filter=*/std::set<std::string>(),
std::move(callback));
}
void ClipboardHistoryControllerImpl::BlockGetHistoryValuesForTest() {
get_history_values_blocker_for_test_.reset();
get_history_values_blocker_for_test_ = std::make_unique<base::OneShotEvent>();
}
void ClipboardHistoryControllerImpl::ResumeGetHistoryValuesForTest() {
DCHECK(get_history_values_blocker_for_test_);
get_history_values_blocker_for_test_->Signal();
}
bool ClipboardHistoryControllerImpl::ShouldShowNewFeatureBadge() const {
return chromeos::features::IsClipboardHistoryContextMenuNudgeEnabled() &&
nudge_controller_->ShouldShowNewFeatureBadge();
}
void ClipboardHistoryControllerImpl::MarkNewFeatureBadgeShown() {
nudge_controller_->MarkNewFeatureBadgeShown();
}
void ClipboardHistoryControllerImpl::OnScreenshotNotificationCreated() {
nudge_controller_->MarkScreenshotNotificationShown();
}
bool ClipboardHistoryControllerImpl::CanShowMenu() const {
return !IsEmpty() && ClipboardHistoryUtil::IsEnabledInCurrentMode();
}
bool ClipboardHistoryControllerImpl::IsEmpty() const {
return clipboard_history_->IsEmpty();
}
std::unique_ptr<ScopedClipboardHistoryPause>
ClipboardHistoryControllerImpl::CreateScopedPause() {
return std::make_unique<ScopedClipboardHistoryPauseImpl>(
clipboard_history_.get());
}
void ClipboardHistoryControllerImpl::GetHistoryValues(
const std::set<std::string>& item_id_filter,
GetHistoryValuesCallback callback) const {
// Map of ClipboardHistoryItem IDs to their corresponding bitmaps.
std::map<base::UnguessableToken, SkBitmap> bitmaps_to_be_encoded;
// Get the clipboard data for each clipboard history item.
for (auto& item : clipboard_history_->GetItems()) {
// If the |item_id_filter| contains values, then only return the clipboard
// items included in it.
if (!item_id_filter.empty() &&
item_id_filter.find(item.id().ToString()) == item_id_filter.end()) {
continue;
}
if (ash::ClipboardHistoryUtil::CalculateDisplayFormat(item.data()) ==
ash::ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kPng) {
const auto& maybe_png = item.data().maybe_png();
if (!maybe_png.has_value()) {
// The clipboard contains an image which has not yet been encoded to a
// PNG.
auto maybe_bitmap = item.data().GetBitmapIfPngNotEncoded();
DCHECK(maybe_bitmap.has_value());
bitmaps_to_be_encoded.emplace(item.id(),
std::move(maybe_bitmap.value()));
}
}
}
// Map of ClipboardHistoryItem ID to its encoded PNG. Since encoding images
// may happen on separate threads, a lock is used to ensure thread-safe
// insertion into `encoded_pngs`.
auto encoded_pngs = std::make_unique<
std::map<base::UnguessableToken, std::vector<uint8_t>>>();
auto* encoded_pngs_ptr = encoded_pngs.get();
// Post back to this sequence once all images have been encoded.
base::RepeatingClosure barrier = base::BarrierClosure(
bitmaps_to_be_encoded.size(),
base::BindPostTask(
base::SequencedTaskRunnerHandle::Get(),
base::BindOnce(
&ClipboardHistoryControllerImpl::GetHistoryValuesWithEncodedPNGs,
weak_ptr_factory_.GetWeakPtr(), item_id_filter,
std::move(callback), std::move(encoded_pngs))));
// Encode images on background threads.
for (auto id_and_bitmap : bitmaps_to_be_encoded) {
base::ThreadPool::PostTask(
FROM_HERE, base::BindOnce(&EncodeBitmapToPNG, barrier, encoded_pngs_ptr,
std::move(id_and_bitmap.first),
std::move(id_and_bitmap.second)));
}
if (!new_bitmap_to_write_while_encoding_for_test_.isNull()) {
ui::ScopedClipboardWriter scw(ui::ClipboardBuffer::kCopyPaste);
scw.WriteImage(new_bitmap_to_write_while_encoding_for_test_);
new_bitmap_to_write_while_encoding_for_test_.reset();
}
}
void ClipboardHistoryControllerImpl::GetHistoryValuesWithEncodedPNGs(
const std::set<std::string>& item_id_filter,
GetHistoryValuesCallback callback,
std::unique_ptr<std::map<base::UnguessableToken, std::vector<uint8_t>>>
encoded_pngs) {
// If a test is performing some work that must be done before history values
// are returned, wait to run this function until that work is finished.
if (get_history_values_blocker_for_test_ &&
!get_history_values_blocker_for_test_->is_signaled()) {
get_history_values_blocker_for_test_->Post(
FROM_HERE,
base::BindOnce(
&ClipboardHistoryControllerImpl::GetHistoryValuesWithEncodedPNGs,
weak_ptr_factory_.GetWeakPtr(), item_id_filter, std::move(callback),
std::move(encoded_pngs)));
return;
}
base::Value item_results(base::Value::Type::LIST);
DCHECK(encoded_pngs);
// Check after asynchronous PNG encoding finishes to make sure we have not
// entered a state where clipboard history is disabled, e.g., a locked screen.
if (!ClipboardHistoryUtil::IsEnabledInCurrentMode()) {
std::move(callback).Run(std::move(item_results));
return;
}
bool all_images_encoded = true;
// Get the clipboard data for each clipboard history item.
for (auto& item : clipboard_history_->GetItems()) {
// If the |item_id_filter| contains values, then only return the clipboard
// items included in it.
if (!item_id_filter.empty() &&
item_id_filter.find(item.id().ToString()) == item_id_filter.end()) {
continue;
}
base::Value item_value(base::Value::Type::DICTIONARY);
switch (ash::ClipboardHistoryUtil::CalculateDisplayFormat(item.data())) {
case ash::ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kPng: {
if (!item.data().maybe_png().has_value()) {
// The clipboard contains an image which has not yet been encoded to a
// PNG. Hopefully we just finished encoding and the PNG can be found
// in `encoded_pngs`, otherwise this item was added while other PNGs
// were being encoded.
auto png_it = encoded_pngs->find(item.id());
if (png_it == encoded_pngs->end()) {
// Can't find the encoded PNG. We'll need to restart
// GetHistoryValues from the top, but allow this for loop to finish
// to let PNGs we've already encoded get set to their appropriate
// clipboards, to avoid re-encoding.
all_images_encoded = false;
} else {
item.data().SetPngDataAfterEncoding(std::move(png_it->second));
}
}
const auto& maybe_png = item.data().maybe_png();
if (maybe_png.has_value()) {
item_value.SetKey(kImageDataKey, base::Value(webui::GetPngDataUrl(
maybe_png.value().data(),
maybe_png.value().size())));
item_value.SetKey(kFormatDataKey, base::Value(kPngFormat));
}
break;
}
case ash::ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kHtml: {
const SkBitmap& bitmap =
*(resource_manager_->GetImageModel(item).GetImage().ToSkBitmap());
item_value.SetKey(kImageDataKey,
base::Value(webui::GetBitmapDataUrl(bitmap)));
item_value.SetKey(kFormatDataKey, base::Value(kHtmlFormat));
break;
}
case ash::ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kText:
item_value.SetKey(kTextDataKey, base::Value(item.data().text()));
item_value.SetKey(kFormatDataKey, base::Value(kTextFormat));
break;
case ash::ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kFile: {
std::string file_name =
base::UTF16ToUTF8(resource_manager_->GetLabel(item));
item_value.SetKey(kTextDataKey, base::Value(file_name));
ScopedLightModeAsDefault scoped_light_mode_as_default;
std::string data_url = webui::GetBitmapDataUrl(
*ClipboardHistoryUtil::GetIconForFileClipboardItem(item, file_name)
.bitmap());
item_value.SetKey(kImageDataKey, base::Value(data_url));
item_value.SetKey(kFormatDataKey, base::Value(kFileFormat));
break;
}
}
item_value.SetKey("id", base::Value(item.id().ToString()));
item_value.SetKey("timeCopied",
base::Value(item.time_copied().ToJsTimeIgnoringNull()));
item_results.Append(std::move(item_value));
}
if (!all_images_encoded) {
GetHistoryValues(item_id_filter, std::move(callback));
return;
}
std::move(callback).Run(std::move(item_results));
}
std::vector<std::string> ClipboardHistoryControllerImpl::GetHistoryItemIds()
const {
std::vector<std::string> item_ids;
for (const auto& item : history()->GetItems()) {
item_ids.push_back(item.id().ToString());
}
return item_ids;
}
bool ClipboardHistoryControllerImpl::PasteClipboardItemById(
const std::string& item_id) {
if (currently_pasting_)
return false;
auto* active_window = window_util::GetActiveWindow();
if (!active_window)
return false;
for (const auto& item : history()->GetItems()) {
if (item.id().ToString() == item_id) {
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(
&ClipboardHistoryControllerImpl::PasteClipboardHistoryItem,
weak_ptr_factory_.GetWeakPtr(), active_window, item,
ClipboardHistoryPasteType::kRichTextVirtualKeyboard));
return true;
}
}
return false;
}
bool ClipboardHistoryControllerImpl::DeleteClipboardItemById(
const std::string& item_id) {
for (const auto& item : history()->GetItems()) {
if (item.id().ToString() == item_id) {
DeleteClipboardHistoryItem(item);
return true;
}
}
return false;
}
void ClipboardHistoryControllerImpl::OnClipboardHistoryItemAdded(
const ClipboardHistoryItem& item,
bool is_duplicate) {
for (auto& observer : observers_)
observer.OnClipboardHistoryItemListAddedOrRemoved();
}
void ClipboardHistoryControllerImpl::OnClipboardHistoryItemRemoved(
const ClipboardHistoryItem& item) {
for (auto& observer : observers_)
observer.OnClipboardHistoryItemListAddedOrRemoved();
}
void ClipboardHistoryControllerImpl::OnClipboardHistoryCleared() {
// Prevent clipboard contents getting restored if the Clipboard is cleared
// soon after a `PasteMenuItemData()`.
weak_ptr_factory_.InvalidateWeakPtrs();
if (!IsMenuShowing())
return;
context_menu_->Cancel();
}
void ClipboardHistoryControllerImpl::OnOperationConfirmed(bool copy) {
static int confirmed_paste_count = 0;
// Here we assume that a paste operation from the clipboard history menu never
// interleaves with a copy operation or a paste operation from other ways (
// including pressing the ctrl-v accelerator or clicking a context menu
// option). In other words, when `pastes_to_be_confirmed_` is positive, it
// means that the incoming operation should be a paste from clipboard history.
// It should be held in most cases given that the clipboard history menu is
// always closed after one paste and it usually takes relatively long time for
// a user to conduct the next copy or paste. For this metric, we are tolerable
// of a small portion of erroneous recordings.
// When `pastes_to_be_confirmed_` is positive, `copy` should be
// false in most cases based on the assumption above. But theoretically
// `copy` could be true.
if (pastes_to_be_confirmed_ > 0 && !copy) {
++confirmed_paste_count;
--pastes_to_be_confirmed_;
} else {
// Reset if the assumption is not held for some reasons.
DCHECK_LE(0, pastes_to_be_confirmed_);
if (pastes_to_be_confirmed_ > 0)
pastes_to_be_confirmed_ = 0;
DCHECK_LE(0, confirmed_paste_count);
if (confirmed_paste_count) {
base::UmaHistogramCounts100("Ash.ClipboardHistory.ConsecutivePastes",
confirmed_paste_count);
confirmed_paste_count = 0;
}
}
if (confirmed_operation_callback_for_test_)
confirmed_operation_callback_for_test_.Run(/*success=*/true);
}
void ClipboardHistoryControllerImpl::OnCachedImageModelUpdated(
const std::vector<base::UnguessableToken>& menu_item_ids) {
for (auto& observer : observers_)
observer.OnClipboardHistoryItemsUpdated(menu_item_ids);
}
void ClipboardHistoryControllerImpl::ExecuteCommand(int command_id,
int event_flags) {
DCHECK(context_menu_);
DCHECK_GE(command_id, ClipboardHistoryUtil::kFirstItemCommandId);
DCHECK_LE(command_id, ClipboardHistoryUtil::kMaxItemCommandId);
using Action = ClipboardHistoryUtil::Action;
Action action = context_menu_->GetActionForCommandId(command_id);
switch (action) {
case Action::kPaste:
// Create a scope for the variables used in this case so that they can be
// deallocated from the stack.
{
bool paste_plain_text = event_flags & ui::EF_SHIFT_DOWN;
// There are no specific flags that indicate a paste triggered by a
// keystroke, so assume by default that keystroke was the event source
// and then check for the other known possibilities. This assumption may
// cause pastes from unknown sources to be incorrectly captured as
// keystroke pastes, but we do not expect such cases to significantly
// alter metrics.
ClipboardHistoryPasteType paste_type =
paste_plain_text ? ClipboardHistoryPasteType::kPlainTextKeystroke
: ClipboardHistoryPasteType::kRichTextKeystroke;
if (event_flags & ui::EF_MOUSE_BUTTON) {
paste_type = paste_plain_text
? ClipboardHistoryPasteType::kPlainTextMouse
: ClipboardHistoryPasteType::kRichTextMouse;
} else if (event_flags & ui::EF_FROM_TOUCH) {
paste_type = paste_plain_text
? ClipboardHistoryPasteType::kPlainTextTouch
: ClipboardHistoryPasteType::kRichTextTouch;
}
PasteMenuItemData(command_id, paste_type);
return;
}
case Action::kDelete:
DeleteItemWithCommandId(command_id);
return;
case Action::kSelect:
context_menu_->SelectMenuItemWithCommandId(command_id);
return;
case Action::kSelectItemHoveredByMouse:
context_menu_->SelectMenuItemHoveredByMouse();
return;
case Action::kEmpty:
NOTREACHED();
return;
}
}
void ClipboardHistoryControllerImpl::PasteMenuItemData(
int command_id,
ClipboardHistoryPasteType paste_type) {
UMA_HISTOGRAM_ENUMERATION(
"Ash.ClipboardHistory.ContextMenu.MenuOptionSelected", command_id,
ClipboardHistoryUtil::kMaxCommandId);
// Deactivate ClipboardImageModelFactory prior to pasting to ensure that any
// modifications to the clipboard for HTML rendering purposes are reversed.
ClipboardImageModelFactory::Get()->Deactivate();
// Force close the context menu. Failure to do so before dispatching our
// synthetic key event will result in the context menu consuming the event.
DCHECK(context_menu_);
context_menu_->Cancel();
auto* active_window = window_util::GetActiveWindow();
if (!active_window)
return;
const ClipboardHistoryItem& selected_item =
context_menu_->GetItemFromCommandId(command_id);
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&ClipboardHistoryControllerImpl::PasteClipboardHistoryItem,
weak_ptr_factory_.GetWeakPtr(), active_window,
selected_item, paste_type));
}
void ClipboardHistoryControllerImpl::PasteClipboardHistoryItem(
aura::Window* intended_window,
ClipboardHistoryItem item,
ClipboardHistoryPasteType paste_type) {
// It's possible that the window could change or we could enter a disabled
// mode after posting the `PasteClipboardHistoryItem()` task.
if (!intended_window || intended_window != window_util::GetActiveWindow() ||
!ClipboardHistoryUtil::IsEnabledInCurrentMode()) {
if (confirmed_operation_callback_for_test_)
confirmed_operation_callback_for_test_.Run(/*success=*/false);
return;
}
auto* clipboard = GetClipboard();
std::unique_ptr<ui::ClipboardData> original_data;
// If necessary, replace the clipboard's |original_data| temporarily so that
// we can paste the selected history item.
ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory);
const auto* current_clipboard_data = clipboard->GetClipboardData(&data_dst);
bool paste_plain_text = IsPlainTextPaste(paste_type);
if (paste_plain_text || !current_clipboard_data ||
*current_clipboard_data != item.data()) {
std::unique_ptr<ui::ClipboardData> temp_data;
if (paste_plain_text) {
// When the shift key is pressed, we only paste plain text.
temp_data = std::make_unique<ui::ClipboardData>();
temp_data->set_commit_time(item.data().commit_time());
temp_data->set_text(item.data().text());
ui::DataTransferEndpoint* data_src = item.data().source();
if (data_src) {
temp_data->set_source(
std::make_unique<ui::DataTransferEndpoint>(*data_src));
}
} else {
temp_data = std::make_unique<ui::ClipboardData>(item.data());
}
// Pause clipboard history when manipulating the clipboard for a paste.
ScopedClipboardHistoryPauseImpl scoped_pause(clipboard_history_.get());
original_data = clipboard->WriteClipboardData(std::move(temp_data));
}
auto* host = GetWindowTreeHostForDisplay(
display::Screen::GetScreen()->GetDisplayForNewWindows().id());
DCHECK(host);
ui::KeyEvent ctrl_press(ui::ET_KEY_PRESSED, ui::VKEY_CONTROL, ui::EF_NONE);
host->DeliverEventToSink(&ctrl_press);
ui::KeyEvent v_press(ui::ET_KEY_PRESSED, ui::VKEY_V, ui::EF_CONTROL_DOWN);
host->DeliverEventToSink(&v_press);
ui::KeyEvent v_release(ui::ET_KEY_RELEASED, ui::VKEY_V, ui::EF_CONTROL_DOWN);
host->DeliverEventToSink(&v_release);
ui::KeyEvent ctrl_release(ui::ET_KEY_RELEASED, ui::VKEY_CONTROL, ui::EF_NONE);
host->DeliverEventToSink(&ctrl_release);
++pastes_to_be_confirmed_;
base::UmaHistogramEnumeration("Ash.ClipboardHistory.PasteType", paste_type);
for (auto& observer : observers_)
observer.OnClipboardHistoryPasted();
// `original_data` only exists if the clipboard was modified.
if (!original_data)
return;
currently_pasting_ = true;
// Replace the original item back on top of the clipboard. Some apps take a
// long time to receive the paste event, also some apps will read from the
// clipboard multiple times per paste. Wait a bit before replacing the item
// back onto the clipboard.
base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
FROM_HERE,
base::BindOnce(
[](const base::WeakPtr<ClipboardHistoryControllerImpl>& weak_ptr,
std::unique_ptr<ui::ClipboardData> original_data) {
// When restoring the original item back on top of the clipboard we
// need to pause clipboard history. Failure to do so will result in
// the original item being re-recorded when this restoration step
// should actually be opaque to the user.
std::unique_ptr<ScopedClipboardHistoryPauseImpl> scoped_pause;
if (weak_ptr) {
weak_ptr->currently_pasting_ = false;
scoped_pause = std::make_unique<ScopedClipboardHistoryPauseImpl>(
weak_ptr->clipboard_history_.get());
}
GetClipboard()->WriteClipboardData(std::move(original_data));
},
weak_ptr_factory_.GetWeakPtr(), std::move(original_data)),
buffer_restoration_delay_for_test_.value_or(base::Milliseconds(200)));
}
void ClipboardHistoryControllerImpl::DeleteSelectedMenuItemIfAny() {
DCHECK(context_menu_);
auto selected_command = context_menu_->GetSelectedMenuItemCommand();
// Return early if no item is selected.
if (!selected_command.has_value())
return;
DeleteItemWithCommandId(*selected_command);
}
void ClipboardHistoryControllerImpl::DeleteItemWithCommandId(int command_id) {
DCHECK(context_menu_);
// Pressing VKEY_DELETE is handled here via AcceleratorTarget because the
// contextual menu consumes the key event. Record the "pressing the delete
// button" histogram here because this action does the same thing as
// activating the button directly via click/tap. There is no special handling
// for pasting an item via VKEY_RETURN because in that case the menu does not
// process the key event.
const auto& to_be_deleted_item =
context_menu_->GetItemFromCommandId(command_id);
DeleteClipboardHistoryItem(to_be_deleted_item);
// If the item to be deleted is the last one, close the whole menu.
if (context_menu_->GetMenuItemsCount() == 1) {
context_menu_->Cancel();
return;
}
context_menu_->RemoveMenuItemWithCommandId(command_id);
}
void ClipboardHistoryControllerImpl::DeleteClipboardHistoryItem(
const ClipboardHistoryItem& item) {
ClipboardHistoryUtil::RecordClipboardHistoryItemDeleted(item);
clipboard_history_->RemoveItemForId(item.id());
}
void ClipboardHistoryControllerImpl::AdvancePseudoFocus(bool reverse) {
DCHECK(context_menu_);
context_menu_->AdvancePseudoFocus(reverse);
}
gfx::Rect ClipboardHistoryControllerImpl::CalculateAnchorRect() const {
display::Display display = display::Screen::GetScreen()->GetPrimaryDisplay();
auto* host = ash::GetWindowTreeHostForDisplay(display.id());
// Some web apps render the caret in an IFrame, and we will not get the
// bounds in that case.
// TODO(https://crbug.com/1099930): Show the menu in the middle of the
// webview if the bounds are empty.
ui::TextInputClient* text_input_client =
host->GetInputMethod()->GetTextInputClient();
// `text_input_client` may be null. For example, in clamshell mode and without
// any window open.
const gfx::Rect textfield_bounds =
text_input_client ? text_input_client->GetCaretBounds() : gfx::Rect();
// Note that the width of caret's bounds may be zero in some views (such as
// the search bar of Google search web page). So we cannot use
// gfx::Size::IsEmpty() here. In addition, the applications using IFrame may
// provide unreliable `textfield_bounds` which are not fully contained by the
// display bounds.
const bool textfield_bounds_are_valid =
textfield_bounds.size() != gfx::Size() &&
IsRectContainedByAnyDisplay(textfield_bounds);
if (textfield_bounds_are_valid)
return textfield_bounds;
return gfx::Rect(display::Screen::GetScreen()->GetCursorScreenPoint(),
gfx::Size());
}
void ClipboardHistoryControllerImpl::OnMenuClosed() {
accelerator_target_->OnMenuClosed();
// Reset `context_menu_` in the asynchronous way. Because the menu may be
// accessed after `OnMenuClosed()` is called.
menu_task_timer_.Start(
FROM_HERE, base::TimeDelta(),
base::BindOnce(
[](const base::WeakPtr<ClipboardHistoryControllerImpl>&
controller_weak_ptr) {
if (controller_weak_ptr)
controller_weak_ptr->context_menu_.reset();
},
weak_ptr_factory_.GetWeakPtr()));
}
} // namespace ash