holding_space_view_delegate.cc 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  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/system/holding_space/holding_space_view_delegate.h"
  5. #include "ash/public/cpp/holding_space/holding_space_client.h"
  6. #include "ash/public/cpp/holding_space/holding_space_constants.h"
  7. #include "ash/public/cpp/holding_space/holding_space_controller.h"
  8. #include "ash/public/cpp/holding_space/holding_space_item.h"
  9. #include "ash/public/cpp/holding_space/holding_space_metrics.h"
  10. #include "ash/public/cpp/holding_space/holding_space_model.h"
  11. #include "ash/public/cpp/holding_space/holding_space_progress.h"
  12. #include "ash/public/cpp/holding_space/holding_space_util.h"
  13. #include "ash/resources/vector_icons/vector_icons.h"
  14. #include "ash/strings/grit/ash_strings.h"
  15. #include "ash/system/holding_space/holding_space_drag_util.h"
  16. #include "ash/system/holding_space/holding_space_item_view.h"
  17. #include "ash/system/holding_space/holding_space_tray.h"
  18. #include "ash/system/holding_space/holding_space_tray_bubble.h"
  19. #include "base/bind.h"
  20. #include "base/callback_helpers.h"
  21. #include "base/containers/contains.h"
  22. #include "base/containers/cxx20_erase_vector.h"
  23. #include "net/base/mime_util.h"
  24. #include "ui/accessibility/ax_action_data.h"
  25. #include "ui/accessibility/ax_enums.mojom.h"
  26. #include "ui/base/dragdrop/drag_drop_types.h"
  27. #include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
  28. #include "ui/base/dragdrop/os_exchange_data_provider.h"
  29. #include "ui/base/l10n/l10n_util.h"
  30. #include "ui/base/models/simple_menu_model.h"
  31. #include "ui/color/color_id.h"
  32. #include "ui/views/controls/menu/menu_runner.h"
  33. #include "ui/views/vector_icons.h"
  34. #include "ui/views/view.h"
  35. namespace ash {
  36. namespace {
  37. // It is expected that all holding space views share the same delegate in order
  38. // to support multiple selections which requires a shared state. We cache the
  39. // singleton `instance` in order to enforce this requirement.
  40. HoldingSpaceViewDelegate* instance = nullptr;
  41. // Helpers ---------------------------------------------------------------------
  42. // Returns the holding space items associated with the specified `views`.
  43. std::vector<const HoldingSpaceItem*> GetItems(
  44. const std::vector<const HoldingSpaceItemView*>& views) {
  45. std::vector<const HoldingSpaceItem*> items;
  46. for (const HoldingSpaceItemView* view : views)
  47. items.push_back(view->item());
  48. return items;
  49. }
  50. // Returns the subset of `views` in the range of `start` and `end` (inclusive).
  51. std::vector<HoldingSpaceItemView*> GetViewsInRange(
  52. const std::vector<HoldingSpaceItemView*>& views,
  53. HoldingSpaceItemView* start,
  54. HoldingSpaceItemView* end) {
  55. if (!start || !end)
  56. return {};
  57. bool found_start = false;
  58. bool found_end = false;
  59. std::vector<HoldingSpaceItemView*> range;
  60. for (HoldingSpaceItemView* view : views) {
  61. if (view == start)
  62. found_start = true;
  63. if (view == end)
  64. found_end = true;
  65. if (found_start || found_end)
  66. range.push_back(view);
  67. if (found_start && found_end)
  68. break;
  69. }
  70. DCHECK(found_start);
  71. DCHECK(found_end);
  72. return range;
  73. }
  74. // Attempts to open the holding space items associated with the given `views`.
  75. void OpenItems(const std::vector<const HoldingSpaceItemView*>& views) {
  76. DCHECK_GE(views.size(), 1u);
  77. HoldingSpaceController::Get()->client()->OpenItems(GetItems(views),
  78. base::DoNothing());
  79. }
  80. } // namespace
  81. // HoldingSpaceViewDelegate::ScopedSelectionRestore ----------------------------
  82. HoldingSpaceViewDelegate::ScopedSelectionRestore::ScopedSelectionRestore(
  83. HoldingSpaceViewDelegate* delegate)
  84. : delegate_(delegate) {
  85. // Save selection.
  86. for (const HoldingSpaceItemView* view : delegate_->GetSelection())
  87. selected_item_ids_.push_back(view->item_id());
  88. // Save `selected_range_start_`.
  89. if (delegate_->selected_range_start_)
  90. selected_range_start_item_id_ = delegate_->selected_range_start_->item_id();
  91. // Save `selected_range_end_`.
  92. if (delegate_->selected_range_end_)
  93. selected_range_end_item_id_ = delegate_->selected_range_end_->item_id();
  94. }
  95. HoldingSpaceViewDelegate::ScopedSelectionRestore::~ScopedSelectionRestore() {
  96. // Restore selection.
  97. delegate_->SetSelection(selected_item_ids_);
  98. if (!selected_range_start_item_id_ || !selected_range_end_item_id_)
  99. return;
  100. HoldingSpaceItemView* selected_range_start = nullptr;
  101. HoldingSpaceItemView* selected_range_end = nullptr;
  102. for (HoldingSpaceItemView* view :
  103. delegate_->bubble_->GetHoldingSpaceItemViews()) {
  104. // Cache `selected_range_start`.
  105. if (selected_range_start_item_id_ == view->item_id())
  106. selected_range_start = view;
  107. // Cache `selected_range_end`.
  108. if (selected_range_end_item_id_ == view->item_id())
  109. selected_range_end = view;
  110. // Restore `selected_range_start_` and `selected_range_end_` iff both are
  111. // still in existence during the restoration process.
  112. if (selected_range_start && selected_range_end) {
  113. delegate_->selected_range_start_ = selected_range_start;
  114. delegate_->selected_range_end_ = selected_range_end;
  115. break;
  116. }
  117. }
  118. }
  119. // HoldingSpaceViewDelegate ----------------------------------------------------
  120. HoldingSpaceViewDelegate::HoldingSpaceViewDelegate(
  121. HoldingSpaceTrayBubble* bubble)
  122. : bubble_(bubble) {
  123. DCHECK_EQ(nullptr, instance);
  124. instance = this;
  125. // Multi-select is the only selection UI in tablet mode. Outside of tablet
  126. // mode, selection UI is based on the `selection_size_`.
  127. selection_ui_ = TabletMode::Get()->InTabletMode()
  128. ? SelectionUi::kMultiSelect
  129. : SelectionUi::kSingleSelect;
  130. tablet_mode_observer_.Observe(TabletMode::Get());
  131. }
  132. HoldingSpaceViewDelegate::~HoldingSpaceViewDelegate() {
  133. DCHECK_EQ(instance, this);
  134. instance = nullptr;
  135. }
  136. void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewCreated(
  137. HoldingSpaceItemView* view) {
  138. if (view->selected()) {
  139. ++selection_size_;
  140. UpdateSelectionUi();
  141. }
  142. }
  143. void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewDestroying(
  144. HoldingSpaceItemView* view) {
  145. // If either endpoint of the selected range is destroyed, clear the cache so
  146. // that the next range-based selection attempt will start from scratch.
  147. if (selected_range_start_ == view || selected_range_end_ == view) {
  148. selected_range_start_ = nullptr;
  149. selected_range_end_ = nullptr;
  150. }
  151. if (view->selected()) {
  152. --selection_size_;
  153. UpdateSelectionUi();
  154. }
  155. }
  156. bool HoldingSpaceViewDelegate::OnHoldingSpaceItemViewAccessibleAction(
  157. HoldingSpaceItemView* view,
  158. const ui::AXActionData& action_data) {
  159. // When performing the default accessible action (e.g. Search + Space), open
  160. // the selected holding space items. If `view` is not part of the current
  161. // selection it will become the entire selection.
  162. if (action_data.action == ax::mojom::Action::kDoDefault) {
  163. if (!view->selected())
  164. SetSelection(view);
  165. OpenItems(GetSelection());
  166. return true;
  167. }
  168. // When showing the context menu via accessible action (e.g. Search + M),
  169. // ensure that `view` is part of the current selection. If it is not part of
  170. // the current selection it will become the entire selection.
  171. if (action_data.action == ax::mojom::Action::kShowContextMenu) {
  172. if (!view->selected())
  173. SetSelection(view);
  174. // Return false so that the views framework will show the context menu.
  175. return false;
  176. }
  177. return false;
  178. }
  179. bool HoldingSpaceViewDelegate::OnHoldingSpaceItemViewGestureEvent(
  180. HoldingSpaceItemView* view,
  181. const ui::GestureEvent& event) {
  182. // The user may alternate between using mouse and touch inputs. Treat gesture
  183. // events as mouse events when tracking range-based selection so that if
  184. // the user switches back to using mouse input, selection state will be
  185. // determined based on this most recent interaction with `view`.
  186. selected_range_start_ = view;
  187. selected_range_end_ = view;
  188. // When a long press or two finger tap gesture occurs we are going to show the
  189. // context menu. Ensure that the pressed `view` is part of the selection.
  190. if (event.type() == ui::ET_GESTURE_LONG_PRESS ||
  191. event.type() == ui::ET_GESTURE_TWO_FINGER_TAP) {
  192. view->SetSelected(true);
  193. return false;
  194. }
  195. // If a scroll begin gesture is received while the context menu is showing,
  196. // that means the user is trying to initiate a drag. Close the context menu
  197. // and start the item drag.
  198. if (event.type() == ui::ET_GESTURE_SCROLL_BEGIN && context_menu_runner_ &&
  199. context_menu_runner_->IsRunning()) {
  200. context_menu_runner_.reset();
  201. view->StartDrag(event, ui::mojom::DragEventSource::kTouch);
  202. return false;
  203. }
  204. if (event.type() != ui::ET_GESTURE_TAP)
  205. return false;
  206. // When a tap gesture occurs and *no* views are currently selected, select and
  207. // open the tapped `view`. Note that the tap `event` should *not* propagate
  208. // further. Failure to halt propagation would result in the gesture reaching
  209. // the child bubble which clears selection state.
  210. if (GetSelection().empty()) {
  211. SetSelection(view);
  212. OpenItems(GetSelection());
  213. return true;
  214. }
  215. // When a tap gesture occurs and a selection *does* exist, the selected state
  216. // of the tapped `view` is toggled. Note that the tap event should *not*
  217. // propagate further. Failure to halt propagation would result in the gesture
  218. // reaching the child bubble which clears selection state.
  219. view->SetSelected(!view->selected());
  220. return true;
  221. }
  222. bool HoldingSpaceViewDelegate::OnHoldingSpaceItemViewKeyPressed(
  223. HoldingSpaceItemView* view,
  224. const ui::KeyEvent& event) {
  225. // The ENTER key should open all selected holding space items. If `view` isn't
  226. // already part of the selection, it will become the entire selection.
  227. if (event.key_code() == ui::KeyboardCode::VKEY_RETURN) {
  228. if (!view->selected())
  229. SetSelection(view);
  230. OpenItems(GetSelection());
  231. return true;
  232. }
  233. return false;
  234. }
  235. bool HoldingSpaceViewDelegate::OnHoldingSpaceItemViewMousePressed(
  236. HoldingSpaceItemView* view,
  237. const ui::MouseEvent& event) {
  238. // Since we are starting a new mouse pressed/released sequence, we need to
  239. // clear any view that we had cached to ignore mouse released events for.
  240. ignore_mouse_released_ = nullptr;
  241. // If SHIFT is *not* pressed, set `view` as the starting point for range-based
  242. // selection so that the next time the user shift-clicks, selection state
  243. // will be updated in the range of `view` and the view being shift-clicked.
  244. // Note that `view` is also set as the starting point if previously unset.
  245. if (!event.IsShiftDown() || !selected_range_start_)
  246. selected_range_start_ = view;
  247. // When a `view` is pressed it becomes the new end for range-based selection.
  248. // Note that this is performed in a scoped closure runner in order to give
  249. // `SetSelectedRange()` a chance to run and clean up any previous range-based
  250. // selection.
  251. base::ScopedClosureRunner set_selected_range_end(base::BindOnce(
  252. [](HoldingSpaceItemView** selected_range_end,
  253. HoldingSpaceItemView* view) { *selected_range_end = view; },
  254. &selected_range_end_, view));
  255. // If the SHIFT key is down, the user is attempting a range-based selection.
  256. // Remove from the selection the previously selected range and instead add
  257. // the newly selected range to the selection. Note that the next mouse
  258. // released event on `view` is ignored if this is not a double click so that
  259. // `view` isn't accidentally unselected right after having selected it. If
  260. // this is a double click, the mouse released event will be handled to launch
  261. // the selected holding space items.
  262. if (event.IsShiftDown()) {
  263. if (!(event.flags() & ui::EF_IS_DOUBLE_CLICK))
  264. ignore_mouse_released_ = view;
  265. SetSelectedRange(selected_range_start_, /*end=*/view);
  266. return true;
  267. }
  268. // If the `view` is already selected, mouse press is a no-op. Actions taken on
  269. // selected views are performed on mouse released in order to give drag/drop
  270. // a chance to take effect (assuming that drag thresholds are met).
  271. if (view->selected())
  272. return true;
  273. // If the CTRL key is down, we need to add `view` to the current selection.
  274. // We're going to need to ignore the next mouse released event on `view` if
  275. // this is not a double click so that we don't unselect `view` accidentally
  276. // right after having selected it. If this is a double click, the mouse
  277. // released event will be handled to launch the selected holding space items.
  278. if (event.IsControlDown()) {
  279. if (!(event.flags() & ui::EF_IS_DOUBLE_CLICK))
  280. ignore_mouse_released_ = view;
  281. view->SetSelected(true);
  282. return true;
  283. }
  284. // In the absence of any modifiers, pressing an unselected `view` will cause
  285. // `view` to become the current selection. Previous selections are cleared.
  286. SetSelection(view);
  287. return true;
  288. }
  289. void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewMouseReleased(
  290. HoldingSpaceItemView* view,
  291. const ui::MouseEvent& event) {
  292. // We should always clear `ignore_mouse_released_` since that property should
  293. // affect at most one press/release sequence.
  294. views::View* const old_ignore_mouse_released = ignore_mouse_released_;
  295. ignore_mouse_released_ = nullptr;
  296. // We might be ignoring mouse released events for `view` if it was just
  297. // selected on mouse pressed. In this case, no-op here.
  298. if (old_ignore_mouse_released == view)
  299. return;
  300. // If the right mouse button is released we're showing the context menu. In
  301. // this case, no-op here.
  302. if (event.IsRightMouseButton())
  303. return;
  304. // If this mouse released `event` is part of a double click, we should open
  305. // the items associated with the current selection. It is expected that the
  306. // `view` being clicked is already part of the selection.
  307. if (event.flags() & ui::EF_IS_DOUBLE_CLICK) {
  308. DCHECK(view->selected());
  309. OpenItems(GetSelection());
  310. return;
  311. }
  312. // If the CTRL key is down, mouse release should toggle the selected state of
  313. // `view`. It's possible that the current selection be empty after doing so.
  314. if (event.IsControlDown()) {
  315. view->SetSelected(!view->selected());
  316. return;
  317. }
  318. // This mouse released `event` is not part of a double click, nor were there
  319. // any modifiers which resulted in special handling. In this case, the `view`
  320. // under the mouse should become the only selected view.
  321. SetSelection(view);
  322. }
  323. void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewPrimaryActionPressed(
  324. HoldingSpaceItemView* view) {
  325. if (!view->selected())
  326. ClearSelection();
  327. }
  328. void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewSecondaryActionPressed(
  329. HoldingSpaceItemView* view) {
  330. if (!view->selected())
  331. ClearSelection();
  332. }
  333. void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewSelectedChanged(
  334. HoldingSpaceItemView* view) {
  335. selection_size_ += view->selected() ? 1 : -1;
  336. UpdateSelectionUi();
  337. }
  338. bool HoldingSpaceViewDelegate::OnHoldingSpaceTrayBubbleKeyPressed(
  339. const ui::KeyEvent& event) {
  340. // The ENTER key should open all selected holding space items.
  341. if (event.key_code() == ui::KeyboardCode::VKEY_RETURN) {
  342. if (!GetSelection().empty()) {
  343. OpenItems(GetSelection());
  344. return true;
  345. }
  346. }
  347. return false;
  348. }
  349. void HoldingSpaceViewDelegate::OnHoldingSpaceTrayChildBubbleGestureEvent(
  350. const ui::GestureEvent& event) {
  351. if (event.type() == ui::ET_GESTURE_TAP)
  352. ClearSelection();
  353. }
  354. void HoldingSpaceViewDelegate::OnHoldingSpaceTrayChildBubbleMousePressed(
  355. const ui::MouseEvent& event) {
  356. ClearSelection();
  357. }
  358. base::RepeatingClosureList::Subscription
  359. HoldingSpaceViewDelegate::AddSelectionUiChangedCallback(
  360. base::RepeatingClosureList::CallbackType callback) {
  361. return selection_ui_changed_callbacks_.Add(std::move(callback));
  362. }
  363. void HoldingSpaceViewDelegate::UpdateTrayVisibility() {
  364. bubble_->tray()->UpdateVisibility();
  365. }
  366. void HoldingSpaceViewDelegate::ShowContextMenuForViewImpl(
  367. views::View* source,
  368. const gfx::Point& point,
  369. ui::MenuSourceType source_type) {
  370. // In touch mode, gesture events continue to be sent to holding space views
  371. // after showing the context menu so that it can be aborted if the user
  372. // initiates a drag sequence. This means both `ui::ET_GESTURE_LONG_TAP` and
  373. // `ui::ET_GESTURE_LONG_PRESS` may be received while showing the context menu
  374. // which would result in trying to show the context menu twice. This would not
  375. // be a fatal failure but would result in UI jank.
  376. if (context_menu_runner_ && context_menu_runner_->IsRunning())
  377. return;
  378. int run_types = views::MenuRunner::USE_ASH_SYS_UI_LAYOUT |
  379. views::MenuRunner::CONTEXT_MENU |
  380. views::MenuRunner::FIXED_ANCHOR;
  381. // In touch mode the context menu may be aborted if the user initiates a drag.
  382. // In order to determine if the gesture resulting in this context menu being
  383. // shown was actually the start of a drag sequence, holding space views will
  384. // have to receive events that would otherwise be consumed by the `MenuHost`.
  385. if (source_type == ui::MenuSourceType::MENU_SOURCE_TOUCH)
  386. run_types |= views::MenuRunner::SEND_GESTURE_EVENTS_TO_OWNER;
  387. context_menu_runner_ =
  388. std::make_unique<views::MenuRunner>(BuildMenuModel(), run_types);
  389. context_menu_runner_->RunMenuAt(
  390. source->GetWidget(), nullptr /*button_controller*/,
  391. source->GetBoundsInScreen(),
  392. views::MenuAnchorPosition::kBubbleBottomRight, source_type);
  393. }
  394. bool HoldingSpaceViewDelegate::CanStartDragForView(
  395. views::View* sender,
  396. const gfx::Point& press_pt,
  397. const gfx::Point& current_pt) {
  398. const gfx::Vector2d delta = current_pt - press_pt;
  399. return views::View::ExceededDragThreshold(delta);
  400. }
  401. int HoldingSpaceViewDelegate::GetDragOperationsForView(
  402. views::View* sender,
  403. const gfx::Point& press_pt) {
  404. return ui::DragDropTypes::DRAG_COPY;
  405. }
  406. void HoldingSpaceViewDelegate::WriteDragDataForView(views::View* sender,
  407. const gfx::Point& press_pt,
  408. ui::OSExchangeData* data) {
  409. std::vector<const HoldingSpaceItemView*> selection = GetSelection();
  410. DCHECK_GE(selection.size(), 1u);
  411. holding_space_metrics::RecordItemAction(
  412. GetItems(selection), holding_space_metrics::ItemAction::kDrag);
  413. // Drag image.
  414. gfx::ImageSkia drag_image;
  415. gfx::Vector2d drag_offset;
  416. holding_space_util::CreateDragImage(selection, &drag_image, &drag_offset);
  417. data->provider().SetDragImage(std::move(drag_image), drag_offset);
  418. // Payload.
  419. std::vector<ui::FileInfo> filenames;
  420. for (const HoldingSpaceItemView* view : selection) {
  421. const base::FilePath& file_path = view->item()->file_path();
  422. filenames.push_back(ui::FileInfo(file_path, file_path.BaseName()));
  423. }
  424. data->SetFilenames(filenames);
  425. }
  426. void HoldingSpaceViewDelegate::ExecuteCommand(int command, int event_flags) {
  427. const std::vector<const HoldingSpaceItem*> items(GetItems(GetSelection()));
  428. DCHECK_GE(items.size(), 1u);
  429. const auto command_id = static_cast<HoldingSpaceCommandId>(command);
  430. HoldingSpaceClient* const client = HoldingSpaceController::Get()->client();
  431. switch (command_id) {
  432. case HoldingSpaceCommandId::kCopyImageToClipboard:
  433. DCHECK_EQ(items.size(), 1u);
  434. client->CopyImageToClipboard(*items.front(), base::DoNothing());
  435. break;
  436. case HoldingSpaceCommandId::kPinItem:
  437. client->PinItems(items);
  438. break;
  439. case HoldingSpaceCommandId::kRemoveItem:
  440. HoldingSpaceController::Get()->model()->RemoveIf(base::BindRepeating(
  441. [](const std::vector<const HoldingSpaceItem*>& items,
  442. const HoldingSpaceItem* item) {
  443. const bool remove = base::Contains(items, item);
  444. if (remove) {
  445. holding_space_metrics::RecordItemAction(
  446. {item}, holding_space_metrics::ItemAction::kRemove);
  447. }
  448. return remove;
  449. },
  450. std::cref(items)));
  451. break;
  452. case HoldingSpaceCommandId::kShowInFolder:
  453. DCHECK_EQ(items.size(), 1u);
  454. client->ShowItemInFolder(*items.front(), base::DoNothing());
  455. break;
  456. case HoldingSpaceCommandId::kUnpinItem:
  457. client->UnpinItems(items);
  458. break;
  459. default:
  460. if (holding_space_util::IsInProgressCommand(command_id)) {
  461. for (const HoldingSpaceItem* item : items) {
  462. if (!holding_space_util::ExecuteInProgressCommand(item, command_id))
  463. NOTREACHED();
  464. }
  465. } else {
  466. NOTREACHED();
  467. }
  468. break;
  469. }
  470. }
  471. void HoldingSpaceViewDelegate::OnTabletModeStarted() {
  472. UpdateSelectionUi();
  473. }
  474. void HoldingSpaceViewDelegate::OnTabletModeEnded() {
  475. UpdateSelectionUi();
  476. }
  477. ui::SimpleMenuModel* HoldingSpaceViewDelegate::BuildMenuModel() {
  478. context_menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
  479. std::vector<const HoldingSpaceItemView*> selection = GetSelection();
  480. DCHECK_GE(selection.size(), 1u);
  481. // Whether any item in `selection` is complete.
  482. bool is_any_item_complete = false;
  483. // Whether all items in `selection` are removable.
  484. bool is_removable = true;
  485. // A value for `is_pinnable` will only be present if the `selection` contains
  486. // at least one holding space item which is *not* in-progress. In-progress
  487. // items are ignored with respect to pin-/unpin-ability.
  488. absl::optional<bool> is_pinnable;
  489. // A value for `in_progress_commands` will only be present if the `selection`
  490. // does *not* contain any items which are complete.
  491. absl::optional<std::vector<HoldingSpaceItem::InProgressCommand>>
  492. in_progress_commands;
  493. HoldingSpaceModel* const model = HoldingSpaceController::Get()->model();
  494. for (const HoldingSpaceItemView* view : selection) {
  495. const HoldingSpaceItem* item = view->item();
  496. // In-progress commands are only available if supported by the entire
  497. // `selection`. In-progress commands supported by only a subset of the
  498. // `selection` are removed.
  499. if (!item->progress().IsComplete() && !is_any_item_complete) {
  500. if (!in_progress_commands.has_value()) {
  501. in_progress_commands = item->in_progress_commands();
  502. } else {
  503. base::EraseIf(in_progress_commands.value(),
  504. [&](const HoldingSpaceItem::InProgressCommand&
  505. in_progress_command) {
  506. return !holding_space_util::SupportsInProgressCommand(
  507. item, in_progress_command.command_id);
  508. });
  509. }
  510. } else {
  511. in_progress_commands = absl::nullopt;
  512. is_any_item_complete = true;
  513. }
  514. // The "Remove" command should only be present if *all* of the selected
  515. // holding space items are removable.
  516. is_removable &= item->type() != HoldingSpaceItem::Type::kPinnedFile &&
  517. item->progress().IsComplete();
  518. // In-progress holding space items are ignored with respect to the pin-/
  519. // unpin-ability of the `selection`.
  520. if (!item->progress().IsComplete())
  521. continue;
  522. // The "Pin" command should be present if *any* selected holding space item
  523. // is unpinned. When executing this command, any holding space items that
  524. // are already pinned will be ignored.
  525. is_pinnable = is_pinnable.value_or(false) ||
  526. !model->ContainsItem(HoldingSpaceItem::Type::kPinnedFile,
  527. item->file_path());
  528. }
  529. struct MenuItemModel {
  530. const HoldingSpaceCommandId command_id;
  531. const int label_id;
  532. const gfx::VectorIcon& icon;
  533. };
  534. using MenuSectionModel = std::vector<MenuItemModel>;
  535. std::vector<MenuSectionModel> menu_sections(1);
  536. // In-progress commands.
  537. if (in_progress_commands.has_value()) {
  538. for (const HoldingSpaceItem::InProgressCommand& in_progress_command :
  539. in_progress_commands.value()) {
  540. menu_sections.back().emplace_back(
  541. MenuItemModel{.command_id = in_progress_command.command_id,
  542. .label_id = in_progress_command.label_id,
  543. .icon = *in_progress_command.icon});
  544. }
  545. }
  546. // The in-progress commands are separated from other commands.
  547. if (!menu_sections.back().empty())
  548. menu_sections.emplace_back();
  549. if (selection.size() == 1u) {
  550. // The "Show in folder" command should only be present if there is only one
  551. // holding space item selected.
  552. menu_sections.back().emplace_back(MenuItemModel{
  553. .command_id = HoldingSpaceCommandId::kShowInFolder,
  554. .label_id = IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_SHOW_IN_FOLDER,
  555. .icon = kFolderIcon});
  556. std::string mime_type;
  557. const bool is_image =
  558. net::GetMimeTypeFromFile(selection.front()->item()->file_path(),
  559. &mime_type) &&
  560. net::MatchesMimeType(kMimeTypeImage, mime_type);
  561. if (is_image) {
  562. // The "Copy image" command should only be present if there is only one
  563. // holding space item selected and that item is backed by an image file.
  564. menu_sections.back().emplace_back(MenuItemModel{
  565. .command_id = HoldingSpaceCommandId::kCopyImageToClipboard,
  566. .label_id =
  567. IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_COPY_IMAGE_TO_CLIPBOARD,
  568. .icon = kCopyIcon});
  569. }
  570. }
  571. if (is_pinnable.has_value()) {
  572. if (is_pinnable.value()) {
  573. menu_sections.back().emplace_back(
  574. MenuItemModel{.command_id = HoldingSpaceCommandId::kPinItem,
  575. .label_id = IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_PIN,
  576. .icon = views::kPinIcon});
  577. } else {
  578. menu_sections.back().emplace_back(
  579. MenuItemModel{.command_id = HoldingSpaceCommandId::kUnpinItem,
  580. .label_id = IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_UNPIN,
  581. .icon = views::kUnpinIcon});
  582. }
  583. }
  584. if (is_removable) {
  585. menu_sections.back().emplace_back(
  586. MenuItemModel{.command_id = HoldingSpaceCommandId::kRemoveItem,
  587. .label_id = IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_REMOVE,
  588. .icon = kCancelCircleOutlineIcon});
  589. }
  590. // Add modeled `menu_sections` to the `context_menu_model_`.
  591. for (const MenuSectionModel& menu_section : menu_sections) {
  592. if (menu_section.empty())
  593. continue;
  594. // Each `menu_section` should be separated by a normal separator.
  595. if (context_menu_model_->GetItemCount()) {
  596. context_menu_model_->AddSeparator(
  597. ui::MenuSeparatorType::NORMAL_SEPARATOR);
  598. }
  599. // Each `menu_section` should contain their respective `menu_item`s.
  600. for (const MenuItemModel& menu_item : menu_section) {
  601. context_menu_model_->AddItemWithIcon(
  602. static_cast<int>(menu_item.command_id),
  603. l10n_util::GetStringUTF16(menu_item.label_id),
  604. ui::ImageModel::FromVectorIcon(menu_item.icon,
  605. ui::kColorAshSystemUIMenuIcon,
  606. kHoldingSpaceIconSize));
  607. }
  608. }
  609. return context_menu_model_.get();
  610. }
  611. std::vector<const HoldingSpaceItemView*>
  612. HoldingSpaceViewDelegate::GetSelection() {
  613. std::vector<const HoldingSpaceItemView*> selection;
  614. for (const HoldingSpaceItemView* view : bubble_->GetHoldingSpaceItemViews()) {
  615. if (view->selected())
  616. selection.push_back(view);
  617. }
  618. DCHECK_EQ(selection.size(), selection_size_);
  619. return selection;
  620. }
  621. void HoldingSpaceViewDelegate::ClearSelection() {
  622. SetSelection(std::vector<std::string>());
  623. }
  624. void HoldingSpaceViewDelegate::SetSelection(HoldingSpaceItemView* selection) {
  625. SetSelection({selection->item_id()});
  626. }
  627. void HoldingSpaceViewDelegate::SetSelection(
  628. const std::vector<std::string>& item_ids) {
  629. std::vector<HoldingSpaceItemView*> selection;
  630. for (HoldingSpaceItemView* view : bubble_->GetHoldingSpaceItemViews()) {
  631. view->SetSelected(base::Contains(item_ids, view->item_id()));
  632. if (view->selected())
  633. selection.push_back(view);
  634. }
  635. if (selection.size() == 1u) {
  636. selected_range_start_ = selection.front();
  637. selected_range_end_ = selection.front();
  638. } else {
  639. selected_range_start_ = nullptr;
  640. selected_range_end_ = nullptr;
  641. }
  642. }
  643. void HoldingSpaceViewDelegate::SetSelectedRange(HoldingSpaceItemView* start,
  644. HoldingSpaceItemView* end) {
  645. const std::vector<HoldingSpaceItemView*> views =
  646. bubble_->GetHoldingSpaceItemViews();
  647. for (HoldingSpaceItemView* view :
  648. GetViewsInRange(views, selected_range_start_, selected_range_end_)) {
  649. view->SetSelected(false);
  650. }
  651. selected_range_start_ = start;
  652. selected_range_end_ = end;
  653. for (HoldingSpaceItemView* view :
  654. GetViewsInRange(views, selected_range_start_, selected_range_end_)) {
  655. view->SetSelected(true);
  656. }
  657. }
  658. void HoldingSpaceViewDelegate::UpdateSelectionUi() {
  659. const SelectionUi selection_ui =
  660. TabletMode::Get()->InTabletMode() || selection_size_ > 1u
  661. ? SelectionUi::kMultiSelect
  662. : SelectionUi::kSingleSelect;
  663. if (selection_ui_ == selection_ui)
  664. return;
  665. selection_ui_ = selection_ui;
  666. selection_ui_changed_callbacks_.Notify();
  667. }
  668. } // namespace ash