blob: 2d3ebd181eecf692a52bb0284ca5c4504f829aec [file] [log] [blame]
// Copyright 2018 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/paged_view_structure.h"
#include <algorithm>
#include "ash/app_list/model/app_list_item.h"
#include "ash/app_list/views/app_list_item_view.h"
#include "ash/app_list/views/apps_grid_view.h"
#include "ui/views/view_model.h"
namespace app_list {
PagedViewStructure::PagedViewStructure(AppsGridView* apps_grid_view)
: apps_grid_view_(apps_grid_view) {}
PagedViewStructure::PagedViewStructure(const PagedViewStructure& other) =
default;
PagedViewStructure::~PagedViewStructure() = default;
void PagedViewStructure::LoadFromMetadata() {
auto* view_model = apps_grid_view_->view_model();
const auto* item_list = apps_grid_view_->item_list_;
int model_index = 0;
pages_.clear();
pages_.emplace_back();
for (size_t i = 0; i < item_list->item_count(); ++i) {
const auto* item = item_list->item_at(i);
auto* current_page = &pages_.back();
if (item->is_page_break()) {
// Create a new page if a "page break" item is detected and current page
// is not empty. Otherwise, ignore the "page break" item.
if (!current_page->empty())
pages_.emplace_back();
continue;
}
// Create a new page if the current page is full.
const size_t current_page_max_items =
apps_grid_view_->TilesPerPage(pages_.size() - 1);
if (current_page->size() == current_page_max_items) {
pages_.emplace_back();
current_page = &pages_.back();
}
current_page->emplace_back(view_model->view_at(model_index++));
}
// Remove trailing empty page if exist.
if (pages_.back().empty())
pages_.erase(pages_.end() - 1);
}
void PagedViewStructure::SaveToMetadata() {
auto* item_list = apps_grid_view_->item_list_;
size_t item_index = 0;
for (const auto& page : pages_) {
// Skip all "page break" items before current page and after previous page.
while (item_index < item_list->item_count() &&
item_list->item_at(item_index)->is_page_break()) {
++item_index;
}
item_index += page.size();
if (item_index < item_list->item_count() &&
!item_list->item_at(item_index)->is_page_break()) {
// There's no "page break" item at the end of current page, so add one to
// push overflowing items to next page.
apps_grid_view_->model_->AddPageBreakItemAfter(
item_list->item_at(item_index - 1));
}
}
// Note that we do not remove redundant "page break" items here because the
// item list we can access here may not be complete (e.g. Devices that do not
// support ARC++ or Crostini apps filter out those items.). We leave this
// operation to AppListSyncableService which has complete item list.
}
bool PagedViewStructure::Sanitize() {
bool changed = false;
std::vector<AppListItemView*> overflow_views;
auto iter = pages_.begin();
while (iter != pages_.end() || !overflow_views.empty()) {
if (iter == pages_.end()) {
// Add additional page if overflowing item views remain.
pages_.emplace_back();
iter = pages_.end() - 1;
changed = true;
}
const size_t max_item_views =
apps_grid_view_->TilesPerPage(static_cast<int>(iter - pages_.begin()));
auto& page = *iter;
if (!overflow_views.empty()) {
// Put overflowing item views in current page.
page.insert(page.begin(), overflow_views.begin(), overflow_views.end());
overflow_views.clear();
changed = true;
}
if (page.empty()) {
// Remove empty page.
iter = pages_.erase(iter);
changed = true;
continue;
}
if (page.size() > max_item_views) {
// Remove overflowing item views from current page.
overflow_views.insert(overflow_views.begin(),
page.begin() + max_item_views, page.end());
page.erase(page.begin() + max_item_views, page.end());
changed = true;
}
++iter;
}
return changed;
}
void PagedViewStructure::Move(AppListItemView* view,
const GridIndex& target_index) {
RemoveWithoutSanitize(view);
Add(view, target_index);
}
void PagedViewStructure::Remove(AppListItemView* view) {
RemoveWithoutSanitize(view);
Sanitize();
}
void PagedViewStructure::RemoveWithoutSanitize(AppListItemView* view) {
for (auto& page : pages_) {
auto iter = std::find(page.begin(), page.end(), view);
if (iter != page.end()) {
page.erase(iter);
break;
}
}
}
void PagedViewStructure::Add(AppListItemView* view,
const GridIndex& target_index) {
AddWithoutSanitize(view, target_index);
Sanitize();
}
void PagedViewStructure::AddWithoutSanitize(AppListItemView* view,
const GridIndex& target_index) {
const int view_structure_size = total_pages();
DCHECK((target_index.page < view_structure_size &&
target_index.slot <= items_on_page(target_index.page)) ||
(target_index.page == view_structure_size && target_index.slot == 0));
if (target_index.page == view_structure_size)
pages_.emplace_back();
auto& page = pages_[target_index.page];
page.insert(page.begin() + target_index.slot, view);
}
GridIndex PagedViewStructure::GetIndexFromModelIndex(int model_index) const {
AppListItemView* view = apps_grid_view_->view_model()->view_at(model_index);
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] == view)
return GridIndex(i, j);
}
}
return GetLastTargetIndex();
}
int PagedViewStructure::GetModelIndexFromIndex(const GridIndex& index) const {
auto* view_model = apps_grid_view_->view_model();
if (index.page >= total_pages() || index.slot >= items_on_page(index.page))
return view_model->view_size();
AppListItemView* view = pages_[index.page][index.slot];
return view_model->GetIndexOfView(view);
}
GridIndex PagedViewStructure::GetLastTargetIndex() const {
if (apps_grid_view_->view_model()->view_size() == 0)
return GridIndex(0, 0);
int last_page_index = total_pages() - 1;
int target_slot = 0;
auto& last_page = pages_.back();
for (size_t i = 0; i < last_page.size(); ++i) {
// Skip the item view being dragged if it exists in the last page.
if (last_page[i] != apps_grid_view_->drag_view_)
++target_slot;
}
if (target_slot == apps_grid_view_->TilesPerPage(last_page_index)) {
// The last page is full, so the last target visual index is the first slot
// in the next new page.
target_slot = 0;
++last_page_index;
}
return GridIndex(last_page_index, target_slot);
}
GridIndex PagedViewStructure::GetLastTargetIndexOfPage(int page_index) const {
const int page_size = total_pages();
DCHECK_LT(0, apps_grid_view_->view_model()->view_size());
DCHECK_LE(page_index, page_size);
if (page_index == page_size)
return GridIndex(page_index, 0);
int target_slot = 0;
auto& page = pages_[page_index];
for (size_t i = 0; i < page.size(); ++i) {
// Skip the item view being dragged if it exists in the specified
// page_index.
if (page[i] != apps_grid_view_->drag_view())
++target_slot;
}
if (target_slot == apps_grid_view_->TilesPerPage(page_index)) {
// The specified page is full, so the last target visual index is the last
// slot in the page_index.
--target_slot;
}
return GridIndex(page_index, target_slot);
}
int PagedViewStructure::GetTargetModelIndexForMove(
AppListItemView* moved_view,
const GridIndex& index) const {
int target_model_index = 0;
const int max_page = std::min(index.page, total_pages());
for (int i = 0; i < max_page; ++i) {
auto& page = pages_[i];
target_model_index += page.size();
// Skip the item view to be moved in the page if found.
auto iter = std::find(page.begin(), page.end(), moved_view);
if (iter != page.end())
--target_model_index;
}
// If the target visual index is in the same page, do not skip the item view
// because the following item views will fill the gap in the page.
target_model_index += index.slot;
return target_model_index;
}
int PagedViewStructure::GetTargetItemIndexForMove(
AppListItemView* moved_view,
const GridIndex& index) const {
GridIndex current_index(0, 0);
size_t current_item_index = 0;
size_t offset = 0;
const auto* item_list = apps_grid_view_->item_list_;
// Skip the leading "page break" items.
while (current_item_index < item_list->item_count() &&
item_list->item_at(current_item_index)->is_page_break()) {
++current_item_index;
}
while (current_item_index < item_list->item_count()) {
while (current_item_index < item_list->item_count() &&
!item_list->item_at(current_item_index)->is_page_break() &&
current_index != index) {
if (moved_view->item() == item_list->item_at(current_item_index) &&
current_index.page < index.page) {
// If the item view is moved to a following page, we need to skip the
// item view. If the view is moved to the same page, do not skip the
// item view because the following item views will fill the gap left
// after dragging complete.
offset = 1;
}
++current_index.slot;
++current_item_index;
}
if (current_index == index)
return current_item_index - offset;
// Skip the "page break" items at the end of the page.
while (current_item_index < item_list->item_count() &&
item_list->item_at(current_item_index)->is_page_break()) {
++current_item_index;
}
++current_index.page;
current_index.slot = 0;
}
DCHECK(current_index == index);
return current_item_index - offset;
}
bool PagedViewStructure::IsValidReorderTargetIndex(
const GridIndex& index) const {
if (apps_grid_view_->IsValidIndex(index))
return true;
// The user can drag an item view to another page's end.
if (index.page <= total_pages() &&
GetLastTargetIndexOfPage(index.page) == index) {
return true;
}
return false;
}
bool PagedViewStructure::IsFullPage(int page_index) const {
if (page_index >= total_pages())
return false;
return static_cast<int>(pages_[page_index].size()) ==
apps_grid_view_->TilesPerPage(page_index);
}
} // namespace app_list