clipboard_history_item_view.cc 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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/views/clipboard_history_item_view.h"
  5. #include "ash/clipboard/clipboard_history_item.h"
  6. #include "ash/clipboard/views/clipboard_history_bitmap_item_view.h"
  7. #include "ash/clipboard/views/clipboard_history_delete_button.h"
  8. #include "ash/clipboard/views/clipboard_history_file_item_view.h"
  9. #include "ash/clipboard/views/clipboard_history_main_button.h"
  10. #include "ash/clipboard/views/clipboard_history_text_item_view.h"
  11. #include "ash/clipboard/views/clipboard_history_view_constants.h"
  12. #include "base/auto_reset.h"
  13. #include "base/bind.h"
  14. #include "base/metrics/histogram_macros.h"
  15. #include "base/strings/utf_string_conversions.h"
  16. #include "ui/accessibility/ax_node_data.h"
  17. #include "ui/base/clipboard/clipboard_data.h"
  18. #include "ui/views/accessibility/view_accessibility.h"
  19. #include "ui/views/animation/ink_drop.h"
  20. #include "ui/views/border.h"
  21. #include "ui/views/controls/menu/menu_config.h"
  22. #include "ui/views/controls/menu/menu_item_view.h"
  23. #include "ui/views/layout/fill_layout.h"
  24. namespace ash {
  25. namespace {
  26. using Action = ClipboardHistoryUtil::Action;
  27. } // namespace
  28. ClipboardHistoryItemView::ContentsView::ContentsView(
  29. ClipboardHistoryItemView* container)
  30. : container_(container) {
  31. SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
  32. SetBorder(views::CreateEmptyBorder(ClipboardHistoryViews::kContentsInsets));
  33. }
  34. ClipboardHistoryItemView::ContentsView::~ContentsView() = default;
  35. void ClipboardHistoryItemView::ContentsView::InstallDeleteButton() {
  36. delete_button_ = CreateDeleteButton();
  37. }
  38. void ClipboardHistoryItemView::ContentsView::OnHostPseudoFocusUpdated() {
  39. delete_button_->SetVisible(container_->ShouldShowDeleteButton());
  40. const bool focused =
  41. (container_->pseudo_focus_ == PseudoFocus::kDeleteButton);
  42. views::InkDrop::Get(delete_button_)->GetInkDrop()->SetFocused(focused);
  43. if (focused) {
  44. delete_button_->NotifyAccessibilityEvent(ax::mojom::Event::kHover,
  45. /*send_native_event*/ true);
  46. }
  47. }
  48. const char* ClipboardHistoryItemView::ContentsView::GetClassName() const {
  49. return "ContenstView";
  50. }
  51. // Accepts the event only when |delete_button_| should be the handler.
  52. bool ClipboardHistoryItemView::ContentsView::DoesIntersectRect(
  53. const views::View* target,
  54. const gfx::Rect& rect) const {
  55. if (!delete_button_->GetVisible())
  56. return false;
  57. gfx::RectF rect_in_delete_button(rect);
  58. ConvertRectToTarget(this, delete_button_, &rect_in_delete_button);
  59. return delete_button_->HitTestRect(
  60. gfx::ToEnclosedRect(rect_in_delete_button));
  61. }
  62. // static
  63. std::unique_ptr<ClipboardHistoryItemView>
  64. ClipboardHistoryItemView::CreateFromClipboardHistoryItem(
  65. const ClipboardHistoryItem& item,
  66. const ClipboardHistoryResourceManager* resource_manager,
  67. views::MenuItemView* container) {
  68. const auto display_format =
  69. ClipboardHistoryUtil::CalculateDisplayFormat(item.data());
  70. UMA_HISTOGRAM_ENUMERATION(
  71. "Ash.ClipboardHistory.ContextMenu.DisplayFormatShown", display_format);
  72. switch (display_format) {
  73. case ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kText:
  74. return std::make_unique<ClipboardHistoryTextItemView>(&item, container);
  75. case ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kPng:
  76. case ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kHtml:
  77. return std::make_unique<ClipboardHistoryBitmapItemView>(
  78. &item, resource_manager, container);
  79. case ClipboardHistoryUtil::ClipboardHistoryDisplayFormat::kFile:
  80. return std::make_unique<ClipboardHistoryFileItemView>(&item, container);
  81. }
  82. }
  83. ClipboardHistoryItemView::~ClipboardHistoryItemView() = default;
  84. ClipboardHistoryItemView::ClipboardHistoryItemView(
  85. const ClipboardHistoryItem* clipboard_history_item,
  86. views::MenuItemView* container)
  87. : clipboard_history_item_(clipboard_history_item), container_(container) {}
  88. bool ClipboardHistoryItemView::AdvancePseudoFocus(bool reverse) {
  89. if (pseudo_focus_ == PseudoFocus::kEmpty) {
  90. InitiatePseudoFocus(reverse);
  91. return true;
  92. }
  93. // When the menu item is disabled, only the delete button is able to work.
  94. if (!container_->GetEnabled()) {
  95. DCHECK_EQ(PseudoFocus::kDeleteButton, pseudo_focus_);
  96. SetPseudoFocus(PseudoFocus::kEmpty);
  97. return false;
  98. }
  99. DCHECK(pseudo_focus_ == PseudoFocus::kMainButton ||
  100. pseudo_focus_ == PseudoFocus::kDeleteButton);
  101. int new_pseudo_focus = pseudo_focus_;
  102. bool move_focus_out = false;
  103. if (reverse) {
  104. --new_pseudo_focus;
  105. if (new_pseudo_focus == PseudoFocus::kEmpty)
  106. move_focus_out = true;
  107. } else {
  108. ++new_pseudo_focus;
  109. if (new_pseudo_focus == PseudoFocus::kMaxValue)
  110. move_focus_out = true;
  111. }
  112. if (move_focus_out) {
  113. SetPseudoFocus(PseudoFocus::kEmpty);
  114. return false;
  115. }
  116. SetPseudoFocus(static_cast<PseudoFocus>(new_pseudo_focus));
  117. return true;
  118. }
  119. void ClipboardHistoryItemView::HandleDeleteButtonPressEvent(
  120. const ui::Event& event) {
  121. Activate(Action::kDelete, event.flags());
  122. }
  123. void ClipboardHistoryItemView::HandleMainButtonPressEvent(
  124. const ui::Event& event) {
  125. // Note that the callback may be triggered through the ENTER key when
  126. // the delete button is under the pseudo focus. Because the delete
  127. // button is not hot-tracked by the menu controller. Meanwhile, the menu
  128. // controller always sends the key event to the hot-tracked view.
  129. // TODO(https://crbug.com/1144994): Modify this part after the clipboard
  130. // history menu code is refactored.
  131. // When an item view is under gesture tap, it may be not under pseudo
  132. // focus yet.
  133. if (event.type() == ui::ET_GESTURE_TAP)
  134. pseudo_focus_ = PseudoFocus::kMainButton;
  135. Activate(CalculateActionForMainButtonClick(), event.flags());
  136. }
  137. void ClipboardHistoryItemView::Init() {
  138. SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);
  139. GetViewAccessibility().OverrideRole(ax::mojom::Role::kMenuItem);
  140. SetLayoutManager(std::make_unique<views::FillLayout>());
  141. // Ensures that MainButton is below any other child views.
  142. main_button_ =
  143. AddChildView(std::make_unique<ClipboardHistoryMainButton>(this));
  144. contents_view_ = AddChildView(CreateContentsView());
  145. subscription_ = container_->AddSelectedChangedCallback(base::BindRepeating(
  146. &ClipboardHistoryItemView::OnSelectionChanged, base::Unretained(this)));
  147. }
  148. void ClipboardHistoryItemView::MaybeHandleGestureEventFromMainButton(
  149. ui::GestureEvent* event) {
  150. // `event` is always handled here if the menu item view is under the gesture
  151. // long press. It prevents other event handlers from introducing side effects.
  152. // For example, if `main_button_` handles the ui::ET_GESTURE_END event,
  153. // `main_button_`'s state will be reset. However, `main_button_` is expected
  154. // to be at the "hovered" state when the menu item is selected.
  155. if (under_gesture_long_press_) {
  156. DCHECK_NE(ui::ET_GESTURE_LONG_PRESS, event->type());
  157. if (event->type() == ui::ET_GESTURE_END)
  158. under_gesture_long_press_ = false;
  159. event->SetHandled();
  160. return;
  161. }
  162. if (event->type() == ui::ET_GESTURE_LONG_PRESS) {
  163. under_gesture_long_press_ = true;
  164. switch (pseudo_focus_) {
  165. case PseudoFocus::kEmpty:
  166. // Select the menu item if it is not selected yet.
  167. Activate(Action::kSelect, event->flags());
  168. break;
  169. case PseudoFocus::kMainButton: {
  170. // The menu item is already selected so show the delete button if the
  171. // button is hidden.
  172. views::View* delete_button = contents_view_->delete_button();
  173. if (!delete_button->GetVisible())
  174. delete_button->SetVisible(true);
  175. break;
  176. }
  177. case PseudoFocus::kDeleteButton:
  178. // The delete button already shows, so do nothing.
  179. DCHECK(contents_view_->delete_button()->GetVisible());
  180. break;
  181. case PseudoFocus::kMaxValue:
  182. NOTREACHED();
  183. break;
  184. }
  185. event->SetHandled();
  186. }
  187. }
  188. void ClipboardHistoryItemView::OnSelectionChanged() {
  189. if (!container_->IsSelected()) {
  190. SetPseudoFocus(PseudoFocus::kEmpty);
  191. return;
  192. }
  193. // If the pseudo focus is moved from another item view via focus traversal,
  194. // `pseudo_focus_` is already up to date.
  195. if (pseudo_focus_ != PseudoFocus::kEmpty)
  196. return;
  197. InitiatePseudoFocus(/*reverse=*/false);
  198. }
  199. bool ClipboardHistoryItemView::ShouldHighlight() const {
  200. return pseudo_focus_ == PseudoFocus::kMainButton;
  201. }
  202. void ClipboardHistoryItemView::OnMouseClickOnDescendantCanceled() {
  203. // When mouse click is canceled, mouse may hover a different menu item from
  204. // the one where the click event started. A typical way is to move the mouse
  205. // while pressing the mouse left button. Hence, update the menu selection due
  206. // to the mouse location change.
  207. Activate(ClipboardHistoryUtil::Action::kSelectItemHoveredByMouse,
  208. ui::EF_NONE);
  209. }
  210. void ClipboardHistoryItemView::MaybeRecordButtonPressedHistogram() const {
  211. switch (action_) {
  212. case Action::kDelete:
  213. ClipboardHistoryUtil::RecordClipboardHistoryItemDeleted(
  214. *clipboard_history_item_);
  215. return;
  216. case Action::kPaste:
  217. ClipboardHistoryUtil::RecordClipboardHistoryItemPasted(
  218. *clipboard_history_item_);
  219. return;
  220. case Action::kSelect:
  221. case Action::kSelectItemHoveredByMouse:
  222. return;
  223. case Action::kEmpty:
  224. NOTREACHED();
  225. return;
  226. }
  227. }
  228. gfx::Size ClipboardHistoryItemView::CalculatePreferredSize() const {
  229. const int preferred_width =
  230. views::MenuConfig::instance().touchable_menu_min_width;
  231. return gfx::Size(preferred_width, GetHeightForWidth(preferred_width));
  232. }
  233. void ClipboardHistoryItemView::GetAccessibleNodeData(ui::AXNodeData* data) {
  234. data->SetName(GetAccessibleName());
  235. }
  236. void ClipboardHistoryItemView::Activate(Action action, int event_flags) {
  237. DCHECK_EQ(Action::kEmpty, action_);
  238. DCHECK_NE(action_, action);
  239. base::AutoReset<Action> action_to_take(&action_, action);
  240. MaybeRecordButtonPressedHistogram();
  241. views::MenuDelegate* delegate = container_->GetDelegate();
  242. const int command_id = container_->GetCommand();
  243. DCHECK(delegate->IsCommandEnabled(command_id));
  244. delegate->ExecuteCommand(command_id, event_flags);
  245. }
  246. Action ClipboardHistoryItemView::CalculateActionForMainButtonClick() const {
  247. // `main_button_` may be clicked when the delete button is under the pseudo
  248. // focus. It happens when a user presses the ENTER key. Note that the menu
  249. // controller sends the accelerator to the hot-tracked view and `main_button_`
  250. // is hot-tracked when the delete button is under the pseudo focus. The menu
  251. // controller should not hot-track the delete button. Otherwise, pressing the
  252. // up/down arrow key will select a delete button instead of a neighboring
  253. // menu item.
  254. switch (pseudo_focus_) {
  255. case PseudoFocus::kMainButton:
  256. return Action::kPaste;
  257. case PseudoFocus::kDeleteButton:
  258. return Action::kDelete;
  259. case PseudoFocus::kEmpty:
  260. case PseudoFocus::kMaxValue:
  261. NOTREACHED();
  262. return Action::kEmpty;
  263. }
  264. }
  265. bool ClipboardHistoryItemView::ShouldShowDeleteButton() const {
  266. return (pseudo_focus_ == PseudoFocus::kMainButton && IsMouseHovered()) ||
  267. pseudo_focus_ == PseudoFocus::kDeleteButton ||
  268. under_gesture_long_press_;
  269. }
  270. void ClipboardHistoryItemView::InitiatePseudoFocus(bool reverse) {
  271. PseudoFocus target_pseudo_focus;
  272. if (!container_->GetEnabled() || reverse)
  273. target_pseudo_focus = PseudoFocus::kDeleteButton;
  274. else
  275. target_pseudo_focus = PseudoFocus::kMainButton;
  276. SetPseudoFocus(target_pseudo_focus);
  277. }
  278. void ClipboardHistoryItemView::SetPseudoFocus(PseudoFocus new_pseudo_focus) {
  279. DCHECK_NE(PseudoFocus::kMaxValue, new_pseudo_focus);
  280. if (pseudo_focus_ == new_pseudo_focus)
  281. return;
  282. pseudo_focus_ = new_pseudo_focus;
  283. if (pseudo_focus_ == PseudoFocus::kMainButton) {
  284. NotifyAccessibilityEvent(ax::mojom::Event::kSelection,
  285. /*send_native_event=*/true);
  286. }
  287. contents_view_->OnHostPseudoFocusUpdated();
  288. main_button_->OnHostPseudoFocusUpdated();
  289. }
  290. } // namespace ash