blob: 1dd5ab9ba3250e556dadb9cb3206c99bfe9ac240 [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 <memory>
#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_controller.h"
#include "ash/clipboard/scoped_clipboard_history_pause_impl.h"
#include "ash/public/cpp/clipboard_image_model_factory.h"
#include "ash/public/cpp/file_icon_util.h"
#include "ash/public/cpp/window_tree_host_lookup.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/style/ash_color_provider.h"
#include "base/base64.h"
#include "base/files/file_path.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.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/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/display/screen.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/codec/png_codec.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";
ui::ClipboardNonBacked* GetClipboard() {
auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread();
DCHECK(clipboard);
return clipboard;
}
bool IsRectContainedByAnyDisplay(const gfx::Rect& rect) {
const std::vector<display::Display>& displays =
display::Screen::GetScreen()->GetAllDisplays();
for (const auto& display : displays) {
if (display.bounds().Contains(rect))
return true;
}
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::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::ShowMenuByAccelerator() {
if (IsMenuShowing()) {
ExecuteSelectedMenuItem(ui::EF_COMMAND_DOWN);
return;
}
ShowMenu(CalculateAnchorRect(), ui::MENU_SOURCE_KEYBOARD);
}
gfx::Rect ClipboardHistoryControllerImpl::GetMenuBoundsInScreenForTest() const {
return context_menu_->GetMenuBoundsInScreenForTest();
}
void ClipboardHistoryControllerImpl::ShowMenu(
const gfx::Rect& anchor_rect,
ui::MenuSourceType source_type) {
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();
// 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();
}
bool ClipboardHistoryControllerImpl::CanShowMenu() const {
return !clipboard_history_->IsEmpty() &&
ClipboardHistoryUtil::IsEnabledInCurrentMode();
}
std::unique_ptr<ScopedClipboardHistoryPause>
ClipboardHistoryControllerImpl::CreateScopedPause() {
return std::make_unique<ScopedClipboardHistoryPauseImpl>(
clipboard_history_.get());
}
base::Value ClipboardHistoryControllerImpl::GetHistoryValues(
const std::set<std::string>& item_id_filter) const {
base::Value item_results(base::Value::Type::LIST);
// Get the clipboard data for each clipboard history item.
for (const auto& item : 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::kBitmap:
item_value.SetKey(
kImageDataKey,
base::Value(webui::GetBitmapDataUrl(item.data().bitmap())));
break;
case ash::ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kHtml: {
const SkBitmap& bitmap =
*(resource_manager_->GetImageModel(item).GetImage().ToSkBitmap());
item_value.SetKey(kImageDataKey,
base::Value(webui::GetBitmapDataUrl(bitmap)));
break;
}
case ash::ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kText:
item_value.SetKey(kTextDataKey, base::Value(item.data().text()));
break;
case ash::ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kFile: {
item_value.SetKey(
kTextDataKey,
base::Value(base::UTF16ToUTF8(resource_manager_->GetLabel(item))));
gfx::ImageSkia image = GetIconForPath(
base::FilePath(item.data().text()),
ash::AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kIconColorPrimary));
item_value.SetKey(
kImageDataKey,
base::Value(webui::GetBitmapDataUrl(*image.bitmap())));
break;
}
}
item_value.SetKey("idToken", base::Value(item.id().ToString()));
item_results.Append(std::move(item_value));
}
return 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;
for (const auto& item : history()->GetItems()) {
if (item.id().ToString() == item_id) {
PasteClipboardHistoryItem(item, /*paste_plain_text=*/false);
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();
context_menu_.reset();
}
void ClipboardHistoryControllerImpl::OnCachedImageModelUpdated(
const std::vector<base::UnguessableToken>& menu_item_ids) {
for (auto& observer : observers_)
observer.OnClipboardHistoryItemsUpdated(menu_item_ids);
}
void ClipboardHistoryControllerImpl::ExecuteSelectedMenuItem(int event_flags) {
DCHECK(IsMenuShowing());
auto command = context_menu_->GetSelectedMenuItemCommand();
// If no menu item is currently selected, we'll fallback to the first item.
PasteMenuItemData(command.value_or(ClipboardHistoryUtil::kFirstItemCommandId),
event_flags);
}
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:
PasteMenuItemData(command_id, event_flags & ui::EF_SHIFT_DOWN);
return;
case Action::kDelete:
DeleteItemWithCommandId(command_id);
return;
case Action::kSelect:
context_menu_->SelectMenuItemWithCommandId(command_id);
return;
case Action::kEmpty:
NOTREACHED();
return;
}
}
void ClipboardHistoryControllerImpl::PasteMenuItemData(int command_id,
bool paste_plain_text) {
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();
const ClipboardHistoryItem& selected_item =
context_menu_->GetItemFromCommandId(command_id);
PasteClipboardHistoryItem(selected_item, paste_plain_text);
}
void ClipboardHistoryControllerImpl::PasteClipboardHistoryItem(
const ClipboardHistoryItem& item,
bool paste_plain_text) {
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);
if (paste_plain_text ||
item.data() != *clipboard->GetClipboardData(&data_dst)) {
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_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));
}
ui::KeyEvent control_press(/*type=*/ui::ET_KEY_PRESSED, ui::VKEY_CONTROL,
/*code=*/static_cast<ui::DomCode>(0), /*flags=*/0);
auto* host = GetWindowTreeHostForDisplay(
display::Screen::GetScreen()->GetDisplayForNewWindows().id());
DCHECK(host);
host->DeliverEventToSink(&control_press);
ui::KeyEvent v_press(/*type=*/ui::ET_KEY_PRESSED, ui::VKEY_V,
/*code=*/static_cast<ui::DomCode>(0),
/*flags=*/ui::EF_CONTROL_DOWN);
host->DeliverEventToSink(&v_press);
ui::KeyEvent v_release(/*type=*/ui::ET_KEY_RELEASED, ui::VKEY_V,
/*code=*/static_cast<ui::DomCode>(0),
/*flags=*/ui::EF_CONTROL_DOWN);
host->DeliverEventToSink(&v_release);
ui::KeyEvent control_release(/*type=*/ui::ET_KEY_RELEASED, ui::VKEY_CONTROL,
/*code=*/static_cast<ui::DomCode>(0),
/*flags=*/0);
host->DeliverEventToSink(&control_release);
for (auto& observer : observers_)
observer.OnClipboardHistoryPasted();
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)),
base::TimeDelta::FromMilliseconds(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();
context_menu_.reset();
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