/* * Copyright (C) 2011 The original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.zapta.apps.maniana.model; import static com.zapta.apps.maniana.util.Assertions.check; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; import javax.annotation.Nullable; import com.zapta.apps.maniana.annotations.ApplicationScope; import com.zapta.apps.maniana.annotations.VisibleForTesting; /** * Contains the data of a single page. * * @author Tal Dayan */ @ApplicationScope public class PageModel { /** List of items in the order they are displayed to the user */ private final List<ItemModel> mItems = new ArrayList<ItemModel>(); /** * List of items to restore in case of undo operation. If empty, the page has no active undo * operation. Note that undo operatons are per page, not for the entire model. */ private final List<ItemModel> mUndoItems = new ArrayList<ItemModel>(); public PageModel() { } /** For testing only. */ @VisibleForTesting List<ItemModel> getUndoItemsCloneForTesting() { return new ArrayList<ItemModel>(mUndoItems); } /** For testing only. */ @VisibleForTesting void appendUndoItemForTesting(ItemModel item) { mUndoItems.add(item); } /** Get a list iterator over the items. Allows to deleted items while iterating. */ public final ListIterator<ItemModel> listIterator() { return mItems.listIterator(); } /** Clear all items and undo buffer */ public final void clear() { mItems.clear(); clearUndo(); } /** Clear undo buffer. Does nothing if undo buffer is not active. */ public final void clearUndo() { mUndoItems.clear(); } /** Test if this page has an active undo operation. */ public final boolean hasUndo() { return !mUndoItems.isEmpty(); } /** Insert a new item at given index. */ public final void insertItem(int itemIndex, ItemModel item) { mItems.add(itemIndex, item); } /** * Remove item at given index. * * @return the removed item. */ public final ItemModel removeItem(int itemIndex) { return mItems.remove(itemIndex); } /** * Get item at given index. */ public final ItemModel getItem(int itemIndex) { return mItems.get(itemIndex); } /** Get number of items in this page. */ public final int itemCount() { return mItems.size(); } /** Get number of incomplete items in this page. */ public final int pendingItemCount() { int count = 0; for (ItemModel item : mItems) { if (!item.isCompleted()) { count++; } } return count; } /** * Copy cloned items from other page. Undo buffer is not changed. */ public final void copyItemsFrom(PageModel otherModel) { mItems.clear(); for (ItemModel otherItem : otherModel.mItems) { mItems.add(new ItemModel(otherItem)); } } /** * Perform and clear the active undo operation. The method asserts that an undo operation is * active. * * @return the number of items resotred by the undo operation. */ public final int performUndo() { check(hasUndo(), "Undo info not found"); final int n = mUndoItems.size(); // NOTE(tal): we add the items at the begining of the list, preserving their // relative order. mItems.addAll(0, mUndoItems); mUndoItems.clear(); return n; } /** * Remove item at given index and setup a matching undo operation. * * @return the deleted item. */ public final void removeItemWithUndo(int itemIndex) { final ItemModel deletedItem = removeItem(itemIndex); mUndoItems.clear(); mUndoItems.add(deletedItem); } /** Append item to end of undo list. Item should not be in any page item list. */ public final void appendItemToUndo(ItemModel item) { mUndoItems.add(item); } /** Append an item at the end of the page */ public void appendItem(ItemModel item) { mItems.add(item); } public final void restoreBackup(PageModel newPage) { // Move all existing items to the undo buffer mUndoItems.clear(); mItems.clear(); // Add copies of the items in the new page for (ItemModel item : newPage.mItems) { final ItemModel newItem = new ItemModel(item); mItems.add(newItem); } } /** * Peform a page organization operation. * * @param deleteCompletedItems indicates if the operation should also delete all completed * items. * @param itemOfInterestItem optional item index or -1 if not specified. If specified, this is * the index of an item of interest. Upon return, the summary contains the location of * the new index of the item of interest or -1 if the item was not specified or was * deleted. * @param summary an object is set with the operation summary. */ public final void organizePageWithUndo(boolean deleteCompletedItems, int itemOfInterestIndex, OrganizePageSummary summary) { summary.clear(); @Nullable final ItemModel itemOfInterest = (itemOfInterestIndex == -1) ? null : mItems .get(itemOfInterestIndex); // We clear any old undo only if the current operation actually deletes items. boolean oldUndoCleared = false; // Count completed items. If deleteCompletedItems than also delete them // and add to undo buffer. Also detects if the items are out of sorting order. boolean isOutOfOrder = false; { ListIterator<ItemModel> iterator = mItems.listIterator(); int maxItemGroupIndexSoFar = 0; while (iterator.hasNext()) { final ItemModel item = iterator.next(); if (item.isCompleted()) { summary.completedItemsFound++; if (deleteCompletedItems) { // Here to delete item. if (!oldUndoCleared) { mUndoItems.clear(); oldUndoCleared = true; } iterator.remove(); mUndoItems.add(item); summary.completedItemsDeleted++; // Do not continue to the item order checking below since this item // was deleted from the list. continue; } } // Here when the item was left in the list final int itemGroupIndex = item.sortingGroupIndex(); if (itemGroupIndex < maxItemGroupIndexSoFar) { isOutOfOrder = true; } else { maxItemGroupIndexSoFar = itemGroupIndex; } } } // If out of order, sort by groups, preserving order within each group. if (isOutOfOrder) { final ItemModel[] newOrder = new ItemModel[mItems.size()]; int itemsCopied = 0; // Performing one pass per sorting group. On each pass, picking the items of that // group and adding to newOrder. This algorithm is optimized for minimal object // creation. for (int groupIndex = 0; groupIndex < ItemModelReadOnly.SORTING_GROUPS; groupIndex++) { for (int itemIndex = 0; itemIndex < mItems.size(); itemIndex++) { final ItemModel item = mItems.get(itemIndex); // Classify item by the group it belongs to. final int itemGroupIndex = item.sortingGroupIndex(); if (itemGroupIndex == groupIndex) { newOrder[itemsCopied] = item; itemsCopied++; } } } // Copy the newOrder array to mItems check(mItems.size() == itemsCopied); for (int i = 0; i < itemsCopied; i++) { mItems.set(i, newOrder[i]); } } // If requested, locate new location of item of interest. if (itemOfInterest != null) { final int itemOfInteresetNewIndex = mItems.indexOf(itemOfInterest); if (itemOfInteresetNewIndex >= 0) { summary.itemOfInterestNewIndex = itemOfInteresetNewIndex; } } summary.orderChanged = isOutOfOrder; } public final boolean isPageSorted() { int maxGroupIndexSoFar = 0; for (ItemModel item : mItems) { int itemGroupIndex = item.sortingGroupIndex(); if (itemGroupIndex < maxGroupIndexSoFar) { return false; } maxGroupIndexSoFar = itemGroupIndex; } return true; } }