clipboard_history_menu_model_adapter.cc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. // Copyright 2020 The Chromium Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style license that can be
  3. // found in the LICENSE file.
  4. #include "ash/clipboard/clipboard_history_menu_model_adapter.h"
  5. #include "ash/clipboard/clipboard_history.h"
  6. #include "ash/clipboard/clipboard_history_util.h"
  7. #include "ash/clipboard/views/clipboard_history_item_view.h"
  8. #include "ash/public/cpp/clipboard_image_model_factory.h"
  9. #include "ash/wm/window_util.h"
  10. #include "base/bind.h"
  11. #include "base/metrics/histogram_macros.h"
  12. #include "ui/accessibility/ax_enums.mojom.h"
  13. #include "ui/base/clipboard/clipboard.h"
  14. #include "ui/base/l10n/l10n_util.h"
  15. #include "ui/base/ui_base_types.h"
  16. #include "ui/gfx/geometry/rect.h"
  17. #include "ui/strings/grit/ui_strings.h"
  18. #include "ui/views/accessibility/view_accessibility.h"
  19. #include "ui/views/controls/menu/menu_item_view.h"
  20. #include "ui/views/controls/menu/menu_runner.h"
  21. #include "ui/views/controls/menu/menu_types.h"
  22. #include "ui/views/controls/menu/submenu_view.h"
  23. #include "ui/views/widget/widget.h"
  24. namespace ash {
  25. // ClipboardHistoryMenuModelAdapter::ScopedA11yIgnore --------------------------
  26. // The scoped class to disable a11y for all items views.
  27. class ClipboardHistoryMenuModelAdapter::ScopedA11yIgnore {
  28. public:
  29. explicit ScopedA11yIgnore(
  30. ClipboardHistoryMenuModelAdapter* menu_model_adapter)
  31. : menu_model_adapter_(menu_model_adapter) {
  32. SetIgnoreA11yForAllItemViews(true);
  33. }
  34. ~ScopedA11yIgnore() { SetIgnoreA11yForAllItemViews(false); }
  35. private:
  36. void SetIgnoreA11yForAllItemViews(bool ignore) {
  37. for (auto& item_view_command_id_pair :
  38. menu_model_adapter_->item_views_by_command_id_) {
  39. views::View* item_view = item_view_command_id_pair.second;
  40. item_view->GetViewAccessibility().OverrideIsIgnored(ignore);
  41. }
  42. }
  43. ClipboardHistoryMenuModelAdapter* const menu_model_adapter_;
  44. };
  45. // ClipboardHistoryMenuModelAdapter --------------------------------------------
  46. // static
  47. std::unique_ptr<ClipboardHistoryMenuModelAdapter>
  48. ClipboardHistoryMenuModelAdapter::Create(
  49. ui::SimpleMenuModel::Delegate* delegate,
  50. base::RepeatingClosure menu_closed_callback,
  51. const ClipboardHistory* clipboard_history,
  52. const ClipboardHistoryResourceManager* resource_manager) {
  53. return base::WrapUnique(new ClipboardHistoryMenuModelAdapter(
  54. std::make_unique<ui::SimpleMenuModel>(delegate),
  55. std::move(menu_closed_callback), clipboard_history, resource_manager));
  56. }
  57. ClipboardHistoryMenuModelAdapter::~ClipboardHistoryMenuModelAdapter() = default;
  58. void ClipboardHistoryMenuModelAdapter::Run(
  59. const gfx::Rect& anchor_rect,
  60. ui::MenuSourceType source_type) {
  61. DCHECK(!root_view_);
  62. DCHECK(model_);
  63. DCHECK(item_snapshots_.empty());
  64. DCHECK(item_views_by_command_id_.empty());
  65. // `Run()` should be called at most once for an instance.
  66. DCHECK(!run_before_);
  67. run_before_ = true;
  68. menu_open_time_ = base::TimeTicks::Now();
  69. int command_id = ClipboardHistoryUtil::kFirstItemCommandId;
  70. const auto& items = clipboard_history_->GetItems();
  71. // Do not include the final kDeleteCommandId item in histograms, because it
  72. // is not shown.
  73. UMA_HISTOGRAM_COUNTS_100(
  74. "Ash.ClipboardHistory.ContextMenu.NumberOfItemsShown", items.size());
  75. const ui::DataTransferEndpoint data_dst(ui::EndpointType::kDefault,
  76. /*notify_if_restricted=*/false);
  77. for (const auto& item : items) {
  78. model_->AddItem(command_id, std::u16string());
  79. item_snapshots_.emplace(command_id, item);
  80. ++command_id;
  81. }
  82. // Start async rendering of HTML, if any exists.
  83. ClipboardImageModelFactory::Get()->Activate();
  84. root_view_ = CreateMenu();
  85. root_view_->SetTitle(
  86. l10n_util::GetStringUTF16(IDS_CLIPBOARD_HISTORY_MENU_TITLE));
  87. menu_runner_ = std::make_unique<views::MenuRunner>(
  88. root_view_, views::MenuRunner::CONTEXT_MENU |
  89. views::MenuRunner::USE_ASH_SYS_UI_LAYOUT |
  90. views::MenuRunner::FIXED_ANCHOR);
  91. menu_runner_->RunMenuAt(
  92. /*widget_owner=*/nullptr, /*menu_button_controller=*/nullptr, anchor_rect,
  93. views::MenuAnchorPosition::kBubbleBottomRight, source_type);
  94. }
  95. bool ClipboardHistoryMenuModelAdapter::IsRunning() const {
  96. return menu_runner_ && menu_runner_->IsRunning();
  97. }
  98. void ClipboardHistoryMenuModelAdapter::Cancel() {
  99. DCHECK(menu_runner_);
  100. menu_runner_->Cancel();
  101. }
  102. absl::optional<int>
  103. ClipboardHistoryMenuModelAdapter::GetSelectedMenuItemCommand() const {
  104. DCHECK(root_view_);
  105. // `root_view_` may be selected if no menu item is under selection.
  106. auto* menu_item = root_view_->GetMenuController()->GetSelectedMenuItem();
  107. return menu_item && menu_item != root_view_
  108. ? absl::make_optional(menu_item->GetCommand())
  109. : absl::nullopt;
  110. }
  111. const ClipboardHistoryItem&
  112. ClipboardHistoryMenuModelAdapter::GetItemFromCommandId(int command_id) const {
  113. auto iter = item_snapshots_.find(command_id);
  114. DCHECK(iter != item_snapshots_.cend());
  115. return iter->second;
  116. }
  117. size_t ClipboardHistoryMenuModelAdapter::GetMenuItemsCount() const {
  118. // We should not use `root_view_` to retrieve the item count. Because the
  119. // menu item view is removed from `root_view_` asynchronously.
  120. return item_views_by_command_id_.size();
  121. }
  122. void ClipboardHistoryMenuModelAdapter::SelectMenuItemWithCommandId(
  123. int command_id) {
  124. views::MenuItemView* selected_menu_item =
  125. root_view_->GetMenuItemByID(command_id);
  126. DCHECK(IsRunning());
  127. views::MenuController::GetActiveInstance()->SelectItemAndOpenSubmenu(
  128. selected_menu_item);
  129. }
  130. void ClipboardHistoryMenuModelAdapter::SelectMenuItemHoveredByMouse() {
  131. // Find the menu item hovered by mouse.
  132. auto iter =
  133. std::find_if(item_views_by_command_id_.cbegin(),
  134. item_views_by_command_id_.cend(), [](const auto& iterator) {
  135. const views::View* item_view = iterator.second;
  136. return item_view->IsMouseHovered();
  137. });
  138. if (iter == item_views_by_command_id_.cend()) {
  139. // If no item is hovered by mouse, cancel the selection on the child menu
  140. // item by selecting the root menu item.
  141. views::MenuController::GetActiveInstance()->SelectItemAndOpenSubmenu(
  142. root_view_);
  143. } else {
  144. SelectMenuItemWithCommandId(iter->first);
  145. }
  146. }
  147. void ClipboardHistoryMenuModelAdapter::RemoveMenuItemWithCommandId(
  148. int command_id) {
  149. // Calculate `new_selected_command_id` before removing the item specified by
  150. // `command_id` from data structures because the item to be removed is
  151. // needed in calculation.
  152. absl::optional<int> new_selected_command_id =
  153. CalculateSelectedCommandIdAfterDeletion(command_id);
  154. // Disable a11y for all item views. It ensures that when deleting multiple
  155. // item views, only the one finally selected is announced.
  156. if (!item_deletion_in_progress_count_) {
  157. DCHECK(!scoped_ignore_);
  158. scoped_ignore_ = std::make_unique<ScopedA11yIgnore>(this);
  159. }
  160. // Update the menu item selection.
  161. if (new_selected_command_id.has_value()) {
  162. SelectMenuItemWithCommandId(*new_selected_command_id);
  163. } else {
  164. views::MenuController::GetActiveInstance()->SelectItemAndOpenSubmenu(
  165. root_view_);
  166. }
  167. auto item_view_to_delete_iter = item_views_by_command_id_.find(command_id);
  168. DCHECK(item_view_to_delete_iter != item_views_by_command_id_.cend());
  169. views::View* item_view_to_delete = item_view_to_delete_iter->second;
  170. // Configure `item_view_to_delete` to serve a11y features.
  171. views::ViewAccessibility& view_accessibility =
  172. item_view_to_delete->GetViewAccessibility();
  173. // Polish the a11y announcement for deletion operation.
  174. view_accessibility.OverrideDescription(
  175. l10n_util::GetStringUTF16(IDS_CLIPBOARD_HISTORY_ITEM_DELETION));
  176. // Enable a11y announcement for the view to be deleted.
  177. view_accessibility.OverrideIsIgnored(false);
  178. // Disabling `item_view_to_delete` is more like implementation details.
  179. // So do not expose it to users.
  180. view_accessibility.OverrideIsEnabled(true);
  181. // Specify `item_view_to_delete`'s position in the set. Without calling
  182. // `OverridePosInSet()`, the menu's size after deletion may be announced.
  183. const int pos_in_set = std::distance(item_views_by_command_id_.begin(),
  184. item_view_to_delete_iter) +
  185. 1;
  186. view_accessibility.OverridePosInSet(pos_in_set,
  187. item_views_by_command_id_.size());
  188. // Disable views to be removed in order to prevent them from handling
  189. // events.
  190. root_view_->GetMenuItemByID(command_id)->SetEnabled(false);
  191. item_view_to_delete->SetEnabled(false);
  192. item_views_by_command_id_.erase(item_view_to_delete_iter);
  193. auto item_to_delete = item_snapshots_.find(command_id);
  194. DCHECK(item_to_delete != item_snapshots_.end());
  195. item_snapshots_.erase(item_to_delete);
  196. // The current selected menu item may be accessed after item deletion. So
  197. // postpone the menu item deletion.
  198. ++item_deletion_in_progress_count_;
  199. base::SequencedTaskRunnerHandle::Get()->PostTask(
  200. FROM_HERE,
  201. base::BindOnce(&ClipboardHistoryMenuModelAdapter::RemoveItemView,
  202. weak_ptr_factory_.GetWeakPtr(), command_id));
  203. }
  204. void ClipboardHistoryMenuModelAdapter::AdvancePseudoFocus(bool reverse) {
  205. absl::optional<int> selected_command = GetSelectedMenuItemCommand();
  206. // If no item is selected, select the topmost or bottom menu item depending
  207. // on the focus move direction.
  208. if (!selected_command.has_value()) {
  209. SelectMenuItemWithCommandId(
  210. reverse ? item_views_by_command_id_.rbegin()->first
  211. : ClipboardHistoryUtil::kFirstItemCommandId);
  212. return;
  213. }
  214. AdvancePseudoFocusFromSelectedItem(reverse);
  215. }
  216. ClipboardHistoryUtil::Action
  217. ClipboardHistoryMenuModelAdapter::GetActionForCommandId(int command_id) const {
  218. auto selected_item_iter = item_views_by_command_id_.find(command_id);
  219. DCHECK(selected_item_iter != item_views_by_command_id_.cend());
  220. return selected_item_iter->second->action();
  221. }
  222. gfx::Rect ClipboardHistoryMenuModelAdapter::GetMenuBoundsInScreenForTest()
  223. const {
  224. DCHECK(root_view_);
  225. return root_view_->GetSubmenu()->GetBoundsInScreen();
  226. }
  227. const views::MenuItemView*
  228. ClipboardHistoryMenuModelAdapter::GetMenuItemViewAtForTest(size_t index) const {
  229. DCHECK(root_view_);
  230. return root_view_->GetSubmenu()->GetMenuItemAt(index);
  231. }
  232. views::MenuItemView* ClipboardHistoryMenuModelAdapter::GetMenuItemViewAtForTest(
  233. size_t index) {
  234. return const_cast<views::MenuItemView*>(
  235. const_cast<const ClipboardHistoryMenuModelAdapter*>(this)
  236. ->GetMenuItemViewAtForTest(index));
  237. }
  238. ClipboardHistoryMenuModelAdapter::ClipboardHistoryMenuModelAdapter(
  239. std::unique_ptr<ui::SimpleMenuModel> model,
  240. base::RepeatingClosure menu_closed_callback,
  241. const ClipboardHistory* clipboard_history,
  242. const ClipboardHistoryResourceManager* resource_manager)
  243. : views::MenuModelAdapter(model.get(), std::move(menu_closed_callback)),
  244. model_(std::move(model)),
  245. clipboard_history_(clipboard_history),
  246. resource_manager_(resource_manager) {}
  247. void ClipboardHistoryMenuModelAdapter::AdvancePseudoFocusFromSelectedItem(
  248. bool reverse) {
  249. absl::optional<int> selected_item_command = GetSelectedMenuItemCommand();
  250. DCHECK(selected_item_command.has_value());
  251. auto selected_item_iter =
  252. item_views_by_command_id_.find(*selected_item_command);
  253. DCHECK(selected_item_iter != item_views_by_command_id_.end());
  254. ClipboardHistoryItemView* selected_item_view = selected_item_iter->second;
  255. // Move the pseudo focus on the selected item view. Return early if the
  256. // focused view does not change.
  257. const bool selected_item_has_focus =
  258. selected_item_view->AdvancePseudoFocus(reverse);
  259. if (selected_item_has_focus)
  260. return;
  261. int next_selected_item_command = -1;
  262. ClipboardHistoryItemView* next_focused_view = nullptr;
  263. if (reverse) {
  264. auto next_focused_item_iter =
  265. selected_item_iter == item_views_by_command_id_.begin()
  266. ? item_views_by_command_id_.rbegin()
  267. : std::make_reverse_iterator(selected_item_iter);
  268. next_selected_item_command = next_focused_item_iter->first;
  269. next_focused_view = next_focused_item_iter->second;
  270. } else {
  271. auto next_focused_item_iter = std::next(selected_item_iter, 1);
  272. if (next_focused_item_iter == item_views_by_command_id_.end())
  273. next_focused_item_iter = item_views_by_command_id_.begin();
  274. next_selected_item_command = next_focused_item_iter->first;
  275. next_focused_view = next_focused_item_iter->second;
  276. }
  277. // Advancing pseudo focus should precede the item selection. Because when an
  278. // item view is selected, the selected view does not overwrite its pseudo
  279. // focus if its pseudo focus is non-empty. It can ensure that the pseudo
  280. // focus and the corresponding UI appearance update only once.
  281. next_focused_view->AdvancePseudoFocus(reverse);
  282. SelectMenuItemWithCommandId(next_selected_item_command);
  283. }
  284. int ClipboardHistoryMenuModelAdapter::CalculateSelectedCommandIdAfterDeletion(
  285. int command_id) const {
  286. // If the menu item view to be deleted is the last one, Cancel()
  287. // should be called so this function should not be hit.
  288. DCHECK_GT(item_snapshots_.size(), 1u);
  289. auto item_to_delete = item_snapshots_.find(command_id);
  290. DCHECK(item_to_delete != item_snapshots_.cend());
  291. // Use the menu item right after the one to be deleted if any. Otherwise,
  292. // select the previous one.
  293. auto next_item_iter = item_to_delete;
  294. ++next_item_iter;
  295. if (next_item_iter != item_snapshots_.cend())
  296. return next_item_iter->first;
  297. auto previous_item_iter = item_to_delete;
  298. --previous_item_iter;
  299. return previous_item_iter->first;
  300. }
  301. void ClipboardHistoryMenuModelAdapter::RemoveItemView(int command_id) {
  302. absl::optional<int> original_selected_command_id =
  303. GetSelectedMenuItemCommand();
  304. // The menu item view and its corresponding command should be removed at the
  305. // same time. Otherwise, it may run into check errors.
  306. model_->RemoveItemAt(model_->GetIndexOfCommandId(command_id).value());
  307. root_view_->RemoveMenuItem(root_view_->GetMenuItemByID(command_id));
  308. root_view_->ChildrenChanged();
  309. --item_deletion_in_progress_count_;
  310. // Re-enable a11y for all item views when item deletion finally completes.
  311. if (!item_deletion_in_progress_count_) {
  312. DCHECK(scoped_ignore_);
  313. scoped_ignore_.reset();
  314. }
  315. // `ChildrenChanged()` clears the selection. So restore the selection.
  316. if (original_selected_command_id.has_value())
  317. SelectMenuItemWithCommandId(*original_selected_command_id);
  318. }
  319. views::MenuItemView* ClipboardHistoryMenuModelAdapter::AppendMenuItem(
  320. views::MenuItemView* menu,
  321. ui::MenuModel* model,
  322. size_t index) {
  323. const int command_id = model->GetCommandIdAt(index);
  324. views::MenuItemView* container = menu->AppendMenuItem(command_id);
  325. // Ignore `container` in accessibility events handling. Let `item_view`
  326. // handle.
  327. container->GetViewAccessibility().OverrideIsIgnored(true);
  328. // Margins are managed by `ClipboardHistoryItemView`.
  329. container->SetMargins(/*top_margin=*/0, /*bottom_margin=*/0);
  330. std::unique_ptr<ClipboardHistoryItemView> item_view =
  331. ClipboardHistoryItemView::CreateFromClipboardHistoryItem(
  332. GetItemFromCommandId(command_id), resource_manager_, container);
  333. item_view->Init();
  334. item_views_by_command_id_.insert(std::make_pair(command_id, item_view.get()));
  335. container->AddChildView(std::move(item_view));
  336. return container;
  337. }
  338. void ClipboardHistoryMenuModelAdapter::OnMenuClosed(views::MenuItemView* menu) {
  339. // Terminate alive asynchronous calls on `RemoveItemView()`. It is pointless
  340. // to update views when the menu is closed.
  341. // Note that data members related to the asynchronous calls, such as
  342. // `item_deletion_in_progress_count_` and `scoped_ignore_`, are not reset.
  343. // Because when hitting here, this instance is going to be destructed soon.
  344. weak_ptr_factory_.InvalidateWeakPtrs();
  345. ClipboardImageModelFactory::Get()->Deactivate();
  346. const base::TimeDelta user_journey_time =
  347. base::TimeTicks::Now() - menu_open_time_;
  348. UMA_HISTOGRAM_TIMES("Ash.ClipboardHistory.ContextMenu.UserJourneyTime",
  349. user_journey_time);
  350. views::MenuModelAdapter::OnMenuClosed(menu);
  351. item_views_by_command_id_.clear();
  352. // This implementation of MenuModelAdapter does not have a widget so we need
  353. // to manually notify the accessibility side of the closed menu.
  354. aura::Window* active_window = window_util::GetActiveWindow();
  355. if (!active_window)
  356. return;
  357. views::Widget* active_widget =
  358. views::Widget::GetWidgetForNativeView(active_window);
  359. DCHECK(active_widget);
  360. views::View* focused_view =
  361. active_widget->GetFocusManager()->GetFocusedView();
  362. if (focused_view) {
  363. focused_view->NotifyAccessibilityEvent(ax::mojom::Event::kMenuEnd,
  364. /*send_native_event=*/true);
  365. }
  366. }
  367. } // namespace ash