// Copyright 2020 The Chromium Authors
// 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_view_delegate.h"
#include <vector>
#include "ash/constants/ash_features.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_file.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/public/cpp/holding_space/holding_space_progress.h"
#include "ash/public/cpp/holding_space/holding_space_util.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 "ash/system/holding_space/holding_space_tray.h"
#include "ash/system/holding_space/holding_space_tray_bubble.h"
#include "base/containers/contains.h"
#include "base/memory/raw_ref.h"
#include "base/memory/weak_ptr.h"
#include "base/task/sequenced_task_runner.h"
#include "net/base/mime_util.h"
#include "third_party/abseil-cpp/absl/cleanup/cleanup.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/color/color_id.h"
#include "ui/display/screen.h"
#include "ui/display/tablet_state.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/vector_icons.h"
#include "ui/views/view.h"
namespace ash {
namespace {
// It is expected that all holding space views share the same delegate in order
// to support multiple selections which requires a shared state. We cache the
// singleton `instance` in order to enforce this requirement.
HoldingSpaceViewDelegate* 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;
}
// Returns the subset of `views` in the range of `start` and `end` (inclusive).
std::vector<HoldingSpaceItemView*> GetViewsInRange(
const std::vector<HoldingSpaceItemView*>& views,
HoldingSpaceItemView* start,
HoldingSpaceItemView* end) {
if (!start || !end)
return {};
bool found_start = false;
bool found_end = false;
std::vector<HoldingSpaceItemView*> range;
for (HoldingSpaceItemView* view : views) {
if (view == start)
found_start = true;
if (view == end)
found_end = true;
if (found_start || found_end)
range.push_back(view);
if (found_start && found_end)
break;
}
DCHECK(found_start);
DCHECK(found_end);
return range;
}
} // namespace
// HoldingSpaceViewDelegate::ScopedSelectionRestore ----------------------------
HoldingSpaceViewDelegate::ScopedSelectionRestore::ScopedSelectionRestore(
HoldingSpaceViewDelegate* delegate)
: delegate_(delegate) {
// Save selection.
for (const HoldingSpaceItemView* view : delegate_->GetSelection())
selected_item_ids_.push_back(view->item_id());
// Save `selected_range_start_`.
if (delegate_->selected_range_start_)
selected_range_start_item_id_ = delegate_->selected_range_start_->item_id();
// Save `selected_range_end_`.
if (delegate_->selected_range_end_)
selected_range_end_item_id_ = delegate_->selected_range_end_->item_id();
}
HoldingSpaceViewDelegate::ScopedSelectionRestore::~ScopedSelectionRestore() {
// Restore selection.
delegate_->SetSelection(selected_item_ids_);
if (!selected_range_start_item_id_ || !selected_range_end_item_id_)
return;
HoldingSpaceItemView* selected_range_start = nullptr;
HoldingSpaceItemView* selected_range_end = nullptr;
for (HoldingSpaceItemView* view :
delegate_->bubble_->GetHoldingSpaceItemViews()) {
// Cache `selected_range_start`.
if (selected_range_start_item_id_ == view->item_id())
selected_range_start = view;
// Cache `selected_range_end`.
if (selected_range_end_item_id_ == view->item_id())
selected_range_end = view;
// Restore `selected_range_start_` and `selected_range_end_` iff both are
// still in existence during the restoration process.
if (selected_range_start && selected_range_end) {
delegate_->selected_range_start_ = selected_range_start;
delegate_->selected_range_end_ = selected_range_end;
break;
}
}
}
// HoldingSpaceViewDelegate ----------------------------------------------------
HoldingSpaceViewDelegate::HoldingSpaceViewDelegate(
HoldingSpaceTrayBubble* bubble)
: bubble_(bubble) {
DCHECK_EQ(nullptr, instance);
instance = this;
// Multi-select is the only selection UI in tablet mode. Outside of tablet
// mode, selection UI is based on the `selection_size_`.
selection_ui_ = display::Screen::GetScreen()->InTabletMode()
? SelectionUi::kMultiSelect
: SelectionUi::kSingleSelect;
}
HoldingSpaceViewDelegate::~HoldingSpaceViewDelegate() {
DCHECK_EQ(instance, this);
instance = nullptr;
}
void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewCreated(
HoldingSpaceItemView* view) {
if (view->selected()) {
++selection_size_;
UpdateSelectionUi();
}
}
void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewDestroying(
HoldingSpaceItemView* view) {
// If either endpoint of the selected range is destroyed, clear the cache so
// that the next range-based selection attempt will start from scratch.
if (selected_range_start_ == view || selected_range_end_ == view) {
selected_range_start_ = nullptr;
selected_range_end_ = nullptr;
}
if (view->selected()) {
--selection_size_;
UpdateSelectionUi();
}
}
bool HoldingSpaceViewDelegate::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);
OpenItemsAndScheduleClose(
GetSelection(), holding_space_metrics::EventSource::kHoldingSpaceItem);
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;
}
bool HoldingSpaceViewDelegate::OnHoldingSpaceItemViewGestureEvent(
HoldingSpaceItemView* view,
const ui::GestureEvent& event) {
// The user may alternate between using mouse and touch inputs. Treat gesture
// events as mouse events when tracking range-based selection so that if
// the user switches back to using mouse input, selection state will be
// determined based on this most recent interaction with `view`.
selected_range_start_ = view;
selected_range_end_ = view;
// When a long press or two finger tap gesture occurs we are going to show the
// context menu. Ensure that the pressed `view` is part of the selection.
if (event.type() == ui::EventType::kGestureLongPress ||
event.type() == ui::EventType::kGestureTwoFingerTap) {
view->SetSelected(true);
return false;
}
// 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::EventType::kGestureScrollBegin &&
context_menu_runner_ && context_menu_runner_->IsRunning()) {
context_menu_runner_.reset();
view->StartDrag(event, ui::mojom::DragEventSource::kTouch);
return false;
}
if (event.type() != ui::EventType::kGestureTap) {
return false;
}
// When a tap gesture occurs and *no* views are currently selected, select and
// open the tapped `view`. Note that the tap `event` should *not* propagate
// further. Failure to halt propagation would result in the gesture reaching
// the child bubble which clears selection state.
if (GetSelection().empty()) {
SetSelection(view);
OpenItemsAndScheduleClose(
GetSelection(), holding_space_metrics::EventSource::kHoldingSpaceItem);
return true;
}
// When a tap gesture occurs and a selection *does* exist, the selected state
// of the tapped `view` is toggled. Note that the tap event should *not*
// propagate further. Failure to halt propagation would result in the gesture
// reaching the child bubble which clears selection state.
view->SetSelected(!view->selected());
return true;
}
bool HoldingSpaceViewDelegate::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);
OpenItemsAndScheduleClose(
GetSelection(), holding_space_metrics::EventSource::kHoldingSpaceItem);
return true;
}
return false;
}
bool HoldingSpaceViewDelegate::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 SHIFT is *not* pressed, set `view` as the starting point for range-based
// selection so that the next time the user shift-clicks, selection state
// will be updated in the range of `view` and the view being shift-clicked.
// Note that `view` is also set as the starting point if previously unset.
if (!event.IsShiftDown() || !selected_range_start_)
selected_range_start_ = view;
// When a `view` is pressed it becomes the new end for range-based selection.
// Note that this is performed in a scoped closure runner in order to give
// `SetSelectedRange()` a chance to run and clean up any previous range-based
// selection.
absl::Cleanup set_selected_range_end = [this, view] {
selected_range_end_ = view;
};
// If the SHIFT key is down, the user is attempting a range-based selection.
// Remove from the selection the previously selected range and instead add
// the newly selected range to the selection. Note that the next mouse
// released event on `view` is ignored if this is not a double click so that
// `view` isn't accidentally unselected right after having selected it. If
// this is a double click, the mouse released event will be handled to launch
// the selected holding space items.
if (event.IsShiftDown()) {
if (!(event.flags() & ui::EF_IS_DOUBLE_CLICK))
ignore_mouse_released_ = view;
SetSelectedRange(selected_range_start_, /*end=*/view);
return true;
}
// 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 CTRL 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` if
// this is not a double click so that we don't unselect `view` accidentally
// right after having selected it. If this is a double click, the mouse
// released event will be handled to launch the selected holding space items.
if (event.IsControlDown()) {
if (!(event.flags() & ui::EF_IS_DOUBLE_CLICK))
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 HoldingSpaceViewDelegate::OnHoldingSpaceItemViewMouseReleased(
HoldingSpaceItemView* view,
const ui::MouseEvent& event) {
// We should always clear `ignore_mouse_released_` since that property should
// affect at most one press/release sequence.
views::View* const old_ignore_mouse_released = ignore_mouse_released_;
ignore_mouse_released_ = nullptr;
// We might be ignoring mouse released events for `view` if it was just
// selected on mouse pressed. In this case, no-op here.
if (old_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 this mouse released `event` is part of a double click, we should open
// the items associated with the current selection. It is expected that the
// `view` being clicked is already part of the selection.
if (event.flags() & ui::EF_IS_DOUBLE_CLICK) {
DCHECK(view->selected());
OpenItemsAndScheduleClose(
GetSelection(), holding_space_metrics::EventSource::kHoldingSpaceItem);
return;
}
// If the CTRL key is down, mouse release should toggle the selected state of
// `view`. It's possible that the current selection be empty after doing so.
if (event.IsControlDown()) {
view->SetSelected(!view->selected());
return;
}
// This mouse released `event` is not part of a double click, nor were there
// any modifiers which resulted in special handling. In this case, the `view`
// under the mouse should become the only selected view.
SetSelection(view);
}
void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewPrimaryActionPressed(
HoldingSpaceItemView* view) {
if (!view->selected())
ClearSelection();
}
void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewSecondaryActionPressed(
HoldingSpaceItemView* view) {
if (!view->selected())
ClearSelection();
}
void HoldingSpaceViewDelegate::OnHoldingSpaceItemViewSelectedChanged(
HoldingSpaceItemView* view) {
selection_size_ += view->selected() ? 1 : -1;
UpdateSelectionUi();
}
bool HoldingSpaceViewDelegate::OnHoldingSpaceTrayBubbleKeyPressed(
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()) {
OpenItemsAndScheduleClose(
GetSelection(),
holding_space_metrics::EventSource::kHoldingSpaceBubble);
return true;
}
}
return false;
}
void HoldingSpaceViewDelegate::OnHoldingSpaceTrayChildBubbleGestureEvent(
const ui::GestureEvent& event) {
if (event.type() == ui::EventType::kGestureTap) {
ClearSelection();
}
}
void HoldingSpaceViewDelegate::OnHoldingSpaceTrayChildBubbleMousePressed(
const ui::MouseEvent& event) {
ClearSelection();
}
base::RepeatingClosureList::Subscription
HoldingSpaceViewDelegate::AddSelectionUiChangedCallback(
base::RepeatingClosureList::CallbackType callback) {
return selection_ui_changed_callbacks_.Add(std::move(callback));
}
void HoldingSpaceViewDelegate::UpdateTrayVisibility() {
bubble_->tray()->UpdateVisibility();
}
void HoldingSpaceViewDelegate::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::EventType::kGestureLongTap`
// and `ui::EventType::kGestureLongPress` 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_ASH_SYS_UI_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);
context_menu_runner_->RunMenuAt(
source->GetWidget(), nullptr /*button_controller*/,
source->GetBoundsInScreen(),
views::MenuAnchorPosition::kBubbleBottomRight, source_type);
}
bool HoldingSpaceViewDelegate::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 HoldingSpaceViewDelegate::GetDragOperationsForView(
views::View* sender,
const gfx::Point& press_pt) {
return ui::DragDropTypes::DRAG_COPY;
}
void HoldingSpaceViewDelegate::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,
holding_space_metrics::EventSource::kHoldingSpaceItem);
// Drag image.
gfx::ImageSkia drag_image;
gfx::Vector2d drag_offset;
holding_space_util::CreateDragImage(
selection, &drag_image, &drag_offset,
bubble_->GetBubbleView()->GetColorProvider());
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().file_path;
filenames.push_back(ui::FileInfo(file_path, file_path.BaseName()));
}
data->SetFilenames(filenames);
}
void HoldingSpaceViewDelegate::ExecuteCommand(int command, int event_flags) {
const std::vector<const HoldingSpaceItem*> items(GetItems(GetSelection()));
DCHECK_GE(items.size(), 1u);
const auto command_id = static_cast<HoldingSpaceCommandId>(command);
HoldingSpaceClient* const client = HoldingSpaceController::Get()->client();
switch (command_id) {
case HoldingSpaceCommandId::kCopyImageToClipboard:
DCHECK_EQ(items.size(), 1u);
client->CopyImageToClipboard(
*items.front(),
holding_space_metrics::EventSource::kHoldingSpaceItemContextMenu,
base::DoNothing());
break;
case HoldingSpaceCommandId::kPinItem:
client->PinItems(
items,
holding_space_metrics::EventSource::kHoldingSpaceItemContextMenu);
break;
case HoldingSpaceCommandId::kRemoveItem: {
std::vector<base::FilePath> suggested_file_paths;
HoldingSpaceController::Get()->model()->RemoveIf(base::BindRepeating(
[](const std::vector<const HoldingSpaceItem*>& items,
std::vector<base::FilePath>& suggested_file_paths,
const HoldingSpaceItem* item) {
const bool remove = base::Contains(items, item);
if (remove) {
if (HoldingSpaceItem::IsSuggestionType(item->type())) {
suggested_file_paths.push_back(item->file().file_path);
}
holding_space_metrics::RecordItemAction(
{item}, holding_space_metrics::ItemAction::kRemove,
holding_space_metrics::EventSource::
kHoldingSpaceItemContextMenu);
}
return remove;
},
std::cref(items), std::ref(suggested_file_paths)));
HoldingSpaceController::Get()->client()->RemoveSuggestions(
suggested_file_paths);
break;
}
case HoldingSpaceCommandId::kShowInFolder:
DCHECK_EQ(items.size(), 1u);
client->ShowItemInFolder(
*items.front(),
holding_space_metrics::EventSource::kHoldingSpaceItemContextMenu,
base::DoNothing());
break;
case HoldingSpaceCommandId::kUnpinItem:
client->UnpinItems(
items,
holding_space_metrics::EventSource::kHoldingSpaceItemContextMenu);
break;
default:
if (holding_space_util::IsInProgressCommand(command_id)) {
for (const HoldingSpaceItem* item : items) {
if (!holding_space_util::ExecuteInProgressCommand(
item, command_id,
holding_space_metrics::EventSource::
kHoldingSpaceItemContextMenu)) {
NOTREACHED();
}
}
} else {
NOTREACHED();
}
break;
}
}
void HoldingSpaceViewDelegate::OnDisplayTabletStateChanged(
display::TabletState state) {
if (state == display::TabletState::kInClamshellMode ||
state == display::TabletState::kInTabletMode) {
UpdateSelectionUi();
}
}
ui::SimpleMenuModel* HoldingSpaceViewDelegate::BuildMenuModel() {
context_menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
std::vector<const HoldingSpaceItemView*> selection = GetSelection();
DCHECK_GE(selection.size(), 1u);
// Whether any item in `selection` is complete.
bool is_any_item_complete = false;
// Whether all items in `selection` are removable.
bool is_removable = true;
// A value for `is_pinnable` will only be present if the `selection` contains
// at least one holding space item which is *not* in-progress. In-progress
// items are ignored with respect to pin-/unpin-ability.
std::optional<bool> is_pinnable;
// A value for `in_progress_commands` will only be present if the `selection`
// does *not* contain any items which are complete.
std::optional<std::vector<HoldingSpaceItem::InProgressCommand>>
in_progress_commands;
HoldingSpaceModel* const model = HoldingSpaceController::Get()->model();
for (const HoldingSpaceItemView* view : selection) {
const HoldingSpaceItem* item = view->item();
// In-progress commands are only available if supported by the entire
// `selection`. In-progress commands supported by only a subset of the
// `selection` are removed.
if (!item->progress().IsComplete() && !is_any_item_complete) {
if (!in_progress_commands.has_value()) {
in_progress_commands = item->in_progress_commands();
} else {
std::erase_if(in_progress_commands.value(),
[&](const HoldingSpaceItem::InProgressCommand&
in_progress_command) {
return !holding_space_util::SupportsInProgressCommand(
item, in_progress_command.command_id);
});
}
} else {
in_progress_commands = std::nullopt;
is_any_item_complete = true;
}
// The "Remove" command should only be present if *all* of the selected
// holding space items are removable.
is_removable &= item->type() != HoldingSpaceItem::Type::kPinnedFile &&
item->progress().IsComplete();
// In-progress holding space items are ignored with respect to the pin-/
// unpin-ability of the `selection`.
if (!item->progress().IsComplete())
continue;
// 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.
is_pinnable = is_pinnable.value_or(false) ||
!model->ContainsItem(HoldingSpaceItem::Type::kPinnedFile,
item->file().file_path);
}
struct MenuItemModel {
const HoldingSpaceCommandId command_id;
const int label_id;
const raw_ref<const gfx::VectorIcon> icon;
};
using MenuSectionModel = std::vector<MenuItemModel>;
std::vector<MenuSectionModel> menu_sections(1);
// In-progress commands.
if (in_progress_commands.has_value()) {
for (const HoldingSpaceItem::InProgressCommand& in_progress_command :
in_progress_commands.value()) {
// `kOpenItem` is not accessible from the context menu.
if (in_progress_command.command_id != HoldingSpaceCommandId::kOpenItem) {
menu_sections.back().emplace_back(
MenuItemModel{.command_id = in_progress_command.command_id,
.label_id = in_progress_command.label_id,
.icon = raw_ref(*in_progress_command.icon)});
}
}
}
// The in-progress commands are separated from other commands.
if (!menu_sections.back().empty())
menu_sections.emplace_back();
if (selection.size() == 1u) {
// The "Show in folder" command should only be present if there is only one
// holding space item selected.
menu_sections.back().emplace_back(MenuItemModel{
.command_id = HoldingSpaceCommandId::kShowInFolder,
.label_id = IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_SHOW_IN_FOLDER,
.icon = raw_ref(kFolderIcon)});
std::string ext = selection.front()->item()->file().file_path.Extension();
std::string mime_type;
const bool is_image =
!ext.empty() &&
net::GetWellKnownMimeTypeFromExtension(ext.substr(1), &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.
menu_sections.back().emplace_back(MenuItemModel{
.command_id = HoldingSpaceCommandId::kCopyImageToClipboard,
.label_id =
IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_COPY_IMAGE_TO_CLIPBOARD,
.icon = raw_ref(kCopyIcon)});
}
}
if (is_pinnable.has_value()) {
if (is_pinnable.value()) {
menu_sections.back().emplace_back(
MenuItemModel{.command_id = HoldingSpaceCommandId::kPinItem,
.label_id = IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_PIN,
.icon = raw_ref(views::kPinIcon)});
} else {
menu_sections.back().emplace_back(
MenuItemModel{.command_id = HoldingSpaceCommandId::kUnpinItem,
.label_id = IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_UNPIN,
.icon = raw_ref(views::kUnpinIcon)});
}
}
if (is_removable) {
menu_sections.back().emplace_back(
MenuItemModel{.command_id = HoldingSpaceCommandId::kRemoveItem,
.label_id = IDS_ASH_HOLDING_SPACE_CONTEXT_MENU_REMOVE,
.icon = raw_ref(kCancelCircleOutlineIcon)});
}
// Add modeled `menu_sections` to the `context_menu_model_`.
for (const MenuSectionModel& menu_section : menu_sections) {
if (menu_section.empty())
continue;
// Each `menu_section` should be separated by a normal separator.
if (context_menu_model_->GetItemCount()) {
context_menu_model_->AddSeparator(
ui::MenuSeparatorType::NORMAL_SEPARATOR);
}
// Each `menu_section` should contain their respective `menu_item`s.
for (const MenuItemModel& menu_item : menu_section) {
context_menu_model_->AddItemWithIcon(
static_cast<int>(menu_item.command_id),
l10n_util::GetStringUTF16(menu_item.label_id),
ui::ImageModel::FromVectorIcon(*menu_item.icon,
ui::kColorAshSystemUIMenuIcon,
kHoldingSpaceIconSize));
}
}
return context_menu_model_.get();
}
std::vector<const HoldingSpaceItemView*>
HoldingSpaceViewDelegate::GetSelection() {
std::vector<const HoldingSpaceItemView*> selection;
if (bubble_) { // Maybe be `nullptr` in testing.
for (const HoldingSpaceItemView* view :
bubble_->GetHoldingSpaceItemViews()) {
if (view->selected())
selection.push_back(view);
}
}
DCHECK_EQ(selection.size(), selection_size_);
return selection;
}
void HoldingSpaceViewDelegate::ClearSelection() {
SetSelection(std::vector<std::string>());
}
void HoldingSpaceViewDelegate::SetSelection(HoldingSpaceItemView* selection) {
SetSelection({selection->item_id()});
}
void HoldingSpaceViewDelegate::SetSelection(
const std::vector<std::string>& item_ids) {
std::vector<HoldingSpaceItemView*> selection;
if (bubble_) { // May be `nullptr` in testing.
for (HoldingSpaceItemView* view : bubble_->GetHoldingSpaceItemViews()) {
view->SetSelected(base::Contains(item_ids, view->item_id()));
if (view->selected())
selection.push_back(view);
}
}
if (selection.size() == 1u) {
selected_range_start_ = selection.front();
selected_range_end_ = selection.front();
} else {
selected_range_start_ = nullptr;
selected_range_end_ = nullptr;
}
}
void HoldingSpaceViewDelegate::SetSelectedRange(HoldingSpaceItemView* start,
HoldingSpaceItemView* end) {
const std::vector<HoldingSpaceItemView*> views =
bubble_->GetHoldingSpaceItemViews();
for (HoldingSpaceItemView* view :
GetViewsInRange(views, selected_range_start_, selected_range_end_)) {
view->SetSelected(false);
}
selected_range_start_ = start;
selected_range_end_ = end;
for (HoldingSpaceItemView* view :
GetViewsInRange(views, selected_range_start_, selected_range_end_)) {
view->SetSelected(true);
}
}
void HoldingSpaceViewDelegate::UpdateSelectionUi() {
const SelectionUi selection_ui =
display::Screen::GetScreen()->InTabletMode() || selection_size_ > 1u
? SelectionUi::kMultiSelect
: SelectionUi::kSingleSelect;
if (selection_ui_ == selection_ui)
return;
selection_ui_ = selection_ui;
selection_ui_changed_callbacks_.Notify();
}
void HoldingSpaceViewDelegate::OpenItemsAndScheduleClose(
const std::vector<const HoldingSpaceItemView*>& views,
holding_space_metrics::EventSource event_source) {
DCHECK_GE(views.size(), 1u);
// This `PostTask()` will result in the destruction of the view delegate if it
// has not already been destroyed.
base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE,
base::BindOnce(
[](const base::WeakPtr<HoldingSpaceViewDelegate>& weak_ptr) {
if (weak_ptr)
weak_ptr->bubble_->tray()->CloseBubble();
},
weak_factory_.GetMutableWeakPtr()));
HoldingSpaceController::Get()->client()->OpenItems(
GetItems(views), event_source, base::DoNothing());
}
} // namespace ash