// Copyright 2012 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/app_list/views/apps_grid_view.h"
#include <algorithm>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "ash/app_list/app_list_item_util.h"
#include "ash/app_list/app_list_metrics.h"
#include "ash/app_list/app_list_model_provider.h"
#include "ash/app_list/app_list_util.h"
#include "ash/app_list/app_list_view_delegate.h"
#include "ash/app_list/apps_grid_row_change_animator.h"
#include "ash/app_list/grid_index.h"
#include "ash/app_list/model/app_list_folder_item.h"
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/model/app_list_model.h"
#include "ash/app_list/views/app_list_a11y_announcer.h"
#include "ash/app_list/views/app_list_drag_and_drop_host.h"
#include "ash/app_list/views/app_list_folder_controller.h"
#include "ash/app_list/views/app_list_item_view.h"
#include "ash/app_list/views/app_list_keyboard_controller.h"
#include "ash/app_list/views/app_list_view_util.h"
#include "ash/app_list/views/apps_grid_context_menu.h"
#include "ash/app_list/views/apps_grid_view_folder_delegate.h"
#include "ash/app_list/views/ghost_image_view.h"
#include "ash/app_list/views/pulsing_block_view.h"
#include "ash/constants/ash_features.h"
#include "ash/drag_drop/drag_drop_controller.h"
#include "ash/public/cpp/app_list/app_list_config.h"
#include "ash/public/cpp/app_list/app_list_features.h"
#include "ash/public/cpp/app_list/app_list_types.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/public/cpp/shelf_types.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/notreached.h"
#include "base/ranges/algorithm.h"
#include "base/time/time.h"
#include "ui/aura/window.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.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/animation/animation_sequence_block.h"
#include "ui/views/controls/label.h"
#include "ui/views/view_observer.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
// Distance a drag needs to be from the app grid to be considered 'outside', at
// which point we rearrange the apps to their pre-drag configuration, as a drop
// then would be canceled. We have a buffer to make it easier to drag apps to
// other pages.
constexpr int kDragBufferPx = 20;
// Time delay before shelf starts to handle icon drag operation,
// such as shelf icons re-layout.
constexpr base::TimeDelta kShelfHandleIconDragDelay = base::Milliseconds(500);
// The drag and drop proxy should get scaled by this factor.
constexpr float kDragAndDropProxyScale = 1.2f;
// Delays in milliseconds to show re-order preview.
constexpr int kReorderDelay = 120;
// Delays in milliseconds to show folder item reparent UI.
constexpr int kFolderItemReparentDelay = 50;
// Maximum vertical and horizontal spacing between tiles.
constexpr int kMaximumTileSpacing = 96;
// Maximum horizontal spacing between tiles.
constexpr int kMaximumHorizontalTileSpacing = 128;
// The ratio of the slide offset to the tile size.
constexpr float kFadeAnimationOffsetRatio = 0.25f;
// The time duration of the fade in animation used for apps grid reorder.
constexpr base::TimeDelta kFadeInAnimationDuration = base::Milliseconds(400);
// The time duration of the fade out animation used for apps grid reorder.
constexpr base::TimeDelta kFadeOutAnimationDuration = base::Milliseconds(100);
// Constants for folder item view relocation animation - the animation runs
// after closing a folder view if the shown folder item view location within the
// apps grid changed while the folder view was open.
// The folder view animates in the old folder item location, then the folder
// item view animates out at the old location, other items move into their
// correct spot, and after a delay, the folder item view animates into its new
// location.
//
// The duration of the folder item view fade out animation.
constexpr base::TimeDelta kFolderItemFadeOutDuration = base::Milliseconds(100);
// The duraction of the folder item view fade in animation.
constexpr base::TimeDelta kFolderItemFadeInDuration = base::Milliseconds(300);
// The delay for starting the folder item view fade in after the item view was
// faded out.
constexpr base::TimeDelta kFolderItemFadeInDelay = base::Milliseconds(300);
// The base time duration for item bounds animations.
constexpr base::TimeDelta kItemBoundsBaseAnimationDuration =
base::Milliseconds(300);
// The additional time duration for each subsequent row/slot used for creating a
// cascading item bounds animation.
constexpr base::TimeDelta kItemBoundsAnimationOffsetDuration =
base::Milliseconds(50);
bool IsOEMFolderItem(AppListItem* item) {
return IsFolderItem(item) && item->AsFolderItem()->folder_type() ==
AppListFolderItem::FOLDER_TYPE_OEM;
}
// Apply `transform` to `bounds` at an origin of (0,0) so that the scaling
// part of the transform does not modify the position or size.
gfx::Rect ApplyTransformAtOrigin(const gfx::Rect& in_bounds,
const gfx::Transform& transform) {
gfx::Rect out_bounds;
out_bounds = transform.MapRect(out_bounds);
out_bounds.Offset(in_bounds.OffsetFromOrigin());
out_bounds.set_size(in_bounds.size());
return out_bounds;
}
// Return the pointer that was used for generating the event from the event
// flags.
AppsGridView::Pointer GetPointerTypeForDragAndDrop() {
if (Shell::Get()->drag_drop_controller()->event_source() ==
ui::mojom::DragEventSource::kMouse) {
return AppsGridView::MOUSE;
}
return AppsGridView::TOUCH;
}
} // namespace
// static
constexpr int AppsGridView::kDefaultAnimationDuration;
// AppsGridView::VisibleItemIndexRange -----------------------------------------
AppsGridView::VisibleItemIndexRange::VisibleItemIndexRange() = default;
AppsGridView::VisibleItemIndexRange::VisibleItemIndexRange(size_t first_index,
size_t last_index)
: first_index(first_index), last_index(last_index) {}
AppsGridView::VisibleItemIndexRange::~VisibleItemIndexRange() = default;
// AppsGridView::FolderIconItemHider -------------------------------------------
// Class used to hide an icon depicting an app list item from an folder item
// icon image (which contains images of top app items in the folder).
// Used during drag icon drop animation to hide the dragged item from the folder
// icon (if the item is being dropped into a folder) while the drag icon is
// still visible.
// It gracefully handles the folder item getting deleted before the
// `FolderIconItemHider` instance gets reset, so it should be safe to use in
// asynchronous manner without extra folder item existence checks.
class AppsGridView::FolderIconItemHider : public AppListItemObserver,
public views::ViewObserver {
public:
FolderIconItemHider(AppListItemView* folder_item_view,
AppListItem* item_icon_to_hide)
: item_view_(folder_item_view),
folder_item_(folder_item_view->item()->AsFolderItem()) {
item_view_->AddObserver(this);
// Notify the folder item that `item_icon_to_hide` is being dragged, so the
// dragged item is ignored while generating the folder icon image. This
// effectively hides the drag item image from the overall folder icon.
item_view_->UpdateDraggedItem(item_icon_to_hide);
folder_item_->NotifyOfDraggedItem(item_icon_to_hide);
folder_item_observer_.Observe(folder_item_.get());
}
~FolderIconItemHider() override {
if (item_view_) {
item_view_->RemoveObserver(this);
item_view_->UpdateDraggedItem(nullptr);
}
if (folder_item_) {
folder_item_->NotifyOfDraggedItem(nullptr);
}
}
// views::ViewObserver:
void OnViewIsDeleting(views::View* observed_view) override {
DCHECK_EQ(item_view_, observed_view);
item_view_ = nullptr;
folder_item_ = nullptr;
folder_item_observer_.Reset();
}
// AppListItemObserver:
void ItemBeingDestroyed() override {
item_view_->RemoveObserver(this);
item_view_ = nullptr;
folder_item_ = nullptr;
folder_item_observer_.Reset();
}
private:
// The item view of `folder_item_`;
raw_ptr<AppListItemView> item_view_;
raw_ptr<AppListFolderItem> folder_item_;
base::ScopedObservation<AppListItem, AppListItemObserver>
folder_item_observer_{this};
};
// Class that while in scope hides a drag view in such way that the drag view
// keeps receiving mouse/gesture events. Used to hide the dragged view while a
// drag icon proxy for the drag item is shown. It gracefully handles the case
// where it outlives the hidden dragged view, so it should be safe to be used
// asynchronously without extra view existence checks.
class AppsGridView::DragViewHider : public views::ViewObserver {
public:
explicit DragViewHider(AppListItemView* drag_view) : drag_view_(drag_view) {
DCHECK(drag_view_->layer());
drag_view_->layer()->SetOpacity(0.0f);
view_observer_.Observe(drag_view_.get());
}
~DragViewHider() override {
if (drag_view_ && drag_view_->layer())
drag_view_->layer()->SetOpacity(1.0f);
}
// views::ViewObserver:
void OnViewIsDeleting(views::View* view) override {
drag_view_ = nullptr;
view_observer_.Reset();
}
const views::View* drag_view() const { return drag_view_; }
private:
raw_ptr<AppListItemView> drag_view_;
base::ScopedObservation<views::View, views::ViewObserver> view_observer_{
this};
};
// Class used by AppsGridView to track whether app list model is being updated
// by the AppsGridView (by setting `updating_model_`). While this is in scope:
// (1) Do not cancel in progress drag due to app list model changes, and
// (2) Delay `view_structure_` sanitization until the app list model update
// finishes, and
// (3) Ignore apps grid layout
class AppsGridView::ScopedModelUpdate {
public:
explicit ScopedModelUpdate(AppsGridView* apps_grid_view)
: apps_grid_view_(apps_grid_view),
initial_grid_size_(apps_grid_view_->GetTileGridSize()) {
DCHECK(!apps_grid_view_->updating_model_);
apps_grid_view_->updating_model_ = true;
// One model update may elicit multiple changes on apps grid layout. For
// example, moving one item out of a folder may empty the parent folder then
// have the folder deleted. Therefore ignore layout when `ScopedModelUpdate`
// is in the scope to avoid handling temporary layout.
DCHECK(!apps_grid_view_->ignore_layout_);
apps_grid_view_->ignore_layout_ = true;
}
ScopedModelUpdate(const ScopedModelUpdate&) = delete;
ScopedModelUpdate& operator=(const ScopedModelUpdate&) = delete;
~ScopedModelUpdate() {
DCHECK(apps_grid_view_->updating_model_);
apps_grid_view_->updating_model_ = false;
DCHECK(apps_grid_view_->ignore_layout_);
apps_grid_view_->ignore_layout_ = false;
// Perform update for the final layout.
apps_grid_view_->ScheduleLayout(initial_grid_size_);
}
private:
const raw_ptr<AppsGridView> apps_grid_view_;
const gfx::Size initial_grid_size_;
};
// An implicit animation observer that runs a callback to restore the grid after
// the animation is done.
class AnimationObserverToRestoreGrid : public ui::ImplicitAnimationObserver {
public:
explicit AnimationObserverToRestoreGrid(base::OnceClosure cb)
: animation_completion_callback_(std::move(cb)) {}
~AnimationObserverToRestoreGrid() override {
// Required due to RequiresNotificationWhenAnimatorDestroyed() returning
// true.
StopObservingImplicitAnimations();
}
// ui::ImplicitAnimationObserver:
void OnImplicitAnimationsCompleted() override {
if (animation_completion_callback_) {
std::move(animation_completion_callback_).Run();
}
delete this;
}
bool RequiresNotificationWhenAnimatorDestroyed() const override {
return true;
}
private:
base::OnceClosure animation_completion_callback_;
};
AppsGridView::AppsGridView(AppListA11yAnnouncer* a11y_announcer,
AppListViewDelegate* app_list_view_delegate,
AppsGridViewFolderDelegate* folder_delegate,
AppListFolderController* folder_controller,
AppListKeyboardController* keyboard_controller)
: folder_delegate_(folder_delegate),
folder_controller_(folder_controller),
a11y_announcer_(a11y_announcer),
app_list_view_delegate_(app_list_view_delegate),
keyboard_controller_(keyboard_controller) {
DCHECK(a11y_announcer_);
DCHECK(app_list_view_delegate_);
// Top-level grids must have a folder controller.
if (!folder_delegate_) {
DCHECK(folder_controller_);
}
SetPaintToLayer(ui::LAYER_NOT_DRAWN);
items_container_ = AddChildView(std::make_unique<views::View>());
items_container_->SetPaintToLayer();
items_container_->layer()->SetFillsBoundsOpaquely(false);
GetViewAccessibility().SetRole(ax::mojom::Role::kGroup);
// Override the a11y name of top level apps grid.
if (!folder_delegate) {
GetViewAccessibility().SetName(
l10n_util::GetStringUTF16(IDS_ASH_LAUNCHER_APPS_GRID_A11Y_NAME),
ax::mojom::NameFrom::kAttribute);
}
if (!IsTabletMode()) {
// `context_menu_` is only set in clamshell mode. The sort options in tablet
// mode are handled in RootWindowController with ShelfContextMenuModel.
context_menu_ = std::make_unique<AppsGridContextMenu>(
AppsGridContextMenu::GridType::kAppsGrid);
set_context_menu_controller(context_menu_.get());
}
row_change_animator_ = std::make_unique<AppsGridRowChangeAnimator>(this);
}
AppsGridView::~AppsGridView() {
// Coming here |drag_view_| should already be canceled since otherwise the
// drag would disappear after the app list got animated away and closed,
// which would look odd.
DCHECK(!drag_item_);
if (model_) {
model_->RemoveObserver(this);
}
if (item_list_) {
item_list_->RemoveObserver(this);
}
set_context_menu_controller(nullptr);
// Abort reorder animation before `view_model_` is cleared.
MaybeAbortWholeGridAnimation();
// Reset `folder_icon_item_hider_` before clearing the view model to prevent
// accessing the AppListItemView after it is deleted.
folder_icon_item_hider_.reset();
view_model_.Clear();
pulsing_blocks_model_.Clear();
RemoveAllChildViews();
folder_to_open_after_drag_icon_animation_.clear();
// To prevent a call to |OnDragIconDropDone()| from an existing drag image
// animation.
weak_factory_.InvalidateWeakPtrs();
drag_icon_proxy_.reset();
drag_image_layer_.reset();
}
void AppsGridView::UpdateAppListConfig(const AppListConfig* app_list_config) {
app_list_config_ = app_list_config;
// The app list item view icon sizes depend on the app list config, so they
// have to be refreshed.
for (size_t i = 0; i < view_model_.view_size(); ++i)
view_model_.view_at(i)->UpdateAppListConfig(app_list_config);
if (current_ghost_view_) {
CreateGhostImageView();
}
}
void AppsGridView::SetFixedTilePadding(int horizontal_padding,
int vertical_padding) {
has_fixed_tile_padding_ = true;
horizontal_tile_padding_ = horizontal_padding;
vertical_tile_padding_ = vertical_padding;
}
gfx::Size AppsGridView::GetTotalTileSize(int page) const {
gfx::Rect rect(GetTileViewSize());
rect.Inset(GetTilePadding(page));
return rect.size();
}
gfx::Size AppsGridView::GetMinimumTileGridSize(int cols,
int rows_per_page) const {
const gfx::Size tile_size = GetTileViewSize();
return gfx::Size(tile_size.width() * cols,
tile_size.height() * rows_per_page);
}
gfx::Size AppsGridView::GetMaximumTileGridSize(int cols,
int rows_per_page) const {
const gfx::Size tile_size = GetTileViewSize();
return gfx::Size(
tile_size.width() * cols + kMaximumHorizontalTileSpacing * (cols - 1),
tile_size.height() * rows_per_page +
kMaximumTileSpacing * (rows_per_page - 1));
}
void AppsGridView::ResetForShowApps() {
CancelDragWithNoDropAnimation();
layer()->SetOpacity(1.0f);
SetVisible(true);
// The number of model items should be the same as item views.
if (item_list_) {
CHECK_EQ(item_list_->item_count(), view_model_.view_size());
}
}
void AppsGridView::EndDragCallback(
const ui::DropTargetEvent& event,
ui::mojom::DragOperation& output_drag_op,
std::unique_ptr<ui::LayerTreeOwner> drag_image_layer_owner) {
DCHECK(app_list_features::IsDragAndDropRefactorEnabled());
output_drag_op = ui::mojom::DragOperation::kMove;
if (drag_item_) {
drag_image_layer_ = std::move(drag_image_layer_owner);
EndDrag(/*cancel=*/false);
}
}
void AppsGridView::CancelDragWithNoDropAnimation() {
EndDrag(/*cancel=*/true);
drag_view_hider_.reset();
folder_icon_item_hider_.reset();
if (!folder_to_open_after_drag_icon_animation_.empty()) {
open_folder_info_.reset();
}
folder_to_open_after_drag_icon_animation_.clear();
drag_icon_proxy_.reset();
drag_image_layer_.reset();
}
void AppsGridView::DisableFocusForShowingActiveFolder(bool disabled) {
for (const auto& entry : view_model_.entries())
entry.view->SetEnabled(!disabled);
// Ignore the grid view in accessibility tree so that items inside it will not
// be accessed by ChromeVox.
SetViewIgnoredForAccessibility(this, disabled);
}
void AppsGridView::SetModel(AppListModel* model) {
if (model_) {
model_->RemoveObserver(this);
}
model_ = model;
if (model_) {
model_->AddObserver(this);
}
Update();
}
void AppsGridView::SetItemList(AppListItemList* item_list) {
DCHECK_GT(cols_, 0);
DCHECK(app_list_config_);
if (item_list_) {
item_list_->RemoveObserver(this);
}
item_list_ = item_list;
if (item_list_) {
item_list_->AddObserver(this);
}
Update();
}
bool AppsGridView::IsInFolder() const {
return !!folder_delegate_;
}
void AppsGridView::SetSelectedView(AppListItemView* view) {
if (IsSelectedView(view) || IsDraggedView(view)) {
return;
}
GridIndex index = GetIndexOfView(view);
if (IsValidIndex(index)) {
SetSelectedItemByIndex(index);
}
}
void AppsGridView::ClearSelectedView() {
selected_view_ = nullptr;
}
bool AppsGridView::IsSelectedView(const AppListItemView* view) const {
return selected_view_ == view;
}
bool AppsGridView::InitiateDrag(AppListItemView* view,
const gfx::Point& location,
const gfx::Point& root_location,
base::OnceClosure drag_start_callback,
base::OnceClosure drag_end_callback) {
DCHECK(!app_list_features::IsDragAndDropRefactorEnabled());
DCHECK(view);
if (drag_item_ || pulsing_blocks_model_.view_size()) {
return false;
}
DVLOG(1) << "Initiate drag";
drag_start_callback_ = std::move(drag_start_callback);
drag_end_callback_ = std::move(drag_end_callback);
// Finalize previous drag icon animation if it's still in progress.
drag_view_hider_.reset();
folder_icon_item_hider_.reset();
if (!folder_to_open_after_drag_icon_animation_.empty()) {
open_folder_info_.reset();
}
folder_to_open_after_drag_icon_animation_.clear();
drag_icon_proxy_.reset();
PrepareItemsForBoundsAnimation();
drag_view_ = view;
drag_item_ = view->item();
// Dragged view should have focus. This also fixed the issue
// https://crbug.com/834682.
drag_view_->RequestFocus();
drag_view_init_index_ = GetIndexOfView(drag_view_);
reorder_placeholder_ = drag_view_init_index_;
ExtractDragLocation(root_location, &drag_start_grid_view_);
return true;
}
void AppsGridView::StartDragAndDropHostDragAfterLongPress() {
TryStartDragAndDropHostDrag(TOUCH);
}
void AppsGridView::TryStartDragAndDropHostDrag(Pointer pointer) {
// Stopping the animation may have invalidated our drag view due to the
// view hierarchy changing.
if (!drag_item_) {
return;
}
drag_pointer_ = pointer;
if (!dragging_for_reparent_item_) {
StartDragAndDropHostDrag();
}
if (drag_start_callback_) {
std::move(drag_start_callback_).Run();
}
}
bool AppsGridView::UpdateDragFromItem(bool is_touch,
const ui::LocatedEvent& event) {
DCHECK(!app_list_features::IsDragAndDropRefactorEnabled());
if (!drag_item_) {
return false; // Drag canceled.
}
gfx::Point drag_point_in_grid_view;
ExtractDragLocation(event.root_location(), &drag_point_in_grid_view);
const Pointer pointer = is_touch ? TOUCH : MOUSE;
UpdateDrag(pointer, drag_point_in_grid_view);
if (!IsDragging()) {
return false;
}
// If a drag and drop host is provided, see if the drag operation needs to be
// forwarded.
gfx::Point drag_point_in_screen = event.root_location();
::wm::ConvertPointToScreen(GetWidget()->GetNativeWindow()->GetRootWindow(),
&drag_point_in_screen);
DispatchDragEventToDragAndDropHost(drag_point_in_screen);
if (drag_icon_proxy_) {
drag_icon_proxy_->UpdatePosition(drag_point_in_screen);
}
return true;
}
void AppsGridView::UpdateDrag(Pointer pointer, const gfx::Point& point) {
if (folder_delegate_) {
UpdateDragStateInsideFolder(pointer, point);
}
if (!drag_item_) {
return; // Drag canceled.
}
// If folder is currently open from the grid, delay drag updates until the
// folder finishes closing.
if (open_folder_info_) {
// Only handle pointers that initiated the drag - e.g. ignore drag events
// that come from touch if a mouse drag is currently in progress.
if (drag_pointer_ == pointer) {
last_drag_point_ = point;
}
return;
}
gfx::Vector2d drag_vector(point - drag_start_grid_view_);
if (!app_list_features::IsDragAndDropRefactorEnabled()) {
if (ExceededDragThreshold(drag_vector)) {
if (!IsDragging()) {
TryStartDragAndDropHostDrag(pointer);
}
MaybeStartCardifiedView();
}
if (drag_pointer_ != pointer) {
return;
}
} else {
MaybeStartCardifiedView();
}
last_drag_point_ = point;
const GridIndex last_drop_target = drop_target_;
DropTargetRegion last_drop_target_region = drop_target_region_;
UpdateDropTargetRegion();
const bool has_page_flip = MaybeStartPageFlip();
const bool is_scrolling = MaybeAutoScroll();
if (is_scrolling || has_page_flip) {
// Don't do reordering while auto-scrolling, or flipping page, otherwise
// there is too much motion during the drag.
reorder_timer_.Stop();
// Reset the previous drop target.
if (last_drop_target_region == ON_ITEM)
SetAsFolderDroppingTarget(last_drop_target, false);
return;
}
if (last_drop_target != drop_target_ ||
last_drop_target_region != drop_target_region_) {
if (last_drop_target_region == ON_ITEM)
SetAsFolderDroppingTarget(last_drop_target, false);
if (drop_target_region_ == ON_ITEM && DraggedItemCanEnterFolder() &&
DropTargetIsValidFolder()) {
reorder_timer_.Stop();
MaybeCreateFolderDroppingAccessibilityEvent();
SetAsFolderDroppingTarget(drop_target_, true);
BeginHideCurrentGhostImageView();
} else if ((drop_target_region_ == ON_ITEM ||
drop_target_region_ == NEAR_ITEM) &&
!folder_delegate_) {
// If the drag changes regions from |BETWEEN_ITEMS| to |NEAR_ITEM| the
// timer should reset, so that we gain the extra time from hovering near
// the item
if (last_drop_target_region == BETWEEN_ITEMS) {
reorder_timer_.Stop();
}
reorder_timer_.Start(FROM_HERE, base::Milliseconds(kReorderDelay * 5),
this, &AppsGridView::OnReorderTimer);
} else if (drop_target_region_ != NO_TARGET) {
// If none of the above cases evaluated true, then all of the possible
// drop regions should result in a fast reorder.
reorder_timer_.Start(FROM_HERE, base::Milliseconds(kReorderDelay), this,
&AppsGridView::OnReorderTimer);
}
}
}
void AppsGridView::EndDrag(bool cancel) {
DVLOG(1) << "EndDrag cancel=" << cancel;
// EndDrag was called before if |drag_view_| is nullptr.
if (!drag_item_) {
return;
}
AppListItem* drag_item = drag_item_;
// Whether an icon was actually dragged (and not just clicked).
const bool was_dragging = IsDragging();
// Coming here a drag and drop was in progress.
const bool landed_in_drag_and_drop_host =
forward_events_to_drag_and_drop_host_;
// The ID of the folder to which the item gets dropped. It will get set when
// the item is moved to a folder.
std::string target_folder_id;
// The animation direction used for the ideal bounds animation.
bool top_to_bottom_animation = reorder_placeholder_ < drop_target_;
bool is_drag_drop_refactor_enabled =
app_list_features::IsDragAndDropRefactorEnabled();
if (forward_events_to_drag_and_drop_host_) {
DCHECK(!IsDraggingForReparentInRootLevelGridView() ||
!is_drag_drop_refactor_enabled);
forward_events_to_drag_and_drop_host_ = false;
// Pass the drag icon proxy on to the drag and drop host, so the drag and
// drop host handles the animation to drop the icon proxy into correct spot.
drag_and_drop_host_->EndDrag(cancel, std::move(drag_icon_proxy_));
if (!is_drag_drop_refactor_enabled &&
IsDraggingForReparentInHiddenGridView()) {
EndDragForReparentInHiddenFolderGridView();
folder_delegate_->DispatchEndDragEventForReparent(
true /* events_forwarded_to_drag_drop_host */,
cancel /* cancel_drag */, std::move(drag_icon_proxy_));
return;
}
} else {
if (IsDraggingForReparentInHiddenGridView()) {
EndDragForReparentInHiddenFolderGridView();
// Forward the EndDrag event to the root level grid view.
folder_delegate_->DispatchEndDragEventForReparent(
false /* events_forwarded_to_drag_drop_host */,
cancel /* cancel_drag */, std::move(drag_icon_proxy_));
return;
}
if (IsDraggingForReparentInRootLevelGridView()) {
// An EndDrag can be received during a reparent via a model change. This
// is always a cancel and needs to be forwarded to the folder.
if (cancel) {
DCHECK_EQ(!reparent_drag_cancellation_, is_drag_drop_refactor_enabled);
if (reparent_drag_cancellation_) {
std::move(reparent_drag_cancellation_).Run();
return;
}
} else {
UpdateDropTargetRegion();
EndDragFromReparentItemInRootLevel(nullptr, false, false, nullptr);
return;
}
}
if (!cancel && was_dragging) {
// Regular drag ending path, ie, not for reparenting.
UpdateDropTargetRegion();
if (drop_target_region_ == ON_ITEM && DraggedItemCanEnterFolder() &&
DropTargetIsValidFolder()) {
// Adding an item to a folder moves items similarly to moving it to the
// end of the list, so set as a top_to_bottom animation direction.
top_to_bottom_animation = true;
bool is_new_folder = false;
if (MoveItemToFolder(drag_item_, drop_target_, kMoveByDragIntoFolder,
&target_folder_id, &is_new_folder)) {
MaybeCreateFolderDroppingAccessibilityEvent();
if (is_new_folder) {
folder_to_open_after_drag_icon_animation_ = target_folder_id;
SetOpenFolderInfo(target_folder_id, drop_target_,
reorder_placeholder_);
}
// If item drag created a folder, layout the grid to ensure the
// created folder's bounds are correct. Note that `open_folder_info_`
// affects ideal item bounds, so `DeprecatedLayoutImmediately()` needs
// to be called after `SetOpenFolderInfo()`.
DeprecatedLayoutImmediately();
}
} else if (IsValidIndex(drop_target_)) {
// Ensure reorder event has already been announced by the end of drag.
MaybeCreateDragReorderAccessibilityEvent();
MoveItemInModel(drag_item_, drop_target_);
RecordAppMovingTypeMetrics(folder_delegate_ ? kReorderByDragInFolder
: kReorderByDragInTopLevel);
}
}
}
// Issue 439055: MoveItemToFolder() can sometimes delete |drag_view_|
if (drag_view_ && landed_in_drag_and_drop_host) {
// Move the item directly to the target location, avoiding the
// "zip back" animation if the user was pinning it to the shelf.
int i = drop_target_.slot;
gfx::Rect bounds = view_model_.ideal_bounds(i);
drag_view_->SetBoundsRect(bounds);
drag_view_hider_.reset();
}
SetAsFolderDroppingTarget(drop_target_, false);
ClearDragState();
UpdatePaging();
if (GetWidget()) {
// Normally layout cancels any animations. At this point there may be a
// pending layout, force it now so that one isn't triggered part way through
// the animation. Further, ignore this layout so that the position isn't
// reset.
DCHECK(!ignore_layout_);
base::AutoReset<bool> auto_reset(&ignore_layout_, true);
GetWidget()->LayoutRootViewIfNecessary();
}
if (cardified_state_)
MaybeEndCardifiedView();
else
AnimateToIdealBounds(top_to_bottom_animation);
if (!cancel) {
// Select the page where dragged item is dropped. Avoid doing so when the
// dragged item ends up in a folder.
const size_t model_index = GetModelIndexOfItem(drag_item);
if (model_index < view_model_.view_size())
EnsureViewVisible(GetGridIndexFromIndexInViewModel(model_index));
}
// Hide the |current_ghost_view_| for item drag that started
// within |apps_grid_view_|.
BeginHideCurrentGhostImageView();
if (was_dragging)
SetFocusAfterEndDrag(drag_item); // Maybe focus the search box.
AnimateDragIconToTargetPosition(drag_item, target_folder_id);
}
AppListItemView* AppsGridView::GetItemViewForItem(const std::string& item_id) {
const AppListItem* const item = item_list_->FindItem(item_id);
if (!item) {
return nullptr;
}
return GetItemViewAt(GetModelIndexOfItem(item));
}
AppListItemView* AppsGridView::GetItemViewAt(size_t index) const {
return (index < view_model_.view_size()) ? view_model_.view_at(index)
: nullptr;
}
void AppsGridView::InitiateDragFromReparentItemInRootLevelGridView(
Pointer pointer,
AppListItemView* original_drag_view,
const gfx::Point& drag_point,
base::OnceClosure cancellation_callback) {
DCHECK(!app_list_features::IsDragAndDropRefactorEnabled());
DVLOG(1) << __FUNCTION__;
DCHECK(original_drag_view && !drag_view_);
DCHECK(!dragging_for_reparent_item_);
const gfx::Size initial_grid_size = GetTileGridSize();
// Since the item is new, its placeholder is conceptually at the back of the
// entire apps grid.
reorder_placeholder_ =
GetGridIndexFromIndexInViewModel(view_model()->view_size());
PrepareItemsForBoundsAnimation();
drag_pointer_ = pointer;
drag_item_ = original_drag_view->item();
drag_start_grid_view_ = drag_point;
// Set the flag in root level grid view.
dragging_for_reparent_item_ = true;
reparent_drag_cancellation_ = std::move(cancellation_callback);
UpdatePaging();
if (GetTileGridSize() != initial_grid_size) {
PreferredSizeChanged();
}
}
void AppsGridView::UpdateDragFromReparentItem(Pointer pointer,
const gfx::Point& drag_point) {
// Note that if a cancel ocurrs while reparenting, the |drag_view_| in both
// root and folder grid views is cleared, so the check in UpdateDragFromItem()
// for |drag_view_| being nullptr (in the folder grid) is sufficient.
DCHECK(drag_item_);
DCHECK(IsDraggingForReparentInRootLevelGridView());
UpdateDrag(pointer, drag_point);
}
void AppsGridView::SetOpenFolderInfo(const std::string& folder_id,
const GridIndex& target_folder_position,
const GridIndex& position_to_skip) {
GridIndex expected_folder_position = target_folder_position;
// If the target view is positioned after `position_to_skip`, move the
// target one slot earlier, as `position_to_skip` is assumed about to be
// emptied.
if (position_to_skip.IsValid() &&
position_to_skip < expected_folder_position &&
expected_folder_position.slot > 0) {
--expected_folder_position.slot;
}
open_folder_info_ = {.item_id = folder_id,
.grid_index = expected_folder_position};
}
void AppsGridView::ShowFolderForView(AppListItemView* folder_view,
bool new_folder) {
DCHECK(open_folder_info_);
// Guard against invalid folder view.
if (!folder_view || !folder_view->is_folder()) {
open_folder_info_.reset();
return;
}
folder_controller_->ShowFolderForItemView(
folder_view,
/*focus_name_input=*/new_folder,
base::BindOnce(&AppsGridView::FolderHidden, weak_factory_.GetWeakPtr(),
folder_view->item()->id()));
}
void AppsGridView::FolderHidden(const std::string& item_id) {
if (!open_folder_info_ || open_folder_info_->item_id != item_id) {
return;
}
// Find the folder item location in the app list model to determine whether
// the item view location changed while the folder was closed (in which case
// the folder location change should be animated).
AppListItemView* item_view = nullptr;
int model_index = -1;
for (size_t i = 0; i < view_model_.view_size(); ++i) {
AppListItemView* view = view_model_.view_at(i);
if (view == drag_view_) {
continue;
}
++model_index;
if (view->item()->id() == item_id) {
item_view = view;
break;
}
}
// If the item view is gone, or the location in the grid did not change,
// the folder item should not be animated - immediately update apps grid state
// for folder hide.
if (!item_view || GetGridIndexFromIndexInViewModel(model_index) ==
open_folder_info_->grid_index) {
open_folder_info_.reset();
OnFolderHideAnimationDone();
return;
}
// When folder animates out, remaining items will animate to their ideal
// bounds - ensure their layers are created (and marked not to fill bounds
// opaquely).
PrepareItemsForBoundsAnimation();
// Animate the folder item view out from its original location.
reordering_folder_view_ = item_view;
views::AnimationBuilder animation;
animation.OnEnded(base::BindOnce(&AppsGridView::AnimateFolderItemViewIn,
weak_factory_.GetWeakPtr()));
animation.OnAborted(base::BindOnce(&AppsGridView::AnimateFolderItemViewIn,
weak_factory_.GetWeakPtr()));
gfx::Transform scale;
scale.Scale(0.5, 0.5);
scale = gfx::TransformAboutPivot(
gfx::RectF(item_view->GetLocalBounds()).CenterPoint(), scale);
animation.Once()
.SetDuration(kFolderItemFadeOutDuration)
.SetTransform(item_view->layer(), scale, gfx::Tween::FAST_OUT_LINEAR_IN)
.SetOpacity(item_view->layer(), 0.0f, gfx::Tween::FAST_OUT_LINEAR_IN);
}
void AppsGridView::AnimateFolderItemViewIn() {
const GridIndex before_index =
open_folder_info_ ? open_folder_info_->grid_index : GridIndex();
const GridIndex after_index =
GetIndexOfView(reordering_folder_view_.value_or(nullptr));
const bool top_to_bottom_animation = before_index < after_index;
// Once folder item view fades out, animate remaining items into their target
// location, and schedule the folder item view fade-in (note that
// `AnimateToIdealBounds()` updates `reordering_folder_view_` bounds without
// animation).
open_folder_info_.reset();
AnimateToIdealBounds(top_to_bottom_animation);
if (!reordering_folder_view_) {
return;
}
views::AnimationBuilder()
.OnEnded(base::BindOnce(&AppsGridView::OnFolderHideAnimationDone,
weak_factory_.GetWeakPtr()))
.OnAborted(base::BindOnce(&AppsGridView::OnFolderHideAnimationDone,
weak_factory_.GetWeakPtr()))
.Once()
.At(kFolderItemFadeInDelay)
.SetDuration(kFolderItemFadeInDuration)
.SetTransform(reordering_folder_view_.value()->layer(), gfx::Transform(),
gfx::Tween::ACCEL_LIN_DECEL_100_3)
.SetOpacity(reordering_folder_view_.value()->layer(), 1.0f,
gfx::Tween::ACCEL_LIN_DECEL_100_3);
}
void AppsGridView::OnFolderHideAnimationDone() {
reordering_folder_view_.reset();
DestroyLayerItemsIfNotNeeded();
if (IsDraggingForReparentInRootLevelGridView()) {
MaybeStartCardifiedView();
UpdateDrag(drag_pointer_, last_drag_point_);
}
}
bool AppsGridView::IsDragging() const {
return drag_pointer_ != NONE;
}
bool AppsGridView::IsDraggedView(const AppListItemView* view) const {
return drag_item_ == view->item();
}
void AppsGridView::ClearDragState() {
current_ghost_location_ = GridIndex();
last_folder_dropping_a11y_event_location_ = GridIndex();
last_reorder_a11y_event_location_ = GridIndex();
drop_target_region_ = NO_TARGET;
drag_pointer_ = NONE;
drop_target_ = GridIndex();
reorder_placeholder_ = GridIndex();
drag_start_grid_view_ = gfx::Point();
// Drag may end before |host_drag_start_timer_| gets fired.
if (host_drag_start_timer_.IsRunning())
host_drag_start_timer_.AbandonAndStop();
if (folder_item_reparent_timer_.IsRunning())
folder_item_reparent_timer_.Stop();
MaybeStopPageFlip();
StopAutoScroll();
if (drag_item_ && app_list_features::IsDragAndDropRefactorEnabled()) {
drag_item_->RemoveObserver(this);
}
drag_view_ = nullptr;
drag_item_ = nullptr;
drag_out_of_folder_container_ = false;
dragging_for_reparent_item_ = false;
extra_page_opened_ = false;
reparent_drag_cancellation_.Reset();
drag_start_callback_.Reset();
if (drag_end_callback_) {
std::move(drag_end_callback_).Run();
}
}
void AppsGridView::SetDragAndDropHostOfCurrentAppList(
ApplicationDragAndDropHost* drag_and_drop_host) {
if (drag_and_drop_host_ == drag_and_drop_host) {
return;
}
drag_and_drop_host_ = drag_and_drop_host;
forward_events_to_drag_and_drop_host_ = false;
if (host_drag_start_timer_.IsRunning())
host_drag_start_timer_.AbandonAndStop();
}
bool AppsGridView::IsAnimatingView(AppListItemView* view) const {
return view->layer() && view->layer()->GetAnimator()->is_animating();
}
gfx::Size AppsGridView::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
return GetTileGridSize();
}
bool AppsGridView::GetDropFormats(
int* formats,
std::set<ui::ClipboardFormatType>* format_types) {
if (app_list_features::IsDragAndDropRefactorEnabled()) {
format_types->insert(GetAppItemFormatType());
}
return true;
}
bool AppsGridView::CanDrop(const OSExchangeData& data) {
if (ShouldContainerHandleDragEvents()) {
return false;
}
return WillAcceptDropEvent(data);
}
bool AppsGridView::WillAcceptDropEvent(const OSExchangeData& data) {
if (!app_list_features::IsDragAndDropRefactorEnabled()) {
return true;
}
// Ignore drop events if the app list is syncing.
if (pulsing_blocks_model_.view_size()) {
return false;
}
auto app_id = GetAppInfoFromDropDataForAppType(data);
if (!app_id.has_value() || app_id->IsValid()) {
return false;
}
std::set<ui::ClipboardFormatType> format_types;
GetDropFormats(nullptr, &format_types);
return data.HasAnyFormat(0, format_types);
}
void AppsGridView::OnDragExited() {
if (!app_list_features::IsDragAndDropRefactorEnabled()) {
views::View::OnDragExited();
return;
}
// When the drag and drop host is a folder apps grid, close the folder when
// drag exits folder grid bounds.
// TODO(b/261985897): Add timer to close folder bounds.
if (folder_delegate_) {
if (drag_view_) {
folder_delegate_->ReparentItem(drag_pointer_, drag_view_,
last_drag_point_);
}
if (item_list_) {
// Do not observe any data change since it is going to be hidden.
item_list_->RemoveObserver(this);
}
item_list_ = nullptr;
dragging_for_reparent_item_ = true;
folder_delegate_->Close();
}
if (drag_view_) {
drag_view_->ClearItemDraggingState();
}
CancelDragWithNoDropAnimation();
}
void AppsGridView::ItemBeingDestroyed() {
DCHECK(drag_item_);
DCHECK(app_list_features::IsDragAndDropRefactorEnabled());
EndDrag(/*cancel=*/true);
DCHECK(!drag_item_);
}
void AppsGridView::OnDragEntered(const ui::DropTargetEvent& event) {
if (!app_list_features::IsDragAndDropRefactorEnabled()) {
views::View::OnDragEntered(event);
return;
}
// Ignore drag events if the app list is syncing.
if (pulsing_blocks_model_.view_size()) {
return;
}
auto app_info = GetAppInfoFromDropDataForAppType(event.data());
if (!app_info || app_info->IsValid()) {
return;
}
drag_item_ = AppListModelProvider::Get()->model()->FindItem(app_info->app_id);
if (!drag_item_) {
return;
}
drag_item_->AddObserver(this);
// Finalize previous drag icon animation if it's still in progress.
drag_view_hider_.reset();
folder_icon_item_hider_.reset();
folder_to_open_after_drag_icon_animation_.clear();
drag_icon_proxy_.reset();
PrepareItemsForBoundsAnimation();
drag_pointer_ = GetPointerTypeForDragAndDrop();
drag_view_ = GetItemViewAt(GetModelIndexOfItem(drag_item_));
if (drag_view_) {
drag_view_hider_ = std::make_unique<DragViewHider>(drag_view_);
// Dragged view should have focus. This also fixed the issue
// https://crbug.com/834682.
drag_view_->RequestFocus();
drag_view_init_index_ = GetIndexOfView(drag_view_);
} else {
dragging_for_reparent_item_ = true;
}
const gfx::Size initial_grid_size = GetTileGridSize();
reorder_placeholder_ =
drag_view_ ? drag_view_init_index_
: GetGridIndexFromIndexInViewModel(view_model()->view_size());
UpdatePaging();
// When reparenting drag, the preferred grid size may change if there are no
// extra slots on the grid for the placeholder item.
if (GetTileGridSize() != initial_grid_size) {
PreferredSizeChanged();
}
ExtractDragLocation(event.root_location(), &drag_start_grid_view_);
}
int AppsGridView::OnDragUpdated(const ui::DropTargetEvent& event) {
if (app_list_features::IsDragAndDropRefactorEnabled()) {
UpdateDrag(drag_pointer_, event.location());
}
return ui::DragDropTypes::DRAG_MOVE;
}
void AppsGridView::UpdateControlVisibility(AppListViewState app_list_state) {
SetVisible(app_list_state == AppListViewState::kFullscreenAllApps ||
app_list_state == AppListViewState::kFullscreenSearch);
}
views::View::DropCallback AppsGridView::GetDropCallback(
const ui::DropTargetEvent& event) {
return app_list_features::IsDragAndDropRefactorEnabled()
? base::BindOnce(&AppsGridView::EndDragCallback,
base::Unretained(this))
: base::DoNothing();
}
bool AppsGridView::OnKeyPressed(const ui::KeyEvent& event) {
// The user may press VKEY_CONTROL before an arrow key when intending to do an
// app move with control+arrow.
if (event.key_code() == ui::VKEY_CONTROL) {
return true;
}
if (selected_view_ && IsArrowKeyEvent(event) && event.IsControlDown()) {
HandleKeyboardAppOperations(event.key_code(), event.IsShiftDown());
return true;
}
// Let the FocusManager handle Left/Right keys.
if (!IsUnhandledUpDownKeyEvent(event)) {
return false;
}
const bool arrow_up = event.key_code() == ui::VKEY_UP;
return HandleVerticalFocusMovement(arrow_up);
}
bool AppsGridView::OnKeyReleased(const ui::KeyEvent& event) {
if (event.IsControlDown() || !handling_keyboard_move_) {
return false;
}
handling_keyboard_move_ = false;
RecordAppMovingTypeMetrics(folder_delegate_ ? kReorderByKeyboardInFolder
: kReorderByKeyboardInTopLevel);
return false;
}
void AppsGridView::ViewHierarchyChanged(
const views::ViewHierarchyChangedDetails& details) {
if (!details.is_add && details.parent == items_container_.get()) {
// The view being delete should not have reference in |view_model_|.
CHECK(!view_model_.GetIndexOfView(details.child).has_value());
if (selected_view_.get() == details.child) {
selected_view_ = nullptr;
}
if (drag_view_.get() == details.child) {
drag_view_ = nullptr;
}
if (current_ghost_view_.get() == details.child) {
current_ghost_view_ = nullptr;
}
if (last_ghost_view_.get() == details.child) {
last_ghost_view_ = nullptr;
}
if (reordering_folder_view_ && *reordering_folder_view_ == details.child)
reordering_folder_view_.reset();
row_change_animator_->CancelAnimation(details.child);
}
}
void AppsGridView::Update() {
// Abort reorder animation before `view_model_` is cleared.
MaybeAbortWholeGridAnimation();
view_model_.Clear();
pulsing_blocks_model_.Clear();
items_container_->RemoveAllChildViews();
DCHECK(!selected_view_);
DCHECK(!drag_view_);
std::vector<AppListItemView*> item_views;
if (item_list_ && item_list_->item_count()) {
for (size_t i = 0; i < item_list_->item_count(); ++i) {
std::unique_ptr<AppListItemView> view = CreateViewForItemAtIndex(i);
view_model_.Add(view.get(), view_model_.view_size());
item_views.push_back(items_container_->AddChildView(std::move(view)));
}
}
UpdateColsAndRowsForFolder();
UpdatePaging();
UpdatePulsingBlockViews();
PreferredSizeChanged();
// Icon load can change the item position in the view model, so don't iterate
// over view model to get items to update.
for (auto* item_view : item_views) {
item_view->InitializeIconLoader();
}
if (!folder_delegate_) {
RecordPageMetrics();
}
}
base::TimeDelta AppsGridView::GetPulsingBlockAnimationDelayForIndex(
int block_index) {
// The column in which the last AppListItemViews is located.
// |view_model_| only contains synced AppListItemViews and not
// PulsingBlockViews.
const int last_non_block_view_column = view_model_.view_size() % cols_;
// The index of the pulsing block view related to the |view_model_|.
const int block_index_in_view_model = view_model_.view_size() + block_index;
const base::TimeDelta staging_step_delay = base::Milliseconds(100);
// Depending of the row and column for the pulsing block, we stage the pulsing
// animation so it sweeps at a 45 degree angle from the upper left to the
// lower right.
return staging_step_delay *
((last_non_block_view_column + block_index) / cols_) +
staging_step_delay * (block_index_in_view_model % cols_);
}
void AppsGridView::OnSwapAnimationDone(views::View* placeholder,
AppListItemView* app_view) {
delete placeholder;
if (view_model_.GetIndexOfView(app_view).has_value() &&
!ItemViewsRequireLayers())
app_view->DestroyLayer();
UpdatePulsingBlockViews();
}
AppListItemView* AppsGridView::MaybeSwapPlaceholderAsset(size_t index) {
AppListItemView* view =
items_container_->AddChildViewAt(CreateViewForItemAtIndex(index), index);
view_model_.Add(view, index);
const bool placeholder_in_view_index = index == (view_model_.view_size() - 1);
const bool is_syncing =
model_ && model_->status() == AppListModelStatus::kStatusSyncing;
const bool should_animate_placeholder_swap =
pulsing_blocks_model_.view_size() > 0 && is_syncing &&
placeholder_in_view_index;
if (should_animate_placeholder_swap) {
PulsingBlockView* placeholder =
items_container_->AddChildView(std::make_unique<PulsingBlockView>(
app_list_config_->grid_icon_size(), base::TimeDelta(),
app_list_config_->grid_icon_dimension() / 2.f));
placeholder->SetBoundsRect(view->bounds());
placeholder->SetPaintToLayer();
view->EnsureLayer();
view->layer()->SetOpacity(0);
views::AnimationBuilder()
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(base::BindOnce(&AppsGridView::OnSwapAnimationDone,
weak_factory_.GetWeakPtr(), placeholder, view))
.OnAborted(base::BindOnce(&AppsGridView::OnSwapAnimationDone,
weak_factory_.GetWeakPtr(), placeholder,
view))
.Once()
.SetDuration(base::Milliseconds(200))
.SetOpacity(placeholder->layer(), 0.0f, gfx::Tween::LINEAR)
.SetOpacity(view->layer(), 1.0f, gfx::Tween::LINEAR);
} else {
UpdatePulsingBlockViews();
}
return view;
}
void AppsGridView::UpdatePulsingBlockViews() {
if (!model_ || model_->status() != AppListModelStatus::kStatusSyncing) {
pulsing_blocks_model_.Clear();
return;
}
const size_t desired_count =
GetNumberOfPulsingBlocksToShow(item_list_ ? item_list_->item_count() : 0);
if (pulsing_blocks_model_.view_size() == desired_count) {
return;
}
pulsing_blocks_model_.Clear();
while (pulsing_blocks_model_.view_size() < desired_count) {
base::TimeDelta time = GetPulsingBlockAnimationDelayForIndex(
pulsing_blocks_model_.view_size());
auto view = std::make_unique<PulsingBlockView>(
app_list_config_->grid_icon_size(), time,
app_list_config_->grid_icon_dimension() / 2.f);
pulsing_blocks_model_.Add(view.get(), pulsing_blocks_model_.view_size());
items_container_->AddChildView(std::move(view));
}
}
std::unique_ptr<AppListItemView> AppsGridView::CreateViewForItemAtIndex(
size_t index) {
// The |drag_view_| might be pending for deletion, therefore |view_model_|
// may have one more item than |item_list_|.
DCHECK_LE(index, item_list_->item_count());
auto view = std::make_unique<AppListItemView>(
app_list_config_, this, item_list_->item_at(index),
app_list_view_delegate_, AppListItemView::Context::kAppsGridView);
if (ItemViewsRequireLayers()) {
view->EnsureLayer();
}
if (cardified_state_) {
view->EnterCardifyState();
}
return view;
}
void AppsGridView::SetSelectedItemByIndex(const GridIndex& index) {
if (GetIndexOfView(selected_view_) == index) {
return;
}
AppListItemView* new_selection = GetViewAtIndex(index);
if (!new_selection) {
return; // Keep current selection.
}
if (selected_view_) {
selected_view_->SchedulePaint();
}
EnsureViewVisible(index);
selected_view_ = new_selection;
selected_view_->SchedulePaint();
selected_view_->NotifyAccessibilityEvent(ax::mojom::Event::kFocus, true);
if (selected_view_->HasNotificationBadge()) {
a11y_announcer_->AnnounceItemNotificationBadge(
selected_view_->title()->GetText());
}
}
int AppsGridView::GetIndexInViewModel(const GridIndex& index) const {
if (index.page == 0) {
return index.slot;
}
// NOTE: Non-zero page implies that the grid supports paging, so
// `TilesPerPage()` should return non-null optional.
const int first_page_size = *TilesPerPage(0);
const int default_page_size = *TilesPerPage(1);
return first_page_size + (index.page - 1) * default_page_size + index.slot;
}
GridIndex AppsGridView::GetIndexOfView(const AppListItemView* view) const {
const auto model_index = view_model_.GetIndexOfView(view);
if (!model_index.has_value()) {
return GridIndex();
}
return GetGridIndexFromIndexInViewModel(model_index.value());
}
AppListItemView* AppsGridView::GetViewAtIndex(const GridIndex& index) const {
if (!IsValidIndex(index)) {
return nullptr;
}
const size_t model_index = GetIndexInViewModel(index);
return GetItemViewAt(model_index);
}
std::optional<int> AppsGridView::TilesPerPage(int page) const {
const std::optional<int> max_rows = GetMaxRowsInPage(page);
if (!max_rows.has_value()) {
return std::nullopt;
}
return *max_rows * cols();
}
bool AppsGridView::IsAnimatingCardifiedState() const {
return false;
}
bool AppsGridView::MaybeStartPageFlip() {
return false;
}
void AppsGridView::SetMaxColumnsInternal(int max_cols) {
if (max_cols_ == max_cols) {
return;
}
max_cols_ = max_cols;
if (IsInFolder()) {
UpdateColsAndRowsForFolder();
} else {
cols_ = max_cols_;
}
}
void AppsGridView::SetIdealBoundsForViewToGridIndex(
size_t view_index_in_model,
const GridIndex& view_grid_index) {
gfx::Rect tile_bounds = GetExpectedTileBounds(view_grid_index);
tile_bounds.Offset(CalculateTransitionOffset(view_grid_index.page));
if (view_index_in_model < view_model_.view_size()) {
view_model_.set_ideal_bounds(view_index_in_model, tile_bounds);
} else {
pulsing_blocks_model_.set_ideal_bounds(
view_index_in_model - view_model_.view_size(), tile_bounds);
}
}
void AppsGridView::CalculateIdealBounds() {
AppListItemView* view_with_locked_position = nullptr;
if (open_folder_info_)
view_with_locked_position = GetItemViewForItem(open_folder_info_->item_id);
std::set<GridIndex> reserved_slots;
reserved_slots.insert(reorder_placeholder_);
if (open_folder_info_) {
reserved_slots.insert(open_folder_info_->grid_index);
}
const size_t total_views =
view_model_.view_size() + pulsing_blocks_model_.view_size();
int slot_index = 0;
for (size_t i = 0; i < total_views; ++i) {
// NOTE: Because of pulsing blocks, `i` can count up to a value higher than
// the view model size. So verify that `i` is less than the view model size
// before fetching at index `i` from the view model.
if (i < view_model_.view_size() && view_model_.view_at(i) == drag_view_) {
continue;
}
if (i < view_model_.view_size() &&
view_model_.view_at(i) == view_with_locked_position) {
SetIdealBoundsForViewToGridIndex(i, open_folder_info_->grid_index);
continue;
}
GridIndex view_index = GetGridIndexFromIndexInViewModel(slot_index);
// Leaves a blank space in the grid for the current reorder placeholder.
while (reserved_slots.count(view_index)) {
++slot_index;
view_index = GetGridIndexFromIndexInViewModel(slot_index);
}
if (i < view_model_.view_size())
view_model_.view_at(i)->SetMostRecentGridIndex(view_index, cols_);
SetIdealBoundsForViewToGridIndex(i, view_index);
++slot_index;
}
}
void AppsGridView::AnimateToIdealBounds(bool is_animating_top_to_bottom) {
if (layer()->GetCompositor()) {
item_reorder_animation_tracker_ =
layer()->GetCompositor()->RequestNewThroughputTracker();
item_reorder_animation_tracker_->Start(
metrics_util::ForSmoothnessV3(base::BindRepeating(
&ReportItemDragReorderAnimationSmoothness, IsTabletMode())));
}
gfx::Rect visible_bounds(GetVisibleBounds());
gfx::Point visible_origin = visible_bounds.origin();
ConvertPointToTarget(this, items_container_, &visible_origin);
visible_bounds.set_origin(visible_origin);
CalculateIdealBounds();
std::unique_ptr<views::AnimationBuilder> animation;
auto init_animation = [&]() -> std::unique_ptr<views::AnimationBuilder> {
std::unique_ptr<views::AnimationBuilder> animation =
std::make_unique<views::AnimationBuilder>();
animation
->SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(base::BindOnce(&AppsGridView::OnIdealBoundsAnimationDone,
weak_factory_.GetWeakPtr()))
.OnAborted(base::BindOnce(&AppsGridView::OnIdealBoundsAnimationDone,
weak_factory_.GetWeakPtr()))
.Once()
.SetDuration(kItemBoundsBaseAnimationDuration);
return animation;
};
base::AutoReset<bool> auto_reset(&setting_up_ideal_bounds_animation_, true);
const bool is_animating_multiple_rows = WillAnimateMultipleRows();
// A duration which is incremented for cascading item animations.
base::TimeDelta animation_duration = kItemBoundsBaseAnimationDuration;
// Keeps track of the current slot/row for the current `animation_duration`.
int animation_duration_index = -1;
for (size_t i = 0; i < view_model_.view_size(); ++i) {
// When not animating top to bottom, reverse the direction of iteration, so
// bottom animating items have the shortest `animation_duration`.
const size_t current_view_index =
is_animating_top_to_bottom ? i : view_model_.view_size() - 1 - i;
AppListItemView* view = GetItemViewAt(current_view_index);
const gfx::Rect& target_bounds =
view_model_.ideal_bounds(current_view_index);
gfx::Rect current_bounds = view->GetMirroredBounds();
if (view->bounds() == target_bounds) {
continue;
}
const bool current_visible = visible_bounds.Intersects(current_bounds);
const bool target_visible = visible_bounds.Intersects(target_bounds);
const bool visible =
!IsViewExplicitlyHidden(view) && (current_visible || target_visible);
if (!visible) {
view->SetBoundsRect(target_bounds);
continue;
}
const int view_row = view->most_recent_grid_index().slot / cols_;
const int view_slot = view->most_recent_grid_index().slot;
// When animating multiple rows, each row of items will have an animation
// duration that is increased at each new row. When animating items within a
// single row, the duration will be increased at each new item slot.
const int current_animation_duration_index =
is_animating_multiple_rows ? view_row : view_slot;
// Increment the `animation_duration` when the `animation_duration_index`
// has been initialized and the current index has changed.
if (animation_duration_index != -1 &&
animation_duration_index != current_animation_duration_index) {
animation_duration += kItemBoundsAnimationOffsetDuration;
}
animation_duration_index = current_animation_duration_index;
if (view->has_pending_row_change()) {
view->EnsureLayer();
view->reset_has_pending_row_change();
if (!animation) {
animation = init_animation();
}
animation->GetCurrentSequence()
.At(base::TimeDelta())
.SetDuration(animation_duration);
row_change_animator_->AnimateBetweenRows(
view, current_bounds, target_bounds,
&animation->GetCurrentSequence());
} else {
view->EnsureLayer();
// Update `current_bounds` to include the current layer transform of
// `view`.
if (IsAnimatingView(view)) {
current_bounds =
ApplyTransformAtOrigin(current_bounds, view->layer()->transform());
}
gfx::Transform transform =
gfx::TransformBetweenRects(gfx::RectF(GetMirroredRect(target_bounds)),
gfx::RectF(current_bounds));
view->layer()->SetTransform(transform);
view->SetBoundsRect(target_bounds);
if (!animation) {
animation = init_animation();
}
animation->GetCurrentSequence()
.At(base::TimeDelta())
.SetDuration(animation_duration)
.SetTransform(view->layer(), gfx::Transform(),
gfx::Tween::ACCEL_40_DECEL_100_3);
}
}
}
bool AppsGridView::WillAnimateMultipleRows() {
for (size_t i = 0; i < view_model_.view_size(); ++i) {
// Return true if an item will animate to a new row.
if (GetItemViewAt(i)->has_pending_row_change()) {
return true;
}
}
return false;
}
void AppsGridView::ExtractDragLocation(const gfx::Point& root_location,
gfx::Point* drag_point) {
// Use root location of |event| instead of location in |drag_view_|'s
// coordinates because |drag_view_| has a scale transform and location
// could have integer round error and causes jitter.
*drag_point = root_location;
DCHECK(GetWidget());
aura::Window::ConvertPointToTarget(
GetWidget()->GetNativeWindow()->GetRootWindow(),
GetWidget()->GetNativeWindow(), drag_point);
views::View::ConvertPointFromWidget(this, drag_point);
}
void AppsGridView::UpdateDropTargetRegion() {
DCHECK(drag_item_);
gfx::Point point = last_drag_point_;
point.set_x(GetMirroredXInView(point.x()));
if (IsPointWithinDragBuffer(point)) {
if (DragPointIsOverItem(point)) {
drop_target_region_ = ON_ITEM;
drop_target_ = GetNearestTileIndexForPoint(point);
return;
}
UpdateDropTargetForReorder(point);
drop_target_region_ = DragIsCloseToItem(point) ? NEAR_ITEM : BETWEEN_ITEMS;
return;
}
// Reset the reorder target to the original position if the cursor is outside
// the drag buffer or an item is dragged to a full page either from a folder
// or another page.
if (IsDraggingForReparentInRootLevelGridView()) {
drop_target_region_ = NO_TARGET;
return;
}
drop_target_ = drag_view_init_index_;
drop_target_region_ = DragIsCloseToItem(point) ? NEAR_ITEM : BETWEEN_ITEMS;
}
bool AppsGridView::DropTargetIsValidFolder() {
AppListItemView* target_view =
GetViewDisplayedAtSlotOnCurrentPage(drop_target_.slot);
if (!target_view) {
return false;
}
AppListItem* target_item = target_view->item();
// Items can only be dropped into non-folders (which have no children) or
// folders that have fewer than the max allowed items.
// The OEM folder does not allow drag/drop of other items into it.
if (target_item->IsFolderFull() || IsOEMFolderItem(target_item)) {
return false;
}
if (!IsValidIndex(drop_target_)) {
return false;
}
return true;
}
bool AppsGridView::DragPointIsOverItem(const gfx::Point& point) {
// The reorder placeholder shouldn't count as a unique item
GridIndex nearest_tile_index(GetNearestTileIndexForPoint(point));
if (!IsValidIndex(nearest_tile_index) ||
nearest_tile_index == reorder_placeholder_) {
return false;
}
int distance_to_tile_center =
(point - GetExpectedTileBounds(nearest_tile_index).CenterPoint())
.Length();
if (distance_to_tile_center >
(app_list_config_->folder_bubble_radius() *
(cardified_state_ ? GetAppsGridCardifiedScale() : 1.0f))) {
return false;
}
return true;
}
void AppsGridView::AnimateDragIconToTargetPosition(
AppListItem* drag_item,
const std::string& target_folder_id) {
// If drag icon proxy had not been created, just reshow the drag view.
const bool is_drag_and_drop_refactor_enabled =
app_list_features::IsDragAndDropRefactorEnabled();
if (!drag_icon_proxy_ && !is_drag_and_drop_refactor_enabled) {
OnDragIconDropDone();
return;
}
if (is_drag_and_drop_refactor_enabled && !drag_image_layer_) {
OnDragIconDropDone();
return;
}
AppListItemView* target_folder_view =
!target_folder_id.empty() ? GetItemViewForItem(target_folder_id)
: nullptr;
// Calculate target item bounds.
gfx::Rect drag_icon_drop_bounds;
if (target_folder_id.empty()) {
// Find the view for drag item, and use its ideal bounds to calculate target
// drop bounds.
for (size_t i = 0; i < view_model_.view_size(); ++i) {
if (view_model_.view_at(i)->item() != drag_item) {
continue;
}
// Get the expected drag item view location.
const gfx::Rect drag_view_ideal_bounds = view_model_.ideal_bounds(i);
// Get icon bounds in the drag view coordinates.
drag_icon_drop_bounds = AppListItemView::GetIconBoundsForTargetViewBounds(
app_list_config_, drag_view_ideal_bounds,
drag_item->is_folder() ? app_list_config_->folder_icon_size()
: app_list_config_->grid_icon_size(),
1.0f);
break;
}
} else if (target_folder_view) {
// Calculate target bounds of dragged item.
drag_icon_drop_bounds =
GetTargetIconRectInFolder(drag_item, target_folder_view);
}
// Unable to calculate target bounds - bail out and reshow the drag view.
if (drag_icon_drop_bounds.IsEmpty()) {
OnDragIconDropDone();
return;
}
if (target_folder_view) {
DCHECK(target_folder_view->is_folder());
folder_icon_item_hider_ =
std::make_unique<FolderIconItemHider>(target_folder_view, drag_item);
}
drag_icon_drop_bounds =
items_container_->GetMirroredRect(drag_icon_drop_bounds);
// Convert target bounds to in screen coordinates expected by drag icon proxy.
views::View::ConvertRectToScreen(items_container_, &drag_icon_drop_bounds);
if (is_drag_and_drop_refactor_enabled) {
// Ensure target bounds are in the same coordinates as the drag image layer.
wm::ConvertRectFromScreen(
items_container_->GetWidget()->GetNativeWindow()->GetRootWindow(),
&drag_icon_drop_bounds);
ui::Layer* target_layer = drag_image_layer_->root();
if (target_layer) {
target_layer->GetAnimator()->AbortAllAnimations();
gfx::Rect current_bounds = target_layer->bounds();
if (current_bounds.IsEmpty()) {
OnDragIconDropDone();
return;
}
ui::ScopedLayerAnimationSettings animation_settings(
target_layer->GetAnimator());
animation_settings.SetTweenType(gfx::Tween::FAST_OUT_LINEAR_IN);
animation_settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_SET_NEW_TARGET);
animation_settings.AddObserver(
new AnimationObserverToRestoreGrid(base::BindOnce(
&AppsGridView::OnDragIconDropDone, weak_factory_.GetWeakPtr())));
target_layer->SetTransform(gfx::TransformBetweenRects(
gfx::RectF(current_bounds), gfx::RectF(drag_icon_drop_bounds)));
}
return;
}
drag_icon_proxy_->AnimateToBoundsAndCloseWidget(
drag_icon_drop_bounds, base::BindOnce(&AppsGridView::OnDragIconDropDone,
base::Unretained(this)));
}
void AppsGridView::OnDragIconDropDone() {
drag_view_hider_.reset();
folder_icon_item_hider_.reset();
drag_icon_proxy_.reset();
drag_image_layer_.reset();
DestroyLayerItemsIfNotNeeded();
if (!folder_to_open_after_drag_icon_animation_.empty()) {
AppListItemView* folder_view =
GetItemViewForItem(folder_to_open_after_drag_icon_animation_);
folder_to_open_after_drag_icon_animation_.clear();
ShowFolderForView(folder_view, /*new_folder=*/true);
}
}
bool AppsGridView::DraggedItemCanEnterFolder() {
if (!IsFolderItem(drag_item_) && !folder_delegate_) {
return true;
}
return false;
}
void AppsGridView::UpdateDropTargetForReorder(const gfx::Point& point) {
gfx::Rect bounds = GetContentsBounds();
bounds.Inset(GetTilePadding(GetSelectedPage()));
GridIndex nearest_tile_index = GetNearestTileIndexForPoint(point);
gfx::Point reorder_placeholder_center =
GetExpectedTileBounds(reorder_placeholder_).CenterPoint();
int x_offset_direction = 0;
if (nearest_tile_index == reorder_placeholder_) {
x_offset_direction = reorder_placeholder_center.x() <= point.x() ? -1 : 1;
} else {
x_offset_direction = reorder_placeholder_ < nearest_tile_index ? -1 : 1;
}
const gfx::Size total_tile_size = GetTotalTileSize(GetSelectedPage());
int row = nearest_tile_index.slot / cols_;
// Offset the target column based on the direction of the target. This will
// result in earlier targets getting their reorder zone shifted backwards
// and later targets getting their reorder zones shifted forwards.
//
// This makes reordering feel like the user is slotting items into the spaces
// between apps.
int x_offset = x_offset_direction *
(total_tile_size.width() / 2 -
app_list_config_->folder_bubble_radius() *
(cardified_state_ ? GetAppsGridCardifiedScale() : 1.0f));
const int selected_page = GetSelectedPage();
int col = (point.x() - bounds.x() + x_offset -
GetGridCenteringOffset(selected_page).x()) /
total_tile_size.width();
col = std::clamp(col, 0, cols_ - 1);
GridIndex max_target_index;
if (selected_page == GetTotalPages() - 1) {
// On the last page, cap the target index at the view model size.
max_target_index = GetGridIndexFromIndexInViewModel(
view_model()->view_size() -
(HasExtraSlotForReorderPlaceholder() ? 0 : 1));
} else {
max_target_index =
GridIndex(selected_page, *TilesPerPage(selected_page) - 1);
}
drop_target_ =
std::min(GridIndex(selected_page, row * cols_ + col), max_target_index);
DCHECK(IsValidIndex(drop_target_))
<< drop_target_.ToString() << " selected page " << selected_page
<< " row " << row << " col " << col << " " << max_target_index.ToString();
}
bool AppsGridView::DragIsCloseToItem(const gfx::Point& point) {
DCHECK(drag_item_);
GridIndex nearest_tile_index = GetNearestTileIndexForPoint(point);
if (nearest_tile_index == reorder_placeholder_) {
return false;
}
const int distance_to_tile_center =
(point - GetExpectedTileBounds(nearest_tile_index).CenterPoint())
.Length();
// The minimum of |forty_percent_icon_spacing| and |double_icon_radius| is
// chosen to give an acceptable spacing on displays of any resolution: when
// items are very close together, using |forty_percent_icon_spacing| will
// prevent overlap and leave a reasonable gap, whereas when icons are very far
// apart, using |double_icon_radius| will prevent us from juding an overly
// large region as 'nearby'
const int forty_percent_icon_spacing =
(app_list_config_->grid_tile_width() + horizontal_tile_padding_ * 2) *
0.4 * (cardified_state_ ? GetAppsGridCardifiedScale() : 1.0f);
const int double_icon_radius =
app_list_config_->folder_bubble_radius() * 2 *
(cardified_state_ ? GetAppsGridCardifiedScale() : 1.0f);
const int minimum_drag_distance_for_reorder =
std::min(forty_percent_icon_spacing, double_icon_radius);
if (distance_to_tile_center < minimum_drag_distance_for_reorder) {
return true;
}
return false;
}
void AppsGridView::OnReorderTimer() {
const GridIndex before_index = reorder_placeholder_;
reorder_placeholder_ = drop_target_;
const GridIndex after_index = reorder_placeholder_;
MaybeCreateDragReorderAccessibilityEvent();
AnimateToIdealBounds(/*top to bottom animation=*/before_index < after_index);
CreateGhostImageView();
}
void AppsGridView::OnFolderItemReparentTimer(Pointer pointer) {
DCHECK(folder_delegate_);
if (drag_out_of_folder_container_ && drag_view_) {
if (!app_list_features::IsDragAndDropRefactorEnabled()) {
folder_delegate_->ReparentItem(pointer, drag_view_, last_drag_point_);
}
// Set the flag in the folder's grid view.
dragging_for_reparent_item_ = true;
// Do not observe any data change since it is going to be hidden.
item_list_->RemoveObserver(this);
item_list_ = nullptr;
}
}
void AppsGridView::UpdateDragStateInsideFolder(Pointer pointer,
const gfx::Point& drag_point) {
if (IsUnderOEMFolder()) {
return;
}
if (!app_list_features::IsDragAndDropRefactorEnabled() &&
IsDraggingForReparentInHiddenGridView()) {
// Dispatch drag event to root level grid view for re-parenting folder
// folder item purpose.
DispatchDragEventForReparent(pointer, drag_point);
return;
}
// Calculate if the drag_view_ is dragged out of the folder's container
// ink bubble.
bool is_item_dragged_out_of_folder =
folder_delegate_->IsDragPointOutsideOfFolder(drag_point);
if (is_item_dragged_out_of_folder) {
if (!drag_out_of_folder_container_) {
folder_item_reparent_timer_.Start(
FROM_HERE, base::Milliseconds(kFolderItemReparentDelay),
base::BindOnce(&AppsGridView::OnFolderItemReparentTimer,
base::Unretained(this), pointer));
drag_out_of_folder_container_ = true;
}
} else {
folder_item_reparent_timer_.Stop();
drag_out_of_folder_container_ = false;
}
}
bool AppsGridView::IsDraggingForReparentInRootLevelGridView() const {
return (!folder_delegate_ && dragging_for_reparent_item_);
}
bool AppsGridView::IsDraggingForReparentInHiddenGridView() const {
return (folder_delegate_ && dragging_for_reparent_item_);
}
gfx::Rect AppsGridView::GetTargetIconRectInFolder(
AppListItem* drag_item,
AppListItemView* folder_item_view) {
const gfx::Rect view_ideal_bounds = view_model_.ideal_bounds(
view_model_.GetIndexOfView(folder_item_view).value());
const gfx::Rect icon_ideal_bounds =
folder_item_view->GetIconBoundsForTargetViewBounds(
app_list_config_, view_ideal_bounds,
folder_item_view->GetDragImage().size(), /*icon_scale=*/1.0f);
AppListFolderItem* folder_item = folder_item_view->item()->AsFolderItem();
return folder_item->GetTargetIconRectInFolderForItem(
*app_list_config_, drag_item, icon_ideal_bounds);
}
bool AppsGridView::IsUnderOEMFolder() {
if (!folder_delegate_) {
return false;
}
return folder_delegate_->IsOEMFolder();
}
void AppsGridView::HandleKeyboardAppOperations(ui::KeyboardCode key_code,
bool folder) {
DCHECK(selected_view_);
// Do not allow keyboard operations during drag.
if (drag_view_) {
return;
}
if (folder) {
if (folder_delegate_)
folder_delegate_->HandleKeyboardReparent(selected_view_, key_code);
else
HandleKeyboardFoldering(key_code);
} else {
HandleKeyboardMove(key_code);
}
}
void AppsGridView::HandleKeyboardFoldering(ui::KeyboardCode key_code) {
const GridIndex source_index = GetIndexOfView(selected_view_);
const GridIndex target_index = GetTargetGridIndexForKeyboardMove(key_code);
if (!CanMoveSelectedToTargetForKeyboardFoldering(target_index)) {
return;
}
const std::u16string moving_view_title = selected_view_->title()->GetText();
AppListItemView* target_view =
GetViewDisplayedAtSlotOnCurrentPage(target_index.slot);
const std::u16string target_view_title = target_view->title()->GetText();
const bool target_view_is_folder = target_view->is_folder();
std::string folder_id;
bool is_new_folder = false;
if (MoveItemToFolder(selected_view_->item(), target_index,
kMoveByKeyboardIntoFolder, &folder_id, &is_new_folder)) {
a11y_announcer_->AnnounceKeyboardFoldering(
moving_view_title, target_view_title, target_view_is_folder);
AppListItemView* folder_view = GetItemViewForItem(folder_id);
if (folder_view) {
if (is_new_folder) {
SetOpenFolderInfo(folder_id, target_index, source_index);
ShowFolderForView(folder_view, /*new_folder=*/true);
} else {
folder_view->RequestFocus();
}
}
// Layout the grid to ensure the created folder's bounds are correct.
// Note that `open_folder_info_` affects ideal item bounds, so
// `DeprecatedLayoutImmediately()` needs to be called after
// `SetOpenFolderInfo()`.
DeprecatedLayoutImmediately();
UpdatePaging();
}
}
bool AppsGridView::CanMoveSelectedToTargetForKeyboardFoldering(
const GridIndex& target_index) const {
DCHECK(selected_view_);
// To folder an item, the item must be moved into the folder, not the folder
// moved over the item.
const AppListItem* selected_item = selected_view_->item();
if (selected_item->is_folder()) {
return false;
}
// Do not allow foldering across pages because the destination folder cannot
// be seen.
if (target_index.page != GetIndexOfView(selected_view_).page) {
return false;
}
return true;
}
bool AppsGridView::HandleVerticalFocusMovement(bool arrow_up) {
views::View* focused = GetFocusManager()->GetFocusedView();
if (!views::IsViewClass<AppListItemView>(focused)) {
return false;
}
const GridIndex source_index =
GetIndexOfView(static_cast<const AppListItemView*>(focused));
int target_page = source_index.page;
int target_row = source_index.slot / cols_ + (arrow_up ? -1 : 1);
int target_col = source_index.slot % cols_;
if (target_row < 0) {
// Move focus to the last row of previous page if target row is negative.
--target_page;
// |target_page| may be invalid which makes |target_row| invalid, but
// |target_row| will not be used if |target_page| is invalid.
target_row = (GetNumberOfItemsOnPage(target_page) - 1) / cols_;
} else if (target_row > (GetNumberOfItemsOnPage(target_page) - 1) / cols_) {
// Move focus to the first row of next page if target row is beyond range.
++target_page;
target_row = 0;
}
if (target_page < 0) {
// Move focus up outside the apps grid if target page is negative.
if (keyboard_controller_ &&
keyboard_controller_->MoveFocusUpFromAppsGrid(target_col)) {
// The delegate handled the focus move.
return true;
}
// Move focus backwards from the first item in the grid.
views::View* v = GetFocusManager()->GetNextFocusableView(
view_model_.view_at(0), /*starting_widget=*/nullptr, /*reverse=*/true,
/*dont_loop=*/false);
DCHECK(v);
v->RequestFocus();
return true;
}
if (target_page >= GetTotalPages()) {
// Move focus down outside the apps grid if target page is beyond range.
views::View* v = GetFocusManager()->GetNextFocusableView(
view_model_.view_at(view_model_.view_size() - 1),
/*starting_widget=*/nullptr, /*reverse=*/false,
/*dont_loop=*/false);
DCHECK(v);
v->RequestFocus();
return true;
}
GridIndex target_index(target_page, target_row * cols_ + target_col);
// Ensure the focus is within the range of the target page.
target_index.slot =
std::min(GetNumberOfItemsOnPage(target_page) - 1, target_index.slot);
if (IsValidIndex(target_index)) {
GetViewAtIndex(target_index)->RequestFocus();
return true;
}
return false;
}
void AppsGridView::UpdateColsAndRowsForFolder() {
if (!folder_delegate_) {
return;
}
const int item_count = item_list_ ? item_list_->item_count() : 0;
// Ensure that there is always at least one column.
if (item_count == 0) {
cols_ = 1;
} else {
int preferred_cols = std::sqrt(item_list_->item_count() - 1) + 1;
cols_ = std::clamp(preferred_cols, 1, max_cols_);
}
PreferredSizeChanged();
}
void AppsGridView::DispatchDragEventForReparent(Pointer pointer,
const gfx::Point& drag_point) {
DCHECK(!app_list_features::IsDragAndDropRefactorEnabled());
folder_delegate_->DispatchDragEventForReparent(pointer, drag_point);
}
void AppsGridView::EndDragFromReparentItemInRootLevel(
AppListItemView* original_parent_item_view,
bool events_forwarded_to_drag_drop_host,
bool cancel_drag,
std::unique_ptr<AppDragIconProxy> drag_icon_proxy) {
DCHECK(!IsInFolder());
// EndDrag was called before if |drag_view_| is nullptr.
if (!drag_item_) {
return;
}
drag_icon_proxy_ = std::move(drag_icon_proxy);
AppListItem* drag_item = drag_item_;
DCHECK(IsDraggingForReparentInRootLevelGridView());
bool cancel_reparent = cancel_drag || drop_target_region_ == NO_TARGET;
// The ID of the folder to which the item gets dropped. It will get set when
// the item is moved to a folder. It will be set the to original folder ID if
// reparent is canceled.
std::string target_folder_id;
// Cache the original item folder id, as model updates may destroy the
// original folder item.
const std::string original_folder_id =
app_list_features::IsDragAndDropRefactorEnabled()
? drag_item_->folder_id()
: original_parent_item_view->item()->id();
if (!events_forwarded_to_drag_drop_host && !cancel_reparent) {
UpdateDropTargetRegion();
if (drop_target_region_ == ON_ITEM && DropTargetIsValidFolder() &&
DraggedItemCanEnterFolder()) {
bool is_new_folder = false;
if (MoveItemToFolder(drag_item, drop_target_, kMoveIntoAnotherFolder,
&target_folder_id, &is_new_folder)) {
// Announce folder dropping event before end of drag of reparented item.
MaybeCreateFolderDroppingAccessibilityEvent();
// If move to folder created a folder, layout the grid to ensure the
// created folder's bounds are correct.
DeprecatedLayoutImmediately();
if (is_new_folder) {
folder_to_open_after_drag_icon_animation_ = target_folder_id;
SetOpenFolderInfo(target_folder_id, drop_target_,
reorder_placeholder_);
}
} else {
cancel_reparent = true;
}
} else if (drop_target_region_ != NO_TARGET && IsValidIndex(drop_target_)) {
ReparentItemForReorder(drag_item, drop_target_);
RecordAppMovingTypeMetrics(kMoveByDragOutOfFolder);
// Announce accessibility event before the end of drag for reparented
// item.
MaybeCreateDragReorderAccessibilityEvent();
} else {
NOTREACHED();
}
}
if (cancel_reparent) {
target_folder_id = original_folder_id;
}
SetAsFolderDroppingTarget(drop_target_, false);
const GridIndex before_index = reorder_placeholder_;
const GridIndex after_index = drop_target_;
const bool top_to_bottom_animation = before_index < after_index;
ClearDragState();
UpdatePaging();
if (GetWidget()) {
// Normally layout cancels any animations. At this point there may be a
// pending layout, force it now so that one isn't triggered part way through
// the animation. Further, ignore this layout so that the position isn't
// reset.
DCHECK(!ignore_layout_);
base::AutoReset<bool> auto_reset(&ignore_layout_, true);
GetWidget()->LayoutRootViewIfNecessary();
}
if (cardified_state_)
MaybeEndCardifiedView();
else
AnimateToIdealBounds(top_to_bottom_animation);
// Hide the |current_ghost_view_| after completed drag from within
// folder to |apps_grid_view_|.
BeginHideCurrentGhostImageView();
SetFocusAfterEndDrag(drag_item); // Maybe focus the search box.
AnimateDragIconToTargetPosition(drag_item, target_folder_id);
}
void AppsGridView::EndDragForReparentInHiddenFolderGridView() {
SetAsFolderDroppingTarget(drop_target_, false);
ClearDragState();
// Hide |current_ghost_view_| in the hidden folder grid view.
BeginHideCurrentGhostImageView();
}
void AppsGridView::HandleKeyboardReparent(
AppListItemView* reparented_view,
AppListItemView* original_parent_item_view,
ui::KeyboardCode key_code) {
DCHECK(key_code == ui::VKEY_LEFT || key_code == ui::VKEY_RIGHT ||
key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN);
DCHECK(!folder_delegate_);
DCHECK(view_model_.GetIndexOfView(original_parent_item_view).has_value());
const std::string reparented_item_id = reparented_view->item()->id();
// Set |original_parent_item_view| selected so |target_index| will be
// computed relative to the open folder.
SetSelectedView(original_parent_item_view);
const GridIndex target_index = GetTargetGridIndexForKeyboardReparent(
GetIndexOfView(original_parent_item_view), key_code);
ReparentItemForReorder(reparented_view->item(), target_index);
// `target_index` could point to an invalid/wrong position after reparenting.
// This happens after trying to move the last item from the folder
// to the right (`target_index` is "folder index + 1", but after reparenting
// it actually moves one position back).
const AppListItem* const item_after_reparent =
item_list_->FindItem(reparented_item_id);
DCHECK(item_after_reparent);
const int final_model_index = GetModelIndexOfItem(item_after_reparent);
const GridIndex final_grid_index =
GetGridIndexFromIndexInViewModel(final_model_index);
// Update paging because the move could have resulted in a
// page getting created.
UpdatePaging();
DeprecatedLayoutImmediately();
EnsureViewVisible(final_grid_index);
GetViewAtIndex(final_grid_index)->RequestFocus();
AnnounceReorder(final_grid_index);
RecordAppMovingTypeMetrics(kMoveByKeyboardOutOfFolder);
}
bool AppsGridView::IsTabletMode() const {
return app_list_view_delegate_->IsInTabletMode();
}
views::AnimationBuilder AppsGridView::FadeOutVisibleItemsForReorder(
ReorderAnimationCallback done_callback) {
// The caller of this function is responsible for aborting the old reorder
// process before starting a new one.
DCHECK(!IsUnderWholeGridAnimation());
// Cancel the active bounds animations on item views if any.
CancelAllItemAnimations();
grid_animation_status_ = AppListGridAnimationStatus::kReorderFadeOut;
reorder_animation_tracker_.emplace(
layer()->GetCompositor()->RequestNewThroughputTracker());
reorder_animation_tracker_->Start(metrics_util::ForSmoothnessV3(
base::BindRepeating(&ReportReorderAnimationSmoothness, IsTabletMode())));
views::AnimationBuilder animation_builder;
grid_animation_abort_handle_ = animation_builder.GetAbortHandle();
if (fade_out_start_closure_for_test_)
animation_builder.OnStarted(std::move(fade_out_start_closure_for_test_));
// Set the preemption strategy to be `IMMEDIATELY_ANIMATE_TO_NEW_TARGET` so
// that if there is an existing apps grid animation, fade out animation for
// reorder is still going to run.
animation_builder
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(base::BindOnce(&AppsGridView::OnFadeOutAnimationEnded,
weak_factory_.GetWeakPtr(), done_callback,
/*abort=*/false))
.OnAborted(base::BindOnce(&AppsGridView::OnFadeOutAnimationEnded,
weak_factory_.GetWeakPtr(), done_callback,
/*abort=*/true))
.Once()
.SetDuration(kFadeOutAnimationDuration)
.SetOpacity(layer(), 0.f, gfx::Tween::LINEAR);
return animation_builder;
}
views::AnimationBuilder AppsGridView::FadeInVisibleItemsForReorder(
ReorderAnimationCallback done_callback) {
DCHECK_EQ(AppListGridAnimationStatus::kReorderIntermediaryState,
grid_animation_status_);
DCHECK(!IsItemAnimationRunning());
// When `AppsGridView::OnListItemMoved()` is called due to item reorder,
// the layout updates asynchronously. Meanwhile, calculating the visible item
// range needs the up-to-date layout. Therefore update the layout explicitly
// before calculating `range`.
if (needs_layout()) {
DeprecatedLayoutImmediately();
}
grid_animation_status_ = AppListGridAnimationStatus::kReorderFadeIn;
const std::optional<VisibleItemIndexRange> range = GetVisibleItemIndexRange();
views::AnimationBuilder animation_builder;
// No items to be sorted are visible - return an empty animation builder that
// ends immediately.
if (!range) {
animation_builder
.OnEnded(base::BindOnce(&AppsGridView::OnFadeInAnimationEnded,
weak_factory_.GetWeakPtr(), done_callback,
/*abort=*/true))
.OnAborted(base::BindOnce(&AppsGridView::OnFadeInAnimationEnded,
weak_factory_.GetWeakPtr(), done_callback,
/*abort=*/true))
.Once()
.SetDuration(base::TimeDelta());
return animation_builder;
}
// Only show the visible items during animation to reduce the cost of painting
// that is triggered by view bounds changes due to reorder.
for (size_t visible_view_index = range->first_index;
visible_view_index <= range->last_index; ++visible_view_index) {
view_model_.view_at(visible_view_index)->SetVisible(true);
}
grid_animation_abort_handle_ = animation_builder.GetAbortHandle();
animation_builder
.OnEnded(base::BindOnce(&AppsGridView::OnFadeInAnimationEnded,
weak_factory_.GetWeakPtr(), done_callback,
/*abort=*/false))
.OnAborted(base::BindOnce(&AppsGridView::OnFadeInAnimationEnded,
weak_factory_.GetWeakPtr(), done_callback,
/*abort=*/true))
.Once()
.SetDuration(kFadeInAnimationDuration)
.SetOpacity(layer(), 1.f, gfx::Tween::ACCEL_5_70_DECEL_90);
// Assume all the items matched by the indices in `range` are
// placed on the same page.
const int page_index =
GetGridIndexFromIndexInViewModel(range->first_index).page;
const int base_offset =
kFadeAnimationOffsetRatio * GetTotalTileSize(page_index).height();
// The row of the first visible item.
const int base_row = range->first_index / cols_;
for (size_t visible_view_index = range->first_index;
visible_view_index <= range->last_index; ++visible_view_index) {
// Calculate translate offset for each view. NOTE: The items on the
// different rows have different fade in offsets. The ratio between the
// offset and `base_offset` is (relative_row_index + 2).
const int relative_row_index = visible_view_index / cols_ - base_row;
const int offset = (relative_row_index + 2) * base_offset;
views::View* animated_view = GetItemViewAt(visible_view_index);
PrepareForLayerAnimation(animated_view);
// Create a slide animation on `animted_view` using `sequence_block`'s
// existing time duration.
SlideViewIntoPositionWithSequenceBlock(
animated_view, offset,
/*time_delta=*/std::nullopt, gfx::Tween::ACCEL_5_70_DECEL_90,
&animation_builder.GetCurrentSequence());
}
return animation_builder;
}
void AppsGridView::SlideVisibleItemsForHideContinueSection(int base_offset) {
DCHECK(IsTabletMode()); // This animation is only used in tablet mode.
if (needs_layout()) {
DeprecatedLayoutImmediately();
}
const std::optional<VisibleItemIndexRange> range = GetVisibleItemIndexRange();
// Safety check, unlikely in production.
if (!range) {
return;
}
// The continue section is on the 0th page. Don't animate if a different page
// is selected.
if (GetGridIndexFromIndexInViewModel(range->first_index).page != 0) {
return;
}
grid_animation_status_ = AppListGridAnimationStatus::kHideContinueSection;
views::AnimationBuilder animation_builder;
grid_animation_abort_handle_ = animation_builder.GetAbortHandle();
animation_builder
.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
.OnEnded(
base::BindOnce(&AppsGridView::OnHideContinueSectionAnimationEnded,
weak_factory_.GetWeakPtr()))
.OnAborted(
base::BindOnce(&AppsGridView::OnHideContinueSectionAnimationEnded,
weak_factory_.GetWeakPtr()))
.Once()
.SetDuration(base::Milliseconds(300));
// Animate each row of app icons with a different offset.
for (size_t item_index = range->first_index; item_index <= range->last_index;
++item_index) {
const int row_index = item_index / cols_;
// The 0th row animates base_offset * 3 / 4
// The 1st row animates base_offset * 2 / 4
// The 2nd row animates base_offset * 1 / 4
const int vertical_offset = std::max(0, base_offset * (3 - row_index) / 4);
// Ensure each icon view has a layer. These are cleaned up on animation end.
views::View* icon = GetItemViewAt(item_index);
PrepareForLayerAnimation(icon);
// Slide each icon into position.
SlideViewIntoPositionWithSequenceBlock(
icon, vertical_offset, /*time_delta=*/std::nullopt,
gfx::Tween::ACCEL_LIN_DECEL_100_3,
&animation_builder.GetCurrentSequence());
}
}
void AppsGridView::OnHideContinueSectionAnimationEnded() {
grid_animation_status_ = AppListGridAnimationStatus::kEmpty;
// Clean up the layers created for the app icon views.
DestroyLayerItemsIfNotNeeded();
}
bool AppsGridView::IsItemAnimationRunning() const {
for (size_t i = 0; i < view_model_.view_size(); ++i) {
AppListItemView* view = GetItemViewAt(i);
if (IsAnimatingView(view)) {
return true;
}
}
return false;
}
void AppsGridView::CancelAllItemAnimations() {
// Collect layers and stop animating in another pass to reduce risk of view
// changes to `view_model_` during iteration.
std::vector<ui::Layer*> item_layers;
for (size_t i = 0; i < view_model_.view_size(); ++i) {
AppListItemView* view = GetItemViewAt(i);
if (IsAnimatingView(view)) {
item_layers.push_back(view->layer());
}
}
for (auto* layer : item_layers) {
layer->GetAnimator()->StopAnimating();
}
}
bool AppsGridView::FireFolderItemReparentTimerForTest() {
// With the drag and drop refactor, folder is closed immediately OnDragExit
// without timer. For testing purpuses, return true on this case.
if (app_list_features::IsDragAndDropRefactorEnabled()) {
return true;
}
if (!folder_item_reparent_timer_.IsRunning()) {
return false;
}
folder_item_reparent_timer_.FireNow();
return true;
}
bool AppsGridView::FireDragToShelfTimerForTest() {
if (!host_drag_start_timer_.IsRunning()) {
return false;
}
host_drag_start_timer_.FireNow();
return true;
}
void AppsGridView::AddReorderCallbackForTest(
TestReorderDoneCallbackType done_callback) {
DCHECK(done_callback);
reorder_animation_callback_queue_for_test_.push(std::move(done_callback));
}
void AppsGridView::AddFadeOutAnimationStartClosureForTest(
base::OnceClosure start_closure) {
DCHECK(start_closure);
DCHECK(!fade_out_done_closure_for_test_);
fade_out_start_closure_for_test_ = std::move(start_closure);
}
void AppsGridView::AddFadeOutAnimationDoneClosureForTest(
base::OnceClosure done_closure) {
DCHECK(done_closure);
DCHECK(!fade_out_done_closure_for_test_);
fade_out_done_closure_for_test_ = std::move(done_closure);
}
bool AppsGridView::HasAnyWaitingReorderDoneCallbackForTest() const {
return !reorder_animation_callback_queue_for_test_.empty();
}
void AppsGridView::StartDragAndDropHostDrag() {
// When a drag and drop host is given, the item can be dragged out of the app
// list window. In that case a proxy widget needs to be used.
if (!drag_view_) {
return;
}
// We have to hide the original item since the drag and drop host will do
// the OS dependent code to "lift off the dragged item". Apply the scale
// factor of this view's transform to the dragged view as well.
DCHECK(!IsDraggingForReparentInRootLevelGridView());
gfx::Point location_in_screen = drag_start_grid_view_;
views::View::ConvertPointToScreen(this, &location_in_screen);
const gfx::Point icon_location_in_screen =
drag_view_->GetIconBoundsInScreen().CenterPoint();
const bool is_folder = drag_view_->item()->is_folder();
// Set the refreshed folder shadow size equal to the folder icon background
// circle.
const gfx::Size shadow_size = is_folder
? app_list_config_->icon_visible_size()
: drag_view_->GetDragImage().size();
drag_icon_proxy_ = std::make_unique<AppDragIconProxy>(
GetWidget()->GetNativeWindow()->GetRootWindow(),
drag_view_->GetDragImage(), gfx::ImageSkia(), location_in_screen,
location_in_screen - icon_location_in_screen,
is_folder ? kDragAndDropProxyScale : 1.0f, is_folder, shadow_size);
drag_view_hider_ = std::make_unique<DragViewHider>(drag_view_);
}
void AppsGridView::DispatchDragEventToDragAndDropHost(
const gfx::Point& location_in_screen_coordinates) {
if (!drag_view_ || !drag_and_drop_host_) {
return;
}
const bool should_host_handle_drag = drag_and_drop_host_->ShouldHandleDrag(
drag_view_->item()->id(), location_in_screen_coordinates);
if (!should_host_handle_drag) {
if (host_drag_start_timer_.IsRunning())
host_drag_start_timer_.AbandonAndStop();
// The event was issued inside the app menu and we should get all events.
if (forward_events_to_drag_and_drop_host_) {
// The DnD host was previously called and needs to be informed that the
// session returns to the owner.
forward_events_to_drag_and_drop_host_ = false;
// NOTE: Not passing the drag icon proxy to the drag and drop host because
// the drag operation is still in progress, and remains being handled by
// the apps grid view.
drag_and_drop_host_->EndDrag(true, /*drag_icon_proxy=*/nullptr);
}
return;
}
if (IsFolderItem(drag_view_->item())) {
return;
}
// NOTE: Drag events are forwarded to drag and drop host whenever drag and
// drop host can handle them. At the time of writing, drag and drop host
// bounds and apps grid view bounds are not expected to overlap - if that
// changes, the logic for determining when to forward events to the host
// should be re-evaluated.
DCHECK(should_host_handle_drag);
// If the drag and drop host is not already handling drag events, make sure a
// drag and drop host start timer gets scheduled.
if (!forward_events_to_drag_and_drop_host_) {
if (!host_drag_start_timer_.IsRunning()) {
host_drag_start_timer_.Start(FROM_HERE, kShelfHandleIconDragDelay, this,
&AppsGridView::OnHostDragStartTimerFired);
MaybeStopPageFlip();
StopAutoScroll();
}
return;
}
DCHECK(forward_events_to_drag_and_drop_host_);
if (!drag_and_drop_host_->Drag(location_in_screen_coordinates,
drag_icon_proxy_->GetBoundsInScreen())) {
// The host is not active any longer and we cancel the operation.
forward_events_to_drag_and_drop_host_ = false;
// NOTE: Not passing the drag icon proxy to the drag and drop host because
// the drag operation is still in progress, and remains being handled by
// the apps grid view.
drag_and_drop_host_->EndDrag(true, /*drag_icon_proxy=*/nullptr);
}
}
void AppsGridView::MoveItemInModel(AppListItem* item, const GridIndex& target) {
const std::string item_id = item->id();
size_t current_item_list_index = 0;
bool found = item_list_->FindItemIndex(item_id, ¤t_item_list_index);
CHECK(found);
size_t target_item_list_index = GetIndexInViewModel(target);
{
ScopedModelUpdate update(this);
item_list_->MoveItem(current_item_list_index, target_item_list_index);
}
}
bool AppsGridView::MoveItemToFolder(AppListItem* item,
const GridIndex& target,
AppListAppMovingType move_type,
std::string* folder_id,
bool* is_new_folder) {
const std::string source_item_id = item->id();
const std::string source_folder_id = item->folder_id();
AppListItemView* target_view =
GetViewDisplayedAtSlotOnCurrentPage(target.slot);
DCHECK(target_view);
const std::string target_view_item_id = target_view->item()->id();
// An app is being reparented to its original folder. Just cancel the
// reparent.
if (target_view_item_id == source_folder_id) {
return false;
}
*is_new_folder = !target_view->is_folder();
{
ScopedModelUpdate update(this);
*folder_id = model_->MergeItems(target_view_item_id, source_item_id);
}
if (folder_id->empty()) {
LOG(ERROR) << "Unable to merge into item id: " << target_view_item_id;
return false;
}
if (*is_new_folder)
base::RecordAction(base::UserMetricsAction("AppList_CreateFolder"));
MaybeRecordFolderDeleteUserAction(source_folder_id);
RecordAppMovingTypeMetrics(move_type);
return true;
}
void AppsGridView::ReparentItemForReorder(AppListItem* item,
const GridIndex& target) {
DCHECK(item->IsInFolder());
const std::string item_id = item->id();
const std::string source_folder_id = item->folder_id();
int target_item_index = GetIndexInViewModel(target);
// Move the item from its parent folder to top level item list. Calculate the
// target position in the top level list.
syncer::StringOrdinal target_position;
if (target_item_index < static_cast<int>(item_list_->item_count()))
target_position = item_list_->item_at(target_item_index)->position();
{
ScopedModelUpdate update(this);
model_->MoveItemToRootAt(item, target_position);
}
MaybeRecordFolderDeleteUserAction(source_folder_id);
}
void AppsGridView::MaybeRecordFolderDeleteUserAction(
const std::string& folder_id) {
// Ignore the top-level grid (which isn't a folder and can't be deleted).
if (folder_id.empty()) {
return;
}
// If the folder disappeared from the model, record a user action.
if (!model_->FindFolderItem(folder_id))
base::RecordAction(base::UserMetricsAction("AppList_DeleteFolder"));
}
void AppsGridView::CancelContextMenusOnCurrentPage() {
GridIndex start_index(GetSelectedPage(), 0);
if (!IsValidIndex(start_index)) {
return;
}
const size_t start = GetIndexInViewModel(start_index);
const std::optional<int> tiles_per_page = TilesPerPage(start_index.page);
const size_t end = tiles_per_page ? std::min(view_model_.view_size(),
start + *tiles_per_page)
: view_model_.view_size();
for (size_t i = start; i < end; ++i) {
GetItemViewAt(i)->CancelContextMenu();
}
}
void AppsGridView::DeleteItemViewAtIndex(size_t index) {
AppListItemView* item_view = GetItemViewAt(index);
view_model_.Remove(index);
if (item_view == drag_view_) {
drag_view_ = nullptr;
}
if (open_folder_info_ &&
open_folder_info_->item_id == item_view->item()->id()) {
open_folder_info_.reset();
}
delete item_view;
}
bool AppsGridView::IsPointWithinDragBuffer(const gfx::Point& point) const {
gfx::Rect rect(GetLocalBounds());
rect.Inset(-kDragBufferPx);
return rect.Contains(point);
}
void AppsGridView::ScheduleLayout(const gfx::Size& previous_grid_size) {
if (GetTileGridSize() != previous_grid_size) {
PreferredSizeChanged(); // Calls InvalidateLayout() internally.
} else {
InvalidateLayout();
}
DCHECK(needs_layout());
}
void AppsGridView::OnListItemAdded(size_t index, AppListItem* item) {
const gfx::Size initial_grid_size = GetTileGridSize();
if (!updating_model_) {
EndDrag(true);
}
// Abort reorder animation before a view is added to `view_model_`.
MaybeAbortWholeGridAnimation();
AppListItemView* view = MaybeSwapPlaceholderAsset(index);
if (item == drag_item_) {
drag_view_ = view;
drag_view_hider_ = std::make_unique<DragViewHider>(drag_view_);
}
view->InitializeIconLoader();
// If model update is in progress, paging should be updated when the operation
// that caused the model update completes.
if (!updating_model_) {
UpdatePaging();
UpdateColsAndRowsForFolder();
UpdatePulsingBlockViews();
}
// Schedule a layout, since the grid items may need their bounds updated.
ScheduleLayout(initial_grid_size);
items_container_->NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged,
/*send_native_event=*/true);
// Attempt to animate the transition from a promise app into an actual app
const std::string package_name =
view->item()->GetMetadata()->promise_package_id;
auto found = pending_promise_apps_removals_.find(package_name);
if (item->GetMetadata()->app_status == AppStatus::kReady &&
found != pending_promise_apps_removals_.end()) {
view->AnimateInFromPromiseApp(
found->second,
base::BindRepeating(&AppsGridView::FinishAnimationForPromiseApps,
weak_factory_.GetWeakPtr(), package_name));
}
}
void AppsGridView::FinishAnimationForPromiseApps(
const std::string& pending_app_id) {
PendingAppsMap::iterator pending_app_found =
pending_promise_apps_removals_.find(pending_app_id);
// Discard the pending promise app layer.
if (pending_app_found != pending_promise_apps_removals_.end()) {
auto pending_app_scope(std::move(pending_app_found->second));
pending_promise_apps_removals_.erase(pending_app_found);
}
DestroyLayerItemsIfNotNeeded();
}
void AppsGridView::OnListItemRemoved(size_t index, AppListItem* item) {
const gfx::Size initial_grid_size = GetTileGridSize();
if (!updating_model_) {
EndDrag(true);
}
MaybeDuplicatePromiseAppForRemoval(GetItemViewAt(index));
// Abort reorder animation before a view is deleted from `view_model_`.
MaybeAbortWholeGridAnimation();
DeleteItemViewAtIndex(GetModelIndexOfItem(item));
// If model update is in progress, paging should be updated when the operation
// that caused the model update completes.
if (!updating_model_) {
UpdatePaging();
UpdateColsAndRowsForFolder();
UpdatePulsingBlockViews();
}
// Schedule a layout, since the grid items may need their bounds updated.
ScheduleLayout(initial_grid_size);
items_container_->NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged,
/*send_native_event=*/true);
}
void AppsGridView::MaybeDuplicatePromiseAppForRemoval(
AppListItemView* promise_app_view) {
if (!ash::features::ArePromiseIconsEnabled()) {
return;
}
if (!promise_app_view || !promise_app_view->is_promise_app()) {
return;
}
AppListItem* item = promise_app_view->item();
if (item->app_status() != AppStatus::kInstallSuccess ||
!promise_app_view->IsDrawn()) {
return;
}
bool existing_app_in_grid = false;
// Search along the `view_model_` for an existing app with the same
// package id as the promise app to be removed.
for (const auto& entry : view_model_.entries()) {
AppListItemView* view = views::AsViewClass<AppListItemView>(entry.view);
if (view == promise_app_view) {
continue;
}
if (view->item()->GetMetadata()->promise_package_id == item->id()) {
existing_app_in_grid = true;
break;
}
}
// PromiseApps don't get animation for removal if an app already existst in
// the grid.
if (!existing_app_in_grid) {
AddPendingPromiseAppRemoval(item->id(),
promise_app_view->icon_image_model());
}
}
void AppsGridView::OnListItemMoved(size_t from_index,
size_t to_index,
AppListItem* item) {
// Abort reorder animation if the apps grid is updated by the user.
if (!updating_model_) {
MaybeAbortWholeGridAnimation();
EndDrag(true);
}
// The item is updated in the item list but the view_model is not updated,
// so get current model index by looking up view_model and predict the
// target model index based on its current item index.
size_t from_model_index = GetModelIndexOfItem(item);
view_model_.Move(from_model_index, to_index);
items_container_->ReorderChildView(view_model_.view_at(to_index), to_index);
items_container_->NotifyAccessibilityEvent(ax::mojom::Event::kChildrenChanged,
true /* send_native_event */);
// If model update is in progress, paging should be updated when the operation
// that caused the model update completes.
if (!updating_model_) {
UpdatePaging();
UpdateColsAndRowsForFolder();
UpdatePulsingBlockViews();
}
if (!updating_model_ && GetWidget() && GetWidget()->IsVisible() &&
enable_item_move_animation_) {
AnimateToIdealBounds(/*top to bottom animation=*/from_index < to_index);
} else if (IsUnderWholeGridAnimation()) {
// During reorder animation, multiple items could be moved subsequently so
// use the asynchronous layout to reduce painting cost.
InvalidateLayout();
} else {
DeprecatedLayoutImmediately();
}
}
void AppsGridView::AddPendingPromiseAppRemoval(
const std::string& id,
const ui::ImageModel& default_image) {
auto found = pending_promise_apps_removals_.find(id);
if (found != pending_promise_apps_removals_.end()) {
// A promise app might share app id with other apps in the same package.
// If a promise app removal is already scheduled to be removed for this
// package, just return normally.
return;
}
pending_promise_apps_removals_.emplace(id, default_image);
}
void AppsGridView::OnAppListModelStatusChanged() {
UpdatePulsingBlockViews();
InvalidateLayout();
}
void AppsGridView::DestroyLayerItemsIfNotNeeded() {
if (ItemViewsRequireLayers()) {
return;
}
for (const auto& entry : view_model_.entries()) {
AppListItemView* view = views::AsViewClass<AppListItemView>(entry.view);
// When the item view has finished animating, then also delete the row
// change layer if possible.
row_change_animator_->CancelAnimation(view);
if (!view->AlwaysPaintsToLayer()) {
view->DestroyLayer();
}
}
}
bool AppsGridView::ItemViewsRequireLayers() const {
// Layers required for app list item move animations during drag (to make room
// for the current placeholder).
if (drag_item_ || drag_icon_proxy_ || drag_image_layer_) {
return true;
}
// Bounds animations are in progress, which use layers to animate transforms.
if (IsItemAnimationRunning()) {
return true;
}
// Reorder animation animate app list item layers.
if (IsUnderWholeGridAnimation()) {
return true;
}
// Folder position is changing after folder closure - this involves animating
// folder item view layer out and in, and changing other view's bounds.
if (reordering_folder_view_) {
return true;
}
if (setting_up_ideal_bounds_animation_) {
return true;
}
if (IsAnimatingCardifiedState()) {
return true;
}
return false;
}
GridIndex AppsGridView::GetNearestTileIndexForPoint(
const gfx::Point& point) const {
gfx::Rect bounds = GetContentsBounds();
const int current_page = GetSelectedPage();
bounds.Inset(GetTilePadding(current_page));
const gfx::Size total_tile_size = GetTotalTileSize(current_page);
const gfx::Vector2d grid_offset = GetGridCenteringOffset(current_page);
DCHECK_GT(total_tile_size.width(), 0);
int col = std::clamp(
(point.x() - bounds.x() - grid_offset.x()) / total_tile_size.width(), 0,
cols_ - 1);
DCHECK_GT(total_tile_size.height(), 0);
const int ideal_row =
(point.y() - bounds.y() - grid_offset.y()) / total_tile_size.height();
const std::optional<int> tiles_per_page = TilesPerPage(current_page);
const int row = tiles_per_page
? std::clamp(ideal_row, 0, *tiles_per_page / cols_ - 1)
: std::max(ideal_row, 0);
return GridIndex(current_page, row * cols_ + col);
}
gfx::Rect AppsGridView::GetExpectedTileBounds(const GridIndex& index) const {
if (!cols_) {
return gfx::Rect();
}
gfx::Rect bounds(GetContentsBounds());
bounds.Inset(GetTilePadding(index.page));
int row = index.slot / cols_;
int col = index.slot % cols_;
const gfx::Size total_tile_size = GetTotalTileSize(index.page);
gfx::Rect tile_bounds(gfx::Point(bounds.x() + col * total_tile_size.width(),
bounds.y() + row * total_tile_size.height()),
total_tile_size);
tile_bounds.Offset(GetGridCenteringOffset(index.page));
tile_bounds.Inset(-GetTilePadding(index.page));
return tile_bounds;
}
bool AppsGridView::IsViewHiddenForDrag(const views::View* view) const {
return drag_view_hider_ && drag_view_hider_->drag_view() == view;
}
bool AppsGridView::IsViewHiddenForFolderReorder(const views::View* view) const {
return reordering_folder_view_ && *reordering_folder_view_ == view;
}
bool AppsGridView::IsUnderWholeGridAnimation() const {
return grid_animation_status_ != AppListGridAnimationStatus::kEmpty;
}
bool AppsGridView::IsViewExplicitlyHidden(const views::View* view) const {
return IsViewHiddenForDrag(view) || IsViewHiddenForFolderReorder(view) ||
hidden_view_for_test_ == view;
}
void AppsGridView::MaybeAbortWholeGridAnimation() {
switch (grid_animation_status_) {
case AppListGridAnimationStatus::kEmpty:
case AppListGridAnimationStatus::kReorderIntermediaryState:
// No active whole-grid animation so nothing to do.
break;
case AppListGridAnimationStatus::kReorderFadeOut:
case AppListGridAnimationStatus::kReorderFadeIn:
case AppListGridAnimationStatus::kHideContinueSection:
DCHECK(grid_animation_abort_handle_);
grid_animation_abort_handle_.reset();
break;
}
}
AppListItemView* AppsGridView::GetViewDisplayedAtSlotOnCurrentPage(
int slot) const {
if (slot < 0) {
return nullptr;
}
// Calculate the original bound of the tile at |index|.
gfx::Rect tile_rect =
GetExpectedTileBounds(GridIndex(GetSelectedPage(), slot));
tile_rect.Offset(CalculateTransitionOffset(GetSelectedPage()));
const auto& entries = view_model_.entries();
const auto iter = base::ranges::find_if(entries, [&](const auto& entry) {
return entry.view->bounds() == tile_rect && entry.view.get() != drag_view_;
});
return iter == entries.end() ? nullptr
: static_cast<AppListItemView*>(iter->view);
}
void AppsGridView::SetAsFolderDroppingTarget(const GridIndex& target_index,
bool is_target_folder) {
AppListItemView* target_view =
GetViewDisplayedAtSlotOnCurrentPage(target_index.slot);
if (target_view) {
target_view->SetAsAttemptedFolderTarget(is_target_folder);
if (is_target_folder)
target_view->OnDraggedViewEnter();
else
target_view->OnDraggedViewExit();
}
}
GridIndex AppsGridView::GetTargetGridIndexForKeyboardMove(
ui::KeyboardCode key_code) const {
DCHECK(key_code == ui::VKEY_LEFT || key_code == ui::VKEY_RIGHT ||
key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN);
DCHECK(selected_view_);
const GridIndex source_index = GetIndexOfView(selected_view_);
if (key_code == ui::VKEY_LEFT || key_code == ui::VKEY_RIGHT) {
// Define backward key for traversal based on RTL.
const ui::KeyboardCode backward =
base::i18n::IsRTL() ? ui::VKEY_RIGHT : ui::VKEY_LEFT;
size_t target_model_index =
view_model_.GetIndexOfView(selected_view_).value();
if (target_model_index > 0 || key_code != backward)
target_model_index += (key_code == backward) ? -1 : 1;
// A forward move on the last item in |view_model_| should do nothing.
if (target_model_index == view_model_.view_size()) {
return source_index;
}
return GetIndexOfView(
static_cast<const AppListItemView*>(GetItemViewAt(target_model_index)));
}
// Handle the vertical move. Attempt to place the app in the same column.
int target_page = source_index.page;
int target_row =
source_index.slot / cols_ + (key_code == ui::VKEY_UP ? -1 : 1);
if (target_row < 0) {
// The app will move to the last row of the previous page.
--target_page;
if (target_page < 0) {
return source_index;
}
// When moving up, place the app in the last row.
target_row = (GetNumberOfItemsOnPage(target_page) - 1) / cols_;
} else if (target_row > (GetNumberOfItemsOnPage(target_page) - 1) / cols_) {
// The app will move to the first row of the next page.
++target_page;
if (target_page >= GetTotalPages()) {
return source_index;
}
target_row = 0;
}
// The ideal slot shares a column with |source_index|.
const int ideal_slot = target_row * cols_ + source_index.slot % cols_;
return GridIndex(
target_page,
std::min(GetNumberOfItemsOnPage(target_page) - 1, ideal_slot));
}
GridIndex AppsGridView::GetTargetGridIndexForKeyboardReparent(
const GridIndex& folder_index,
ui::KeyboardCode key_code) const {
DCHECK(!folder_delegate_) << "Reparenting target calculations occur from the "
"root AppsGridView, not the folder AppsGridView";
// A backward move means the item will be placed previous to the folder. To do
// this without displacing other items, place the item in the folders slot.
// The folder will then shift forward.
const ui::KeyboardCode backward =
base::i18n::IsRTL() ? ui::VKEY_RIGHT : ui::VKEY_LEFT;
if (key_code == backward) {
return folder_index;
}
GridIndex target_index = GetTargetGridIndexForKeyboardMove(key_code);
// If the item is expected to be positioned after the parent view,
// `GetTargetGridIndexForKeyboardMove()` may return folder index to indicate
// no-op operation for move (e.g. if the folder is the last item), assuming
// that there are no slots available. Reparent is an insertion operation, so
// creating an extra trailing slot is allowed.
if (target_index == folder_index &&
(key_code != ui::VKEY_UP && key_code != backward)) {
if (IsPageFull(target_index.page))
return GridIndex(target_index.page + 1, 0);
return GridIndex(target_index.page, target_index.slot + 1);
}
// Ensure the item is placed on the same page as the folder when possible.
if (target_index.page < folder_index.page) {
return folder_index;
}
if (target_index.page > folder_index.page) {
const std::optional<int> folder_page_size = TilesPerPage(folder_index.page);
// Target index page being at least 1 indicates paged apps grid, so number
// of tiles per page should be bounded.
DCHECK(folder_page_size);
if (folder_index.slot + 1 < *folder_page_size)
return GridIndex(folder_index.page, folder_index.slot + 1);
}
return target_index;
}
void AppsGridView::HandleKeyboardMove(ui::KeyboardCode key_code) {
DCHECK(selected_view_);
const GridIndex target_index = GetTargetGridIndexForKeyboardMove(key_code);
const GridIndex starting_index = GetIndexOfView(selected_view_);
if (target_index == starting_index || !IsValidIndex(target_index)) {
return;
}
handling_keyboard_move_ = true;
AppListItemView* original_selected_view = selected_view_;
const GridIndex original_selected_view_index =
GetIndexOfView(original_selected_view);
// Moving an AppListItemView is either a swap within the origin page, a swap
// to a full page, or a dump to a page with room. A move within a folder is
// always a swap because there are no gaps.
const bool swap_items =
folder_delegate_ || IsPageFull(target_index.page) ||
target_index.page == original_selected_view_index.page;
AppListItemView* target_view = GetViewAtIndex(target_index);
MoveItemInModel(selected_view_->item(), target_index);
if (swap_items) {
DCHECK(target_view);
MoveItemInModel(target_view->item(), original_selected_view_index);
}
int target_page = target_index.page;
if (!folder_delegate_) {
// Update paging because the move could have resulted in a
// page getting collapsed or created.
UpdatePaging();
// |target_page| may change due to a page collapsing.
target_page = std::min(GetTotalPages() - 1, target_index.page);
}
DeprecatedLayoutImmediately();
EnsureViewVisible(GridIndex(target_page, target_index.slot));
SetSelectedView(original_selected_view);
AnnounceReorder(target_index);
if (target_index.page != original_selected_view_index.page &&
!folder_delegate_) {
RecordPageSwitcherSource(kMoveAppWithKeyboard);
}
}
bool AppsGridView::IsValidIndex(const GridIndex& index) const {
const std::optional<int> tiles_per_page = TilesPerPage(index.page);
const int extra_valid_slots = HasExtraSlotForReorderPlaceholder() ? 1 : 0;
return index.page >= 0 && index.page < GetTotalPages() && index.slot >= 0 &&
(!tiles_per_page || index.slot < *tiles_per_page) &&
static_cast<size_t>(GetIndexInViewModel(index)) <
view_model_.view_size() + extra_valid_slots;
}
size_t AppsGridView::GetModelIndexOfItem(const AppListItem* item) const {
const auto& entries = view_model_.entries();
const auto iter = base::ranges::find(entries, item, [](const auto& entry) {
return static_cast<AppListItemView*>(entry.view)->item();
});
return static_cast<size_t>(std::distance(entries.begin(), iter));
}
int AppsGridView::GetNumberOfItemsOnPage(int page) const {
if (page < 0 || page >= GetTotalPages()) {
return 0;
}
// We are guaranteed not on the last page, so the page must be full.
if (page < GetTotalPages() - 1) {
return *TilesPerPage(page);
}
// We are on the last page, so calculate the number of items on the page.
size_t item_count = view_model_.view_size();
int current_page = 0;
while (current_page < GetTotalPages() - 1) {
std::optional<int> tiles_per_page = TilesPerPage(current_page);
// `current_page` not being the last page implies a paged apps grid view,
// as the grid has more than one page. For paged apps grid view,
// `TilesPerPage()` should be defined.
DCHECK(tiles_per_page);
item_count -= *tiles_per_page;
++current_page;
}
return item_count;
}
void AppsGridView::MaybeCreateFolderDroppingAccessibilityEvent() {
if (!drag_item_ || !drag_view_) {
return;
}
if (drop_target_region_ != ON_ITEM || !DropTargetIsValidFolder() ||
IsFolderItem(drag_item_) || folder_delegate_ ||
drop_target_ == last_folder_dropping_a11y_event_location_) {
return;
}
last_folder_dropping_a11y_event_location_ = drop_target_;
last_reorder_a11y_event_location_ = GridIndex();
AppListItemView* drop_view =
GetViewDisplayedAtSlotOnCurrentPage(drop_target_.slot);
DCHECK(drop_view);
a11y_announcer_->AnnounceFolderDrop(drag_view_->title()->GetText(),
drop_view->title()->GetText(),
drop_view->is_folder());
}
void AppsGridView::MaybeCreateDragReorderAccessibilityEvent() {
if (drop_target_region_ == ON_ITEM && !IsFolderItem(drag_item_)) {
return;
}
// If app was dragged out of folder, no need to announce location for the
// now closed folder.
if (drag_out_of_folder_container_) {
return;
}
// If drop_target is not set or was already reset, then return.
if (drop_target_ == GridIndex()) {
return;
}
// Don't create a11y event if |drop_target| has not changed.
if (last_reorder_a11y_event_location_ == drop_target_) {
return;
}
last_folder_dropping_a11y_event_location_ = GridIndex();
last_reorder_a11y_event_location_ = drop_target_;
AnnounceReorder(last_reorder_a11y_event_location_);
}
void AppsGridView::AnnounceReorder(const GridIndex& target_index) {
const int row =
((target_index.slot - (target_index.slot % cols_)) / cols_) + 1;
const int col = (target_index.slot % cols_) + 1;
if (!GetMaxRowsInPage(0)) {
// Don't announce the page for single-page grids (e.g. scrollable grids).
a11y_announcer_->AnnounceAppsGridReorder(row, col);
} else {
// Announce the page for paged grids.
const int page = target_index.page + 1;
a11y_announcer_->AnnounceAppsGridReorder(page, row, col);
}
}
void AppsGridView::CreateGhostImageView() {
if (!drag_item_) {
return;
}
// OnReorderTimer() can trigger this function even when the
// |reorder_placeholder_| does not change, no need to set a new GhostImageView
// in this case.
if (reorder_placeholder_ == current_ghost_location_) {
return;
}
// When the item is dragged outside the boundaries of the app grid, if the
// |reorder_placeholder_| moves to another page, then do not show a ghost.
if (GetSelectedPage() != reorder_placeholder_.page) {
BeginHideCurrentGhostImageView();
return;
}
BeginHideCurrentGhostImageView();
current_ghost_location_ = reorder_placeholder_;
if (last_ghost_view_) {
delete last_ghost_view_;
}
// Preserve |current_ghost_view_| while it fades out and instantiate a new
// GhostImageView that will fade in.
last_ghost_view_ = current_ghost_view_;
auto current_ghost_view =
std::make_unique<GhostImageView>(reorder_placeholder_);
gfx::Rect ghost_view_bounds = GetExpectedTileBounds(reorder_placeholder_);
ghost_view_bounds.Offset(
CalculateTransitionOffset(reorder_placeholder_.page));
current_ghost_view->Init(ghost_view_bounds,
app_list_config_->grid_focus_corner_radius());
current_ghost_view_ =
items_container_->AddChildView(std::move(current_ghost_view));
current_ghost_view_->FadeIn();
// Adding the ghost view can reorder the child layers of the
// |items_container_| so make sure the background cards remain at the bottom.
StackCardsAtBottom();
}
void AppsGridView::BeginHideCurrentGhostImageView() {
current_ghost_location_ = GridIndex();
if (current_ghost_view_) {
current_ghost_view_->FadeOut();
}
}
void AppsGridView::PrepareItemsForBoundsAnimation() {
for (size_t i = 0; i < view_model_.view_size(); ++i)
view_model_.view_at(i)->EnsureLayer();
}
bool AppsGridView::HasExtraSlotForReorderPlaceholder() const {
return reorder_placeholder_.IsValid() && !drag_view_;
}
void AppsGridView::OnAppListItemViewActivated(
AppListItemView* pressed_item_view,
const ui::Event& event) {
if (IsDragging()) {
return;
}
if (IsFolderItem(pressed_item_view->item())) {
// Note that `folder_controller_` will be null inside a folder apps grid,
// but those grid are not expected to contain folder items.
DCHECK(folder_controller_);
SetOpenFolderInfo(pressed_item_view->item()->id(),
GetIndexOfView(pressed_item_view), GridIndex());
ShowFolderForView(pressed_item_view, /*new_folder=*/false);
return;
}
base::RecordAction(base::UserMetricsAction("AppList_ClickOnApp"));
RecordAppListByCollectionLaunched(pressed_item_view->item()->collection_id(),
/*is_apps_collections_page=*/false);
// Avoid using |item->id()| as the parameter. In some rare situations,
// activating the item may destruct it. Using the reference to an object
// which may be destroyed during the procedure as the function parameter
// may bring the crash like https://crbug.com/990282.
const std::string id = pressed_item_view->item()->id();
const bool is_above_the_fold = IsAboveTheFold(pressed_item_view);
app_list_view_delegate()->ActivateItem(id, event.flags(),
AppListLaunchedFrom::kLaunchedFromGrid,
is_above_the_fold);
}
bool AppsGridView::IsAboveTheFold(AppListItemView* item_view) {
return false;
}
void AppsGridView::OnHostDragStartTimerFired() {
DCHECK(drag_and_drop_host_);
gfx::Point last_drag_point_in_screen = last_drag_point_;
views::View::ConvertPointToScreen(this, &last_drag_point_in_screen);
if (drag_and_drop_host_->StartDrag(drag_view_->item()->id(),
last_drag_point_in_screen,
drag_icon_proxy_->GetBoundsInScreen())) {
// From now on we forward the drag events.
forward_events_to_drag_and_drop_host_ = true;
}
}
void AppsGridView::OnFadeOutAnimationEnded(ReorderAnimationCallback callback,
bool aborted) {
grid_animation_status_ =
AppListGridAnimationStatus::kReorderIntermediaryState;
// Reset with the identical transformation. Because the apps grid view is
// translucent now, setting the layer transform does not bring noticeable
// differences.
layer()->SetTransform(gfx::Transform());
if (aborted) {
// If the fade out animation is aborted, show the apps grid because the fade
// in animation should not be called when the fade out animation is aborted.
layer()->SetOpacity(1.f);
} else {
// Hide all item views before the fade in animation in order to reduce the
// painting cost incurred by the bounds changes because of reorder. The
// fade in animation should be responsible for reshowing the item views that
// are within the visible view port after reorder.
for (size_t view_index = 0; view_index < view_model_.view_size();
++view_index) {
view_model_.view_at(view_index)->SetVisible(false);
}
}
// Before starting the fade in animation, the reordered items should be at
// their final positions instantly.
base::AutoReset auto_reset(&enable_item_move_animation_, false);
callback.Run(aborted);
if (fade_out_done_closure_for_test_)
std::move(fade_out_done_closure_for_test_).Run();
// When the fade out animation is abortted, the fade in animation should not
// run. Hence, the reorder animation ends. The aborted animation's smoothness
// is not reported.
if (aborted) {
grid_animation_status_ = AppListGridAnimationStatus::kEmpty;
MaybeRunNextReorderAnimationCallbackForTest(
/*aborted=*/true, AppListGridAnimationStatus::kReorderFadeOut);
// Reset `reorder_animation_tracker_` without calling Stop() because the
// aborted animation's smoothness is not reported.
reorder_animation_tracker_.reset();
}
}
void AppsGridView::OnFadeInAnimationEnded(ReorderAnimationCallback callback,
bool aborted) {
// If the animation is aborted, reset the apps grid's layer.
if (aborted) {
layer()->SetOpacity(1.f);
}
// Ensure that all item views are visible after fade in animation completes.
for (size_t view_index = 0; view_index < view_model_.view_size();
++view_index) {
view_model_.view_at(view_index)->SetVisible(true);
}
grid_animation_status_ = AppListGridAnimationStatus::kEmpty;
// Do not report the smoothness data for the aborted animation.
if (!aborted) {
reorder_animation_tracker_->Stop();
}
reorder_animation_tracker_.reset();
// Clean app list items' layers.
DestroyLayerItemsIfNotNeeded();
if (!callback.is_null()) {
callback.Run(aborted);
}
MaybeRunNextReorderAnimationCallbackForTest(
aborted, AppListGridAnimationStatus::kReorderFadeIn);
}
void AppsGridView::MaybeRunNextReorderAnimationCallbackForTest(
bool aborted,
AppListGridAnimationStatus animation_source) {
if (reorder_animation_callback_queue_for_test_.empty()) {
return;
}
TestReorderDoneCallbackType front_callback =
std::move(reorder_animation_callback_queue_for_test_.front());
reorder_animation_callback_queue_for_test_.pop();
std::move(front_callback).Run(aborted, animation_source);
}
void AppsGridView::OnIdealBoundsAnimationDone() {
if (item_reorder_animation_tracker_) {
item_reorder_animation_tracker_->Stop();
item_reorder_animation_tracker_.reset();
}
DestroyLayerItemsIfNotNeeded();
}
BEGIN_METADATA(AppsGridView)
END_METADATA
} // namespace ash