| // Copyright 2020 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/system/holding_space/holding_space_item_view_delegate.h" |
| |
| #include "ash/public/cpp/holding_space/holding_space_client.h" |
| #include "ash/public/cpp/holding_space/holding_space_constants.h" |
| #include "ash/public/cpp/holding_space/holding_space_controller.h" |
| #include "ash/public/cpp/holding_space/holding_space_item.h" |
| #include "ash/public/cpp/holding_space/holding_space_metrics.h" |
| #include "ash/public/cpp/holding_space/holding_space_model.h" |
| #include "ash/resources/vector_icons/vector_icons.h" |
| #include "ash/strings/grit/ash_strings.h" |
| #include "ash/system/holding_space/holding_space_drag_util.h" |
| #include "ash/system/holding_space/holding_space_item_view.h" |
| #include "base/bind.h" |
| #include "net/base/mime_util.h" |
| #include "ui/accessibility/ax_action_data.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/base/dragdrop/drag_drop_types.h" |
| #include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" |
| #include "ui/base/dragdrop/os_exchange_data_provider.h" |
| #include "ui/base/l10n/l10n_util.h" |
| #include "ui/base/models/simple_menu_model.h" |
| #include "ui/views/controls/menu/menu_runner.h" |
| #include "ui/views/vector_icons.h" |
| #include "ui/views/view.h" |
| |
| namespace ash { |
| |
| namespace { |
| |
| // It is expected that all `HoldingSpaceItemView`s share the same delegate in |
| // order to support multiple selections. We cache the singleton `instance` in |
| // order to enforce this requirement. |
| HoldingSpaceItemViewDelegate* instance = nullptr; |
| |
| // Helpers --------------------------------------------------------------------- |
| |
| // Returns the holding space items associated with the specified `views`. |
| std::vector<const HoldingSpaceItem*> GetItems( |
| const std::vector<const HoldingSpaceItemView*>& views) { |
| std::vector<const HoldingSpaceItem*> items; |
| for (const HoldingSpaceItemView* view : views) |
| items.push_back(view->item()); |
| return items; |
| } |
| |
| // Attempts to open the holding space items associated with the given `views`. |
| void OpenItems(const std::vector<const HoldingSpaceItemView*>& views) { |
| DCHECK_GE(views.size(), 1u); |
| HoldingSpaceController::Get()->client()->OpenItems(GetItems(views), |
| base::DoNothing()); |
| } |
| |
| } // namespace |
| |
| // HoldingSpaceItemViewDelegate::ScopedSelectionRestore ------------------------ |
| |
| HoldingSpaceItemViewDelegate::ScopedSelectionRestore::ScopedSelectionRestore( |
| HoldingSpaceItemViewDelegate* delegate) |
| : delegate_(delegate) { |
| for (const HoldingSpaceItemView* view : delegate_->GetSelection()) |
| selected_item_ids_.push_back(view->item_id()); |
| } |
| |
| HoldingSpaceItemViewDelegate::ScopedSelectionRestore:: |
| ~ScopedSelectionRestore() { |
| delegate_->SetSelection(selected_item_ids_); |
| } |
| |
| // HoldingSpaceItemViewDelegate ------------------------------------------------ |
| |
| HoldingSpaceItemViewDelegate::HoldingSpaceItemViewDelegate() { |
| DCHECK_EQ(nullptr, instance); |
| instance = this; |
| } |
| |
| HoldingSpaceItemViewDelegate::~HoldingSpaceItemViewDelegate() { |
| DCHECK_EQ(instance, this); |
| instance = nullptr; |
| } |
| |
| void HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewCreated( |
| HoldingSpaceItemView* view) { |
| view_observer_.Add(view); |
| views_.push_back(view); |
| } |
| |
| bool HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewAccessibleAction( |
| HoldingSpaceItemView* view, |
| const ui::AXActionData& action_data) { |
| // When performing the default accessible action (e.g. Search + Space), open |
| // the selected holding space items. If `view` is not part of the current |
| // selection it will become the entire selection. |
| if (action_data.action == ax::mojom::Action::kDoDefault) { |
| if (!view->selected()) |
| SetSelection(view); |
| OpenItems(GetSelection()); |
| return true; |
| } |
| // When showing the context menu via accessible action (e.g. Search + M), |
| // ensure that `view` is part of the current selection. If it is not part of |
| // the current selection it will become the entire selection. |
| if (action_data.action == ax::mojom::Action::kShowContextMenu) { |
| if (!view->selected()) |
| SetSelection(view); |
| // Return false so that the views framework will show the context menu. |
| return false; |
| } |
| return false; |
| } |
| |
| void HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewGestureEvent( |
| HoldingSpaceItemView* view, |
| const ui::GestureEvent& event) { |
| // When a long press gesture occurs we are going to show the context menu. |
| // Ensure that the pressed `view` is the only view selected. |
| if (event.type() == ui::ET_GESTURE_LONG_PRESS) { |
| SetSelection(view); |
| return; |
| } |
| // If a scroll begin gesture is received while the context menu is showing, |
| // that means the user is trying to initiate a drag. Close the context menu |
| // and start the item drag. |
| if (event.type() == ui::ET_GESTURE_SCROLL_BEGIN && context_menu_runner_ && |
| context_menu_runner_->IsRunning()) { |
| context_menu_runner_.reset(); |
| view->StartDrag(event, ui::mojom::DragEventSource::kTouch); |
| return; |
| } |
| // When a tap gesture occurs, we select and open only the item corresponding |
| // to the tapped `view`. |
| if (event.type() == ui::ET_GESTURE_TAP) { |
| SetSelection(view); |
| OpenItems(GetSelection()); |
| } |
| } |
| |
| bool HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewKeyPressed( |
| HoldingSpaceItemView* view, |
| const ui::KeyEvent& event) { |
| // The ENTER key should open all selected holding space items. If `view` isn't |
| // already part of the selection, it will become the entire selection. |
| if (event.key_code() == ui::KeyboardCode::VKEY_RETURN) { |
| if (!view->selected()) |
| SetSelection(view); |
| OpenItems(GetSelection()); |
| return true; |
| } |
| return false; |
| } |
| |
| bool HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewMousePressed( |
| HoldingSpaceItemView* view, |
| const ui::MouseEvent& event) { |
| // Since we are starting a new mouse pressed/released sequence, we need to |
| // clear any view that we had cached to ignore mouse released events for. |
| ignore_mouse_released_ = nullptr; |
| |
| // If the `view` is already selected, mouse press is a no-op. Actions taken on |
| // selected views are performed on mouse released in order to give drag/drop |
| // a chance to take effect (assuming that drag thresholds are met). |
| if (view->selected()) |
| return true; |
| |
| // If the right mouse button is pressed, we're going to be showing the context |
| // menu. Make sure that `view` is part of the current selection. If the SHIFT |
| // key is not down, it should be the entire selection. |
| if (event.IsRightMouseButton()) { |
| if (event.IsShiftDown()) |
| view->SetSelected(true); |
| else |
| SetSelection(view); |
| return true; |
| } |
| |
| // If the SHIFT key is down, we need to add `view` to the current selection. |
| // We're going to need to ignore the next mouse released event on `view` so |
| // that we don't unselect `view` accidentally right after having selected it. |
| if (event.IsShiftDown()) { |
| ignore_mouse_released_ = view; |
| view->SetSelected(true); |
| return true; |
| } |
| |
| // In the absence of any modifiers, pressing an unselected `view` will cause |
| // `view` to become the current selection. Previous selections are cleared. |
| SetSelection(view); |
| return true; |
| } |
| |
| void HoldingSpaceItemViewDelegate::OnHoldingSpaceItemViewMouseReleased( |
| HoldingSpaceItemView* view, |
| const ui::MouseEvent& event) { |
| // We should always clear `ignore_mouse_released_` after this method runs |
| // since that property should affect at most one press/release sequence. |
| base::ScopedClosureRunner clear_ignore_mouse_released(base::BindOnce( |
| [](HoldingSpaceItemView** ignore_mouse_released) { |
| *ignore_mouse_released = nullptr; |
| }, |
| &ignore_mouse_released_)); |
| |
| // We might be ignoring mouse released events for `view` if it was just |
| // selected on mouse pressed. In this case, no-op here. |
| if (ignore_mouse_released_ == view) |
| return; |
| |
| // If the right mouse button is released we're showing the context menu. In |
| // this case, no-op here. |
| if (event.IsRightMouseButton()) |
| return; |
| |
| // If the SHIFT key is down, mouse release should toggle the selected state of |
| // `view`. If `view` is the only selected view, this is a no-op. |
| if (event.IsShiftDown()) { |
| if (GetSelection().size() > 1u) |
| view->SetSelected(!view->selected()); |
| return; |
| } |
| |
| // If this mouse released `event` is part of a double click, we should open |
| // the items associated with the current selection. |
| if (event.flags() & ui::EF_IS_DOUBLE_CLICK) |
| OpenItems(GetSelection()); |
| } |
| |
| bool HoldingSpaceItemViewDelegate::OnHoldingSpaceTrayKeyPressed( |
| const ui::KeyEvent& event) { |
| // The ENTER key should open all selected holding space items. |
| if (event.key_code() == ui::KeyboardCode::VKEY_RETURN) { |
| if (!GetSelection().empty()) { |
| OpenItems(GetSelection()); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void HoldingSpaceItemViewDelegate::ShowContextMenuForViewImpl( |
| views::View* source, |
| const gfx::Point& point, |
| ui::MenuSourceType source_type) { |
| // In touch mode, gesture events continue to be sent to holding space views |
| // after showing the context menu so that it can be aborted if the user |
| // initiates a drag sequence. This means both `ui::ET_GESTURE_LONG_TAP` and |
| // `ui::ET_GESTURE_LONG_PRESS` may be received while showing the context menu |
| // which would result in trying to show the context menu twice. This would not |
| // be a fatal failure but would result in UI jank. |
| if (context_menu_runner_ && context_menu_runner_->IsRunning()) |
| return; |
| |
| int run_types = views::MenuRunner::USE_TOUCHABLE_LAYOUT | |
| views::MenuRunner::CONTEXT_MENU | |
| views::MenuRunner::FIXED_ANCHOR; |
| |
| // In touch mode the context menu may be aborted if the user initiates a drag. |
| // In order to determine if the gesture resulting in this context menu being |
| // shown was actually the start of a drag sequence, holding space views will |
| // have to receive events that would otherwise be consumed by the `MenuHost`. |
| if (source_type == ui::MenuSourceType::MENU_SOURCE_TOUCH) |
| run_types |= views::MenuRunner::SEND_GESTURE_EVENTS_TO_OWNER; |
| |
| context_menu_runner_ = |
| std::make_unique<views::MenuRunner>(BuildMenuModel(), run_types); |
| |
| gfx::Rect bounds = source->GetBoundsInScreen(); |
| bounds.Inset(gfx::Insets(-kHoldingSpaceContextMenuMargin, 0)); |
| |
| context_menu_runner_->RunMenuAt( |
| source->GetWidget(), nullptr /*button_controller*/, bounds, |
| views::MenuAnchorPosition::kTopLeft, source_type); |
| } |
| |
| bool HoldingSpaceItemViewDelegate::CanStartDragForView( |
| views::View* sender, |
| const gfx::Point& press_pt, |
| const gfx::Point& current_pt) { |
| const gfx::Vector2d delta = current_pt - press_pt; |
| return views::View::ExceededDragThreshold(delta); |
| } |
| |
| int HoldingSpaceItemViewDelegate::GetDragOperationsForView( |
| views::View* sender, |
| const gfx::Point& press_pt) { |
| return ui::DragDropTypes::DRAG_COPY; |
| } |
| |
| void HoldingSpaceItemViewDelegate::WriteDragDataForView( |
| views::View* sender, |
| const gfx::Point& press_pt, |
| ui::OSExchangeData* data) { |
| std::vector<const HoldingSpaceItemView*> selection = GetSelection(); |
| DCHECK_GE(selection.size(), 1u); |
| |
| holding_space_metrics::RecordItemAction( |
| GetItems(selection), holding_space_metrics::ItemAction::kDrag); |
| |
| // Drag image. |
| gfx::ImageSkia drag_image; |
| gfx::Vector2d drag_offset; |
| holding_space_util::CreateDragImage(selection, &drag_image, &drag_offset); |
| data->provider().SetDragImage(std::move(drag_image), drag_offset); |
| |
| // Payload. |
| std::vector<ui::FileInfo> filenames; |
| for (const HoldingSpaceItemView* view : selection) { |
| const base::FilePath& file_path = view->item()->file_path(); |
| filenames.push_back(ui::FileInfo(file_path, file_path.BaseName())); |
| } |
| data->SetFilenames(filenames); |
| } |
| |
| void HoldingSpaceItemViewDelegate::OnViewIsDeleting(views::View* view) { |
| base::Erase(views_, view); |
| view_observer_.Remove(view); |
| } |
| |
| void HoldingSpaceItemViewDelegate::ExecuteCommand(int command_id, |
| int event_flags) { |
| std::vector<const HoldingSpaceItemView*> selection = GetSelection(); |
| DCHECK_GE(selection.size(), 1u); |
| |
| switch (command_id) { |
| case HoldingSpaceCommandId::kCopyImageToClipboard: |
| DCHECK_EQ(selection.size(), 1u); |
| HoldingSpaceController::Get()->client()->CopyImageToClipboard( |
| *selection.front()->item(), base::DoNothing()); |
| break; |
| case HoldingSpaceCommandId::kPinItem: |
| HoldingSpaceController::Get()->client()->PinItems(GetItems(selection)); |
| break; |
| case HoldingSpaceCommandId::kShowInFolder: |
| DCHECK_EQ(selection.size(), 1u); |
| HoldingSpaceController::Get()->client()->ShowItemInFolder( |
| *selection.front()->item(), base::DoNothing()); |
| break; |
| case HoldingSpaceCommandId::kUnpinItem: |
| HoldingSpaceController::Get()->client()->UnpinItems(GetItems(selection)); |
| break; |
| } |
| } |
| |
| ui::SimpleMenuModel* HoldingSpaceItemViewDelegate::BuildMenuModel() { |
| context_menu_model_ = std::make_unique<ui::SimpleMenuModel>(this); |
| |
| std::vector<const HoldingSpaceItemView*> selection = GetSelection(); |
| DCHECK_GE(selection.size(), 1u); |
| |
| if (selection.size() == 1u) { |
| // The "Show in folder" command should only be present if there is only one |
| // holding space item selected. |
| context_menu_model_->AddItemWithIcon( |
| HoldingSpaceCommandId::kShowInFolder, |
| l10n_util::GetStringUTF16( |
| IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_SHOW_IN_FOLDER), |
| ui::ImageModel::FromVectorIcon(kFolderIcon)); |
| |
| std::string mime_type; |
| const bool is_image = |
| net::GetMimeTypeFromFile(selection.front()->item()->file_path(), |
| &mime_type) && |
| net::MatchesMimeType(kMimeTypeImage, mime_type); |
| |
| if (is_image) { |
| // The "Copy image" command should only be present if there is only one |
| // holding space item selected and that item is backed by an image file. |
| context_menu_model_->AddItemWithIcon( |
| HoldingSpaceCommandId::kCopyImageToClipboard, |
| l10n_util::GetStringUTF16( |
| IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_COPY_IMAGE_TO_CLIPBOARD), |
| ui::ImageModel::FromVectorIcon(kCopyIcon)); |
| } |
| } |
| |
| const bool is_any_unpinned = std::any_of( |
| selection.begin(), selection.end(), [](const HoldingSpaceItemView* view) { |
| return !HoldingSpaceController::Get()->model()->ContainsItem( |
| HoldingSpaceItem::Type::kPinnedFile, view->item()->file_path()); |
| }); |
| |
| if (is_any_unpinned) { |
| // The "Pin" command should be present if any selected holding space item is |
| // unpinned. When executing this command, any holding space items that are |
| // already pinned will be ignored. |
| context_menu_model_->AddItemWithIcon( |
| HoldingSpaceCommandId::kPinItem, |
| l10n_util::GetStringUTF16(IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_PIN), |
| ui::ImageModel::FromVectorIcon(views::kPinIcon)); |
| } else { |
| // The "Unpin" command should be present only if all selected holding space |
| // items are already pinned. |
| context_menu_model_->AddItemWithIcon( |
| HoldingSpaceCommandId::kUnpinItem, |
| l10n_util::GetStringUTF16(IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_UNPIN), |
| ui::ImageModel::FromVectorIcon(views::kUnpinIcon)); |
| } |
| |
| return context_menu_model_.get(); |
| } |
| |
| std::vector<const HoldingSpaceItemView*> |
| HoldingSpaceItemViewDelegate::GetSelection() { |
| std::vector<const HoldingSpaceItemView*> selection; |
| for (const HoldingSpaceItemView* view : views_) { |
| if (view->selected()) |
| selection.push_back(view); |
| } |
| return selection; |
| } |
| |
| void HoldingSpaceItemViewDelegate::SetSelection(views::View* selection) { |
| for (HoldingSpaceItemView* view : views_) |
| view->SetSelected(view == selection); |
| } |
| |
| void HoldingSpaceItemViewDelegate::SetSelection( |
| const std::vector<std::string>& item_ids) { |
| for (HoldingSpaceItemView* view : views_) |
| view->SetSelected(base::Contains(item_ids, view->item_id())); |
| } |
| |
| } // namespace ash |