| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/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_metrics.h" |
| #include "ash/app_list/app_list_util.h" |
| #include "ash/app_list/app_list_view_delegate.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/paged_view_structure.h" |
| #include "ash/app_list/views/app_drag_icon_proxy.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_folder_view.h" |
| #include "ash/app_list/views/app_list_item_view.h" |
| #include "ash/app_list/views/apps_grid_context_menu.h" |
| #include "ash/app_list/views/apps_grid_view_focus_delegate.h" |
| #include "ash/app_list/views/ghost_image_view.h" |
| #include "ash/app_list/views/pulsing_block_view.h" |
| #include "ash/app_list/views/search_box_view.h" |
| #include "ash/app_list/views/search_result_tile_item_view.h" |
| #include "ash/constants/ash_features.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_switches.h" |
| #include "ash/public/cpp/metrics_util.h" |
| #include "base/bind.h" |
| #include "base/callback_helpers.h" |
| #include "base/cxx17_backports.h" |
| #include "base/guid.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/metrics/user_metrics.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_event_dispatcher.h" |
| #include "ui/base/dragdrop/drag_drop_types.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/display/display.h" |
| #include "ui/display/screen.h" |
| #include "ui/events/event.h" |
| #include "ui/gfx/animation/animation.h" |
| #include "ui/gfx/geometry/vector2d.h" |
| #include "ui/gfx/geometry/vector2d_conversions.h" |
| #include "ui/strings/grit/ui_strings.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/animation/bounds_animator.h" |
| #include "ui/views/border.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/controls/textfield/textfield.h" |
| #include "ui/views/view_observer.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 for productivity launcher. |
| constexpr int kMaximumHorizontalTileSpacingForProductivityLauncher = 128; |
| |
| // RowMoveAnimationDelegate is used when moving an item into a different row. |
| // Before running the animation, the item's layer is re-created and kept in |
| // the original position, then the item is moved to just before its target |
| // position and opacity set to 0. When the animation runs, this delegate moves |
| // the layer and fades it out while fading in the item at the same time. |
| class RowMoveAnimationDelegate : public views::AnimationDelegateViews { |
| public: |
| RowMoveAnimationDelegate(views::View* view, |
| ui::Layer* layer, |
| const gfx::Rect& layer_target) |
| : views::AnimationDelegateViews(view), |
| view_(view), |
| layer_(layer), |
| layer_start_(layer ? layer->bounds() : gfx::Rect()), |
| layer_target_(layer_target) {} |
| |
| RowMoveAnimationDelegate(const RowMoveAnimationDelegate&) = delete; |
| RowMoveAnimationDelegate& operator=(const RowMoveAnimationDelegate&) = delete; |
| |
| ~RowMoveAnimationDelegate() override = default; |
| |
| // views::AnimationDelegateViews: |
| void AnimationProgressed(const gfx::Animation* animation) override { |
| view_->layer()->SetOpacity(animation->GetCurrentValue()); |
| view_->layer()->ScheduleDraw(); |
| |
| if (layer_) { |
| layer_->SetOpacity(1 - animation->GetCurrentValue()); |
| layer_->SetBounds( |
| animation->CurrentValueBetween(layer_start_, layer_target_)); |
| layer_->ScheduleDraw(); |
| } |
| } |
| void AnimationEnded(const gfx::Animation* animation) override { |
| if (layer_) |
| view_->layer()->SetOpacity(1.0f); |
| } |
| void AnimationCanceled(const gfx::Animation* animation) override { |
| if (layer_) |
| view_->layer()->SetOpacity(1.0f); |
| } |
| |
| private: |
| // The view that needs to be wrapped. Owned by views hierarchy. |
| views::View* view_; |
| |
| std::unique_ptr<ui::Layer> layer_; |
| const gfx::Rect layer_start_; |
| const gfx::Rect layer_target_; |
| }; |
| |
| bool IsOEMFolderItem(AppListItem* item) { |
| return IsFolderItem(item) && |
| (static_cast<AppListFolderItem*>(item))->folder_type() == |
| AppListFolderItem::FOLDER_TYPE_OEM; |
| } |
| |
| // Returns the relative horizontal position of a point compared to a rect. -1 |
| // means the point is outside on the left side of the rect. 0 means the point is |
| // within the rect. 1 means it's on the right side of the rect. |
| int CompareHorizontalPointPositionToRect(gfx::Point point, gfx::Rect bounds) { |
| if (point.x() > bounds.right()) |
| return 1; |
| if (point.x() < bounds.x()) |
| return -1; |
| return 0; |
| } |
| |
| } // namespace |
| |
| std::string GridIndex::ToString() const { |
| std::stringstream ss; |
| ss << "Page: " << page << ", Slot: " << slot; |
| return ss.str(); |
| } |
| |
| // static |
| constexpr float AppsGridView::kCardifiedScale; |
| |
| // static |
| constexpr int AppsGridView::kDefaultAnimationDuration; |
| |
| // 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: |
| FolderIconItemHider(AppListFolderItem* folder_item, |
| AppListItem* item_icon_to_hide) |
| : folder_item_(folder_item) { |
| // 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. |
| folder_item_->NotifyOfDraggedItem(item_icon_to_hide); |
| folder_item_observer_.Observe(folder_item_); |
| } |
| |
| ~FolderIconItemHider() override { |
| if (folder_item_) |
| folder_item_->NotifyOfDraggedItem(nullptr); |
| } |
| |
| // AppListItemObserver: |
| void ItemBeingDestroyed() override { |
| folder_item_ = nullptr; |
| folder_item_observer_.Reset(); |
| } |
| |
| private: |
| 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_); |
| } |
| |
| ~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: |
| 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; |
| |
| view_structure_sanitize_lock_ = |
| apps_grid_view_->view_structure_.GetSanitizeLock(); |
| } |
| 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: |
| AppsGridView* const apps_grid_view_; |
| const gfx::Size initial_grid_size_; |
| std::unique_ptr<PagedViewStructure::ScopedSanitizeLock> |
| view_structure_sanitize_lock_; |
| }; |
| |
| AppsGridView::AppsGridView(AppListA11yAnnouncer* a11y_announcer, |
| AppListViewDelegate* app_list_view_delegate, |
| AppsGridViewFolderDelegate* folder_delegate, |
| AppListFolderController* folder_controller, |
| AppsGridViewFocusDelegate* focus_delegate) |
| : folder_delegate_(folder_delegate), |
| folder_controller_(folder_controller), |
| a11y_announcer_(a11y_announcer), |
| app_list_view_delegate_(app_list_view_delegate), |
| focus_delegate_(focus_delegate) { |
| 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); |
| bounds_animator_ = std::make_unique<views::BoundsAnimator>( |
| items_container_, /*use_transforms=*/true); |
| bounds_animator_->AddObserver(this); |
| |
| context_menu_ = std::make_unique<AppsGridContextMenu>(); |
| set_context_menu_controller(context_menu_.get()); |
| } |
| |
| AppsGridView::~AppsGridView() { |
| bounds_animator_->RemoveObserver(this); |
| |
| // 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 (drag_item_) |
| EndDrag(true); |
| |
| if (model_) |
| model_->RemoveObserver(this); |
| |
| if (item_list_) |
| item_list_->RemoveObserver(this); |
| |
| // Cancel animations now, otherwise RemoveAllChildViews() may call back to |
| // ViewHierarchyChanged() during removal, which can lead to double deletes |
| // (because ViewHierarchyChanged() may attempt to delete a view that is part |
| // way through deletion). Note that cancelling animations may cause |
| // AppListItemView to Layout(), which may call back into this object. |
| bounds_animator_->Cancel(); |
| |
| view_model_.Clear(); |
| RemoveAllChildViews(); |
| |
| // `OnBoundsAnimatorDone`, which uses `bounds_animator_`, is called on |
| // `drag_icon_proxy_` destruction. Reset `drag_icon_proxy_` early, while |
| // `bounds_animator_` is still around. |
| drag_icon_proxy_.reset(); |
| } |
| |
| void AppsGridView::Init() { |
| UpdateBorder(); |
| } |
| |
| 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 (int 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(); |
| const int max_horizontal_spacing = |
| features::IsProductivityLauncherEnabled() |
| ? kMaximumHorizontalTileSpacingForProductivityLauncher |
| : kMaximumTileSpacing; |
| |
| return gfx::Size( |
| tile_size.width() * cols + max_horizontal_spacing * (cols - 1), |
| tile_size.height() * rows_per_page + |
| kMaximumTileSpacing * (rows_per_page - 1)); |
| } |
| |
| void AppsGridView::ResetForShowApps() { |
| ClearDragState(); |
| drag_view_hider_.reset(); |
| folder_icon_item_hider_.reset(); |
| drag_icon_proxy_.reset(); |
| |
| layer()->SetOpacity(1.0f); |
| SetVisible(true); |
| |
| // The number of non-page-break-items should be the same as item views. |
| if (item_list_) { |
| int item_count = 0; |
| for (size_t i = 0; i < item_list_->item_count(); ++i) { |
| if (!item_list_->item_at(i)->is_page_break()) |
| ++item_count; |
| } |
| CHECK_EQ(item_count, view_model_.view_size()); |
| } |
| } |
| |
| 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(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(); |
| drag_icon_proxy_.reset(); |
| |
| items_need_layer_for_drag_ = true; |
| for (const auto& entry : view_model_.entries()) |
| static_cast<AppListItemView*>(entry.view)->EnsureLayer(); |
| 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) { |
| 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. |
| |
| gfx::Vector2d drag_vector(point - drag_start_grid_view_); |
| |
| if (ExceededDragThreshold(drag_vector)) { |
| if (!IsDragging()) |
| TryStartDragAndDropHostDrag(pointer); |
| MaybeStartCardifiedView(); |
| } |
| |
| if (drag_pointer_ != pointer) |
| return; |
| |
| last_drag_point_ = point; |
| const GridIndex last_drop_target = drop_target_; |
| DropTargetRegion last_drop_target_region = drop_target_region_; |
| UpdateDropTargetRegion(); |
| |
| MaybeStartPageFlip(); |
| |
| bool is_scrolling = MaybeAutoScroll(); |
| if (is_scrolling) { |
| // Don't do reordering while auto-scrolling, 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; |
| |
| // 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 drag ended by reparenting in a folder. |
| bool reparented_into_folder = false; |
| |
| // This is the folder view to drop an item into. Cache the |drag_view_|'s item |
| // and its bounds for later use in folder dropping animation. |
| AppListItemView* folder_item_view = nullptr; |
| AppListItem* drag_item = drag_item_; |
| |
| if (forward_events_to_drag_and_drop_host_) { |
| DCHECK(!IsDraggingForReparentInRootLevelGridView()); |
| 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 (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. |
| DCHECK(cancel); |
| if (reparent_drag_cancellation_) |
| std::move(reparent_drag_cancellation_).Run(); |
| return; |
| } |
| |
| if (!cancel && was_dragging) { |
| // Regular drag ending path, ie, not for reparenting. |
| UpdateDropTargetRegion(); |
| if (drop_target_region_ == ON_ITEM && DraggedItemCanEnterFolder() && |
| DropTargetIsValidFolder()) { |
| MaybeCreateFolderDroppingAccessibilityEvent(); |
| folder_item_view = MoveItemToFolder(drag_item_, drop_target_); |
| reparented_into_folder = true; |
| } else if (IsValidReorderTargetIndex(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(); |
| } |
| |
| if (folder_item_view) { |
| folder_icon_item_hider_ = std::make_unique<FolderIconItemHider>( |
| static_cast<AppListFolderItem*>(folder_item_view->item()), drag_item); |
| } |
| |
| 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(); |
| |
| if (!cancel) |
| view_structure_.SaveToMetadata(); |
| |
| if (!cancel) { |
| // Select the page where dragged item is dropped. Avoid doing so when the |
| // dragged item ends up in a folder. |
| const int model_index = GetModelIndexOfItem(drag_item); |
| if (model_index < view_model_.view_size()) |
| EnsureViewVisible(view_structure_.GetIndexFromModelIndex(model_index)); |
| } |
| |
| // Hide the |current_ghost_view_| for item drag that started |
| // within |apps_grid_view_|. |
| BeginHideCurrentGhostImageView(); |
| if (was_dragging) |
| SetFocusAfterEndDrag(); // Maybe focus the search box. |
| |
| AnimateDragIconToTargetPosition(reparented_into_folder, drag_item, |
| folder_item_view); |
| } |
| |
| AppListItemView* AppsGridView::GetItemViewAt(int index) const { |
| if (index < 0 || index >= view_model_.view_size()) |
| return nullptr; |
| return view_model_.view_at(index); |
| } |
| |
| void AppsGridView::InitiateDragFromReparentItemInRootLevelGridView( |
| AppListItemView* original_drag_view, |
| const gfx::Point& drag_point, |
| base::OnceClosure cancellation_callback) { |
| DVLOG(1) << __FUNCTION__; |
| DCHECK(original_drag_view && !drag_view_); |
| DCHECK(!dragging_for_reparent_item_); |
| |
| // Since the item is new, its placeholder is conceptually at the back of the |
| // entire apps grid. |
| reorder_placeholder_ = view_structure_.GetLastTargetIndex(); |
| |
| items_need_layer_for_drag_ = true; |
| for (const auto& entry : view_model_.entries()) |
| static_cast<AppListItemView*>(entry.view)->EnsureLayer(); |
| |
| 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); |
| |
| MaybeStartCardifiedView(); |
| } |
| |
| 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); |
| } |
| |
| 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(); |
| |
| 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) { |
| drag_and_drop_host_ = drag_and_drop_host; |
| } |
| |
| bool AppsGridView::IsAnimatingView(AppListItemView* view) { |
| return bounds_animator_->IsAnimating(view); |
| } |
| |
| gfx::Size AppsGridView::CalculatePreferredSize() const { |
| return GetTileGridSize(); |
| } |
| |
| bool AppsGridView::GetDropFormats( |
| int* formats, |
| std::set<ui::ClipboardFormatType>* format_types) { |
| // TODO(koz): Only accept a specific drag type for app shortcuts. |
| *formats = OSExchangeData::FILE_NAME; |
| return true; |
| } |
| |
| bool AppsGridView::CanDrop(const OSExchangeData& data) { |
| return true; |
| } |
| |
| int AppsGridView::OnDragUpdated(const ui::DropTargetEvent& event) { |
| return ui::DragDropTypes::DRAG_MOVE; |
| } |
| |
| void AppsGridView::UpdateControlVisibility(AppListViewState app_list_state, |
| bool is_in_drag) { |
| const bool fullscreen_or_in_drag = |
| is_in_drag || app_list_state == AppListViewState::kFullscreenAllApps || |
| app_list_state == AppListViewState::kFullscreenSearch; |
| SetVisible(fullscreen_or_in_drag); |
| } |
| |
| 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_) { |
| // The view being delete should not have reference in |view_model_|. |
| CHECK_EQ(-1, view_model_.GetIndexOfView(details.child)); |
| |
| if (selected_view_ == details.child) |
| selected_view_ = nullptr; |
| |
| if (drag_view_ == details.child) |
| drag_view_ = nullptr; |
| |
| if (features::IsProductivityLauncherEnabled()) { |
| if (current_ghost_view_ == details.child) |
| current_ghost_view_ = nullptr; |
| if (last_ghost_view_ == details.child) |
| last_ghost_view_ = nullptr; |
| } |
| |
| bounds_animator_->StopAnimatingView(details.child); |
| } |
| } |
| |
| bool AppsGridView::EventIsBetweenOccupiedTiles(const ui::LocatedEvent* event) { |
| gfx::Point mirrored_point(GetMirroredXInView(event->location().x()), |
| event->location().y()); |
| return IsValidIndex(GetNearestTileIndexForPoint(mirrored_point)); |
| } |
| |
| void AppsGridView::Update() { |
| UpdateBorder(); |
| |
| view_model_.Clear(); |
| pulsing_blocks_model_.Clear(); |
| items_container_->RemoveAllChildViews(); |
| |
| DCHECK(!selected_view_); |
| DCHECK(!drag_view_); |
| |
| if (item_list_ && item_list_->item_count()) { |
| for (size_t i = 0; i < item_list_->item_count(); ++i) { |
| // Skip "page break" items. |
| if (item_list_->item_at(i)->is_page_break()) |
| continue; |
| std::unique_ptr<AppListItemView> view = CreateViewForItemAtIndex(i); |
| view_model_.Add(view.get(), view_model_.view_size()); |
| items_container_->AddChildView(std::move(view)); |
| } |
| } |
| view_structure_.LoadFromMetadata(); |
| UpdateColsAndRowsForFolder(); |
| UpdatePaging(); |
| UpdatePulsingBlockViews(); |
| InvalidateLayout(); |
| |
| if (!folder_delegate_) |
| RecordPageMetrics(); |
| } |
| |
| void AppsGridView::UpdatePulsingBlockViews() { |
| const int existing_items = item_list_ ? item_list_->item_count() : 0; |
| const int tablet_page_size = |
| SharedAppListConfig::instance().GetMaxNumOfItemsPerPage(); |
| // For scrolling app list, the "page size" is very large, so cap the number of |
| // pulsing blocks to the size of the tablet mode page (~20 items). |
| const int tiles_per_page = std::min(TilesPerPage(0), tablet_page_size); |
| const int available_slots = |
| tiles_per_page - (existing_items % tiles_per_page); |
| const int desired = |
| model_ && model_->status() == AppListModelStatus::kStatusSyncing |
| ? available_slots |
| : 0; |
| |
| if (pulsing_blocks_model_.view_size() == desired) |
| return; |
| |
| while (pulsing_blocks_model_.view_size() > desired) { |
| PulsingBlockView* view = pulsing_blocks_model_.view_at(0); |
| pulsing_blocks_model_.Remove(0); |
| delete view; |
| } |
| |
| while (pulsing_blocks_model_.view_size() < desired) { |
| auto view = std::make_unique<PulsingBlockView>( |
| GetTotalTileSize(GetTotalPages() - 1), true); |
| pulsing_blocks_model_.Add(view.get(), 0); |
| 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 (items_need_layer_for_drag_) |
| 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()); |
| } |
| } |
| |
| GridIndex AppsGridView::GetIndexOfView(const AppListItemView* view) const { |
| const int model_index = view_model_.GetIndexOfView(view); |
| if (model_index == -1) |
| return GridIndex(); |
| |
| return view_structure_.GetIndexFromModelIndex(model_index); |
| } |
| |
| AppListItemView* AppsGridView::GetViewAtIndex(const GridIndex& index) const { |
| if (!IsValidIndex(index)) |
| return nullptr; |
| |
| const int model_index = view_structure_.GetModelIndexFromIndex(index); |
| return GetItemViewAt(model_index); |
| } |
| |
| int AppsGridView::TilesPerPage(int page) const { |
| const int max_rows = GetMaxRowsInPage(page); |
| |
| // In folders, the grid size depends on the number of items in the page. |
| if (IsInFolder()) { |
| // Leave room for at least one item. |
| if (!view_model()->view_size()) |
| return 1; |
| |
| int rows = (view_model()->view_size() - 1) / cols() + 1; |
| return std::min(max_rows, rows) * cols(); |
| } |
| |
| return max_rows * cols(); |
| } |
| |
| 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::CalculateIdealBounds() { |
| if (view_structure_.mode() == PagedViewStructure::Mode::kPartialPages) { |
| CalculateIdealBoundsForPageStructureWithPartialPages(); |
| return; |
| } |
| |
| const int total_views = |
| view_model_.view_size() + pulsing_blocks_model_.view_size(); |
| int slot_index = 0; |
| for (int i = 0; i < total_views; ++i) { |
| if (i < view_model_.view_size() && view_model_.view_at(i) == drag_view_) |
| continue; |
| |
| GridIndex view_index = view_structure_.GetIndexFromModelIndex(slot_index); |
| |
| // Leaves a blank space in the grid for the current reorder placeholder. |
| if (reorder_placeholder_ == view_index) { |
| ++slot_index; |
| view_index = view_structure_.GetIndexFromModelIndex(slot_index); |
| } |
| |
| gfx::Rect tile_slot = GetExpectedTileBounds(view_index); |
| tile_slot.Offset(CalculateTransitionOffset(view_index.page)); |
| if (i < view_model_.view_size()) { |
| view_model_.set_ideal_bounds(i, tile_slot); |
| } else { |
| pulsing_blocks_model_.set_ideal_bounds(i - view_model_.view_size(), |
| tile_slot); |
| } |
| |
| ++slot_index; |
| } |
| } |
| |
| void AppsGridView::CalculateIdealBoundsForPageStructureWithPartialPages() { |
| DCHECK(!IsInFolder()); |
| DCHECK_EQ(view_structure_.mode(), PagedViewStructure::Mode::kPartialPages); |
| |
| // |view_structure_| should only be updated at the end of drag. So make a |
| // copy of it and only change the copy for calculating the ideal bounds of |
| // each item view. |
| PagedViewStructure copied_view_structure(this); |
| // Allow empty pages in the copied view structure so an app list page does |
| // not get removed when dragging the last item in the page. |
| copied_view_structure.AllowEmptyPages(); |
| |
| { |
| // Delay page overflow sanitization until both drag view was removed, and |
| // reorder placeholder was added to the view structure. |
| std::unique_ptr<PagedViewStructure::ScopedSanitizeLock> sanitize_lock = |
| copied_view_structure.GetSanitizeLock(); |
| copied_view_structure.LoadFromOther(view_structure_); |
| |
| // Remove the item view being dragged. |
| if (drag_view_) |
| copied_view_structure.Remove(drag_view_); |
| |
| // Leave a blank space in the grid for the current reorder placeholder. |
| if (IsValidIndex(reorder_placeholder())) |
| copied_view_structure.Add(nullptr, reorder_placeholder()); |
| } |
| |
| // Convert visual index to ideal bounds. |
| const auto& pages = copied_view_structure.pages(); |
| int model_index = 0; |
| for (size_t i = 0; i < pages.size(); ++i) { |
| auto& page = pages[i]; |
| for (size_t j = 0; j < page.size(); ++j) { |
| if (page[j] == nullptr) |
| continue; |
| |
| // Skip the dragged view |
| if (view_model()->view_at(model_index) == drag_view_) |
| ++model_index; |
| |
| gfx::Rect tile_slot = GetExpectedTileBounds(GridIndex(i, j)); |
| tile_slot.Offset(CalculateTransitionOffset(i)); |
| view_model()->set_ideal_bounds(model_index, tile_slot); |
| ++model_index; |
| } |
| } |
| |
| // All pulsing blocks come after item views. |
| GridIndex pulsing_block_index = copied_view_structure.GetLastTargetIndex(); |
| for (int i = 0; i < pulsing_blocks_model().view_size(); ++i) { |
| if (pulsing_block_index.slot == TilesPerPage(pulsing_block_index.page)) { |
| ++pulsing_block_index.page; |
| pulsing_block_index.slot = 0; |
| } |
| gfx::Rect tile_slot = GetExpectedTileBounds(pulsing_block_index); |
| tile_slot.Offset(CalculateTransitionOffset(pulsing_block_index.page)); |
| pulsing_blocks_model().set_ideal_bounds(i, tile_slot); |
| ++pulsing_block_index.slot; |
| } |
| } |
| |
| void AppsGridView::AnimateToIdealBounds() { |
| 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(); |
| for (int i = 0; i < view_model_.view_size(); ++i) { |
| AppListItemView* view = GetItemViewAt(i); |
| const gfx::Rect& target = view_model_.ideal_bounds(i); |
| if (bounds_animator_->GetTargetBounds(view) == target) |
| continue; |
| |
| const gfx::Rect& current = view->bounds(); |
| const bool current_visible = visible_bounds.Intersects(current); |
| const bool target_visible = visible_bounds.Intersects(target); |
| const bool visible = |
| !IsViewHiddenForDrag(view) && (current_visible || target_visible); |
| |
| const int y_diff = target.y() - current.y(); |
| const int tile_size_height = |
| GetTotalTileSize(view_structure_.GetIndexFromModelIndex(i).page) |
| .height(); |
| if (visible && y_diff && y_diff % tile_size_height == 0) { |
| AnimationBetweenRows(view, current_visible, current, target_visible, |
| target); |
| } else if (visible || bounds_animator_->IsAnimating(view)) { |
| bounds_animator_->AnimateViewTo(view, target); |
| bounds_animator_->SetAnimationDelegate(view, nullptr); |
| } else { |
| view->SetBoundsRect(target); |
| } |
| } |
| |
| // Destroy layers created for drag if they're not longer necessary. |
| if (!bounds_animator_->IsAnimating()) |
| OnBoundsAnimatorDone(bounds_animator_.get()); |
| } |
| |
| void AppsGridView::AnimationBetweenRows(AppListItemView* view, |
| bool animate_current, |
| const gfx::Rect& current, |
| bool animate_target, |
| const gfx::Rect& target) { |
| // Determine page of |current| and |target|. |
| const int current_page = |
| CompareHorizontalPointPositionToRect(current.origin(), GetLocalBounds()); |
| const int target_page = |
| CompareHorizontalPointPositionToRect(target.origin(), GetLocalBounds()); |
| |
| const int dir = current_page < target_page || (current_page == target_page && |
| current.y() < target.y()) |
| ? 1 |
| : -1; |
| |
| std::unique_ptr<ui::Layer> layer; |
| if (view->layer()) { |
| if (animate_current) { |
| layer = view->RecreateLayer(); |
| layer->SuppressPaint(); |
| |
| view->layer()->SetFillsBoundsOpaquely(false); |
| view->layer()->SetOpacity(0.f); |
| } |
| } else { |
| view->EnsureLayer(); |
| } |
| |
| const gfx::Size total_tile_size = GetTotalTileSize(current_page); |
| gfx::Rect current_out(current); |
| current_out.Offset(dir * total_tile_size.width(), 0); |
| |
| gfx::Rect target_in(target); |
| if (animate_target) |
| target_in.Offset(-dir * total_tile_size.width(), 0); |
| bounds_animator_->StopAnimatingView(view); |
| view->SetBoundsRect(target_in); |
| bounds_animator_->AnimateViewTo(view, target); |
| |
| bounds_animator_->SetAnimationDelegate( |
| view, std::make_unique<RowMoveAnimationDelegate>(view, layer.release(), |
| current_out)); |
| } |
| |
| 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_dropping_circle_radius() * |
| (cardified_state_ ? kCardifiedScale : 1.0f))) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| void AppsGridView::AnimateDragIconToTargetPosition( |
| bool dropping_into_folder, |
| AppListItem* drag_item, |
| AppListItemView* target_folder_view) { |
| // If drag icon proxy had not been created, just reshow the drag view. |
| if (!drag_icon_proxy_) { |
| OnDragIconDropDone(); |
| return; |
| } |
| |
| // Calculate target item bounds. |
| gfx::Rect drag_icon_drop_bounds; |
| if (!dropping_into_folder) { |
| // Find the view for drag item, and use its ideal bounds to calculate target |
| // drop bounds. |
| for (int i = 0; i < view_model_.view_size(); ++i) { |
| if (view_model_.view_at(i)->item() != drag_item) |
| continue; |
| |
| auto* drag_view = view_model_.view_at(i); |
| // Get icon bounds in the drag view coordinates. |
| drag_icon_drop_bounds = drag_view->GetIconBounds(); |
| |
| // Get the expected drag item view location. |
| const gfx::Rect drag_view_ideal_bounds = view_model_.ideal_bounds(i); |
| |
| // Position target icon bounds relative to the ideal drag view bounds. |
| drag_icon_drop_bounds.Offset(drag_view_ideal_bounds.x(), |
| drag_view_ideal_bounds.y()); |
| 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; |
| } |
| |
| 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); |
| |
| 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(); |
| OnBoundsAnimatorDone(nullptr); |
| } |
| |
| 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_dropping_circle_radius() * |
| (cardified_state_ ? kCardifiedScale : 1.0f)); |
| const int selected_page = GetSelectedPage(); |
| int col = (point.x() - bounds.x() + x_offset - |
| GetGridCenteringOffset(selected_page).x()) / |
| total_tile_size.width(); |
| col = base::clamp(col, 0, cols_ - 1); |
| drop_target_ = |
| std::min(GridIndex(selected_page, row * cols_ + col), |
| view_structure_.GetLastTargetIndexOfPage(selected_page)); |
| |
| DCHECK(IsValidReorderTargetIndex(drop_target_)) |
| << drop_target_.ToString() << " selected page " << selected_page |
| << " row " << row << " col " << col << " " |
| << view_structure_.GetLastTargetIndexOfPage(drop_target_.page).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_ ? kCardifiedScale : 1.0f); |
| const int double_icon_radius = |
| app_list_config_->folder_dropping_circle_radius() * 2 * |
| (cardified_state_ ? kCardifiedScale : 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() { |
| reorder_placeholder_ = drop_target_; |
| MaybeCreateDragReorderAccessibilityEvent(); |
| AnimateToIdealBounds(); |
| CreateGhostImageView(); |
| } |
| |
| void AppsGridView::OnFolderItemReparentTimer() { |
| DCHECK(folder_delegate_); |
| if (drag_out_of_folder_container_ && drag_view_) { |
| folder_delegate_->ReparentItem(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 (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), this, |
| &AppsGridView::OnFolderItemReparentTimer); |
| 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)); |
| const gfx::Rect icon_ideal_bounds = |
| folder_item_view->GetIconBoundsForTargetViewBounds( |
| app_list_config_, view_ideal_bounds, |
| folder_item_view->GetIconImage().size(), /*icon_scale=*/1.0f); |
| AppListFolderItem* folder_item = |
| static_cast<AppListFolderItem*>(folder_item_view->item()); |
| 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_); |
| |
| 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 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(); |
| |
| AppListItemView* folder_item = |
| MoveItemToFolder(selected_view_->item(), target_index); |
| a11y_announcer_->AnnounceKeyboardFoldering( |
| moving_view_title, target_view_title, target_view_is_folder); |
| Layout(); |
| DCHECK(folder_item->is_folder()); |
| folder_item->RequestFocus(); |
| RecordAppMovingTypeMetrics(kMoveByKeyboardIntoFolder); |
| } |
| |
| 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 (focused->GetClassName() != AppListItemView::kViewClassName) |
| 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 (focus_delegate_ && |
| focus_delegate_->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_ = base::clamp(preferred_cols, 1, max_cols_); |
| } |
| |
| PreferredSizeChanged(); |
| } |
| |
| void AppsGridView::DispatchDragEventForReparent(Pointer pointer, |
| const gfx::Point& drag_point) { |
| 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()); |
| DCHECK_NE(-1, view_model_.GetIndexOfView(original_parent_item_view)); |
| |
| // EndDrag was called before if |drag_view_| is nullptr. |
| if (!drag_item_) |
| return; |
| |
| drag_icon_proxy_ = std::move(drag_icon_proxy); |
| |
| DCHECK(IsDraggingForReparentInRootLevelGridView()); |
| bool cancel_reparent = cancel_drag || drop_target_region_ == NO_TARGET; |
| |
| // This is the folder view to drop an item into. Cache the |drag_view_|'s item |
| // and its bounds for later use in folder dropping animation. |
| AppListItemView* folder_item_view = |
| cancel_reparent ? original_parent_item_view : nullptr; |
| AppListItem* drag_item = drag_item_; |
| |
| if (!events_forwarded_to_drag_drop_host && !cancel_reparent) { |
| UpdateDropTargetRegion(); |
| if (drop_target_region_ == ON_ITEM && DropTargetIsValidFolder() && |
| DraggedItemCanEnterFolder()) { |
| cancel_reparent = !ReparentItemToAnotherFolder(drag_item, drop_target_); |
| // Announce folder dropping event before end of drag of reparented item. |
| MaybeCreateFolderDroppingAccessibilityEvent(); |
| if (!cancel_reparent) { |
| folder_item_view = |
| GetViewDisplayedAtSlotOnCurrentPage(drop_target_.slot); |
| } else { |
| folder_item_view = original_parent_item_view; |
| } |
| } else if (drop_target_region_ != NO_TARGET && |
| IsValidReorderTargetIndex(drop_target_)) { |
| ReparentItemForReorder(drag_item, drop_target_); |
| RecordAppMovingTypeMetrics(kMoveByDragOutOfFolder); |
| // Announce accessibility event before the end of drag for reparented |
| // item. |
| MaybeCreateDragReorderAccessibilityEvent(); |
| } else { |
| NOTREACHED(); |
| } |
| } |
| |
| SetAsFolderDroppingTarget(drop_target_, false); |
| |
| // Hide the drag item icon from the target folder icon. |
| if (folder_item_view) { |
| folder_icon_item_hider_ = std::make_unique<FolderIconItemHider>( |
| static_cast<AppListFolderItem*>(folder_item_view->item()), drag_item); |
| } |
| |
| UpdatePaging(); |
| ClearDragState(); |
| |
| if (cardified_state_) |
| MaybeEndCardifiedView(); |
| else |
| AnimateToIdealBounds(); |
| |
| if (!cancel_reparent) |
| view_structure_.SaveToMetadata(); |
| |
| // Hide the |current_ghost_view_| after completed drag from within |
| // folder to |apps_grid_view_|. |
| BeginHideCurrentGhostImageView(); |
| SetFocusAfterEndDrag(); // Maybe focus the search box. |
| |
| AnimateDragIconToTargetPosition( |
| /*dropping_into_folder=*/cancel_reparent || folder_item_view, drag_item, |
| folder_item_view); |
| } |
| |
| 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_NE(-1, view_model_.GetIndexOfView(original_parent_item_view)); |
| |
| // 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); |
| |
| view_structure_.SaveToMetadata(); |
| |
| // Update paging because the move could have resulted in a |
| // page getting created. |
| UpdatePaging(); |
| |
| Layout(); |
| EnsureViewVisible(target_index); |
| GetViewAtIndex(target_index)->RequestFocus(); |
| AnnounceReorder(target_index); |
| |
| RecordAppMovingTypeMetrics(kMoveByKeyboardOutOfFolder); |
| } |
| |
| void AppsGridView::UpdatePagedViewStructure() { |
| view_structure_.SaveToMetadata(); |
| } |
| |
| bool AppsGridView::IsTabletMode() const { |
| return app_list_view_delegate_->IsInTabletMode(); |
| } |
| |
| bool AppsGridView::IsAnimationRunningForTest() { |
| return bounds_animator_->IsAnimating() || |
| bounds_animation_for_cardified_state_in_progress_ > 0; |
| } |
| |
| void AppsGridView::CancelAnimationsForTest() { |
| bounds_animator_->Cancel(); |
| drag_icon_proxy_.reset(); |
| |
| const int total_views = view_model_.view_size(); |
| for (int i = 0; i < total_views; ++i) { |
| if (view_model_.view_at(i)->layer()) |
| view_model_.view_at(i)->layer()->CompleteAllAnimations(); |
| } |
| } |
| |
| bool AppsGridView::FireFolderItemReparentTimerForTest() { |
| 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::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 use_blurred_background = |
| drag_view_->item()->is_folder() && IsTabletMode(); |
| drag_icon_proxy_ = std::make_unique<AppDragIconProxy>( |
| GetWidget()->GetNativeWindow()->GetRootWindow(), |
| drag_view_->GetIconImage(), location_in_screen, |
| location_in_screen - icon_location_in_screen, |
| drag_view_->item()->is_folder() ? kDragAndDropProxyScale : 1.0f, |
| use_blurred_background); |
| 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); |
| } |
| } |
| |
| bool AppsGridView::IsMoveTargetOnNewPage(const GridIndex& target) const { |
| // This is used to determine whether move should create a page break item, |
| // which is only relevant for page structure with partial pages. |
| DCHECK_EQ(view_structure_.mode(), PagedViewStructure::Mode::kPartialPages); |
| |
| return target.page == GetTotalPages() || |
| (target.page == GetTotalPages() - 1 && |
| view_structure_.GetLastTargetIndexOfPage(target.page).slot == 0); |
| } |
| |
| void AppsGridView::EnsurePageBreakBeforeItem(const std::string& item_id) { |
| DCHECK_EQ(view_structure_.mode(), PagedViewStructure::Mode::kPartialPages); |
| |
| size_t item_list_index = 0; |
| if (item_list_->FindItemIndex(item_id, &item_list_index) && |
| item_list_index > 0 && |
| !item_list_->item_at(item_list_index - 1)->is_page_break()) { |
| model_->AddPageBreakItemAfter(item_list_->item_at(item_list_index - 1)); |
| } |
| } |
| |
| 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 = |
| view_structure_.GetTargetItemListIndexForMove(item, target); |
| |
| const bool moving_to_new_page = |
| view_structure_.mode() == PagedViewStructure::Mode::kPartialPages && |
| IsMoveTargetOnNewPage(target); |
| |
| { |
| ScopedModelUpdate update(this); |
| item_list_->MoveItem(current_item_list_index, target_item_list_index); |
| |
| // If the item is being moved to a new page, ensure that it's preceded by a |
| // page break. |
| if (moving_to_new_page) |
| EnsurePageBreakBeforeItem(item_id); |
| } |
| } |
| |
| AppListItemView* AppsGridView::MoveItemToFolder(AppListItem* item, |
| const GridIndex& target) { |
| const std::string& source_item_id = item->id(); |
| AppListItemView* target_view = |
| GetViewDisplayedAtSlotOnCurrentPage(target.slot); |
| DCHECK(target_view); |
| const std::string& target_view_item_id = target_view->item()->id(); |
| |
| // Check that the item is not being dropped onto itself; this should not |
| // happen, but it can if something allows multiple views to share an |
| // item (e.g., if a folder drop does not clean up properly). |
| DCHECK_NE(source_item_id, target_view_item_id); |
| |
| const bool is_dragged_view = drag_item_ == item; |
| // Make change to data model. |
| std::string folder_item_id; |
| |
| { |
| ScopedModelUpdate update(this); |
| folder_item_id = model_->MergeItems(target_view_item_id, source_item_id); |
| } |
| |
| if (folder_item_id.empty()) { |
| LOG(ERROR) << "Unable to merge into item id: " << target_view_item_id; |
| return nullptr; |
| } |
| |
| const AppListItem* const folder_item = item_list_->FindItem(folder_item_id); |
| if (!folder_item) { |
| LOG(ERROR) << "Unable to find target folder item " << folder_item_id; |
| return nullptr; |
| } |
| |
| if (is_dragged_view) |
| RecordAppMovingTypeMetrics(kMoveByDragIntoFolder); |
| |
| return GetItemViewAt(GetModelIndexOfItem(folder_item)); |
| } |
| |
| 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 = |
| view_structure_.GetTargetItemListIndexForMove(item, 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(); |
| |
| const bool moving_to_new_page = |
| view_structure_.mode() == PagedViewStructure::Mode::kPartialPages && |
| IsMoveTargetOnNewPage(target); |
| |
| { |
| ScopedModelUpdate update(this); |
| model_->MoveItemToRootAt(item, target_position); |
| |
| // If the item is being moved to a new page, ensure that it's preceded by a |
| // page break. |
| if (moving_to_new_page) |
| EnsurePageBreakBeforeItem(item_id); |
| } |
| } |
| |
| bool AppsGridView::ReparentItemToAnotherFolder(AppListItem* item, |
| const GridIndex& target) { |
| DCHECK(IsDraggingForReparentInRootLevelGridView()); |
| |
| AppListItemView* target_view = |
| GetViewDisplayedAtSlotOnCurrentPage(target.slot); |
| if (!target_view) |
| return false; |
| |
| CHECK(item->IsInFolder()); |
| const std::string source_folder_id = item->folder_id(); |
| |
| AppListItem* target_item = target_view->item(); |
| |
| // An app is being reparented to its original folder. Just cancel the |
| // reparent. |
| if (target_item->id() == item->folder_id()) |
| return false; |
| |
| { |
| ScopedModelUpdate update(this); |
| const std::string target_id_after_merge = |
| model_->MergeItems(target_item->id(), item->id()); |
| if (target_id_after_merge.empty()) { |
| LOG(ERROR) << "Unable to reparent to item id: " << target_item->id(); |
| return false; |
| } |
| } |
| |
| RecordAppMovingTypeMetrics(kMoveIntoAnotherFolder); |
| return true; |
| } |
| |
| void AppsGridView::CancelContextMenusOnCurrentPage() { |
| GridIndex start_index(GetSelectedPage(), 0); |
| if (!IsValidIndex(start_index)) |
| return; |
| int start = view_structure_.GetModelIndexFromIndex(start_index); |
| int end = |
| std::min(view_model_.view_size(), start + TilesPerPage(start_index.page)); |
| for (int i = start; i < end; ++i) |
| GetItemViewAt(i)->CancelContextMenu(); |
| } |
| |
| void AppsGridView::DeleteItemViewAtIndex(int index) { |
| AppListItemView* item_view = GetItemViewAt(index); |
| view_model_.Remove(index); |
| view_structure_.Remove(item_view); |
| if (item_view == drag_view_) |
| drag_view_ = nullptr; |
| delete item_view; |
| } |
| |
| bool AppsGridView::IsPointWithinDragBuffer(const gfx::Point& point) const { |
| gfx::Rect rect(GetLocalBounds()); |
| rect.Inset(-kDragBufferPx, -kDragBufferPx, -kDragBufferPx, -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); |
| |
| if (!item->is_page_break()) { |
| int model_index = GetTargetModelIndexFromItemIndex(index); |
| AppListItemView* view = items_container_->AddChildViewAt( |
| CreateViewForItemAtIndex(index), model_index); |
| view_model_.Add(view, model_index); |
| |
| if (item == drag_item_) { |
| drag_view_ = view; |
| drag_view_hider_ = std::make_unique<DragViewHider>(drag_view_); |
| drag_view_->RequestFocus(); |
| } |
| } |
| |
| view_structure_.LoadFromMetadata(); |
| |
| // 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); |
| } |
| |
| void AppsGridView::OnListItemRemoved(size_t index, AppListItem* item) { |
| const gfx::Size initial_grid_size = GetTileGridSize(); |
| |
| if (!updating_model_) |
| EndDrag(true); |
| |
| if (!item->is_page_break()) |
| DeleteItemViewAtIndex(GetModelIndexOfItem(item)); |
| |
| view_structure_.LoadFromMetadata(); |
| |
| // 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); |
| } |
| |
| void AppsGridView::OnListItemMoved(size_t from_index, |
| size_t to_index, |
| AppListItem* item) { |
| if (!updating_model_) |
| EndDrag(true); |
| |
| if (item->is_page_break()) { |
| LOG(ERROR) << "Page break item is moved: " << item->id(); |
| } else { |
| // 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. |
| int from_model_index = GetModelIndexOfItem(item); |
| int to_model_index = GetTargetModelIndexFromItemIndex(to_index); |
| view_model_.Move(from_model_index, to_model_index); |
| items_container_->ReorderChildView(view_model_.view_at(to_model_index), |
| to_model_index); |
| items_container_->NotifyAccessibilityEvent( |
| ax::mojom::Event::kChildrenChanged, true /* send_native_event */); |
| } |
| |
| view_structure_.LoadFromMetadata(); |
| |
| // 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()) |
| AnimateToIdealBounds(); |
| else |
| Layout(); |
| } |
| |
| void AppsGridView::OnAppListModelStatusChanged() { |
| UpdatePulsingBlockViews(); |
| Layout(); |
| SchedulePaint(); |
| } |
| |
| void AppsGridView::OnBoundsAnimatorProgressed(views::BoundsAnimator* animator) { |
| } |
| |
| void AppsGridView::OnBoundsAnimatorDone(views::BoundsAnimator* animator) { |
| if (drag_item_ || drag_icon_proxy_) |
| return; |
| |
| if (bounds_animation_for_cardified_state_in_progress_ || |
| (bounds_animator_ && bounds_animator_->IsAnimating())) { |
| return; |
| } |
| |
| items_need_layer_for_drag_ = false; |
| for (const auto& entry : view_model_.entries()) |
| entry.view->DestroyLayer(); |
| } |
| |
| 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 = base::clamp( |
| (point.x() - bounds.x() - grid_offset.x()) / total_tile_size.width(), 0, |
| cols_ - 1); |
| |
| DCHECK_GT(total_tile_size.height(), 0); |
| int max_row = TilesPerPage(current_page) / cols_ - 1; |
| int row = base::clamp( |
| (point.y() - bounds.y() - grid_offset.y()) / total_tile_size.height(), 0, |
| max_row); |
| |
| 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; |
| } |
| |
| 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 = |
| std::find_if(entries.begin(), entries.end(), [&](const auto& entry) { |
| return entry.view->bounds() == tile_rect && entry.view != 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_); |
| GridIndex target_index; |
| 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; |
| |
| const int target_model_index = view_model_.GetIndexOfView(selected_view_) + |
| ((key_code == backward) ? -1 : 1); |
| |
| // A forward move on the last item in |view_model_| should result in page |
| // creation. |
| if (target_model_index == view_model_.view_size()) { |
| // Only grid structure with partial pages supports page creation by |
| // keyboard move. |
| if (view_structure_.mode() != PagedViewStructure::Mode::kPartialPages) |
| return source_index; |
| // If |source_index| is the last item in the grid on a page by itself, |
| // moving right to a new page should be a no-op. |
| if (view_structure_.items_on_page(source_index.page) == 1) |
| return source_index; |
| return GridIndex(GetTotalPages(), 0); |
| } |
| |
| target_index = GetIndexOfView( |
| static_cast<const AppListItemView*>(GetItemViewAt(std::min( |
| std::max(0, target_model_index), view_model_.view_size() - 1)))); |
| if (view_structure_.mode() == PagedViewStructure::Mode::kPartialPages && |
| key_code == backward && target_index.page < source_index.page && |
| !view_structure_.IsFullPage(target_index.page)) { |
| // Apps swap positions if the target page is the same as the |
| // destination page, or the target page is full. If the page is not |
| // full the app is dumped on the page. Increase the slot in this case |
| // to account for the new available spot. |
| ++target_index.slot; |
| } |
| return target_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 (folder_delegate_) { |
| if (target_page >= GetTotalPages()) |
| return source_index; |
| } else { |
| if (target_page >= view_structure_.total_pages()) { |
| // If |source_index| page only has one item, moving down to a new page |
| // should be a no-op. |
| if (view_structure_.items_on_page(source_index.page) == 1) |
| return source_index; |
| return GridIndex(target_page, 0); |
| } |
| } |
| target_row = 0; |
| } |
| |
| // The ideal slot shares a column with |source_index|. |
| const int ideal_slot = target_row * cols_ + source_index.slot % cols_; |
| if (folder_delegate_) { |
| return GridIndex( |
| target_page, |
| std::min(GetNumberOfItemsOnPage(target_page) - 1, ideal_slot)); |
| } |
| |
| // If the app is being moved to a new page there is 1 extra slot available. |
| const int last_slot_in_target_page = |
| view_structure_.items_on_page(target_page) - |
| (source_index.page != target_page ? 0 : 1); |
| return GridIndex(target_page, std::min(last_slot_in_target_page, 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 (view_structure_.IsFullPage(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; |
| const int folder_page_size = TilesPerPage(folder_index.page); |
| if (target_index.page > folder_index.page && |
| 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 || |
| !IsValidReorderTargetIndex(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_ || view_structure_.IsFullPage(target_index.page) || |
| target_index.page == original_selected_view_index.page; |
| |
| AppListItemView* target_view = GetViewAtIndex(target_index); |
| { |
| // If the move is a two part operation (swap) do not clear the overflow |
| // during the initial move. Clearing the overflow when |target_index| is on |
| // a full page results in the last item being pushed to the next page. |
| std::unique_ptr<PagedViewStructure::ScopedSanitizeLock> sanitize_lock = |
| view_structure_.GetSanitizeLock(); |
| MoveItemInModel(selected_view_->item(), target_index); |
| if (swap_items) { |
| DCHECK(target_view); |
| MoveItemInModel(target_view->item(), original_selected_view_index); |
| } |
| } |
| |
| view_structure_.SaveToMetadata(); |
| |
| 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); |
| } |
| Layout(); |
| 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, IsTabletMode()); |
| } |
| } |
| |
| bool AppsGridView::IsValidIndex(const GridIndex& index) const { |
| return index.page >= 0 && index.page < GetTotalPages() && index.slot >= 0 && |
| index.slot < TilesPerPage(index.page) && |
| view_structure_.GetModelIndexFromIndex(index) < |
| view_model_.view_size(); |
| } |
| |
| bool AppsGridView::IsValidReorderTargetIndex(const GridIndex& index) const { |
| return view_structure_.IsValidReorderTargetIndex(index); |
| } |
| |
| int AppsGridView::GetModelIndexOfItem(const AppListItem* item) const { |
| const auto& entries = view_model_.entries(); |
| const auto iter = |
| std::find_if(entries.begin(), entries.end(), [item](const auto& entry) { |
| return static_cast<AppListItemView*>(entry.view)->item() == item; |
| }); |
| return std::distance(entries.begin(), iter); |
| } |
| |
| int AppsGridView::GetTargetModelIndexFromItemIndex(size_t item_index) { |
| if (folder_delegate_) |
| return item_index; |
| |
| CHECK(item_index <= item_list_->item_count()); |
| int target_model_index = 0; |
| for (size_t i = 0; i < item_index; ++i) { |
| if (!item_list_->item_at(i)->is_page_break()) |
| ++target_model_index; |
| } |
| return target_model_index; |
| } |
| |
| int AppsGridView::GetNumberOfItemsOnPage(int page) const { |
| if (page < 0 || page >= GetTotalPages()) |
| return 0; |
| |
| if (!folder_delegate_ && !features::IsProductivityLauncherEnabled()) |
| return view_structure_.items_on_page(page); |
| |
| // 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. |
| int item_count = view_model_.view_size(); |
| int current_page = 0; |
| while (current_page < GetTotalPages() - 1) { |
| item_count -= TilesPerPage(current_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_view_->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 page = target_index.page + 1; |
| const int row = |
| ((target_index.slot - (target_index.slot % cols_)) / cols_) + 1; |
| const int col = (target_index.slot % cols_) + 1; |
| if (view_structure_.mode() == PagedViewStructure::Mode::kSinglePage) { |
| // 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. |
| a11y_announcer_->AnnounceAppsGridReorder(page, row, col); |
| } |
| } |
| |
| void AppsGridView::CreateGhostImageView() { |
| if (!features::IsProductivityLauncherEnabled()) |
| return; |
| 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(app_list_config_, ghost_view_bounds); |
| current_ghost_view_ = |
| items_container_->AddChildView(std::move(current_ghost_view)); |
| current_ghost_view_->FadeIn(); |
| } |
| |
| void AppsGridView::BeginHideCurrentGhostImageView() { |
| if (!features::IsProductivityLauncherEnabled()) |
| return; |
| |
| current_ghost_location_ = GridIndex(); |
| |
| if (current_ghost_view_) |
| current_ghost_view_->FadeOut(); |
| } |
| |
| 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_); |
| folder_controller_->ShowFolderForItemView(pressed_item_view); |
| return; |
| } |
| |
| base::RecordAction(base::UserMetricsAction("AppList_ClickOnApp")); |
| |
| // 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(); |
| app_list_view_delegate()->ActivateItem( |
| id, event.flags(), AppListLaunchedFrom::kLaunchedFromGrid); |
| } |
| |
| void AppsGridView::OnHostDragStartTimerFired() { |
| 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; |
| } |
| } |
| |
| BEGIN_METADATA(AppsGridView, views::View) |
| END_METADATA |
| |
| } // namespace ash |