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
5 #include "ash/system/holding_space/holding_space_item_view_delegate.h"
6
7 #include "ash/public/cpp/holding_space/holding_space_client.h"
8 #include "ash/public/cpp/holding_space/holding_space_constants.h"
9 #include "ash/public/cpp/holding_space/holding_space_controller.h"
10 #include "ash/public/cpp/holding_space/holding_space_item.h"
11 #include "ash/public/cpp/holding_space/holding_space_metrics.h"
12 #include "ash/public/cpp/holding_space/holding_space_model.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 "base/bind.h"
18 #include "net/base/mime_util.h"
19 #include "ui/accessibility/ax_action_data.h"
20 #include "ui/accessibility/ax_enums.mojom.h"
21 #include "ui/base/dragdrop/drag_drop_types.h"
22 #include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
23 #include "ui/base/dragdrop/os_exchange_data_provider.h"
24 #include "ui/base/l10n/l10n_util.h"
25 #include "ui/base/models/simple_menu_model.h"
26 #include "ui/views/controls/menu/menu_runner.h"
27 #include "ui/views/vector_icons.h"
28 #include "ui/views/view.h"
29
30 namespace ash {
31
32 namespace {
33
34 // It is expected that all `HoldingSpaceItemView`s share the same delegate in
35 // order to support multiple selections. We cache the singleton `instance` in
36 // order to enforce this requirement.
37 HoldingSpaceItemViewDelegate* instance = nullptr;
38
39 // Helpers ---------------------------------------------------------------------
40
41 // Returns the holding space items associated with the specified `views`.
GetItems(const std::vector<const HoldingSpaceItemView * > & views)42 std::vector<const HoldingSpaceItem*> GetItems(
43 const std::vector<const HoldingSpaceItemView*>& views) {
44 std::vector<const HoldingSpaceItem*> items;
45 for (const HoldingSpaceItemView* view : views)
46 items.push_back(view->item());
47 return items;
48 }
49
50 // Attempts to open the holding space items associated with the given `views`.
OpenItems(const std::vector<const HoldingSpaceItemView * > & views)51 void OpenItems(const std::vector<const HoldingSpaceItemView*>& views) {
52 DCHECK_GE(views.size(), 1u);
53 HoldingSpaceController::Get()->client()->OpenItems(GetItems(views),
54 base::DoNothing());
55 }
56
57 } // namespace
58
59 // HoldingSpaceItemViewDelegate ------------------------------------------------
60
HoldingSpaceItemViewDelegate()61 HoldingSpaceItemViewDelegate::HoldingSpaceItemViewDelegate() {
62 DCHECK_EQ(nullptr, instance);
63 instance = this;
64 }
65
~HoldingSpaceItemViewDelegate()66 HoldingSpaceItemViewDelegate::~HoldingSpaceItemViewDelegate() {
67 DCHECK_EQ(instance, this);
68 instance = nullptr;
69 }
70
OnHoldingSpaceItemViewCreated(HoldingSpaceItemView * view)71 void HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewCreated(
72 HoldingSpaceItemView* view) {
73 view_observer_.Add(view);
74 views_.push_back(view);
75 }
76
OnHoldingSpaceItemViewAccessibleAction(HoldingSpaceItemView * view,const ui::AXActionData & action_data)77 bool HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewAccessibleAction(
78 HoldingSpaceItemView* view,
79 const ui::AXActionData& action_data) {
80 // When performing the default accessible action (e.g. Search + Space), open
81 // the selected holding space items. If `view` is not part of the current
82 // selection it will become the entire selection.
83 if (action_data.action == ax::mojom::Action::kDoDefault) {
84 if (!view->selected())
85 SetSelection(view);
86 OpenItems(GetSelection());
87 return true;
88 }
89 // When showing the context menu via accessible action (e.g. Search + M),
90 // ensure that `view` is part of the current selection. If it is not part of
91 // the current selection it will become the entire selection.
92 if (action_data.action == ax::mojom::Action::kShowContextMenu) {
93 if (!view->selected())
94 SetSelection(view);
95 // Return false so that the views framework will show the context menu.
96 return false;
97 }
98 return false;
99 }
100
OnHoldingSpaceItemViewGestureEvent(HoldingSpaceItemView * view,const ui::GestureEvent & event)101 void HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewGestureEvent(
102 HoldingSpaceItemView* view,
103 const ui::GestureEvent& event) {
104 // When a long press gesture occurs we are going to show the context menu.
105 // Ensure that the pressed `view` is the only view selected.
106 if (event.type() == ui::ET_GESTURE_LONG_PRESS) {
107 SetSelection(view);
108 return;
109 }
110 // If a scroll begin gesture is received while the context menu is showing,
111 // that means the user is trying to initiate a drag. Close the context menu
112 // and start the item drag.
113 if (event.type() == ui::ET_GESTURE_SCROLL_BEGIN && context_menu_runner_ &&
114 context_menu_runner_->IsRunning()) {
115 context_menu_runner_.reset();
116 view->StartDrag(event, ui::mojom::DragEventSource::kTouch);
117 return;
118 }
119 // When a tap gesture occurs, we select and open only the item corresponding
120 // to the tapped `view`.
121 if (event.type() == ui::ET_GESTURE_TAP) {
122 SetSelection(view);
123 OpenItems(GetSelection());
124 }
125 }
126
OnHoldingSpaceItemViewKeyPressed(HoldingSpaceItemView * view,const ui::KeyEvent & event)127 bool HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewKeyPressed(
128 HoldingSpaceItemView* view,
129 const ui::KeyEvent& event) {
130 // The ENTER key should open all selected holding space items. If `view` isn't
131 // already part of the selection, it will become the entire selection.
132 if (event.key_code() == ui::KeyboardCode::VKEY_RETURN) {
133 if (!view->selected())
134 SetSelection(view);
135 OpenItems(GetSelection());
136 return true;
137 }
138 return false;
139 }
140
OnHoldingSpaceItemViewMousePressed(HoldingSpaceItemView * view,const ui::MouseEvent & event)141 bool HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewMousePressed(
142 HoldingSpaceItemView* view,
143 const ui::MouseEvent& event) {
144 // Since we are starting a new mouse pressed/released sequence, we need to
145 // clear any view that we had cached to ignore mouse released events for.
146 ignore_mouse_released_ = nullptr;
147
148 // If the `view` is already selected, mouse press is a no-op. Actions taken on
149 // selected views are performed on mouse released in order to give drag/drop
150 // a chance to take effect (assuming that drag thresholds are met).
151 if (view->selected())
152 return true;
153
154 // If the right mouse button is pressed, we're going to be showing the context
155 // menu. Make sure that `view` is part of the current selection. If the SHIFT
156 // key is not down, it should be the entire selection.
157 if (event.IsRightMouseButton()) {
158 if (event.IsShiftDown())
159 view->SetSelected(true);
160 else
161 SetSelection(view);
162 return true;
163 }
164
165 // If the SHIFT key is down, we need to add `view` to the current selection.
166 // We're going to need to ignore the next mouse released event on `view` so
167 // that we don't unselect `view` accidentally right after having selected it.
168 if (event.IsShiftDown()) {
169 ignore_mouse_released_ = view;
170 view->SetSelected(true);
171 return true;
172 }
173
174 // In the absence of any modifiers, pressing an unselected `view` will cause
175 // `view` to become the current selection. Previous selections are cleared.
176 SetSelection(view);
177 return true;
178 }
179
OnHoldingSpaceItemViewMouseReleased(HoldingSpaceItemView * view,const ui::MouseEvent & event)180 void HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewMouseReleased(
181 HoldingSpaceItemView* view,
182 const ui::MouseEvent& event) {
183 // We should always clear `ignore_mouse_released_` after this method runs
184 // since that property should affect at most one press/release sequence.
185 base::ScopedClosureRunner clear_ignore_mouse_released(base::BindOnce(
186 [](HoldingSpaceItemView** ignore_mouse_released) {
187 *ignore_mouse_released = nullptr;
188 },
189 &ignore_mouse_released_));
190
191 // We might be ignoring mouse released events for `view` if it was just
192 // selected on mouse pressed. In this case, no-op here.
193 if (ignore_mouse_released_ == view)
194 return;
195
196 // If the right mouse button is released we're showing the context menu. In
197 // this case, no-op here.
198 if (event.IsRightMouseButton())
199 return;
200
201 // If the SHIFT key is down, mouse release should toggle the selected state of
202 // `view`. If `view` is the only selected view, this is a no-op.
203 if (event.IsShiftDown()) {
204 if (GetSelection().size() > 1u)
205 view->SetSelected(!view->selected());
206 return;
207 }
208
209 // If this mouse released `event` is part of a double click, we should open
210 // the items associated with the current selection.
211 if (event.flags() & ui::EF_IS_DOUBLE_CLICK)
212 OpenItems(GetSelection());
213 }
214
ShowContextMenuForViewImpl(views::View * source,const gfx::Point & point,ui::MenuSourceType source_type)215 void HoldingSpaceItemViewDelegate::ShowContextMenuForViewImpl(
216 views::View* source,
217 const gfx::Point& point,
218 ui::MenuSourceType source_type) {
219 // In touch mode, gesture events continue to be sent to holding space views
220 // after showing the context menu so that it can be aborted if the user
221 // initiates a drag sequence. This means both `ui::ET_GESTURE_LONG_TAP` and
222 // `ui::ET_GESTURE_LONG_PRESS` may be received while showing the context menu
223 // which would result in trying to show the context menu twice. This would not
224 // be a fatal failure but would result in UI jank.
225 if (context_menu_runner_ && context_menu_runner_->IsRunning())
226 return;
227
228 int run_types = views::MenuRunner::USE_TOUCHABLE_LAYOUT |
229 views::MenuRunner::CONTEXT_MENU |
230 views::MenuRunner::FIXED_ANCHOR;
231
232 // In touch mode the context menu may be aborted if the user initiates a drag.
233 // In order to determine if the gesture resulting in this context menu being
234 // shown was actually the start of a drag sequence, holding space views will
235 // have to receive events that would otherwise be consumed by the `MenuHost`.
236 if (source_type == ui::MenuSourceType::MENU_SOURCE_TOUCH)
237 run_types |= views::MenuRunner::SEND_GESTURE_EVENTS_TO_OWNER;
238
239 context_menu_runner_ =
240 std::make_unique<views::MenuRunner>(BuildMenuModel(), run_types);
241
242 gfx::Rect bounds = source->GetBoundsInScreen();
243 bounds.Inset(gfx::Insets(-kHoldingSpaceContextMenuMargin, 0));
244
245 context_menu_runner_->RunMenuAt(
246 source->GetWidget(), nullptr /*button_controller*/, bounds,
247 views::MenuAnchorPosition::kTopLeft, source_type);
248 }
249
CanStartDragForView(views::View * sender,const gfx::Point & press_pt,const gfx::Point & current_pt)250 bool HoldingSpaceItemViewDelegate::CanStartDragForView(
251 views::View* sender,
252 const gfx::Point& press_pt,
253 const gfx::Point& current_pt) {
254 const gfx::Vector2d delta = current_pt - press_pt;
255 return views::View::ExceededDragThreshold(delta);
256 }
257
GetDragOperationsForView(views::View * sender,const gfx::Point & press_pt)258 int HoldingSpaceItemViewDelegate::GetDragOperationsForView(
259 views::View* sender,
260 const gfx::Point& press_pt) {
261 return ui::DragDropTypes::DRAG_COPY;
262 }
263
WriteDragDataForView(views::View * sender,const gfx::Point & press_pt,ui::OSExchangeData * data)264 void HoldingSpaceItemViewDelegate::WriteDragDataForView(
265 views::View* sender,
266 const gfx::Point& press_pt,
267 ui::OSExchangeData* data) {
268 std::vector<const HoldingSpaceItemView*> selection = GetSelection();
269 DCHECK_GE(selection.size(), 1u);
270
271 holding_space_metrics::RecordItemAction(
272 GetItems(selection), holding_space_metrics::ItemAction::kDrag);
273
274 // Drag image.
275 gfx::ImageSkia drag_image;
276 gfx::Vector2d drag_offset;
277 holding_space_util::CreateDragImage(selection, &drag_image, &drag_offset);
278 data->provider().SetDragImage(std::move(drag_image), drag_offset);
279
280 // Payload.
281 std::vector<ui::FileInfo> filenames;
282 for (const HoldingSpaceItemView* view : selection) {
283 const base::FilePath& file_path = view->item()->file_path();
284 filenames.push_back(ui::FileInfo(file_path, file_path.BaseName()));
285 }
286 data->SetFilenames(filenames);
287 }
288
OnViewIsDeleting(views::View * view)289 void HoldingSpaceItemViewDelegate::OnViewIsDeleting(views::View* view) {
290 base::Erase(views_, view);
291 view_observer_.Remove(view);
292 }
293
ExecuteCommand(int command_id,int event_flags)294 void HoldingSpaceItemViewDelegate::ExecuteCommand(int command_id,
295 int event_flags) {
296 std::vector<const HoldingSpaceItemView*> selection = GetSelection();
297 DCHECK_GE(selection.size(), 1u);
298
299 switch (command_id) {
300 case HoldingSpaceCommandId::kCopyImageToClipboard:
301 DCHECK_EQ(selection.size(), 1u);
302 HoldingSpaceController::Get()->client()->CopyImageToClipboard(
303 *selection.front()->item(), base::DoNothing());
304 break;
305 case HoldingSpaceCommandId::kPinItem:
306 HoldingSpaceController::Get()->client()->PinItems(GetItems(selection));
307 break;
308 case HoldingSpaceCommandId::kShowInFolder:
309 DCHECK_EQ(selection.size(), 1u);
310 HoldingSpaceController::Get()->client()->ShowItemInFolder(
311 *selection.front()->item(), base::DoNothing());
312 break;
313 case HoldingSpaceCommandId::kUnpinItem:
314 HoldingSpaceController::Get()->client()->UnpinItems(GetItems(selection));
315 break;
316 }
317 }
318
BuildMenuModel()319 ui::SimpleMenuModel* HoldingSpaceItemViewDelegate::BuildMenuModel() {
320 context_menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
321
322 std::vector<const HoldingSpaceItemView*> selection = GetSelection();
323 DCHECK_GE(selection.size(), 1u);
324
325 if (selection.size() == 1u) {
326 // The "Show in folder" command should only be present if there is only one
327 // holding space item selected.
328 context_menu_model_->AddItemWithIcon(
329 HoldingSpaceCommandId::kShowInFolder,
330 l10n_util::GetStringUTF16(
331 IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_SHOW_IN_FOLDER),
332 ui::ImageModel::FromVectorIcon(kFolderIcon));
333
334 std::string mime_type;
335 const bool is_image =
336 net::GetMimeTypeFromFile(selection.front()->item()->file_path(),
337 &mime_type) &&
338 net::MatchesMimeType(kMimeTypeImage, mime_type);
339
340 if (is_image) {
341 // The "Copy image" command should only be present if there is only one
342 // holding space item selected and that item is backed by an image file.
343 context_menu_model_->AddItemWithIcon(
344 HoldingSpaceCommandId::kCopyImageToClipboard,
345 l10n_util::GetStringUTF16(
346 IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_COPY_IMAGE_TO_CLIPBOARD),
347 ui::ImageModel::FromVectorIcon(kCopyIcon));
348 }
349 }
350
351 const bool is_any_unpinned = std::any_of(
352 selection.begin(), selection.end(), [](const HoldingSpaceItemView* view) {
353 return !HoldingSpaceController::Get()->model()->GetItem(
354 HoldingSpaceItem::GetFileBackedItemId(
355 HoldingSpaceItem::Type::kPinnedFile,
356 view->item()->file_path()));
357 });
358
359 if (is_any_unpinned) {
360 // The "Pin" command should be present if any selected holding space item is
361 // unpinned. When executing this command, any holding space items that are
362 // already pinned will be ignored.
363 context_menu_model_->AddItemWithIcon(
364 HoldingSpaceCommandId::kPinItem,
365 l10n_util::GetStringUTF16(IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_PIN),
366 ui::ImageModel::FromVectorIcon(views::kPinIcon));
367 } else {
368 // The "Unpin" command should be present only if all selected holding space
369 // items are already pinned.
370 context_menu_model_->AddItemWithIcon(
371 HoldingSpaceCommandId::kUnpinItem,
372 l10n_util::GetStringUTF16(IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_UNPIN),
373 ui::ImageModel::FromVectorIcon(views::kUnpinIcon));
374 }
375
376 return context_menu_model_.get();
377 }
378
379 std::vector<const HoldingSpaceItemView*>
GetSelection()380 HoldingSpaceItemViewDelegate::GetSelection() {
381 std::vector<const HoldingSpaceItemView*> selection;
382 for (const HoldingSpaceItemView* view : views_) {
383 if (view->selected())
384 selection.push_back(view);
385 }
386 return selection;
387 }
388
SetSelection(views::View * selection)389 void HoldingSpaceItemViewDelegate::SetSelection(views::View* selection) {
390 for (HoldingSpaceItemView* view : views_)
391 view->SetSelected(view == selection);
392 }
393
394 } // namespace ash
395