| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/clipboard/clipboard_history_controller_impl.h" |
| |
| #include <map> |
| #include <memory> |
| #include <optional> |
| #include <set> |
| #include <vector> |
| |
| #include "ash/accelerators/accelerator_controller_impl.h" |
| #include "ash/clipboard/clipboard_history_controller_delegate.h" |
| #include "ash/clipboard/clipboard_history_item.h" |
| #include "ash/clipboard/clipboard_history_menu_model_adapter.h" |
| #include "ash/clipboard/clipboard_history_resource_manager.h" |
| #include "ash/clipboard/clipboard_history_url_title_fetcher.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/constants/ash_pref_names.h" |
| #include "ash/display/display_util.h" |
| #include "ash/public/cpp/clipboard_image_model_factory.h" |
| #include "ash/public/cpp/window_tree_host_lookup.h" |
| #include "ash/session/session_controller_impl.h" |
| #include "ash/shell.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/style/color_util.h" |
| #include "ash/wm/window_util.h" |
| #include "base/barrier_closure.h" |
| #include "base/check.h" |
| #include "base/check_is_test.h" |
| #include "base/check_op.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_forward.h" |
| #include "base/json/values_util.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/notreached.h" |
| #include "base/one_shot_event.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/synchronization/lock.h" |
| #include "base/system/sys_info.h" |
| #include "base/task/bind_post_task.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/task/thread_pool.h" |
| #include "base/time/time.h" |
| #include "base/unguessable_token.h" |
| #include "chromeos/constants/chromeos_features.h" |
| #include "chromeos/crosapi/mojom/clipboard_history.mojom.h" |
| #include "components/prefs/pref_service.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/accelerators/accelerator.h" |
| #include "ui/base/clipboard/clipboard_data.h" |
| #include "ui/base/clipboard/clipboard_non_backed.h" |
| #include "ui/base/clipboard/clipboard_util.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/simple_menu_model.h" |
| #include "ui/base/webui/web_ui_util.h" |
| #include "ui/color/color_provider_source.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/views/controls/menu/menu_controller.h" |
| |
| #if BUILDFLAG(USE_XKBCOMMON) |
| #include "ui/events/keycodes/xkb_keysym.h" |
| #include "ui/events/ozone/layout/keyboard_layout_engine_manager.h" |
| #include "ui/events/ozone/layout/xkb/xkb_keyboard_layout_engine.h" |
| #endif |
| |
| namespace ash { |
| |
| namespace { |
| |
| // 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::clipboard_util::EncodeBitmapToPng(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(); |
| } |
| |
| // Returns the clipboard instance for the current thread. |
| ui::ClipboardNonBacked* GetClipboard() { |
| auto* clipboard = ui::ClipboardNonBacked::GetForCurrentThread(); |
| DCHECK(clipboard); |
| return clipboard; |
| } |
| |
| // Returns the last active user pref service or `nullptr` if one does not exist. |
| PrefService* GetLastActiveUserPrefService() { |
| return Shell::Get()->session_controller()->GetLastActiveUserPrefService(); |
| } |
| |
| // Returns the time when the menu was last shown for the user associated with |
| // the last active user pref service, or `std::nullopt` if the menu was not |
| // previously marked as having been shown. |
| std::optional<base::Time> GetMenuLastTimeShown() { |
| if (auto* prefs = GetLastActiveUserPrefService()) { |
| if (auto* pref = prefs->FindPreference(prefs::kMultipasteMenuLastTimeShown); |
| pref && !pref->IsDefaultValue()) { |
| return base::ValueToTime(pref->GetValue()); |
| } |
| } |
| return std::nullopt; |
| } |
| |
| // Marks the time when the menu was last shown for the user associated with the |
| // last active user pref service. |
| void MarkMenuLastTimeShown() { |
| if (auto* prefs = GetLastActiveUserPrefService()) { |
| prefs->SetTime(prefs::kMultipasteMenuLastTimeShown, base::Time::Now()); |
| } |
| } |
| |
| // Emits a user action indicating that the clipboard history item at menu index |
| // `command_id` was pasted. |
| void RecordMenuIndexPastedUserAction(int command_id) { |
| // Per guidance in user_metrics.h, use string literals for action names. |
| switch (command_id) { |
| case 1: |
| base::RecordAction( |
| base::UserMetricsAction("Ash_ClipboardHistory_PastedItem1")); |
| break; |
| case 2: |
| base::RecordAction( |
| base::UserMetricsAction("Ash_ClipboardHistory_PastedItem2")); |
| break; |
| case 3: |
| base::RecordAction( |
| base::UserMetricsAction("Ash_ClipboardHistory_PastedItem3")); |
| break; |
| case 4: |
| base::RecordAction( |
| base::UserMetricsAction("Ash_ClipboardHistory_PastedItem4")); |
| break; |
| case 5: |
| base::RecordAction( |
| base::UserMetricsAction("Ash_ClipboardHistory_PastedItem5")); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| void RecordPasteItemIndex(int index) { |
| CHECK_GE(index, clipboard_history_util::kFirstItemCommandId); |
| CHECK_LT(index, clipboard_history_util::kCommandIdBoundary); |
| |
| // Record the paste item's history list index in a histogram to get a |
| // distribution of where in the list users paste from. |
| base::UmaHistogramExactLinear( |
| "Ash.ClipboardHistory.ContextMenu.MenuOptionSelected", index, |
| /*exclusive_max=*/clipboard_history_util::kCommandIdBoundary); |
| |
| // Record the paste item's history list index as a user action to analyze |
| // usage patterns, e.g., how frequently the same index is pasted multiple |
| // times in a row. |
| RecordMenuIndexPastedUserAction(index); |
| } |
| |
| #if BUILDFLAG(USE_XKBCOMMON) |
| // Looks up the DomCode assigned to the keysym. In some edge cases, |
| // such as Dvorak layout, the original DomCode may be different |
| // from US standard layout. |
| ui::DomCode LookUpXkbDomCode(int keysym) { |
| if (!base::SysInfo::IsRunningOnChromeOS()) { |
| // On linux-chromeos, stub layout engine is used. |
| return ui::DomCode::NONE; |
| } |
| auto* layout_engine = |
| ui::KeyboardLayoutEngineManager::GetKeyboardLayoutEngine(); |
| if (!layout_engine) { |
| return ui::DomCode::NONE; |
| } |
| return static_cast<ui::XkbKeyboardLayoutEngine*>(layout_engine) |
| ->GetDomCodeByKeysym(keysym, /*modifiers=*/std::nullopt); |
| } |
| #endif |
| |
| ui::KeyEvent SyntheticCtrlV(ui::EventType type) { |
| ui::DomCode dom_code = ui::DomCode::NONE; |
| #if BUILDFLAG(USE_XKBCOMMON) |
| dom_code = LookUpXkbDomCode(XKB_KEY_v); |
| #endif |
| return dom_code == ui::DomCode::NONE |
| ? ui::KeyEvent(type, ui::VKEY_V, ui::EF_CONTROL_DOWN) |
| : ui::KeyEvent(type, ui::VKEY_V, dom_code, ui::EF_CONTROL_DOWN); |
| } |
| |
| ui::KeyEvent SyntheticCtrl(ui::EventType type) { |
| int flags = type == ui::ET_KEY_PRESSED ? ui::EF_CONTROL_DOWN : ui::EF_NONE; |
| ui::DomCode dom_code = ui::DomCode::NONE; |
| #if BUILDFLAG(USE_XKBCOMMON) |
| dom_code = LookUpXkbDomCode(XKB_KEY_Control_L); |
| #endif |
| return dom_code == ui::DomCode::NONE |
| ? ui::KeyEvent(type, ui::VKEY_CONTROL, flags) |
| : ui::KeyEvent(type, ui::VKEY_CONTROL, dom_code, flags); |
| } |
| |
| void SyntheticPaste( |
| crosapi::mojom::ClipboardHistoryControllerShowSource paste_source) { |
| auto* host = GetWindowTreeHostForDisplay( |
| display::Screen::GetScreen()->GetDisplayForNewWindows().id()); |
| CHECK(host); |
| |
| // Because we do not require the user to release Ctrl+V before selecting a |
| // clipboard history item to paste, the Ctrl+V event we synthesize below may |
| // be discarded as a perceived continuation of the long press. Preempt this |
| // scenario by issuing a Ctrl+V release to ensure that the press and release |
| // below are handled as an independent paste. |
| // TODO(http://b/283533126): Replace this workaround with a long-term fix. |
| if (paste_source == crosapi::mojom::ClipboardHistoryControllerShowSource:: |
| kControlVLongpress) { |
| ui::KeyEvent v_release = SyntheticCtrlV(ui::ET_KEY_RELEASED); |
| host->DeliverEventToSink(&v_release); |
| |
| ui::KeyEvent ctrl_release = SyntheticCtrl(ui::ET_KEY_RELEASED); |
| host->DeliverEventToSink(&ctrl_release); |
| } |
| |
| ui::KeyEvent ctrl_press = SyntheticCtrl(ui::ET_KEY_PRESSED); |
| host->DeliverEventToSink(&ctrl_press); |
| |
| ui::KeyEvent v_press = SyntheticCtrlV(ui::ET_KEY_PRESSED); |
| host->DeliverEventToSink(&v_press); |
| |
| ui::KeyEvent v_release = SyntheticCtrlV(ui::ET_KEY_RELEASED); |
| host->DeliverEventToSink(&v_release); |
| |
| ui::KeyEvent ctrl_release = SyntheticCtrl(ui::ET_KEY_RELEASED); |
| host->DeliverEventToSink(&ctrl_release); |
| } |
| |
| using ClipboardHistoryPasteType = |
| 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: |
| case ClipboardHistoryPasteType::kPlainTextCtrlV: |
| return true; |
| case ClipboardHistoryPasteType::kRichTextAccelerator: |
| case ClipboardHistoryPasteType::kRichTextKeystroke: |
| case ClipboardHistoryPasteType::kRichTextMouse: |
| case ClipboardHistoryPasteType::kRichTextTouch: |
| case ClipboardHistoryPasteType::kRichTextVirtualKeyboard: |
| case ClipboardHistoryPasteType::kRichTextCtrlV: |
| return false; |
| } |
| } |
| |
| ClipboardHistoryPasteType CalculatePasteType( |
| crosapi::mojom::ClipboardHistoryControllerShowSource paste_source, |
| int event_flags) { |
| // 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. |
| const bool paste_plain_text = event_flags & ui::EF_SHIFT_DOWN; |
| |
| if (paste_source == |
| crosapi::mojom::ClipboardHistoryControllerShowSource::kVirtualKeyboard) { |
| return paste_plain_text |
| ? ClipboardHistoryPasteType::kPlainTextVirtualKeyboard |
| : ClipboardHistoryPasteType::kRichTextVirtualKeyboard; |
| } |
| |
| 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; |
| } |
| return paste_type; |
| } |
| |
| } // 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)), |
| paste_first_item_(ui::Accelerator( |
| /*key_code=*/ui::VKEY_V, |
| /*modifiers=*/ui::EF_CONTROL_DOWN, |
| /*key_state=*/ui::Accelerator::KeyState::PRESSED)), |
| paste_first_item_plaintext_(ui::Accelerator( |
| /*key_code=*/ui::VKEY_V, |
| /*modifiers=*/ui::EF_CONTROL_DOWN | 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_, |
| paste_first_item_, paste_first_item_plaintext_}, |
| /*target=*/this); |
| } |
| |
| void OnMenuClosed() { |
| Shell::Get()->accelerator_controller()->UnregisterAll(/*target=*/this); |
| } |
| |
| private: |
| // ui::AcceleratorTarget: |
| bool AcceleratorPressed(const ui::Accelerator& accelerator) override { |
| CHECK(controller_->IsMenuShowing()); |
| |
| if (accelerator == delete_selected_) { |
| HandleDeleteSelected(); |
| } else if (accelerator == tab_navigation_) { |
| HandleTab(); |
| } else if (accelerator == shift_tab_navigation_) { |
| HandleShiftTab(); |
| } else if (accelerator == paste_first_item_) { |
| HandlePasteFirstItem(ClipboardHistoryPasteType::kRichTextCtrlV); |
| } else if (accelerator == paste_first_item_plaintext_) { |
| HandlePasteFirstItem(ClipboardHistoryPasteType::kPlainTextCtrlV); |
| } else { |
| NOTREACHED(); |
| return false; |
| } |
| |
| return true; |
| } |
| |
| bool CanHandleAccelerators() const override { |
| return controller_->IsMenuShowing() || |
| controller_->HasAvailableHistoryItems(); |
| } |
| |
| void HandleDeleteSelected() { controller_->DeleteSelectedMenuItemIfAny(); } |
| |
| void HandleTab() { controller_->AdvancePseudoFocus(/*reverse=*/false); } |
| |
| void HandleShiftTab() { controller_->AdvancePseudoFocus(/*reverse=*/true); } |
| |
| void HandlePasteFirstItem(ClipboardHistoryPasteType paste_type) { |
| const auto first_item_command_id = |
| controller_->context_menu_->GetFirstMenuItemCommand(); |
| CHECK(first_item_command_id); |
| controller_->PasteClipboardItemByCommandId(*first_item_command_id, |
| paste_type); |
| } |
| |
| // The controller responsible for showing the clipboard history menu. |
| const raw_ptr<ClipboardHistoryControllerImpl> controller_; |
| |
| // Deletes the selected menu item. |
| const ui::Accelerator delete_selected_; |
| |
| // Moves the pseudo focus forward. |
| const ui::Accelerator tab_navigation_; |
| |
| // Moves the pseudo focus backward. |
| const ui::Accelerator shift_tab_navigation_; |
| |
| // Pastes the first item in the clipboard history menu. |
| const ui::Accelerator paste_first_item_; |
| |
| // Pastes the plain text data of the first item in the clipboard history menu. |
| const ui::Accelerator paste_first_item_plaintext_; |
| }; |
| |
| // 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. |
| const raw_ptr<ClipboardHistoryControllerImpl> controller_; |
| }; |
| |
| // ClipboardHistoryControllerImpl ---------------------------------------------- |
| |
| ClipboardHistoryControllerImpl::ClipboardHistoryControllerImpl( |
| std::unique_ptr<ClipboardHistoryControllerDelegate> delegate) |
| : delegate_(std::move(delegate)), |
| image_model_factory_(delegate_->CreateImageModelFactory()), |
| url_title_fetcher_(delegate_->CreateUrlTitleFetcher()), |
| clipboard_history_(std::make_unique<ClipboardHistory>()), |
| resource_manager_(std::make_unique<ClipboardHistoryResourceManager>( |
| clipboard_history_.get())), |
| accelerator_target_(std::make_unique<AcceleratorTarget>(this)), |
| nudge_controller_( |
| std::make_unique<ClipboardNudgeController>(clipboard_history_.get())), |
| menu_delegate_(std::make_unique<MenuDelegate>(this)) { |
| if (!image_model_factory_ || !url_title_fetcher_) { |
| CHECK_IS_TEST(); |
| } |
| clipboard_history_->AddObserver(this); |
| resource_manager_->AddObserver(this); |
| SessionController::Get()->AddObserver(this); |
| } |
| |
| ClipboardHistoryControllerImpl::~ClipboardHistoryControllerImpl() { |
| SessionController::Get()->RemoveObserver(this); |
| resource_manager_->RemoveObserver(this); |
| clipboard_history_->RemoveObserver(this); |
| } |
| |
| // static |
| void ClipboardHistoryControllerImpl::RegisterProfilePrefs( |
| PrefRegistrySimple* registry) { |
| ClipboardNudgeController::RegisterProfilePrefs(registry); |
| registry->RegisterTimePref(prefs::kMultipasteMenuLastTimeShown, base::Time()); |
| } |
| |
| void ClipboardHistoryControllerImpl::Shutdown() { |
| if (IsMenuShowing()) { |
| context_menu_->Cancel(/*will_paste_item=*/false); |
| } |
| nudge_controller_.reset(); |
| } |
| |
| bool ClipboardHistoryControllerImpl::IsMenuShowing() const { |
| return context_menu_ && context_menu_->IsRunning(); |
| } |
| |
| void ClipboardHistoryControllerImpl::ToggleMenuShownByAccelerator( |
| bool is_plain_text_paste) { |
| if (IsMenuShowing()) { |
| // Before hiding the menu, paste the selected menu item, or the first item |
| // if none is selected. |
| PasteClipboardItemByCommandId( |
| context_menu_->GetSelectedMenuItemCommand().value_or( |
| clipboard_history_util::kFirstItemCommandId), |
| is_plain_text_paste ? ClipboardHistoryPasteType::kPlainTextAccelerator |
| : ClipboardHistoryPasteType::kRichTextAccelerator); |
| return; |
| } |
| |
| // Do not allow the plain text shortcut to open the menu. |
| if (is_plain_text_paste) { |
| return; |
| } |
| |
| if (clipboard_history_util::IsEnabledInCurrentMode() && IsEmpty()) { |
| nudge_controller_->ShowNudge(ClipboardNudgeType::kZeroStateNudge); |
| return; |
| } |
| |
| ShowMenu(CalculateAnchorRect(), ui::MENU_SOURCE_KEYBOARD, |
| crosapi::mojom::ClipboardHistoryControllerShowSource::kAccelerator); |
| } |
| |
| void ClipboardHistoryControllerImpl::AddObserver( |
| ClipboardHistoryController::Observer* observer) { |
| observers_.AddObserver(observer); |
| } |
| |
| void ClipboardHistoryControllerImpl::RemoveObserver( |
| ClipboardHistoryController::Observer* observer) { |
| observers_.RemoveObserver(observer); |
| } |
| |
| bool ClipboardHistoryControllerImpl::ShowMenu( |
| const gfx::Rect& anchor_rect, |
| ui::MenuSourceType source_type, |
| crosapi::mojom::ClipboardHistoryControllerShowSource show_source) { |
| return ShowMenu(anchor_rect, source_type, show_source, |
| OnMenuClosingCallback()); |
| } |
| |
| bool ClipboardHistoryControllerImpl::ShowMenu( |
| const gfx::Rect& anchor_rect, |
| ui::MenuSourceType source_type, |
| crosapi::mojom::ClipboardHistoryControllerShowSource show_source, |
| OnMenuClosingCallback callback) { |
| if (IsMenuShowing() || !HasAvailableHistoryItems()) { |
| return false; |
| } |
| |
| // Close the running context menu, if any, before showing the clipboard |
| // history menu. |
| if (auto* active_menu_instance = views::MenuController::GetActiveInstance()) { |
| active_menu_instance->Cancel(views::MenuController::ExitType::kAll); |
| } |
| |
| last_menu_source_ = show_source; |
| |
| // `Unretained()` is safe because `this` owns `context_menu_`. |
| context_menu_ = ClipboardHistoryMenuModelAdapter::Create( |
| menu_delegate_.get(), std::move(callback), |
| base::BindRepeating(&ClipboardHistoryControllerImpl::OnMenuClosed, |
| base::Unretained(this)), |
| clipboard_history_.get()); |
| context_menu_->Run(anchor_rect, source_type, show_source, |
| GetMenuLastTimeShown(), |
| nudge_controller_->GetNudgeLastTimeShown()); |
| |
| CHECK(IsMenuShowing()); |
| accelerator_target_->OnMenuShown(); |
| |
| // The first menu item should be selected by 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( |
| clipboard_history_util::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())); |
| |
| MarkMenuLastTimeShown(); |
| base::UmaHistogramEnumeration("Ash.ClipboardHistory.ContextMenu.ShowMenu", |
| show_source); |
| |
| for (auto& observer : observers_) { |
| observer.OnClipboardHistoryMenuShown(show_source); |
| } |
| return true; |
| } |
| |
| bool ClipboardHistoryControllerImpl::IsEmpty() const { |
| return clipboard_history_->IsEmpty(); |
| } |
| |
| void ClipboardHistoryControllerImpl::FireItemUpdateNotificationTimerForTest() { |
| item_update_notification_timer_.FireNow(); |
| } |
| |
| void ClipboardHistoryControllerImpl::GetHistoryValues( |
| GetHistoryValuesCallback callback) const { |
| // Map of `ClipboardHistoryItem` IDs to their corresponding bitmaps. |
| std::map<base::UnguessableToken, SkBitmap> bitmaps_to_be_encoded; |
| for (auto& item : clipboard_history_->GetItems()) { |
| if (item.display_format() == |
| crosapi::mojom::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` IDs to their 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::BindPostTaskToCurrentDefault(base::BindOnce( |
| &ClipboardHistoryControllerImpl::GetHistoryValuesWithEncodedPNGs, |
| weak_ptr_factory_.GetMutableWeakPtr(), 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(); |
| } |
| } |
| |
| gfx::Rect ClipboardHistoryControllerImpl::GetMenuBoundsInScreenForTest() const { |
| return context_menu_->GetMenuBoundsInScreenForTest(); // IN-TEST |
| } |
| |
| 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(); |
| } |
| |
| void ClipboardHistoryControllerImpl::OnScreenshotNotificationCreated() { |
| nudge_controller_->MarkScreenshotNotificationShown(); |
| } |
| |
| bool ClipboardHistoryControllerImpl::HasAvailableHistoryItems() const { |
| return clipboard_history_util::IsEnabledInCurrentMode() && !IsEmpty(); |
| } |
| |
| std::unique_ptr<ScopedClipboardHistoryPause> |
| ClipboardHistoryControllerImpl::CreateScopedPause() { |
| return std::make_unique<ScopedClipboardHistoryPauseImpl>( |
| clipboard_history_.get()); |
| } |
| |
| void ClipboardHistoryControllerImpl::GetHistoryValuesWithEncodedPNGs( |
| 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(), std::move(callback), |
| std::move(encoded_pngs))); |
| return; |
| } |
| |
| std::vector<ClipboardHistoryItem> item_results; |
| |
| // 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 (!clipboard_history_util::IsEnabledInCurrentMode()) { |
| std::move(callback).Run(std::move(item_results)); |
| return; |
| } |
| |
| bool all_images_encoded = true; |
| for (auto& item : clipboard_history_->GetItems()) { |
| if (item.display_format() == |
| crosapi::mojom::ClipboardHistoryDisplayFormat::kPng && |
| !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)); |
| } |
| } |
| |
| item_results.emplace_back(item); |
| } |
| |
| if (!all_images_encoded) { |
| GetHistoryValues(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; |
| if (HasAvailableHistoryItems()) { |
| for (const auto& item : history()->GetItems()) { |
| item_ids.push_back(item.id().ToString()); |
| } |
| } |
| return item_ids; |
| } |
| |
| bool ClipboardHistoryControllerImpl::PasteClipboardItemById( |
| const std::string& item_id, |
| int event_flags, |
| crosapi::mojom::ClipboardHistoryControllerShowSource paste_source) { |
| const std::list<ClipboardHistoryItem>& history_items = history()->GetItems(); |
| auto iter_by_id = std::find_if(history_items.cbegin(), history_items.cend(), |
| [&item_id](const ClipboardHistoryItem& item) { |
| return item.id().ToString() == item_id; |
| }); |
| if (iter_by_id == history_items.cend()) { |
| return false; |
| } |
| |
| RecordPasteItemIndex(std::distance(history_items.cbegin(), iter_by_id) + |
| clipboard_history_util::kFirstItemCommandId); |
| |
| MaybePostPasteTask(*iter_by_id, CalculatePasteType(paste_source, event_flags), |
| paste_source); |
| return true; |
| } |
| |
| 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) { |
| PostItemUpdateNotificationTask(); |
| } |
| |
| void ClipboardHistoryControllerImpl::OnClipboardHistoryItemRemoved( |
| const ClipboardHistoryItem& item) { |
| PostItemUpdateNotificationTask(); |
| } |
| |
| void ClipboardHistoryControllerImpl::OnClipboardHistoryCleared() { |
| // Prevent clipboard contents from being restored if the clipboard history is |
| // cleared shortly after pasting an item. |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| |
| // Notify observers of the history being cleared after invalidating weak |
| // pointers. |
| PostItemUpdateNotificationTask(); |
| |
| // Make sure the menu is closed now that there are no items to show. |
| if (IsMenuShowing()) { |
| context_menu_->Cancel(/*will_paste_item=*/false); |
| } |
| } |
| |
| 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 user-initiated copy or paste operation from another |
| // source, such as pressing the ctrl-v accelerator or clicking a context menu |
| // option. In other words, when `pastes_to_be_confirmed_` is positive, the |
| // next confirmed operation is expected to be a paste from clipboard history. |
| // This assumption should hold in most cases given that the clipboard history |
| // menu is always closed after one paste, and it usually takes a relatively |
| // long time for a user to perform the next copy or paste. For this metric, we |
| // tolerate a small margin of error. |
| if (pastes_to_be_confirmed_ > 0 && !copy) { |
| ++confirmed_paste_count; |
| --pastes_to_be_confirmed_; |
| } else { |
| // Note that both copies and pastes from the standard clipboard cause the |
| // clipboard history consecutive paste count to be emitted and reset. |
| if (confirmed_paste_count > 0) { |
| base::UmaHistogramCounts100("Ash.ClipboardHistory.ConsecutivePastes", |
| confirmed_paste_count); |
| confirmed_paste_count = 0; |
| } |
| |
| if (copy) { |
| // Record copy actions once they are confirmed, rather than when clipboard |
| // data first changes, to allow multiple data changes to be debounced into |
| // a single copy operation. This ensures that each user-initiated copy is |
| // recorded only once. See `ClipboardHistory::OnDataChanged()` for further |
| // explanation. |
| base::RecordAction(base::UserMetricsAction("Ash_Clipboard_CopiedItem")); |
| } else { |
| // Pastes from clipboard history are already recorded in |
| // `PasteMenuItemData()`. Here, we record just pastes from the standard |
| // clipboard, to see how standard clipboard pastes interleave with |
| // clipboard history pastes. |
| base::RecordAction(base::UserMetricsAction("Ash_Clipboard_PastedItem")); |
| } |
| |
| // Verify that this operation did not interleave with a clipboard history |
| // paste. |
| DCHECK_EQ(pastes_to_be_confirmed_, 0); |
| // Whether or not the non-interleaving assumption has held, always reset |
| // `pastes_to_be_confirmed_` to prevent standard clipboard pastes from |
| // possibly being counted as clipboard history pastes, which could |
| // significantly affect the clipboard history consecutive pastes metric. |
| pastes_to_be_confirmed_ = 0; |
| } |
| |
| // Callback will be run after clipboard data restoration. |
| if (confirmed_operation_callback_for_test_ && !clipboard_data_replaced_) { |
| confirmed_operation_callback_for_test_.Run(/*success=*/true); |
| } |
| } |
| |
| void ClipboardHistoryControllerImpl::OnCachedImageModelUpdated( |
| const std::vector<base::UnguessableToken>& menu_item_ids) { |
| PostItemUpdateNotificationTask(); |
| } |
| |
| void ClipboardHistoryControllerImpl::OnSessionStateChanged( |
| session_manager::SessionState state) { |
| PostItemUpdateNotificationTask(); |
| } |
| |
| void ClipboardHistoryControllerImpl::OnLoginStatusChanged( |
| LoginStatus login_status) { |
| PostItemUpdateNotificationTask(); |
| } |
| |
| void ClipboardHistoryControllerImpl::PostItemUpdateNotificationTask() { |
| // Uses the async task to debounce multiple clipboard history changes in |
| // short duration. Restart the timer if it is running. |
| // This is done to avoid notifying observers multiple times if there are |
| // multiple clipboard history changes in a short period. For example, if the |
| // clipboard history reaches the cache limit and a new clipboard history item |
| // arrives at the same time, there would be two clipboard history changes: the |
| // addition of the new item and the removal of an obsolete item. In this case, |
| // this class should only notify observers only once. |
| item_update_notification_timer_.Start( |
| FROM_HERE, base::TimeDelta(), |
| base::BindOnce( |
| &ClipboardHistoryControllerImpl::MaybeNotifyObserversOfItemUpdate, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ClipboardHistoryControllerImpl::MaybeNotifyObserversOfItemUpdate() { |
| const bool has_available_items = HasAvailableHistoryItems(); |
| if (!has_available_items && !has_available_items_in_last_update_) { |
| // There are no available items, and there were none in the last |
| // notification either. Nothing has changed, so return early. |
| return; |
| } |
| |
| for (auto& observer : observers_) { |
| observer.OnClipboardHistoryItemsUpdated(); |
| } |
| has_available_items_in_last_update_ = has_available_items; |
| } |
| |
| void ClipboardHistoryControllerImpl::ExecuteCommand(int command_id, |
| int event_flags) { |
| DCHECK(context_menu_); |
| |
| DCHECK_GE(command_id, clipboard_history_util::kFirstItemCommandId); |
| DCHECK_LE(command_id, clipboard_history_util::kMaxItemCommandId); |
| |
| using Action = clipboard_history_util::Action; |
| Action action = context_menu_->GetActionForCommandId(command_id); |
| switch (action) { |
| case Action::kPaste: |
| PasteClipboardItemByCommandId( |
| command_id, CalculatePasteType(last_menu_source_, event_flags)); |
| 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: |
| DUMP_WILL_BE_NOTREACHED_NORETURN(); |
| return; |
| } |
| } |
| |
| void ClipboardHistoryControllerImpl::PasteClipboardItemByCommandId( |
| int command_id, |
| ClipboardHistoryPasteType paste_type) { |
| // Force close the context menu. Failure to do so before dispatching our |
| // synthetic key event will result in the context menu consuming the event. |
| // When closing the menu, indicate that the menu is closing because of an |
| // imminent paste. Note that in some cases, this will indicate paste intent |
| // for pastes that ultimately fail. For now, this is an acceptable inaccuracy. |
| CHECK(context_menu_); |
| context_menu_->Cancel(/*will_paste_item=*/true); |
| |
| // `command_id` should match the pasted item's index in `context_menu_`. |
| RecordPasteItemIndex(command_id); |
| |
| MaybePostPasteTask(context_menu_->GetItemFromCommandId(command_id), |
| paste_type, last_menu_source_); |
| } |
| |
| void ClipboardHistoryControllerImpl::MaybePostPasteTask( |
| const ClipboardHistoryItem& item, |
| ClipboardHistoryPasteType paste_type, |
| crosapi::mojom::ClipboardHistoryControllerShowSource paste_source) { |
| // Deactivate ClipboardImageModelFactory prior to pasting to ensure that any |
| // modifications to the clipboard for HTML rendering purposes are reversed. |
| // This factory may be nullptr in tests. |
| if (auto* clipboard_image_factory = ClipboardImageModelFactory::Get()) { |
| clipboard_image_factory->Deactivate(); |
| } |
| |
| if (auto* active_window = window_util::GetActiveWindow()) { |
| // Paste asynchronously to ensure ARC windows handle paste events correctly. |
| base::SequencedTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &ClipboardHistoryControllerImpl::PasteClipboardHistoryItem, |
| weak_ptr_factory_.GetWeakPtr(), active_window, item, paste_type, |
| paste_source)); |
| } |
| } |
| |
| void ClipboardHistoryControllerImpl::PasteClipboardHistoryItem( |
| aura::Window* intended_window, |
| ClipboardHistoryItem item, |
| ClipboardHistoryPasteType paste_type, |
| crosapi::mojom::ClipboardHistoryControllerShowSource paste_source) { |
| // Return early if any of these conditions occur: |
| // 1. The original clipboard data has been replaced by an in-progress |
| // clipboard history paste. |
| // 2. The active window has changed. |
| // 3. The clipboard history feature is disabled under the current mode. |
| if (clipboard_data_replaced_ || !intended_window || |
| intended_window != window_util::GetActiveWindow() || |
| !clipboard_history_util::IsEnabledInCurrentMode()) { |
| if (confirmed_operation_callback_for_test_) |
| confirmed_operation_callback_for_test_.Run(/*success=*/false); |
| return; |
| } |
| |
| // Get information about the data to be pasted. |
| bool paste_plain_text = IsPlainTextPaste(paste_type); |
| auto* clipboard = GetClipboard(); |
| ui::DataTransferEndpoint data_dst(ui::EndpointType::kClipboardHistory); |
| const auto* current_clipboard_data = clipboard->GetClipboardData(&data_dst); |
| |
| // Clipboard history pastes are performed by temporarily writing data to the |
| // system clipboard, if necessary, and then issuing a standard paste. |
| // Determine the data we should temporarily write to the clipboard, if any, so |
| // that we can paste the selected history item. |
| std::unique_ptr<ui::ClipboardData> data_to_paste; |
| if (paste_plain_text) { |
| data_to_paste = std::make_unique<ui::ClipboardData>(); |
| data_to_paste->set_commit_time(item.data().commit_time()); |
| data_to_paste->set_text(item.data().text()); |
| auto data_src = item.data().source(); |
| if (data_src) { |
| data_to_paste->set_source(data_src); |
| } |
| } else if (!current_clipboard_data || |
| *current_clipboard_data != item.data()) { |
| data_to_paste = std::make_unique<ui::ClipboardData>(item.data()); |
| } |
| |
| // Pause changes to clipboard history while manipulating the clipboard. |
| std::unique_ptr<ui::ClipboardData> replaced_data; |
| // If necessary, replace the clipboard's current data before issuing a paste. |
| if (data_to_paste) { |
| ScopedClipboardHistoryPauseImpl scoped_pause( |
| clipboard_history_.get(), |
| clipboard_history_util::PauseBehavior::kDefault); |
| replaced_data = |
| GetClipboard()->WriteClipboardData(std::move(data_to_paste)); |
| clipboard_data_replaced_ = !!replaced_data; |
| } |
| |
| ++pastes_to_be_confirmed_; |
| |
| // Use synthetic pastes as a fallback solution. |
| if (!delegate_->Paste()) { |
| SyntheticPaste(paste_source); |
| } |
| |
| clipboard_history_util::RecordClipboardHistoryItemPasted(item); |
| base::UmaHistogramEnumeration("Ash.ClipboardHistory.PasteType", paste_type); |
| base::UmaHistogramEnumeration("Ash.ClipboardHistory.PasteSource", |
| paste_source); |
| |
| for (auto& observer : observers_) { |
| observer.OnClipboardHistoryPasted(); |
| } |
| |
| // If the clipboard was not changed--i.e., we pasted the full data on the |
| // clipboard--then we are done modifying the clipboard buffer. |
| if (!replaced_data) { |
| return; |
| } |
| |
| // Restore the clipboard data asynchronously. Some apps take a long time to |
| // receive the paste event, and some apps will read from the clipboard |
| // multiple times per paste. Wait a bit before writing `data_to_restore` back |
| // to the clipboard. |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, |
| base::BindOnce( |
| [](const base::WeakPtr<ClipboardHistoryControllerImpl>& weak_ptr, |
| std::unique_ptr<ui::ClipboardData> data_to_restore) { |
| std::unique_ptr<ScopedClipboardHistoryPauseImpl> scoped_pause; |
| if (weak_ptr) { |
| // When restoring the original clipboard content, pause clipboard |
| // history to avoid committing data already at the top of the |
| // clipboard history list. |
| scoped_pause = std::make_unique<ScopedClipboardHistoryPauseImpl>( |
| weak_ptr->clipboard_history_.get(), |
| clipboard_history_util::PauseBehavior::kDefault); |
| } |
| GetClipboard()->WriteClipboardData(std::move(data_to_restore)); |
| |
| if (weak_ptr) { |
| weak_ptr->clipboard_data_replaced_ = false; |
| |
| // Confirm the operation after data restoration if needed. |
| if (auto& callback_for_test = |
| weak_ptr->confirmed_operation_callback_for_test_; |
| callback_for_test && !weak_ptr->pastes_to_be_confirmed_) { |
| callback_for_test.Run(/*success=*/true); |
| } |
| } |
| }, |
| weak_ptr_factory_.GetWeakPtr(), std::move(replaced_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(/*will_paste_item=*/false); |
| return; |
| } |
| |
| context_menu_->RemoveMenuItemWithCommandId(command_id); |
| } |
| |
| void ClipboardHistoryControllerImpl::DeleteClipboardHistoryItem( |
| const ClipboardHistoryItem& item) { |
| clipboard_history_util::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 = 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 |