| /* |
| * 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.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.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.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| |
| /** |
| * <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> |
| * |
| * @param <T> the data type of items in the list |
| */ |
| class HasDataPresenter<T> implements HasData<T>, HasKeyProvider<T>, |
| HasKeyboardPagingPolicy { |
| |
| /** |
| * Default iterator over DOM elements. |
| */ |
| static class DefaultElementIterator implements ElementIterator { |
| private Element current; |
| private Element next; |
| private final View<?> view; |
| |
| public DefaultElementIterator(View<?> view, Element first) { |
| this.view = view; |
| next = first; |
| } |
| |
| public boolean hasNext() { |
| return next != null; |
| } |
| |
| public Element next() { |
| if (!hasNext()) { |
| throw new NoSuchElementException(); |
| } |
| current = next; |
| next = next.getNextSiblingElement(); |
| return current; |
| } |
| |
| public void remove() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| /** |
| * Set the selection state of the current element. |
| * |
| * @param selected the selection state |
| * @throws IllegalStateException if {@link #next()} has not been called |
| */ |
| public void setSelected(boolean selected) throws IllegalStateException { |
| if (current == null) { |
| throw new IllegalStateException(); |
| } |
| view.setSelected(current, selected); |
| } |
| } |
| |
| /** |
| * 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 loading state of the data. |
| */ |
| static enum LoadingState { |
| LOADING, // Waiting for data to load. |
| PARTIALLY_LOADED, // Partial page data loaded. |
| LOADED, // All page data loaded. |
| EMPTY; // The data size is 0. |
| } |
| |
| /** |
| * 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); |
| |
| /** |
| * Check whether or not the cells in the view depend on the selection state. |
| * |
| * @return true if cells depend on selection, false if not |
| */ |
| boolean dependsOnSelection(); |
| |
| /** |
| * Get the physical child count. |
| * |
| * @return the child count |
| */ |
| int getChildCount(); |
| |
| /** |
| * Get an iterator over the children of the view. |
| * |
| * @return the iterator |
| */ |
| ElementIterator getChildIterator(); |
| |
| /** |
| * Called when selection changes. |
| */ |
| void onUpdateSelection(); |
| |
| /** |
| * 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 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 |
| */ |
| void replaceAllChildren(List<T> values, SafeHtml html); |
| |
| /** |
| * 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 |
| * @param html the HTML to convert |
| */ |
| void replaceChildren(List<T> values, int start, SafeHtml html); |
| |
| /** |
| * 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); |
| |
| /** |
| * Update an element to reflect its selected state. |
| * |
| * @param elem the element to update |
| * @param selected true if selected, false if not |
| */ |
| void setSelected(Element elem, boolean selected); |
| } |
| |
| /** |
| * 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; |
| |
| private final HasData<T> display; |
| |
| /** |
| * The current keyboard selected row relative to page start. This value should |
| * never be negative. |
| */ |
| private int keyboardSelectedRow = 0; |
| |
| /** |
| * The last row value that was selected with the keyboard. |
| */ |
| private T keyboardSelectedRowValue; |
| |
| 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. |
| */ |
| private SafeHtml lastContents = null; |
| |
| private int pageSize; |
| private int pageStart = 0; |
| |
| /** |
| * Set to true when the page start changes, and we need to do a full refresh. |
| */ |
| private boolean pageStartChangedSinceRender; |
| |
| private int rowCount = 0; |
| |
| private boolean rowCountIsExact; |
| |
| /** |
| * The local cache of data in the view. The 0th index in the list corresponds |
| * to the value at pageStart. |
| */ |
| private final List<T> rowData = new ArrayList<T>(); |
| |
| /** |
| * A local cache of the currently selected rows. We cannot track selected keys |
| * instead because we might end up in an inconsistent state where we render a |
| * subset of a list with duplicate values, styling a value in the subset but |
| * not styling the duplicate value outside of the subset. |
| */ |
| private final Set<Integer> selectedRows = new HashSet<Integer>(); |
| |
| private HandlerRegistration selectionHandler; |
| private SelectionModel<? super T> selectionModel; |
| 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.pageSize = pageSize; |
| this.keyProvider = keyProvider; |
| } |
| |
| 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 {@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(); |
| } |
| |
| /** |
| * 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(pageSize, rowCount - pageStart); |
| } |
| |
| 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 |
| : keyboardSelectedRow; |
| } |
| |
| public KeyboardSelectionPolicy getKeyboardSelectionPolicy() { |
| return keyboardSelectionPolicy; |
| } |
| |
| public ProvidesKey<T> getKeyProvider() { |
| return keyProvider; |
| } |
| |
| /** |
| * Get the overall data size. |
| * |
| * @return the data size |
| */ |
| public int getRowCount() { |
| return rowCount; |
| } |
| |
| /** |
| * Get the list of data within the current range. The 0th index corresponds to |
| * the first value on the page. The data may not be complete or may contain |
| * null values. |
| * |
| * @return the list of data for the current page |
| */ |
| public List<T> getRowData() { |
| return rowData; |
| } |
| |
| public SelectionModel<? super T> getSelectionModel() { |
| return selectionModel; |
| } |
| |
| /** |
| * Return the range of data being displayed. |
| */ |
| public Range getVisibleRange() { |
| return new Range(pageStart, pageSize); |
| } |
| |
| /** |
| * 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 (keyboardSelectedRow < rowData.size() - 1) { |
| return true; |
| } else if (!keyboardPagingPolicy.isLimitedToRange() |
| && (keyboardSelectedRow + pageStart < rowCount - 1 || !rowCountIsExact)) { |
| 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 (keyboardSelectedRow > 0) { |
| return true; |
| } else if (!keyboardPagingPolicy.isLimitedToRange() && pageStart > 0) { |
| return true; |
| } |
| return false; |
| } |
| |
| public boolean isRowCountExact() { |
| return rowCountIsExact; |
| } |
| |
| /** |
| * Move keyboard selection to the last row. |
| */ |
| public void keyboardEnd() { |
| if (!keyboardPagingPolicy.isLimitedToRange()) { |
| setKeyboardSelectedRow(rowCount - 1, true); |
| } |
| } |
| |
| /** |
| * Move keyboard selection to the absolute 0th row. |
| */ |
| public void keyboardHome() { |
| if (!keyboardPagingPolicy.isLimitedToRange()) { |
| setKeyboardSelectedRow(-pageStart, true); |
| } |
| } |
| |
| /** |
| * Move keyboard selection to the next row. |
| */ |
| public void keyboardNext() { |
| if (hasKeyboardNext()) { |
| setKeyboardSelectedRow(keyboardSelectedRow + 1, true); |
| } |
| } |
| |
| /** |
| * Move keyboard selection to the next page. |
| */ |
| public void keyboardNextPage() { |
| if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) { |
| // 0th index of next page. |
| setKeyboardSelectedRow(pageSize, true); |
| } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) { |
| setKeyboardSelectedRow(keyboardSelectedRow + PAGE_INCREMENT, true); |
| } |
| } |
| |
| /** |
| * Move keyboard selection to the previous row. |
| */ |
| public void keyboardPrev() { |
| if (hasKeyboardPrev()) { |
| setKeyboardSelectedRow(keyboardSelectedRow - 1, true); |
| } |
| } |
| |
| /** |
| * Move keyboard selection to the previous page. |
| */ |
| public void keyboardPrevPage() { |
| if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) { |
| // 0th index of previous page. |
| setKeyboardSelectedRow(-pageSize, true); |
| } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) { |
| setKeyboardSelectedRow(keyboardSelectedRow - PAGE_INCREMENT, true); |
| } |
| } |
| |
| /** |
| * Toggle selection of the current keyboard row in the {@link SelectionModel}. |
| */ |
| public void keyboardToggleSelect() { |
| if (KeyboardSelectionPolicy.ENABLED == keyboardSelectionPolicy |
| && selectionModel != null && keyboardSelectedRow >= 0 |
| && keyboardSelectedRow < rowData.size()) { |
| T value = rowData.get(keyboardSelectedRow); |
| if (value != null) { |
| selectionModel.setSelected(value, !selectionModel.isSelected(value)); |
| } |
| } |
| } |
| |
| /** |
| * Redraw the list with the current data. |
| */ |
| public void redraw() { |
| lastContents = null; |
| setRowData(pageStart, rowData); |
| } |
| |
| 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 |
| */ |
| public void setKeyboardSelectedRow(int index, boolean stealFocus) { |
| // Early exit if disabled. |
| if (KeyboardSelectionPolicy.DISABLED == keyboardSelectionPolicy) { |
| return; |
| } |
| boolean isBound = KeyboardSelectionPolicy.BOUND_TO_SELECTION == keyboardSelectionPolicy; |
| |
| // Deselect the old index. |
| if (keyboardSelectedRow >= 0 && keyboardSelectedRow < view.getChildCount()) { |
| view.setKeyboardSelected(keyboardSelectedRow, false, false); |
| if (isBound) { |
| deselectKeyboardValue(); |
| } |
| } |
| |
| // Trim to within bounds. |
| int absIndex = pageStart + index; |
| if (absIndex < 0) { |
| absIndex = 0; |
| } else if (absIndex >= rowCount && rowCountIsExact) { |
| absIndex = rowCount - 1; |
| } |
| index = absIndex - pageStart; |
| if (keyboardPagingPolicy.isLimitedToRange()) { |
| index = Math.max(0, Math.min(index, pageSize - 1)); |
| } |
| |
| // Select the new index. |
| int newPageStart = pageStart; |
| int newPageSize = pageSize; |
| keyboardSelectedRow = 0; |
| if (index >= 0 && index < pageSize) { |
| keyboardSelectedRow = index; |
| if (isBound) { |
| selectKeyboardValue(index); |
| } |
| view.setKeyboardSelected(index, true, 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) { |
| deselectKeyboardValue(); |
| 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 == this.rowCount && isExact == this.rowCountIsExact) { |
| return; |
| } |
| this.rowCount = count; |
| this.rowCountIsExact = isExact; |
| updateLoadingState(); |
| |
| // Update the keyboardSelectedRow. |
| if (keyboardSelectedRow >= count) { |
| keyboardSelectedRow = Math.max(0, count - 1); |
| } |
| |
| // Redraw the current page if it is affected by the new data size. |
| if (updateCachedData()) { |
| redraw(); |
| } |
| |
| // Update the pager. |
| RowCountChangeEvent.fire(display, count, rowCountIsExact); |
| } |
| |
| public void setRowData(int start, List<T> values) { |
| int valuesLength = values.size(); |
| int valuesEnd = start + valuesLength; |
| |
| // Calculate the bounded start (inclusive) and end index (exclusive). |
| int pageEnd = pageStart + pageSize; |
| 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; |
| } |
| |
| // The data size must be at least as large as the data. |
| if (valuesEnd > rowCount) { |
| rowCount = valuesEnd; |
| RowCountChangeEvent.fire(display, rowCount, rowCountIsExact); |
| } |
| |
| // Create placeholders up to the specified index. |
| int cacheOffset = Math.max(0, boundedStart - pageStart - rowData.size()); |
| for (int i = 0; i < cacheOffset; i++) { |
| rowData.add(null); |
| } |
| |
| // If the keyboard selected row is within the data set, clear it out. If the |
| // key still exists, it will be reset below at its new index. |
| Object keyboardSelectedKey = null; |
| int keyboardSelectedAbsoluteRow = pageStart + keyboardSelectedRow; |
| boolean keyboardSelectedInRange = false; |
| boolean keyboardSelectedStillExists = false; |
| if (keyboardSelectedAbsoluteRow >= boundedStart |
| && keyboardSelectedAbsoluteRow < boundedEnd) { |
| keyboardSelectedInRange = true; |
| |
| // If the value is null, then we will select whatever value is at the |
| // selected row. |
| if (keyboardSelectedRowValue != null) { |
| keyboardSelectedKey = getRowValueKey(keyboardSelectedRowValue); |
| keyboardSelectedRow = 0; // Will be set to a non-negative number later. |
| } |
| } |
| |
| // 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 < rowData.size()) { |
| rowData.set(dataIndex, value); |
| } else { |
| rowData.add(value); |
| } |
| |
| // Update our local cache of selected rows. |
| if (selectionModel != null) { |
| if (value != null && selectionModel.isSelected(value)) { |
| selectedRows.add(i); |
| } else { |
| selectedRows.remove(i); |
| } |
| } |
| |
| // Update the keyboard selected index. |
| if (keyboardSelectedKey != null && value != null |
| && keyboardSelectedKey.equals(getRowValueKey(value))) { |
| keyboardSelectedRow = i - pageStart; |
| keyboardSelectedStillExists = true; |
| } |
| } |
| |
| // Construct a run of elements within the range of the data and the page. |
| // boundedStart = start index of the data to replace. |
| // boundedSize = the number of items to replace. |
| boundedStart = pageStartChangedSinceRender ? pageStart : boundedStart; |
| boundedStart -= cacheOffset; |
| List<T> boundedValues = rowData.subList(boundedStart - pageStart, |
| boundedEnd - pageStart); |
| int boundedSize = boundedValues.size(); |
| SafeHtmlBuilder sb = new SafeHtmlBuilder(); |
| view.render(sb, boundedValues, boundedStart, selectionModel); |
| |
| // Update the loading state. |
| updateLoadingState(); |
| |
| // Replace the DOM elements with the new rendered cells. |
| int childCount = view.getChildCount(); |
| if (boundedStart == pageStart |
| && (boundedSize >= childCount || boundedSize >= getCurrentPageSize() || rowData.size() < childCount)) { |
| // If the contents have not changed, we're done. |
| SafeHtml newContents = sb.toSafeHtml(); |
| if (!newContents.equals(lastContents)) { |
| lastContents = newContents; |
| view.replaceAllChildren(boundedValues, newContents); |
| } |
| |
| // Allow the view to reestablish focus after being re-rendered. |
| view.resetFocus(); |
| } else { |
| lastContents = null; |
| view.replaceChildren(boundedValues, boundedStart - pageStart, |
| sb.toSafeHtml()); |
| |
| // Only reset focus if needed. |
| if (keyboardSelectedStillExists) { |
| view.resetFocus(); |
| } |
| } |
| |
| // Reset the pageStartChanged boolean. |
| pageStartChangedSinceRender = false; |
| |
| // Update the keyboard selected value. |
| if (keyboardSelectedInRange && !keyboardSelectedStillExists) { |
| if (keyboardSelectedKey != null) { |
| // We had a value, but its lost. |
| deselectKeyboardValue(); |
| } |
| |
| // Select the selected row based off the row index. |
| if (KeyboardSelectionPolicy.BOUND_TO_SELECTION == keyboardSelectionPolicy) { |
| selectKeyboardValue(keyboardSelectedRow); |
| } |
| } |
| } |
| |
| /** |
| * @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); |
| } |
| |
| 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) { |
| updateSelection(); |
| } |
| }); |
| } |
| |
| // Update the current selection state based on the new model. |
| updateSelection(); |
| } |
| |
| /** |
| * Deselect the keyboard selected value. |
| */ |
| private void deselectKeyboardValue() { |
| if (selectionModel != null && keyboardSelectedRowValue != null) { |
| T curValue = keyboardSelectedRowValue; |
| keyboardSelectedRow = 0; |
| keyboardSelectedRowValue = null; |
| selectionModel.setSelected(curValue, false); |
| } |
| } |
| |
| /** |
| * 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 : keyProvider.getKey(rowValue); |
| } |
| |
| /** |
| * Select the value at the keyboard selected row. |
| * |
| * @param row the row index |
| */ |
| private void selectKeyboardValue(int row) { |
| if (selectionModel != null && row >= 0 && row < rowData.size()) { |
| keyboardSelectedRowValue = rowData.get(row); |
| if (keyboardSelectedRowValue != null) { |
| selectionModel.setSelected(keyboardSelectedRowValue, true); |
| } |
| } |
| } |
| |
| /** |
| * 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 (length < 0) { |
| throw new IllegalArgumentException("Range length cannot be less than 1"); |
| } |
| |
| // Update the page start. |
| final boolean pageStartChanged = (pageStart != start); |
| if (pageStartChanged) { |
| // Trim the data if we aren't clearing it. |
| if (!clearData) { |
| if (start > pageStart) { |
| int increase = start - pageStart; |
| if (rowData.size() > increase) { |
| // Remove the data we no longer need. |
| for (int i = 0; i < increase; i++) { |
| rowData.remove(0); |
| } |
| } else { |
| // We have no overlapping data, so just clear it. |
| rowData.clear(); |
| } |
| } else { |
| int decrease = pageStart - start; |
| if ((rowData.size() > 0) && (decrease < pageSize)) { |
| // Insert null data at the beginning. |
| for (int i = 0; i < decrease; i++) { |
| rowData.add(0, null); |
| } |
| } else { |
| // We have no overlapping data, so just clear it. |
| rowData.clear(); |
| } |
| } |
| } |
| |
| // Update the page start. |
| pageStart = start; |
| pageStartChangedSinceRender = true; |
| } |
| |
| // Update the page size. |
| final boolean pageSizeChanged = (pageSize != length); |
| if (pageSizeChanged) { |
| pageSize = length; |
| } |
| |
| // Clear the data. |
| if (clearData) { |
| rowData.clear(); |
| selectedRows.clear(); |
| } |
| |
| // Update the loading state. |
| updateLoadingState(); |
| |
| // Redraw with the existing data. |
| if (pageStartChanged || clearData || updateCachedData()) { |
| redraw(); |
| } |
| |
| // 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. |
| * |
| * @return true if the data was updated, false if not |
| */ |
| private boolean updateCachedData() { |
| boolean updated = false; |
| int expectedLastIndex = Math.max(0, |
| Math.min(pageSize, rowCount - pageStart)); |
| int lastIndex = rowData.size() - 1; |
| while (lastIndex >= expectedLastIndex) { |
| rowData.remove(lastIndex); |
| selectedRows.remove(lastIndex + pageStart); |
| lastIndex--; |
| updated = true; |
| } |
| return updated; |
| } |
| |
| /** |
| * Update the loading state of the view based on the data size and page size. |
| */ |
| private void updateLoadingState() { |
| int cacheSize = rowData.size(); |
| int curPageSize = isRowCountExact() ? getCurrentPageSize() : pageSize; |
| if (rowCount == 0 && rowCountIsExact) { |
| view.setLoadingState(LoadingState.EMPTY); |
| } else if (cacheSize >= curPageSize) { |
| view.setLoadingState(LoadingState.LOADED); |
| } else if (cacheSize == 0) { |
| view.setLoadingState(LoadingState.LOADING); |
| } else { |
| view.setLoadingState(LoadingState.PARTIALLY_LOADED); |
| } |
| } |
| |
| /** |
| * Update the table based on the current selection. |
| */ |
| private void updateSelection() { |
| view.onUpdateSelection(); |
| |
| // Determine if our selection states are stale. |
| boolean dependsOnSelection = view.dependsOnSelection(); |
| boolean refreshRequired = false; |
| ElementIterator children = view.getChildIterator(); |
| int row = pageStart; |
| for (T value : rowData) { |
| // Increment the child. |
| if (!children.hasNext()) { |
| break; |
| } |
| children.next(); |
| |
| // Update the selection state. |
| boolean selected = selectionModel == null ? false |
| : selectionModel.isSelected(value); |
| if (selected != selectedRows.contains(row)) { |
| refreshRequired = true; |
| if (selected) { |
| selectedRows.add(row); |
| } else { |
| selectedRows.remove(row); |
| } |
| if (!dependsOnSelection) { |
| // The cell doesn't depend on selection, so we only need to update |
| // the style. |
| children.setSelected(selected); |
| } |
| } |
| row++; |
| } |
| |
| // Redraw the entire list if needed. |
| if (refreshRequired && dependsOnSelection) { |
| redraw(); |
| } |
| } |
| } |