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