| /* |
| * Copyright 2010 Google Inc. |
| * |
| * 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.google.gwt.user.cellview.client; |
| |
| import com.google.gwt.core.client.Scheduler; |
| import com.google.gwt.core.client.Scheduler.ScheduledCommand; |
| import com.google.gwt.dom.client.Element; |
| import com.google.gwt.event.shared.EventHandler; |
| import com.google.gwt.event.shared.GwtEvent; |
| import com.google.gwt.event.shared.HandlerRegistration; |
| import com.google.gwt.safehtml.shared.SafeHtml; |
| import com.google.gwt.safehtml.shared.SafeHtmlBuilder; |
| import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState; |
| import com.google.gwt.view.client.CellPreviewEvent; |
| import com.google.gwt.view.client.HasData; |
| import com.google.gwt.view.client.HasKeyProvider; |
| import com.google.gwt.view.client.ProvidesKey; |
| import com.google.gwt.view.client.Range; |
| import com.google.gwt.view.client.RangeChangeEvent; |
| import com.google.gwt.view.client.RowCountChangeEvent; |
| import com.google.gwt.view.client.SelectionChangeEvent; |
| import com.google.gwt.view.client.SelectionModel; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.TreeSet; |
| |
| /** |
| * <p> |
| * Presenter implementation of {@link HasData} that presents data for various |
| * cell based widgets. This class contains most of the shared logic used by |
| * these widgets, making it easier to test the common code. |
| * <p> |
| * <p> |
| * In proper MVP design, user code would interact with the presenter. However, |
| * that would complicate the widget code. Instead, each widget owns its own |
| * presenter and contains its own View. The widget forwards commands through to |
| * the presenter, which then updates the widget via the view. This keeps the |
| * user facing API simpler. |
| * <p> |
| * <p> |
| * Updates are not pushed to the view immediately. Instead, the presenter |
| * collects updates and resolves them all in a finally command. This reduces the |
| * total number of DOM manipulations, and makes it easier to handle side effects |
| * in user code triggered by the rendering pass. The view is responsible for |
| * called {@link #flush()} to force the presenter to synchronize the view when |
| * needed. |
| * </p> |
| * |
| * @param <T> the data type of items in the list |
| */ |
| class HasDataPresenter<T> implements HasData<T>, HasKeyProvider<T>, |
| HasKeyboardPagingPolicy { |
| |
| /** |
| * An iterator over DOM elements. |
| */ |
| static interface ElementIterator extends Iterator<Element> { |
| /** |
| * Set the selection state of the current element. |
| * |
| * @param selected the selection state |
| * @throws IllegalStateException if {@link #next()} has not been called |
| */ |
| void setSelected(boolean selected) throws IllegalStateException; |
| } |
| |
| /** |
| * The view that this presenter presents. |
| * |
| * @param <T> the data type |
| */ |
| static interface View<T> { |
| |
| /** |
| * Add a handler to the view. |
| * |
| * @param <H> the handler type |
| * @param handler the handler to add |
| * @param type the event type |
| */ |
| <H extends EventHandler> HandlerRegistration addHandler(final H handler, |
| GwtEvent.Type<H> type); |
| |
| /** |
| * Construct the HTML that represents the list of values, taking the |
| * selection state into account. |
| * |
| * @param sb the {@link SafeHtmlBuilder} to build into |
| * @param values the values to render |
| * @param start the absolute start index that is being rendered |
| * @param selectionModel the {@link SelectionModel} |
| */ |
| void render(SafeHtmlBuilder sb, List<T> values, int start, |
| SelectionModel<? super T> selectionModel); |
| |
| /** |
| * Replace all children with the specified html. |
| * |
| * @param values the values of the new children |
| * @param html the html to render in the child |
| * @param stealFocus true if the row should steal focus, false if not |
| */ |
| void replaceAllChildren(List<T> values, SafeHtml html, boolean stealFocus); |
| |
| /** |
| * Convert the specified HTML into DOM elements and replace the existing |
| * elements starting at the specified index. If the number of children |
| * specified exceeds the existing number of children, the remaining children |
| * should be appended. |
| * |
| * @param values the values of the new children |
| * @param start the start index to be replaced, relative to the pageStart |
| * @param html the HTML to convert |
| * @param stealFocus true if the row should steal focus, false if not |
| */ |
| void replaceChildren(List<T> values, int start, SafeHtml html, |
| boolean stealFocus); |
| |
| /** |
| * Re-establish focus on an element within the view if the view already had |
| * focus. |
| */ |
| void resetFocus(); |
| |
| /** |
| * Update an element to reflect its keyboard selected state. |
| * |
| * @param index the index of the element relative to page start |
| * @param selected true if selected, false if not |
| * @param stealFocus true if the row should steal focus, false if not |
| */ |
| void setKeyboardSelected(int index, boolean selected, boolean stealFocus); |
| |
| /** |
| * Set the current loading state of the data. |
| * |
| * @param state the loading state |
| */ |
| void setLoadingState(LoadingState state); |
| } |
| |
| /** |
| * Represents the state of the presenter. |
| * |
| * @param <T> the data type of the presenter |
| */ |
| private static class DefaultState<T> implements State<T> { |
| int keyboardSelectedRow = 0; |
| T keyboardSelectedRowValue = null; |
| int pageSize; |
| int pageStart = 0; |
| int rowCount = 0; |
| boolean rowCountIsExact = false; |
| final List<T> rowData = new ArrayList<T>(); |
| final Set<Integer> selectedRows = new HashSet<Integer>(); |
| T selectedValue = null; |
| boolean viewTouched; |
| |
| public DefaultState(int pageSize) { |
| this.pageSize = pageSize; |
| } |
| |
| public int getKeyboardSelectedRow() { |
| return keyboardSelectedRow; |
| } |
| |
| public T getKeyboardSelectedRowValue() { |
| return keyboardSelectedRowValue; |
| } |
| |
| public int getPageSize() { |
| return pageSize; |
| } |
| |
| public int getPageStart() { |
| return pageStart; |
| } |
| |
| public int getRowCount() { |
| return rowCount; |
| } |
| |
| public int getRowDataSize() { |
| return rowData.size(); |
| } |
| |
| public T getRowDataValue(int index) { |
| return rowData.get(index); |
| } |
| |
| public List<T> getRowDataValues() { |
| return Collections.unmodifiableList(rowData); |
| } |
| |
| public T getSelectedValue() { |
| return selectedValue; |
| } |
| |
| public boolean isRowCountExact() { |
| return rowCountIsExact; |
| } |
| |
| /** |
| * {@inheritDoc} |
| * |
| * <p> |
| * The set of selected rows is not maintained in the pending state. This |
| * method should only be called on the state after it has been resolved. |
| * </p> |
| */ |
| public boolean isRowSelected(int index) { |
| return selectedRows.contains(index); |
| } |
| |
| public boolean isViewTouched() { |
| return viewTouched; |
| } |
| } |
| |
| /** |
| * Represents the pending state of the presenter. |
| * |
| * @param <T> the data type of the presenter |
| */ |
| private static class PendingState<T> extends DefaultState<T> { |
| |
| /** |
| * A boolean indicating that the user has keyboard selected a new row. |
| */ |
| private boolean keyboardSelectedRowChanged; |
| |
| /** |
| * A boolean indicating that a change in keyboard selected should cause us |
| * to steal focus. |
| */ |
| private boolean keyboardStealFocus = false; |
| |
| /** |
| * Set to true if a redraw is required. |
| */ |
| private boolean redrawRequired = false; |
| |
| /** |
| * The list of ranges that have been replaced. |
| */ |
| private final List<Range> replacedRanges = new ArrayList<Range>(); |
| |
| public PendingState(State<T> state) { |
| super(state.getPageSize()); |
| this.keyboardSelectedRow = state.getKeyboardSelectedRow(); |
| this.keyboardSelectedRowValue = state.getKeyboardSelectedRowValue(); |
| this.pageSize = state.getPageSize(); |
| this.pageStart = state.getPageStart(); |
| this.rowCount = state.getRowCount(); |
| this.rowCountIsExact = state.isRowCountExact(); |
| this.selectedValue = state.getSelectedValue(); |
| this.viewTouched = state.isViewTouched(); |
| |
| // Copy the row data. |
| int rowDataSize = state.getRowDataSize(); |
| for (int i = 0; i < rowDataSize; i++) { |
| this.rowData.add(state.getRowDataValue(i)); |
| } |
| |
| /* |
| * We do not copy the selected rows from the old state. They will be |
| * resolved from the SelectionModel. |
| */ |
| } |
| |
| /** |
| * Update the range of replaced data. |
| * |
| * @param start the start index |
| * @param end the end index |
| */ |
| public void replaceRange(int start, int end) { |
| replacedRanges.add(new Range(start, end - start)); |
| } |
| } |
| |
| /** |
| * Represents the state of the presenter. |
| * |
| * @param <T> the data type of the presenter |
| */ |
| private static interface State<T> { |
| /** |
| * Get the current keyboard selected row relative to page start. This value |
| * should never be negative. |
| */ |
| int getKeyboardSelectedRow(); |
| |
| /** |
| * Get the last row value that was selected with the keyboard. |
| */ |
| T getKeyboardSelectedRowValue(); |
| |
| /** |
| * Get the number of rows in the current page. |
| */ |
| int getPageSize(); |
| |
| /** |
| * Get the absolute start index of the page. |
| */ |
| int getPageStart(); |
| |
| /** |
| * Get the total number of rows. |
| */ |
| int getRowCount(); |
| |
| /** |
| * Get the size of the row data. |
| */ |
| int getRowDataSize(); |
| |
| /** |
| * Get a specific value from the row data. |
| */ |
| T getRowDataValue(int index); |
| |
| /** |
| * Get all of the row data values in an unmodifiable list. |
| */ |
| List<T> getRowDataValues(); |
| |
| /** |
| * Get the value that is selected in the {@link SelectionModel}. |
| */ |
| T getSelectedValue(); |
| |
| /** |
| * Get a boolean indicating whether the row count is exact or an estimate. |
| */ |
| boolean isRowCountExact(); |
| |
| /** |
| * Check if a row index is selected. |
| * |
| * @param index the row index |
| * @return true if selected, false if not |
| */ |
| boolean isRowSelected(int index); |
| |
| /** |
| * Check if the user interacted with the view at some point. Selection is |
| * not bound to the keyboard selected row until the view is touched. Once |
| * touched, selection is bound from then on. |
| */ |
| boolean isViewTouched(); |
| } |
| |
| /** |
| * The number of rows to jump when PAGE_UP or PAGE_DOWN is pressed and the |
| * {@link KeyboardSelectionPolicy} is |
| * {@link KeyboardSelectionPolicy.INCREMENT_PAGE}. |
| */ |
| static final int PAGE_INCREMENT = 30; |
| |
| /** |
| * The maximum number of times we can try to {@link #resolvePendingState()} |
| * before we assume there is an infinite loop. |
| */ |
| private static final int LOOP_MAXIMUM = 10; |
| |
| /** |
| * The minimum number of rows that need to be replaced before we do a redraw. |
| */ |
| private static final int REDRAW_MINIMUM = 5; |
| |
| /** |
| * The threshold of new data after which we redraw the entire view instead of |
| * replacing specific rows. |
| * |
| * TODO(jlabanca): Find the optimal value for the threshold. |
| */ |
| private static final double REDRAW_THRESHOLD = 0.30; |
| |
| private final HasData<T> display; |
| |
| /** |
| * A boolean indicating that we are in the process of resolving state. |
| */ |
| private boolean isResolvingState; |
| |
| private KeyboardPagingPolicy keyboardPagingPolicy = KeyboardPagingPolicy.CHANGE_PAGE; |
| private KeyboardSelectionPolicy keyboardSelectionPolicy = KeyboardSelectionPolicy.ENABLED; |
| |
| private final ProvidesKey<T> keyProvider; |
| |
| /** |
| * As an optimization, keep track of the last HTML string that we rendered. If |
| * the contents do not change the next time we render, then we don't have to |
| * set inner html. This is useful for apps that continuously refresh the view. |
| */ |
| private SafeHtml lastContents = null; |
| |
| /** |
| * The pending state of the presenter to be pushed to the view. |
| */ |
| private PendingState<T> pendingState; |
| |
| /** |
| * The command used to resolve the pending state. |
| */ |
| private ScheduledCommand pendingStateCommand; |
| |
| /** |
| * A counter used to detect infinite loops in {@link #resolvePendingState()}. |
| * An infinite loop can occur if user code, such as reading the |
| * {@link SelectionModel}, causes the table to have a pending state. |
| */ |
| private int pendingStateLoop = 0; |
| |
| private HandlerRegistration selectionHandler; |
| private SelectionModel<? super T> selectionModel; |
| |
| /** |
| * The current state of the presenter reflected in the view. We intentionally |
| * use the interface, which only has getters, to ensure that we do not |
| * accidently modify the current state. |
| */ |
| private State<T> state; |
| |
| private final View<T> view; |
| |
| /** |
| * Construct a new {@link HasDataPresenter}. |
| * |
| * @param display the display that is being presented |
| * @param view the view implementation |
| * @param pageSize the default page size |
| */ |
| public HasDataPresenter(HasData<T> display, View<T> view, int pageSize, |
| ProvidesKey<T> keyProvider) { |
| this.display = display; |
| this.view = view; |
| this.keyProvider = keyProvider; |
| this.state = new DefaultState<T>(pageSize); |
| } |
| |
| public HandlerRegistration addCellPreviewHandler( |
| CellPreviewEvent.Handler<T> handler) { |
| return view.addHandler(handler, CellPreviewEvent.getType()); |
| } |
| |
| public HandlerRegistration addLoadingStateChangeHandler( |
| LoadingStateChangeEvent.Handler handler) { |
| return view.addHandler(handler, LoadingStateChangeEvent.TYPE); |
| } |
| |
| public HandlerRegistration addRangeChangeHandler( |
| RangeChangeEvent.Handler handler) { |
| return view.addHandler(handler, RangeChangeEvent.getType()); |
| } |
| |
| public HandlerRegistration addRowCountChangeHandler( |
| RowCountChangeEvent.Handler handler) { |
| return view.addHandler(handler, RowCountChangeEvent.getType()); |
| } |
| |
| /** |
| * Clear the row value associated with the keyboard selected row. |
| */ |
| public void clearKeyboardSelectedRowValue() { |
| if (getKeyboardSelectedRowValue() != null) { |
| ensurePendingState().keyboardSelectedRowValue = null; |
| } |
| } |
| |
| /** |
| * Clear the {@link SelectionModel} without updating the view. |
| */ |
| public void clearSelectionModel() { |
| if (selectionHandler != null) { |
| selectionHandler.removeHandler(); |
| selectionHandler = null; |
| } |
| selectionModel = null; |
| } |
| |
| /** |
| * @throws UnsupportedOperationException |
| */ |
| public void fireEvent(GwtEvent<?> event) { |
| // HasData should fire their own events. |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Flush pending changes to the view. |
| */ |
| public void flush() { |
| /* |
| * resolvePendingState can exit early user code applied more pending state, |
| * so we need to loop until we are sure that the pending state is clear. If |
| * the user calls this method while resolving pending state, then do not |
| * attempt to resolve pending state again. |
| */ |
| while (pendingStateCommand != null && !isResolvingState) { |
| resolvePendingState(); |
| } |
| } |
| |
| /** |
| * Get the current page size. This is usually the page size, but can be less |
| * if the data size cannot fill the current page. |
| * |
| * @return the size of the current page |
| */ |
| public int getCurrentPageSize() { |
| return Math.min(getPageSize(), getRowCount() - getPageStart()); |
| } |
| |
| public KeyboardPagingPolicy getKeyboardPagingPolicy() { |
| return keyboardPagingPolicy; |
| } |
| |
| /** |
| * Get the index of the keyboard selected row relative to the page start. |
| * |
| * @return the row index, or -1 if disabled |
| */ |
| public int getKeyboardSelectedRow() { |
| return KeyboardSelectionPolicy.DISABLED == keyboardSelectionPolicy ? -1 |
| : getCurrentState().getKeyboardSelectedRow(); |
| } |
| |
| /** |
| * Get the index of the keyboard selected row relative to the page start as it |
| * appears in the view, regardless of whether or not there is a pending |
| * change. |
| * |
| * @return the row index, or -1 if disabled |
| */ |
| public int getKeyboardSelectedRowInView() { |
| return KeyboardSelectionPolicy.DISABLED == keyboardSelectionPolicy ? -1 |
| : state.getKeyboardSelectedRow(); |
| } |
| |
| /** |
| * Get the value that the user selected. |
| * |
| * @return the value, or null if a value was not selected |
| */ |
| public T getKeyboardSelectedRowValue() { |
| return KeyboardSelectionPolicy.DISABLED == keyboardSelectionPolicy ? null |
| : getCurrentState().getKeyboardSelectedRowValue(); |
| } |
| |
| public KeyboardSelectionPolicy getKeyboardSelectionPolicy() { |
| return keyboardSelectionPolicy; |
| } |
| |
| public ProvidesKey<T> getKeyProvider() { |
| return keyProvider; |
| } |
| |
| /** |
| * Get the overall data size. |
| * |
| * @return the data size |
| */ |
| public int getRowCount() { |
| return getCurrentState().getRowCount(); |
| } |
| |
| public SelectionModel<? super T> getSelectionModel() { |
| return selectionModel; |
| } |
| |
| public T getVisibleItem(int indexOnPage) { |
| return getCurrentState().getRowDataValue(indexOnPage); |
| } |
| |
| public int getVisibleItemCount() { |
| return getCurrentState().getRowDataSize(); |
| } |
| |
| public List<T> getVisibleItems() { |
| return getCurrentState().getRowDataValues(); |
| } |
| |
| /** |
| * Return the range of data being displayed. |
| */ |
| public Range getVisibleRange() { |
| return new Range(getPageStart(), getPageSize()); |
| } |
| |
| /** |
| * Check if the next call to {@link #keyboardNext()} would succeed. |
| * |
| * @return true if there is another row accessible by the keyboard |
| */ |
| public boolean hasKeyboardNext() { |
| if (KeyboardSelectionPolicy.DISABLED == keyboardSelectionPolicy) { |
| return false; |
| } else if (getKeyboardSelectedRow() < getVisibleItemCount() - 1) { |
| return true; |
| } else if (!keyboardPagingPolicy.isLimitedToRange() |
| && (getKeyboardSelectedRow() + getPageStart() < getRowCount() - 1 || !isRowCountExact())) { |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Check if the next call to {@link #keyboardPrevious()} would succeed. |
| * |
| * @return true if there is a previous row accessible by the keyboard |
| */ |
| public boolean hasKeyboardPrev() { |
| if (KeyboardSelectionPolicy.DISABLED == keyboardSelectionPolicy) { |
| return false; |
| } else if (getKeyboardSelectedRow() > 0) { |
| return true; |
| } else if (!keyboardPagingPolicy.isLimitedToRange() && getPageStart() > 0) { |
| return true; |
| } |
| return false; |
| } |
| |
| /** |
| * Check whether or not there is a pending state. If there is a pending state, |
| * views might skip DOM updates and wait for the new data to be rendered when |
| * the pending state is resolved. |
| * |
| * @return true if there is a pending state, false if not |
| */ |
| public boolean hasPendingState() { |
| return pendingState != null; |
| } |
| |
| /** |
| * Check whether or not the data set is empty. That is, the row count is |
| * exactly 0. |
| * |
| * @return true if data set is empty |
| */ |
| public boolean isEmpty() { |
| return isRowCountExact() && getRowCount() == 0; |
| } |
| |
| public boolean isRowCountExact() { |
| return getCurrentState().isRowCountExact(); |
| } |
| |
| /** |
| * Move keyboard selection to the last row. |
| */ |
| public void keyboardEnd() { |
| if (!keyboardPagingPolicy.isLimitedToRange()) { |
| setKeyboardSelectedRow(getRowCount() - 1, true, false); |
| } |
| } |
| |
| /** |
| * Move keyboard selection to the absolute 0th row. |
| */ |
| public void keyboardHome() { |
| if (!keyboardPagingPolicy.isLimitedToRange()) { |
| setKeyboardSelectedRow(-getPageStart(), true, false); |
| } |
| } |
| |
| /** |
| * Move keyboard selection to the next row. |
| */ |
| public void keyboardNext() { |
| if (hasKeyboardNext()) { |
| setKeyboardSelectedRow(getKeyboardSelectedRow() + 1, true, false); |
| } |
| } |
| |
| /** |
| * Move keyboard selection to the next page. |
| */ |
| public void keyboardNextPage() { |
| if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) { |
| // 0th index of next page. |
| setKeyboardSelectedRow(getPageSize(), true, false); |
| } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) { |
| setKeyboardSelectedRow(getKeyboardSelectedRow() + PAGE_INCREMENT, true, |
| false); |
| } |
| } |
| |
| /** |
| * Move keyboard selection to the previous row. |
| */ |
| public void keyboardPrev() { |
| if (hasKeyboardPrev()) { |
| setKeyboardSelectedRow(getKeyboardSelectedRow() - 1, true, false); |
| } |
| } |
| |
| /** |
| * Move keyboard selection to the previous page. |
| */ |
| public void keyboardPrevPage() { |
| if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) { |
| // 0th index of previous page. |
| setKeyboardSelectedRow(-getPageSize(), true, false); |
| } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) { |
| setKeyboardSelectedRow(getKeyboardSelectedRow() - PAGE_INCREMENT, true, |
| false); |
| } |
| } |
| |
| /** |
| * Redraw the list with the current data. |
| */ |
| public void redraw() { |
| lastContents = null; |
| ensurePendingState().redrawRequired = true; |
| } |
| |
| public void setKeyboardPagingPolicy(KeyboardPagingPolicy policy) { |
| if (policy == null) { |
| throw new NullPointerException("KeyboardPagingPolicy cannot be null"); |
| } |
| this.keyboardPagingPolicy = policy; |
| } |
| |
| /** |
| * Set the row index of the keyboard selected element. |
| * |
| * @param index the row index |
| * @param stealFocus true to steal focus |
| * @param forceUpdate force the update even if the row didn't change |
| */ |
| public void setKeyboardSelectedRow(int index, boolean stealFocus, |
| boolean forceUpdate) { |
| // Early exit if disabled. |
| if (KeyboardSelectionPolicy.DISABLED == keyboardSelectionPolicy) { |
| return; |
| } |
| |
| // The user touched the view. |
| ensurePendingState().viewTouched = true; |
| |
| /* |
| * Early exit if the keyboard selected row has not changed and the keyboard |
| * selected value is already set. |
| */ |
| if (!forceUpdate && getKeyboardSelectedRow() == index |
| && getKeyboardSelectedRowValue() != null) { |
| return; |
| } |
| |
| // Trim to within bounds. |
| int pageStart = getPageStart(); |
| int pageSize = getPageSize(); |
| int rowCount = getRowCount(); |
| int absIndex = pageStart + index; |
| if (absIndex >= rowCount && isRowCountExact()) { |
| absIndex = rowCount - 1; |
| } |
| index = Math.max(0, absIndex) - pageStart; |
| if (keyboardPagingPolicy.isLimitedToRange()) { |
| index = Math.max(0, Math.min(index, pageSize - 1)); |
| } |
| |
| // Select the new index. |
| int newPageStart = pageStart; |
| int newPageSize = pageSize; |
| PendingState<T> pending = ensurePendingState(); |
| pending.keyboardSelectedRow = 0; |
| pending.keyboardSelectedRowValue = null; |
| pending.keyboardSelectedRowChanged = true; |
| if (index >= 0 && index < pageSize) { |
| pending.keyboardSelectedRow = index; |
| pending.keyboardSelectedRowValue = index < pending.getRowDataSize() |
| ? ensurePendingState().getRowDataValue(index) : null; |
| pending.keyboardStealFocus = stealFocus; |
| return; |
| } else if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) { |
| // Go to previous page. |
| while (index < 0) { |
| newPageStart -= pageSize; |
| index += pageSize; |
| } |
| |
| // Go to next page. |
| while (index >= pageSize) { |
| newPageStart += pageSize; |
| index -= pageSize; |
| } |
| } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) { |
| // Increase range at the beginning. |
| while (index < 0) { |
| newPageSize += PAGE_INCREMENT; |
| newPageStart -= PAGE_INCREMENT; |
| index += PAGE_INCREMENT; |
| } |
| if (newPageStart < 0) { |
| index += newPageStart; |
| newPageSize += newPageStart; |
| newPageStart = 0; |
| } |
| |
| // Increase range at the end. |
| while (index >= newPageSize) { |
| newPageSize += PAGE_INCREMENT; |
| } |
| if (isRowCountExact()) { |
| newPageSize = Math.min(newPageSize, rowCount - newPageStart); |
| if (index >= rowCount) { |
| index = rowCount - 1; |
| } |
| } |
| } |
| |
| // Update the range if it changed. |
| if (newPageStart != pageStart || newPageSize != pageSize) { |
| pending.keyboardSelectedRow = index; |
| setVisibleRange(new Range(newPageStart, newPageSize), false, false); |
| } |
| } |
| |
| public void setKeyboardSelectionPolicy(KeyboardSelectionPolicy policy) { |
| if (policy == null) { |
| throw new NullPointerException("KeyboardSelectionPolicy cannot be null"); |
| } |
| this.keyboardSelectionPolicy = policy; |
| } |
| |
| /** |
| * @throws UnsupportedOperationException |
| */ |
| public final void setRowCount(int count) { |
| // Views should defer to their own implementation of |
| // setRowCount(int, boolean)) per HasRows spec. |
| throw new UnsupportedOperationException(); |
| } |
| |
| public void setRowCount(int count, boolean isExact) { |
| if (count == getRowCount() && isExact == isRowCountExact()) { |
| return; |
| } |
| ensurePendingState().rowCount = count; |
| ensurePendingState().rowCountIsExact = isExact; |
| |
| // Update the cached data. |
| updateCachedData(); |
| |
| // Update the pager. |
| RowCountChangeEvent.fire(display, count, isExact); |
| } |
| |
| public void setRowData(int start, List<? extends T> values) { |
| int valuesLength = values.size(); |
| int valuesEnd = start + valuesLength; |
| |
| // Calculate the bounded start (inclusive) and end index (exclusive). |
| int pageStart = getPageStart(); |
| int pageEnd = getPageStart() + getPageSize(); |
| int boundedStart = Math.max(start, pageStart); |
| int boundedEnd = Math.min(valuesEnd, pageEnd); |
| if (start != pageStart && boundedStart >= boundedEnd) { |
| // The data is out of range for the current page. |
| // Intentionally allow empty lists that start on the page start. |
| return; |
| } |
| |
| // Create placeholders up to the specified index. |
| PendingState<T> pending = ensurePendingState(); |
| int cacheOffset = Math.max(0, boundedStart - pageStart |
| - getVisibleItemCount()); |
| for (int i = 0; i < cacheOffset; i++) { |
| pending.rowData.add(null); |
| } |
| |
| // Insert the new values into the data array. |
| for (int i = boundedStart; i < boundedEnd; i++) { |
| T value = values.get(i - start); |
| int dataIndex = i - pageStart; |
| if (dataIndex < getVisibleItemCount()) { |
| pending.rowData.set(dataIndex, value); |
| } else { |
| pending.rowData.add(value); |
| } |
| } |
| |
| // Remember the range that has been replaced. |
| pending.replaceRange(boundedStart - cacheOffset, boundedEnd); |
| |
| // Fire a row count change event after updating the data. |
| if (valuesEnd > getRowCount()) { |
| setRowCount(valuesEnd, isRowCountExact()); |
| } |
| } |
| |
| public void setSelectionModel(final SelectionModel<? super T> selectionModel) { |
| clearSelectionModel(); |
| |
| // Set the new selection model. |
| this.selectionModel = selectionModel; |
| if (selectionModel != null) { |
| selectionHandler = selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() { |
| public void onSelectionChange(SelectionChangeEvent event) { |
| // Ensure that we resolve selection. |
| ensurePendingState(); |
| } |
| }); |
| } |
| |
| // Update the current selection state based on the new model. |
| ensurePendingState(); |
| } |
| |
| /** |
| * @throws UnsupportedOperationException |
| */ |
| public final void setVisibleRange(int start, int length) { |
| // Views should defer to their own implementation of setVisibleRange(Range) |
| // per HasRows spec. |
| throw new UnsupportedOperationException(); |
| } |
| |
| public void setVisibleRange(Range range) { |
| setVisibleRange(range, false, false); |
| } |
| |
| public void setVisibleRangeAndClearData(Range range, |
| boolean forceRangeChangeEvent) { |
| setVisibleRange(range, true, forceRangeChangeEvent); |
| } |
| |
| /** |
| * Schedules the command. |
| * |
| * <p> |
| * Protected so that subclasses can override to use an alternative scheduler. |
| * </p> |
| * |
| * @param command the command to execute |
| */ |
| protected void scheduleFinally(ScheduledCommand command) { |
| Scheduler.get().scheduleFinally(command); |
| } |
| |
| /** |
| * Combine the modified row indexes into as many as two {@link Range}s, |
| * optimizing to have the fewest unmodified rows within the ranges. Using two |
| * ranges covers the most common use cases of selecting one row, selecting a |
| * range, moving selection from one row to another, or moving keyboard |
| * selection. |
| * |
| * Visible for testing. |
| * |
| * @param modifiedRows the indexes of modified rows |
| * @return up to two ranges that encompass the modified rows |
| */ |
| List<Range> calculateModifiedRanges(TreeSet<Integer> modifiedRows, |
| int pageStart, int pageEnd) { |
| int rangeStart0 = -1; |
| int rangeEnd0 = -1; |
| int rangeStart1 = -1; |
| int rangeEnd1 = -1; |
| int maxDiff = 0; |
| for (int index : modifiedRows) { |
| if (index < pageStart || index >= pageEnd) { |
| // The index is out of range of the current page. |
| continue; |
| } else if (rangeStart0 == -1) { |
| // Range0 defaults to the first index. |
| rangeStart0 = index; |
| rangeEnd0 = index; |
| } else if (rangeStart1 == -1) { |
| // Range1 defaults to the second index. |
| maxDiff = index - rangeEnd0; |
| rangeStart1 = index; |
| rangeEnd1 = index; |
| } else { |
| int diff = index - rangeEnd1; |
| if (diff > maxDiff) { |
| // Move the old range1 onto range0 and start range1 from this index. |
| rangeEnd0 = rangeEnd1; |
| rangeStart1 = index; |
| rangeEnd1 = index; |
| maxDiff = diff; |
| } else { |
| // Add this index to range1. |
| rangeEnd1 = index; |
| } |
| } |
| } |
| |
| // Convert the range ends to exclusive indexes for calculations. |
| rangeEnd0 += 1; |
| rangeEnd1 += 1; |
| |
| // Combine the ranges if they are continuous. |
| if (rangeStart1 == rangeEnd0) { |
| rangeEnd0 = rangeEnd1; |
| rangeStart1 = -1; |
| rangeEnd1 = -1; |
| } |
| |
| // Return the ranges. |
| List<Range> toRet = new ArrayList<Range>(); |
| if (rangeStart0 != -1) { |
| int rangeLength0 = rangeEnd0 - rangeStart0; |
| toRet.add(new Range(rangeStart0, rangeLength0)); |
| } |
| if (rangeStart1 != -1) { |
| int rangeLength1 = rangeEnd1 - rangeStart1; |
| toRet.add(new Range(rangeStart1, rangeLength1)); |
| } |
| return toRet; |
| } |
| |
| /** |
| * Ensure that a pending {@link DefaultState} exists and return it. |
| * |
| * @return the pending state |
| */ |
| private PendingState<T> ensurePendingState() { |
| // Create the pending state if needed. |
| if (pendingState == null) { |
| pendingState = new PendingState<T>(state); |
| } |
| |
| /* |
| * Schedule a command to resolve the pending state. If a command is already |
| * scheduled, we reschedule a new one to ensure that it happens after any |
| * existing finally commands (such as SelectionModel commands). |
| */ |
| pendingStateCommand = new ScheduledCommand() { |
| public void execute() { |
| // Verify that this command was the last one scheduled. |
| if (pendingStateCommand == this) { |
| resolvePendingState(); |
| } |
| } |
| }; |
| scheduleFinally(pendingStateCommand); |
| |
| // Return the pending state. |
| return pendingState; |
| } |
| |
| /** |
| * Find the index within the {@link State} of the best match for the specified |
| * row value. The best match is a row value with the same key, closest to the |
| * initial index. |
| * |
| * @param state the state to search |
| * @param value the value to find |
| * @param initialIndex the initial index of the value |
| * @return the best match index, or -1 if not found |
| */ |
| private int findIndexOfBestMatch(State<T> state, T value, int initialIndex) { |
| // Get the key for the value. |
| Object key = getRowValueKey(value); |
| if (key == null) { |
| return -1; |
| } |
| |
| int bestMatchIndex = -1; |
| int bestMatchDiff = Integer.MAX_VALUE; |
| int rowDataCount = state.getRowDataSize(); |
| for (int i = 0; i < rowDataCount; i++) { |
| T curValue = state.getRowDataValue(i); |
| Object curKey = getRowValueKey(curValue); |
| if (key.equals(curKey)) { |
| int diff = Math.abs(initialIndex - i); |
| if (diff < bestMatchDiff) { |
| bestMatchIndex = i; |
| bestMatchDiff = diff; |
| } |
| } |
| } |
| return bestMatchIndex; |
| } |
| |
| /** |
| * Get the current state of the presenter. |
| * |
| * @return the pending state if one exists, otherwise the state |
| */ |
| private State<T> getCurrentState() { |
| return pendingState == null ? state : pendingState; |
| } |
| |
| private int getPageSize() { |
| return getCurrentState().getPageSize(); |
| } |
| |
| private int getPageStart() { |
| return getCurrentState().getPageStart(); |
| } |
| |
| /** |
| * Get the key for the specified row value. |
| * |
| * @param rowValue the row value |
| * @return the key |
| */ |
| private Object getRowValueKey(T rowValue) { |
| return (keyProvider == null || rowValue == null) ? rowValue |
| : keyProvider.getKey(rowValue); |
| } |
| |
| /** |
| * Resolve the pending state and push updates to the view. |
| */ |
| private void resolvePendingState() { |
| pendingStateCommand = null; |
| |
| // Early exit if there is no pending state. |
| if (pendingState == null) { |
| pendingStateLoop = 0; |
| return; |
| } |
| |
| /* |
| * Check for an infinite loop. This can happen if user code accessed in this |
| * method modifies the pending state and flushes changes. |
| */ |
| pendingStateLoop++; |
| if (pendingStateLoop > LOOP_MAXIMUM) { |
| pendingStateLoop = 0; // Let user code handle exception and try again. |
| throw new IllegalStateException( |
| "A possible infinite loop has been detected in a Cell Widget. This " |
| + "usually happens when your SelectionModel triggers a " |
| + "SelectionChangeEvent when SelectionModel.isSelection() is " |
| + "called, which causes the table to redraw continuously."); |
| } |
| |
| /* |
| * Check for conflicting state resolution code. This can happen if the |
| * View's render methods modify the view and flush the pending state. |
| */ |
| if (isResolvingState) { |
| throw new IllegalStateException( |
| "The Cell Widget is attempting to render itself within the render " |
| + "loop. This usually happens when your render code modifies the " |
| + "state of the Cell Widget then accesses data or elements " |
| + "within the Widget."); |
| } |
| isResolvingState = true; |
| |
| // Keep track of the absolute indexes of modified rows. |
| TreeSet<Integer> modifiedRows = new TreeSet<Integer>(); |
| |
| // Get the values used for calculations. |
| State<T> oldState = state; |
| PendingState<T> pending = pendingState; |
| int pageStart = pending.getPageStart(); |
| int pageSize = pending.getPageSize(); |
| int pageEnd = pageStart + pageSize; |
| int rowDataCount = pending.getRowDataSize(); |
| |
| /* |
| * Resolve keyboard selection. If the row value still exists, use its index. |
| * If the row value exists in multiple places, use the closest index. If the |
| * row value not longer exists, use the current index. |
| */ |
| pending.keyboardSelectedRow = Math.max(0, |
| Math.min(pending.keyboardSelectedRow, rowDataCount - 1)); |
| if (KeyboardSelectionPolicy.DISABLED == keyboardSelectionPolicy) { |
| // Clear the keyboard selected state. |
| pending.keyboardSelectedRow = 0; |
| pending.keyboardSelectedRowValue = null; |
| } else if (pending.keyboardSelectedRowChanged) { |
| // Choose the row value based on the index. |
| pending.keyboardSelectedRowValue = rowDataCount > 0 |
| ? pending.getRowDataValue(pending.keyboardSelectedRow) : null; |
| } else if (pending.keyboardSelectedRowValue != null) { |
| // Choose the index based on the row value. |
| int bestMatchIndex = findIndexOfBestMatch(pending, |
| pending.keyboardSelectedRowValue, pending.keyboardSelectedRow); |
| if (bestMatchIndex >= 0) { |
| // A match was found. |
| pending.keyboardSelectedRow = bestMatchIndex; |
| pending.keyboardSelectedRowValue = rowDataCount > 0 |
| ? pending.getRowDataValue(pending.keyboardSelectedRow) : null; |
| } else { |
| // No match was found, so reset to 0. |
| pending.keyboardSelectedRow = 0; |
| pending.keyboardSelectedRowValue = null; |
| } |
| } |
| |
| /* |
| * Update the SelectionModel based on the keyboard selected value. This must |
| * happen before we read the selection state. We only bind to selection |
| * after the user has interacted with the widget at least once. This |
| * prevents values from being selected by default. |
| */ |
| try { |
| if (KeyboardSelectionPolicy.BOUND_TO_SELECTION == keyboardSelectionPolicy |
| && selectionModel != null && pending.viewTouched) { |
| T oldValue = oldState.getSelectedValue(); |
| Object oldKey = getRowValueKey(oldValue); |
| T newValue = rowDataCount > 0 |
| ? pending.getRowDataValue(pending.getKeyboardSelectedRow()) : null; |
| Object newKey = getRowValueKey(newValue); |
| /* |
| * Do not deselect the old value unless we have a new value to select, |
| * or we will have a null selection event while we wait for asynchronous |
| * data to load. |
| */ |
| if (newKey != null && !newKey.equals(oldKey)) { |
| // Check both values for selection before setting selection, or the |
| // selection model may resolve state early. |
| boolean oldValueWasSelected = (oldValue == null) ? false |
| : selectionModel.isSelected(oldValue); |
| boolean newValueWasSelected = (newValue == null) ? false |
| : selectionModel.isSelected(newValue); |
| |
| // Deselect the old value. |
| if (oldValueWasSelected) { |
| selectionModel.setSelected(oldValue, false); |
| } |
| |
| // Select the new value. |
| pending.selectedValue = newValue; |
| if (newValue != null && !newValueWasSelected) { |
| selectionModel.setSelected(newValue, true); |
| } |
| } |
| } |
| } catch (RuntimeException e) { |
| // Unlock the rendering loop if the user SelectionModel throw an error. |
| isResolvingState = false; |
| throw e; |
| } |
| |
| // If the keyboard row changes, add it to the modified set. |
| boolean keyboardRowChanged = pending.keyboardSelectedRowChanged |
| || (oldState.getKeyboardSelectedRow() != pending.keyboardSelectedRow) |
| || (oldState.getKeyboardSelectedRowValue() == null && pending.keyboardSelectedRowValue != null); |
| |
| /* |
| * Resolve selection. Check the selection status of all row values in the |
| * pending state and compare them to the status in the old state. If we know |
| * longer have a SelectionModel but had selected rows, we still need to |
| * update the rows. |
| */ |
| for (int i = pageStart; i < pageStart + rowDataCount; i++) { |
| // Check the new selection state. |
| T rowValue = pending.getRowDataValue(i - pageStart); |
| boolean isSelected = (rowValue != null && selectionModel != null && selectionModel.isSelected(rowValue)); |
| |
| // Compare to the old selection state. |
| boolean wasSelected = oldState.isRowSelected(i); |
| if (isSelected) { |
| pending.selectedRows.add(i); |
| if (!wasSelected) { |
| modifiedRows.add(i); |
| } |
| } else if (wasSelected) { |
| modifiedRows.add(i); |
| } |
| } |
| |
| /* |
| * We called methods in user code that could modify the view, so early exit |
| * if there is a new pending state waiting to be resolved. |
| */ |
| if (pendingStateCommand != null) { |
| isResolvingState = false; |
| return; |
| } |
| pendingStateLoop = 0; |
| |
| // Swap the states. |
| state = pendingState; |
| pendingState = null; |
| |
| // Add the replaced ranges as modified rows. |
| boolean replacedEmptyRange = false; |
| for (Range replacedRange : pending.replacedRanges) { |
| int start = replacedRange.getStart(); |
| int length = replacedRange.getLength(); |
| // If the user set an empty range, pass it through to the view. |
| if (length == 0) { |
| replacedEmptyRange = true; |
| } |
| for (int i = start; i < start + length; i++) { |
| modifiedRows.add(i); |
| } |
| } |
| |
| // Add keyboard rows to modified rows if we are going to render anyway. |
| if (modifiedRows.size() > 0 && keyboardRowChanged) { |
| modifiedRows.add(oldState.getKeyboardSelectedRow()); |
| modifiedRows.add(pending.keyboardSelectedRow); |
| } |
| |
| // Calculate the modified ranges. |
| List<Range> modifiedRanges = calculateModifiedRanges(modifiedRows, |
| pageStart, pageEnd); |
| Range range0 = modifiedRanges.size() > 0 ? modifiedRanges.get(0) : null; |
| Range range1 = modifiedRanges.size() > 1 ? modifiedRanges.get(1) : null; |
| int replaceDiff = 0; // The total number of rows to replace. |
| for (Range range : modifiedRanges) { |
| replaceDiff += range.getLength(); |
| } |
| |
| /* |
| * Check the various conditions that require redraw. |
| */ |
| int oldPageStart = oldState.getPageStart(); |
| int oldPageSize = oldState.getPageSize(); |
| int oldRowDataCount = oldState.getRowDataSize(); |
| boolean redrawRequired = pending.redrawRequired; |
| if (pageStart != oldPageStart) { |
| // Redraw if pageStart changes. |
| redrawRequired = true; |
| } else if (rowDataCount < oldRowDataCount) { |
| // Redraw if we have trimmed the row data. |
| redrawRequired = true; |
| } else if (range1 == null && range0 != null |
| && range0.getStart() == pageStart |
| && (replaceDiff >= oldRowDataCount || replaceDiff > oldPageSize)) { |
| // Redraw if the new data completely overlaps the old data. |
| redrawRequired = true; |
| } else if (replaceDiff >= REDRAW_MINIMUM |
| && replaceDiff > REDRAW_THRESHOLD * oldRowDataCount) { |
| /* |
| * Redraw if the number of modified rows represents a large portion of the |
| * view, defined as greater than 30% of the rows (minimum of 5). |
| */ |
| redrawRequired = true; |
| } else if (replacedEmptyRange && oldRowDataCount == 0) { |
| /* |
| * If the user replaced an empty range, pass it to the view. This is a |
| * useful edge case that provides consistency in the way data is pushed to |
| * the view. |
| */ |
| redrawRequired = true; |
| } |
| |
| // Update the loading state in the view. |
| updateLoadingState(); |
| |
| /* |
| * Push changes to the view. |
| */ |
| try { |
| if (redrawRequired) { |
| // Redraw the entire content. |
| SafeHtmlBuilder sb = new SafeHtmlBuilder(); |
| view.render(sb, pending.rowData, pending.pageStart, selectionModel); |
| SafeHtml newContents = sb.toSafeHtml(); |
| if (!newContents.equals(lastContents)) { |
| lastContents = newContents; |
| view.replaceAllChildren(pending.rowData, newContents, |
| pending.keyboardStealFocus); |
| } |
| view.resetFocus(); |
| } else if (range0 != null) { |
| // Replace specific rows. |
| lastContents = null; |
| |
| // Replace range0. |
| { |
| int absStart = range0.getStart(); |
| int relStart = absStart - pageStart; |
| SafeHtmlBuilder sb = new SafeHtmlBuilder(); |
| List<T> replaceValues = pending.rowData.subList(relStart, relStart |
| + range0.getLength()); |
| view.render(sb, replaceValues, absStart, selectionModel); |
| view.replaceChildren(replaceValues, relStart, sb.toSafeHtml(), |
| pending.keyboardStealFocus); |
| } |
| |
| // Replace range1 if it exists. |
| if (range1 != null) { |
| int absStart = range1.getStart(); |
| int relStart = absStart - pageStart; |
| SafeHtmlBuilder sb = new SafeHtmlBuilder(); |
| List<T> replaceValues = pending.rowData.subList(relStart, relStart |
| + range1.getLength()); |
| view.render(sb, replaceValues, absStart, selectionModel); |
| view.replaceChildren(replaceValues, relStart, sb.toSafeHtml(), |
| pending.keyboardStealFocus); |
| } |
| |
| view.resetFocus(); |
| } else if (keyboardRowChanged) { |
| // Update the keyboard selected rows without redrawing. |
| // Deselect the old keyboard row. |
| int oldSelectedRow = oldState.getKeyboardSelectedRow(); |
| if (oldSelectedRow >= 0 && oldSelectedRow < rowDataCount) { |
| view.setKeyboardSelected(oldSelectedRow, false, false); |
| } |
| |
| // Select the new keyboard row. |
| int newSelectedRow = pending.getKeyboardSelectedRow(); |
| if (newSelectedRow >= 0 && newSelectedRow < rowDataCount) { |
| view.setKeyboardSelected(newSelectedRow, true, |
| pending.keyboardStealFocus); |
| } |
| } |
| } finally { |
| /* |
| * We are done resolving state, so unlock the rendering loop. We unlock |
| * the loop even if user rendering code throws an error to avoid throwing |
| * an additional, misleading IllegalStateException. |
| */ |
| isResolvingState = false; |
| } |
| } |
| |
| /** |
| * Set the visible {@link Range}, optionally clearing data and/or firing a |
| * {@link RangeChangeEvent}. |
| * |
| * @param range the new {@link Range} |
| * @param clearData true to clear all data |
| * @param forceRangeChangeEvent true to force a {@link RangeChangeEvent} |
| */ |
| private void setVisibleRange(Range range, boolean clearData, |
| boolean forceRangeChangeEvent) { |
| final int start = range.getStart(); |
| final int length = range.getLength(); |
| if (start < 0) { |
| throw new IllegalArgumentException("Range start cannot be less than 0"); |
| } |
| if (length < 0) { |
| throw new IllegalArgumentException("Range length cannot be less than 0"); |
| } |
| |
| // Update the page start. |
| final int pageStart = getPageStart(); |
| final int pageSize = getPageSize(); |
| final boolean pageStartChanged = (pageStart != start); |
| if (pageStartChanged) { |
| PendingState<T> pending = ensurePendingState(); |
| |
| // Trim the data if we aren't clearing it. |
| if (!clearData) { |
| if (start > pageStart) { |
| int increase = start - pageStart; |
| if (getVisibleItemCount() > increase) { |
| // Remove the data we no longer need. |
| for (int i = 0; i < increase; i++) { |
| pending.rowData.remove(0); |
| } |
| } else { |
| // We have no overlapping data, so just clear it. |
| pending.rowData.clear(); |
| } |
| } else { |
| int decrease = pageStart - start; |
| if ((getVisibleItemCount() > 0) && (decrease < pageSize)) { |
| // Insert null data at the beginning. |
| for (int i = 0; i < decrease; i++) { |
| pending.rowData.add(0, null); |
| } |
| |
| // Remember the inserted range because we might return to the same |
| // pageStart in this event loop, which means we won't do a full |
| // redraw, but still need to replace the inserted nulls in the view. |
| pending.replaceRange(start, start + decrease); |
| } else { |
| // We have no overlapping data, so just clear it. |
| pending.rowData.clear(); |
| } |
| } |
| } |
| |
| // Update the page start. |
| pending.pageStart = start; |
| } |
| |
| // Update the page size. |
| final boolean pageSizeChanged = (pageSize != length); |
| if (pageSizeChanged) { |
| ensurePendingState().pageSize = length; |
| } |
| |
| // Clear the data. |
| if (clearData) { |
| ensurePendingState().rowData.clear(); |
| } |
| |
| // Trim the row values if needed. |
| updateCachedData(); |
| |
| // Update the pager and data source if the range changed. |
| if (pageStartChanged || pageSizeChanged || forceRangeChangeEvent) { |
| RangeChangeEvent.fire(display, getVisibleRange()); |
| } |
| } |
| |
| /** |
| * Ensure that the cached data is consistent with the data size. |
| */ |
| private void updateCachedData() { |
| int pageStart = getPageStart(); |
| int expectedLastIndex = Math.max(0, |
| Math.min(getPageSize(), getRowCount() - pageStart)); |
| int lastIndex = getVisibleItemCount() - 1; |
| while (lastIndex >= expectedLastIndex) { |
| ensurePendingState().rowData.remove(lastIndex); |
| lastIndex--; |
| } |
| } |
| |
| /** |
| * Update the loading state of the view based on the data size and page size. |
| */ |
| private void updateLoadingState() { |
| int cacheSize = getVisibleItemCount(); |
| int curPageSize = isRowCountExact() ? getCurrentPageSize() : getPageSize(); |
| if (cacheSize >= curPageSize) { |
| view.setLoadingState(LoadingState.LOADED); |
| } else if (cacheSize == 0) { |
| view.setLoadingState(LoadingState.LOADING); |
| } else { |
| view.setLoadingState(LoadingState.PARTIALLY_LOADED); |
| } |
| } |
| } |