Commit f4e41c93 authored by Andrew Xu's avatar Andrew Xu Committed by Commit Bot

[Multipaste] Navigate the menu selection with the tab key

Enable the menu selection traversal with the tab key. In detail:
(1) If a menu item is under selection and its delete button is hidden,
after pressing the tab key, the delete button should show.
(2) If a menu item's delete button is under pseudo focus, after pressing
the tab key, the next selectable menu item after the current one is
selected.
(3) A menu item view is removed from the root menu asynchronously since
the item to be deleted may be accessed by MenuController.

Bug: 1134422, 1106847
Change-Id: I71abdbca619266f21543172d670960bea8580049
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2448674Reviewed-by: default avatarXiyuan Xia <xiyuan@chromium.org>
Reviewed-by: default avatarDavid Black <dmblack@google.com>
Commit-Queue: Andrew Xu <andrewxu@chromium.org>
Cr-Commit-Position: refs/heads/master@{#819992}
parent f72bd013
......@@ -84,6 +84,14 @@ class ClipboardHistoryControllerImpl::AcceleratorTarget
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;
......@@ -91,19 +99,33 @@ class ClipboardHistoryControllerImpl::AcceleratorTarget
void OnMenuShown() {
Shell::Get()->accelerator_controller()->Register(
{delete_selected_}, /*accelerator_target=*/this);
{delete_selected_, tab_navigation_, shift_tab_navigation_},
/*accelerator_target=*/this);
}
void OnMenuClosed() {
Shell::Get()->accelerator_controller()->Unregister(
{delete_selected_}, /*accelerator_target=*/this);
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 {
DCHECK(delete_selected_ == accelerator);
HandleDeleteSelected();
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;
}
......@@ -111,9 +133,20 @@ class ClipboardHistoryControllerImpl::AcceleratorTarget
return controller_->IsMenuShowing() || controller_->CanShowMenu();
}
void HandleDeleteSelected() {
void HandleDeleteSelected(int event_flags) {
DCHECK(controller_->IsMenuShowing());
controller_->DeleteSelectedMenuItemIfAny();
controller_->ExecuteCommand(ClipboardHistoryUtil::kDeleteCommandId,
event_flags);
}
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.
......@@ -122,6 +155,12 @@ class ClipboardHistoryControllerImpl::AcceleratorTarget
// 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 --------------------------------
......@@ -136,12 +175,7 @@ class ClipboardHistoryControllerImpl::MenuDelegate
// ui::SimpleMenuModel::Delegate:
void ExecuteCommand(int command_id, int event_flags) override {
if (command_id == ClipboardHistoryUtil::kDeleteCommandId) {
controller_->DeleteSelectedMenuItemIfAny();
return;
}
controller_->MenuOptionSelected(command_id, event_flags);
controller_->ExecuteCommand(command_id, event_flags);
}
private:
......@@ -222,8 +256,21 @@ void ClipboardHistoryControllerImpl::ExecuteSelectedMenuItem(int event_flags) {
command.value_or(ClipboardHistoryUtil::kFirstItemCommandId), event_flags);
}
void ClipboardHistoryControllerImpl::MenuOptionSelected(int command_id,
int event_flags) {
void ClipboardHistoryControllerImpl::ExecuteCommand(int command_id,
int event_flags) {
DCHECK(context_menu_);
if (command_id == ClipboardHistoryUtil::kDeleteCommandId) {
DeleteSelectedMenuItemIfAny();
return;
}
DCHECK_GE(command_id, ClipboardHistoryUtil::kFirstItemCommandId);
PasteMenuItemData(command_id, event_flags & ui::EF_SHIFT_DOWN);
}
void ClipboardHistoryControllerImpl::PasteMenuItemData(int command_id,
bool paste_plain_text) {
UMA_HISTOGRAM_ENUMERATION(
"Ash.ClipboardHistory.ContextMenu.MenuOptionSelected", command_id,
ClipboardHistoryUtil::kMaxCommandId);
......@@ -240,12 +287,11 @@ void ClipboardHistoryControllerImpl::MenuOptionSelected(int command_id,
// If necessary, replace the clipboard's |original_data| temporarily so that
// we can paste the selected history item.
const bool shift_key_pressed = event_flags & ui::EF_SHIFT_DOWN;
ui::ClipboardDataEndpoint data_dst(ui::EndpointType::kClipboardHistory);
if (shift_key_pressed ||
if (paste_plain_text ||
selected_item.data() != *clipboard->GetClipboardData(&data_dst)) {
std::unique_ptr<ui::ClipboardData> temp_data;
if (shift_key_pressed) {
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(selected_item.data().text());
......@@ -321,11 +367,12 @@ void ClipboardHistoryControllerImpl::DeleteSelectedMenuItemIfAny() {
return;
}
base::Optional<int> new_selected_command_id =
context_menu_->CalculateSelectedCommandIdAfterDeletion();
context_menu_->RemoveMenuItemWithCommandId(*selected_command);
if (new_selected_command_id.has_value())
context_menu_->SelectMenuItemWithCommandId(*new_selected_command_id);
context_menu_->RemoveSelectedMenuItem();
}
void ClipboardHistoryControllerImpl::AdvancePseudoFocus(bool reverse) {
DCHECK(context_menu_);
context_menu_->AdvancePseudoFocus(reverse);
}
gfx::Rect ClipboardHistoryControllerImpl::CalculateAnchorRect() const {
......
......@@ -62,7 +62,7 @@ class ASH_EXPORT ClipboardHistoryControllerImpl
return nudge_controller_.get();
}
const ClipboardHistoryMenuModelAdapter* context_menu_for_test() const {
ClipboardHistoryMenuModelAdapter* context_menu_for_test() {
return context_menu_.get();
}
......@@ -77,12 +77,22 @@ class ASH_EXPORT ClipboardHistoryControllerImpl
bool CanShowMenu() const override;
void ExecuteSelectedMenuItem(int event_flags);
void MenuOptionSelected(int command_id, int event_flags);
// Executes the command specified by `command_id` with the given event flags.
void ExecuteCommand(int command_id, int event_flags);
// Paste the clipboard data of the menu item specified by `command_id`.
// `paste_plain_text` indicates whether the plain text instead of the
// clipboard data should be pasted.
void PasteMenuItemData(int command_id, bool paste_plain_text);
// Delete the menu item being selected and its corresponding data. If no item
// is selected, do nothing.
void DeleteSelectedMenuItemIfAny();
// Advances the pseudo focus (backward if `reverse` is true).
void AdvancePseudoFocus(bool reverse);
gfx::Rect CalculateAnchorRect() const;
// Called when the contextual menu is closed.
......
......@@ -53,6 +53,7 @@ void ClipboardHistoryMenuModelAdapter::Run(
DCHECK(!root_view_);
DCHECK(model_);
DCHECK(item_snapshots_.empty());
DCHECK(item_views_by_command_id_.empty());
menu_open_time_ = base::TimeTicks::Now();
......@@ -119,7 +120,9 @@ ClipboardHistoryMenuModelAdapter::GetItemFromCommandId(int command_id) const {
}
int ClipboardHistoryMenuModelAdapter::GetMenuItemsCount() const {
return root_view_->GetSubmenu()->GetRowCount();
// We should not use `root_view_` to retrieve the item count. Because the
// menu item view is removed from `root_view_` asynchronously.
return item_views_by_command_id_.size();
}
void ClipboardHistoryMenuModelAdapter::SelectMenuItemWithCommandId(
......@@ -131,15 +134,125 @@ void ClipboardHistoryMenuModelAdapter::SelectMenuItemWithCommandId(
selected_menu_item);
}
void ClipboardHistoryMenuModelAdapter::RemoveMenuItemWithCommandId(
int command_id) {
model_->RemoveItemAt(model_->GetIndexOfCommandId(command_id));
root_view_->RemoveMenuItem(root_view_->GetMenuItemByID(command_id));
root_view_->ChildrenChanged();
void ClipboardHistoryMenuModelAdapter::RemoveSelectedMenuItem() {
base::Optional<int> current_selected_command_id =
GetSelectedMenuItemCommand();
DCHECK(current_selected_command_id.has_value());
// Calculate `new_selected_command_id` before removing
// `current_selected_command_id` from data structures because the latter is
// needed in calculation.
base::Optional<int> new_selected_command_id =
CalculateSelectedCommandIdAfterDeletion();
// Update the menu item selection.
if (new_selected_command_id.has_value()) {
SelectMenuItemWithCommandId(*new_selected_command_id);
} else {
views::MenuController::GetActiveInstance()->SelectItemAndOpenSubmenu(
root_view_);
}
auto item_view_to_delete =
item_views_by_command_id_.find(*current_selected_command_id);
DCHECK(item_view_to_delete != item_views_by_command_id_.cend());
// Disable views to be removed in order to prevent them from handling events.
root_view_->GetMenuItemByID(*current_selected_command_id)->SetEnabled(false);
item_view_to_delete->second->SetEnabled(false);
item_views_by_command_id_.erase(item_view_to_delete);
auto item_to_delete = item_snapshots_.find(command_id);
auto item_to_delete = item_snapshots_.find(*current_selected_command_id);
DCHECK(item_to_delete != item_snapshots_.end());
item_snapshots_.erase(item_to_delete);
// The current selected menu item may be accessed after item deletion. So
// postpone the menu item deletion.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE,
base::BindOnce(&ClipboardHistoryMenuModelAdapter::RemoveItemView,
weak_ptr_factory_.GetWeakPtr(),
*current_selected_command_id));
}
void ClipboardHistoryMenuModelAdapter::AdvancePseudoFocus(bool reverse) {
base::Optional<int> selected_command = GetSelectedMenuItemCommand();
// If no item is selected, select the topmost or bottom menu item depending
// on the focus move direction.
if (!selected_command.has_value()) {
SelectMenuItemWithCommandId(
reverse ? item_views_by_command_id_.rbegin()->first
: ClipboardHistoryUtil::kFirstItemCommandId);
return;
}
AdvancePseudoFocusFromSelectedItem(reverse);
}
gfx::Rect ClipboardHistoryMenuModelAdapter::GetMenuBoundsInScreenForTest()
const {
DCHECK(root_view_);
return root_view_->GetSubmenu()->GetBoundsInScreen();
}
const views::MenuItemView*
ClipboardHistoryMenuModelAdapter::GetMenuItemViewAtForTest(int index) const {
DCHECK(root_view_);
return root_view_->GetSubmenu()->GetMenuItemAt(index);
}
ClipboardHistoryMenuModelAdapter::ClipboardHistoryMenuModelAdapter(
std::unique_ptr<ui::SimpleMenuModel> model,
base::RepeatingClosure menu_closed_callback,
const ClipboardHistory* clipboard_history,
const ClipboardHistoryResourceManager* resource_manager)
: views::MenuModelAdapter(model.get(), std::move(menu_closed_callback)),
model_(std::move(model)),
clipboard_history_(clipboard_history),
resource_manager_(resource_manager) {}
void ClipboardHistoryMenuModelAdapter::AdvancePseudoFocusFromSelectedItem(
bool reverse) {
base::Optional<int> selected_item_command = GetSelectedMenuItemCommand();
DCHECK(selected_item_command.has_value());
auto selected_item_iter =
item_views_by_command_id_.find(*selected_item_command);
DCHECK(selected_item_iter != item_views_by_command_id_.end());
ClipboardHistoryItemView* selected_item_view = selected_item_iter->second;
// Move the pseudo focus on the selected item view. Return early if the
// focused view does not change.
const bool selected_item_has_focus =
selected_item_view->AdvancePseudoFocus(reverse);
if (selected_item_has_focus)
return;
int next_selected_item_command = -1;
ClipboardHistoryItemView* next_focused_view = nullptr;
if (reverse) {
auto next_focused_item_iter =
selected_item_iter == item_views_by_command_id_.begin()
? item_views_by_command_id_.rbegin()
: std::make_reverse_iterator(selected_item_iter);
next_selected_item_command = next_focused_item_iter->first;
next_focused_view = next_focused_item_iter->second;
} else {
auto next_focused_item_iter = std::next(selected_item_iter, 1);
if (next_focused_item_iter == item_views_by_command_id_.end())
next_focused_item_iter = item_views_by_command_id_.begin();
next_selected_item_command = next_focused_item_iter->first;
next_focused_view = next_focused_item_iter->second;
}
// Advancing pseudo focus should precede the item selection. Because when an
// item view is selected, the selected view does not overwrite its pseudo
// focus if its pseudo focus is non-empty. It can ensure that the pseudo focus
// and the corresponding UI appearance update only once.
next_focused_view->AdvancePseudoFocus(reverse);
SelectMenuItemWithCommandId(next_selected_item_command);
}
base::Optional<int>
......@@ -175,27 +288,23 @@ ClipboardHistoryMenuModelAdapter::CalculateSelectedCommandIdAfterDeletion()
return base::nullopt;
}
gfx::Rect ClipboardHistoryMenuModelAdapter::GetMenuBoundsInScreenForTest()
const {
DCHECK(root_view_);
return root_view_->GetSubmenu()->GetBoundsInScreen();
}
void ClipboardHistoryMenuModelAdapter::RemoveItemView(int command_id) {
base::Optional<int> original_selected_command_id =
GetSelectedMenuItemCommand();
const views::MenuItemView*
ClipboardHistoryMenuModelAdapter::GetMenuItemViewAtForTest(int index) const {
DCHECK(root_view_);
return root_view_->GetSubmenu()->GetMenuItemAt(index);
}
// The menu item view and its corresponding command should be removed at the
// same time. Otherwise, it may run into check errors.
model_->RemoveItemAt(model_->GetIndexOfCommandId(command_id));
root_view_->RemoveMenuItem(root_view_->GetMenuItemByID(command_id));
root_view_->ChildrenChanged();
ClipboardHistoryMenuModelAdapter::ClipboardHistoryMenuModelAdapter(
std::unique_ptr<ui::SimpleMenuModel> model,
base::RepeatingClosure menu_closed_callback,
const ClipboardHistory* clipboard_history,
const ClipboardHistoryResourceManager* resource_manager)
: views::MenuModelAdapter(model.get(), std::move(menu_closed_callback)),
model_(std::move(model)),
clipboard_history_(clipboard_history),
resource_manager_(resource_manager) {}
// `ChildrenChanged()` clears the selection. So restore the selection.
if (original_selected_command_id.has_value())
SelectMenuItemWithCommandId(*original_selected_command_id);
if (item_removal_callback_for_test_)
item_removal_callback_for_test_.Run();
}
views::MenuItemView* ClipboardHistoryMenuModelAdapter::AppendMenuItem(
views::MenuItemView* menu,
......@@ -216,6 +325,7 @@ views::MenuItemView* ClipboardHistoryMenuModelAdapter::AppendMenuItem(
ClipboardHistoryItemView::CreateFromClipboardHistoryItem(
GetItemFromCommandId(command_id), resource_manager_, container);
item_view->Init();
item_views_by_command_id_.insert(std::make_pair(command_id, item_view.get()));
container->AddChildView(std::move(item_view));
return container;
......@@ -228,6 +338,7 @@ void ClipboardHistoryMenuModelAdapter::OnMenuClosed(views::MenuItemView* menu) {
UMA_HISTOGRAM_TIMES("Ash.ClipboardHistory.ContextMenu.UserJourneyTime",
user_journey_time);
views::MenuModelAdapter::OnMenuClosed(menu);
item_views_by_command_id_.clear();
}
} // namespace ash
......@@ -27,6 +27,7 @@ namespace ash {
class ClipboardHistory;
class ClipboardHistoryItem;
class ClipboardHistoryItemView;
class ClipboardHistoryResourceManager;
// Used to show the clipboard history menu, which holds the last few things
......@@ -70,19 +71,21 @@ class ASH_EXPORT ClipboardHistoryMenuModelAdapter : views::MenuModelAdapter {
// Selects the menu item specified by `command_id`.
void SelectMenuItemWithCommandId(int command_id);
// Removes the menu item specified by `command_id`.
void RemoveMenuItemWithCommandId(int command_id);
// Removes the selected menu item.
void RemoveSelectedMenuItem();
// Returns the command id of the menu item to be selected if any after the
// current selected item is deleted. If no menu item is selectable
// after deletion, an absent value is returned.
base::Optional<int> CalculateSelectedCommandIdAfterDeletion() const;
// Advances the pseudo focus (backward if `reverse` is true).
void AdvancePseudoFocus(bool reverse);
// Returns menu bounds in screen coordinates.
gfx::Rect GetMenuBoundsInScreenForTest() const;
const views::MenuItemView* GetMenuItemViewAtForTest(int index) const;
void set_item_removal_callback_for_test(base::RepeatingClosure new_callback) {
item_removal_callback_for_test_ = std::move(new_callback);
}
private:
ClipboardHistoryMenuModelAdapter(
std::unique_ptr<ui::SimpleMenuModel> model,
......@@ -90,6 +93,18 @@ class ASH_EXPORT ClipboardHistoryMenuModelAdapter : views::MenuModelAdapter {
const ClipboardHistory* clipboard_history,
const ClipboardHistoryResourceManager* resource_manager);
// Advances the pseduo focus from the selected history item view (backward if
// `reverse` is true).
void AdvancePseudoFocusFromSelectedItem(bool reverse);
// Returns the command id of the menu item to be selected if any after the
// current selected item is deleted. If no menu item is selectable
// after deletion, an absent value is returned.
base::Optional<int> CalculateSelectedCommandIdAfterDeletion() const;
// Removes the item view specified by `command_id` from the root menu.
void RemoveItemView(int command_id);
// views::MenuModelAdapter:
views::MenuItemView* AppendMenuItem(views::MenuItemView* menu,
ui::MenuModel* model,
......@@ -112,13 +127,24 @@ class ASH_EXPORT ClipboardHistoryMenuModelAdapter : views::MenuModelAdapter {
// possible inconsistency between the menu model data and the clipboard
// history data. For example, a new item is added to `clipboard_history_`
// while the menu is showing.
// It updates synchronously when a item is removed.
std::map<int, ClipboardHistoryItem> item_snapshots_;
// Stores mappings between command ids and history item view pointers.
// It updates synchronously when a item is removed.
std::map<int, ClipboardHistoryItemView*> item_views_by_command_id_;
const ClipboardHistory* const clipboard_history_;
// Resource manager used to fetch image models. Owned by
// ClipboardHistoryController.
const ClipboardHistoryResourceManager* const resource_manager_;
// Called when an item view is removed from the root menu.
base::RepeatingClosure item_removal_callback_for_test_;
base::WeakPtrFactory<ClipboardHistoryMenuModelAdapter> weak_ptr_factory_{
this};
};
} // namespace ash
......
......@@ -170,11 +170,53 @@ void ClipboardHistoryItemView::Init() {
}
void ClipboardHistoryItemView::OnSelectionChanged() {
pseudo_focus_ =
container_->IsSelected() ? PseudoFocus::kMainButton : PseudoFocus::kEmpty;
if (!container_->IsSelected()) {
SetPseudoFocus(PseudoFocus::kEmpty);
return;
}
contents_view_->delete_button()->SetVisible(ShouldShowDeleteButton());
main_button_->SchedulePaint();
// If the pseudo focus is moved from another item view via focus traversal,
// `pseudo_focus_` is already up to date.
if (pseudo_focus_ != PseudoFocus::kEmpty)
return;
InitiatePseudoFocus(/*reverse=*/false);
}
bool ClipboardHistoryItemView::AdvancePseudoFocus(bool reverse) {
if (pseudo_focus_ == PseudoFocus::kEmpty) {
InitiatePseudoFocus(reverse);
return true;
}
// When the menu item is disabled, only the delete button is able to work.
if (!container_->GetEnabled()) {
DCHECK_EQ(PseudoFocus::kDeleteButton, pseudo_focus_);
SetPseudoFocus(PseudoFocus::kEmpty);
return false;
}
DCHECK(pseudo_focus_ == PseudoFocus::kMainButton ||
pseudo_focus_ == PseudoFocus::kDeleteButton);
int new_pseudo_focus = pseudo_focus_;
bool move_focus_out = false;
if (reverse) {
--new_pseudo_focus;
if (new_pseudo_focus == PseudoFocus::kEmpty)
move_focus_out = true;
} else {
++new_pseudo_focus;
if (new_pseudo_focus == PseudoFocus::kMaxValue)
move_focus_out = true;
}
if (move_focus_out) {
SetPseudoFocus(PseudoFocus::kEmpty);
return false;
}
SetPseudoFocus(static_cast<PseudoFocus>(new_pseudo_focus));
return true;
}
void ClipboardHistoryItemView::RecordButtonPressedHistogram(
......@@ -215,6 +257,7 @@ int ClipboardHistoryItemView::CalculateCommandId() const {
case PseudoFocus::kDeleteButton:
return ClipboardHistoryUtil::kDeleteCommandId;
case PseudoFocus::kEmpty:
case PseudoFocus::kMaxValue:
NOTREACHED();
return -1;
}
......@@ -229,4 +272,23 @@ bool ClipboardHistoryItemView::ShouldShowDeleteButton() const {
pseudo_focus_ == PseudoFocus::kDeleteButton;
}
void ClipboardHistoryItemView::InitiatePseudoFocus(bool reverse) {
PseudoFocus target_pseudo_focus;
if (!container_->GetEnabled() || reverse)
target_pseudo_focus = PseudoFocus::kDeleteButton;
else
target_pseudo_focus = PseudoFocus::kMainButton;
SetPseudoFocus(target_pseudo_focus);
}
void ClipboardHistoryItemView::SetPseudoFocus(PseudoFocus new_pseudo_focus) {
if (pseudo_focus_ == new_pseudo_focus)
return;
pseudo_focus_ = new_pseudo_focus;
contents_view_->delete_button()->SetVisible(ShouldShowDeleteButton());
main_button_->SchedulePaint();
}
} // namespace ash
......@@ -36,6 +36,10 @@ class ClipboardHistoryItemView : public views::View {
// Called when the selection state has changed.
void OnSelectionChanged();
// Advances the pseudo focus (backward if reverse is true). Returns whether
// the view still keeps the pseudo focus.
bool AdvancePseudoFocus(bool reverse);
const views::View* delete_button_for_test() const {
return contents_view_->delete_button();
}
......@@ -107,7 +111,20 @@ class ClipboardHistoryItemView : public views::View {
// user actions on the menu item (like clicking the mouse or triggering an
// accelerator). Note that the child under pseudo focus does not have view
// focus. It is where "pseudo" comes from.
enum class PseudoFocus { kEmpty, kMainButton, kDeleteButton };
// The enumeration types are arranged in the forward focus traversal order.
enum PseudoFocus {
// No child is under pseudo focus.
kEmpty = 0,
// The main button has pseudo focus.
kMainButton = 1,
// The delete button has pseudo focus.
kDeleteButton = 2,
// Marks the end. It should not be assigned to `pseudo_focus_`.
kMaxValue = 3
};
// views::View:
gfx::Size CalculatePreferredSize() const override;
......@@ -123,6 +140,12 @@ class ClipboardHistoryItemView : public views::View {
bool ShouldShowDeleteButton() const;
// Called when receiving pseudo focus for the first time.
void InitiatePseudoFocus(bool reverse);
// Updates `pseudo_focus_` and children visibility.
void SetPseudoFocus(PseudoFocus new_pseudo_focus);
// Owned by ClipboardHistoryMenuModelAdapter.
const ClipboardHistoryItem* const clipboard_history_item_;
......
......@@ -85,7 +85,7 @@ ash::ClipboardHistoryControllerImpl* GetClipboardHistoryController() {
return ash::Shell::Get()->clipboard_history_controller();
}
const ash::ClipboardHistoryMenuModelAdapter* GetContextMenu() {
ash::ClipboardHistoryMenuModelAdapter* GetContextMenu() {
return GetClipboardHistoryController()->context_menu_for_test();
}
......@@ -148,6 +148,14 @@ class ClipboardHistoryWithMultiProfileBrowserTest
Release(key, modifiers);
}
void WaitUntilItemDeletionCompletes() {
auto* context_menu = GetContextMenu();
DCHECK(context_menu);
base::RunLoop run_loop;
context_menu->set_item_removal_callback_for_test(run_loop.QuitClosure());
run_loop.Run();
}
void ShowContextMenuViaAccelerator() {
PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
}
......@@ -289,6 +297,102 @@ IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
->GetVisible());
}
// Verifies the selection traversal via the tab key.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
VerifyTabSelectionTraversal) {
LoginUser(account_id1_);
SetClipboardText("A");
SetClipboardText("B");
ShowContextMenuViaAccelerator();
// Verify the default state right after the menu shows.
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
ASSERT_EQ(2, GetContextMenu()->GetMenuItemsCount());
const views::MenuItemView* first_menu_item_view =
GetMenuItemViewForIndex(/*index=*/0);
ASSERT_TRUE(first_menu_item_view->IsSelected());
const ash::ClipboardHistoryItemView* first_history_item_view =
GetHistoryItemViewForIndex(/*index=*/0);
ASSERT_FALSE(first_history_item_view->delete_button_for_test()->GetVisible());
// Press the tab key. Verify that the first menu item's delete button shows.
PressAndRelease(ui::VKEY_TAB);
EXPECT_TRUE(first_menu_item_view->IsSelected());
ASSERT_TRUE(first_history_item_view->delete_button_for_test()->GetVisible());
const views::MenuItemView* second_menu_item_view =
GetMenuItemViewForIndex(/*index=*/1);
EXPECT_FALSE(second_menu_item_view->IsSelected());
const ash::ClipboardHistoryItemView* second_history_item_view =
GetHistoryItemViewForIndex(/*index=*/1);
EXPECT_FALSE(
second_history_item_view->delete_button_for_test()->GetVisible());
// Press the tab key. Verify that the second menu item is selected while its
// delete button is hidden.
PressAndRelease(ui::VKEY_TAB);
EXPECT_TRUE(second_menu_item_view->IsSelected());
EXPECT_FALSE(
second_history_item_view->delete_button_for_test()->GetVisible());
// Press the tab key. Verify that the second item's delete button shows.
PressAndRelease(ui::VKEY_TAB);
EXPECT_TRUE(second_menu_item_view->IsSelected());
EXPECT_TRUE(second_history_item_view->delete_button_for_test()->GetVisible());
// Press the tab key with the shift key pressed. Verify that the second item
// is selected while its delete button is hidden.
PressAndRelease(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
EXPECT_TRUE(second_menu_item_view->IsSelected());
EXPECT_FALSE(
second_history_item_view->delete_button_for_test()->GetVisible());
// Press the tab key with the shift key pressed. Verify that the first item
// is selected while its delete button is visible.
PressAndRelease(ui::VKEY_TAB, ui::EF_SHIFT_DOWN);
EXPECT_TRUE(first_menu_item_view->IsSelected());
EXPECT_TRUE(first_history_item_view->delete_button_for_test()->GetVisible());
EXPECT_FALSE(second_menu_item_view->IsSelected());
// Press the ENTER key. Verifies that the first item is deleted. The second
// item is selected and its delete button should not show.
PressAndRelease(ui::VKEY_RETURN);
EXPECT_EQ(1, GetContextMenu()->GetMenuItemsCount());
EXPECT_TRUE(second_menu_item_view->IsSelected());
EXPECT_FALSE(
second_history_item_view->delete_button_for_test()->GetVisible());
}
// Verifies the tab traversal on the history menu with only one item.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
VerifyTabTraversalOnOneItemMenu) {
LoginUser(account_id1_);
SetClipboardText("A");
ShowContextMenuViaAccelerator();
// Verify the default state right after the menu shows.
ASSERT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
ASSERT_EQ(1, GetContextMenu()->GetMenuItemsCount());
const ash::ClipboardHistoryItemView* first_history_item_view =
GetHistoryItemViewForIndex(/*index=*/0);
ASSERT_FALSE(first_history_item_view->delete_button_for_test()->GetVisible());
const views::MenuItemView* first_menu_item_view =
GetMenuItemViewForIndex(/*index=*/0);
ASSERT_TRUE(first_menu_item_view->IsSelected());
// Press the tab key. Verify that the delete button is visible.
PressAndRelease(ui::VKEY_TAB);
ASSERT_TRUE(first_history_item_view->delete_button_for_test()->GetVisible());
// Press the tab key. Verify that the delete button is hidden. The menu item
// is still under selection.
PressAndRelease(ui::VKEY_TAB);
ASSERT_FALSE(first_history_item_view->delete_button_for_test()->GetVisible());
EXPECT_TRUE(first_menu_item_view->IsSelected());
}
// Verifies that the history menu is anchored at the cursor's location when
// not having any textfield.
IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
......@@ -338,7 +442,7 @@ IN_PROC_BROWSER_TEST_F(ClipboardHistoryWithMultiProfileBrowserTest,
// Select the first menu item via key then delete it. Verify the menu and the
// clipboard history.
PressAndRelease(ui::KeyboardCode::VKEY_BACK, ui::EF_NONE);
PressAndRelease(ui::KeyboardCode::VKEY_BACK);
EXPECT_EQ(2, GetContextMenu()->GetMenuItemsCount());
EXPECT_TRUE(VerifyClipboardTextData({"B", "A"}));
......@@ -512,7 +616,7 @@ IN_PROC_BROWSER_TEST_F(ClipboardHistoryTextfieldBrowserTest,
SetClipboardText("B");
SetClipboardText("C");
// Verify we can paste the first history item via the ENTER key.
// Verify we can paste the first history item via the COMMAND+V shortcut.
PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
......@@ -541,7 +645,7 @@ IN_PROC_BROWSER_TEST_F(ClipboardHistoryTextfieldBrowserTest,
textfield_->SetText(base::string16());
EXPECT_TRUE(textfield_->GetText().empty());
// Verify we can paste the last history item via the ENTER key.
// Verify we can paste the first history item via the COMMAND+V shortcut.
PressAndRelease(ui::KeyboardCode::VKEY_V, ui::EF_COMMAND_DOWN);
EXPECT_TRUE(GetClipboardHistoryController()->IsMenuShowing());
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment