Refactoring HasDataPresenter to save pending changes in a PendingState, pushing them to the view in a finally command at the end of the event loop. This change reduces the number of times that the view is updated in the same event loop and eliminates some special case logic designed to deal with concurrent updates to the view. The pattern is fairly straightforward. When we receive new data, a new row count, a change in selection, or a change in the keyboard selected row, we update the pending state. In a finally command, we resolve the pending state by comparing it to the current state. If none of the rows changed, we just update the keyboard selected row using DOM manipulation. If less than 30% of the rows changed, we replace the changed rows. If more than 30% of the rows changed, we redraw the entire table.
This change also fixes some bugs. CellTree now replaces the correct data when a range is replaced. Previously, CellTree assumed that the new data always started at index 0. CellBrowser now uses the keyboardSelectedRow index to determine which row is open in each list in the CellBrowser. This removes a lot of logic from CellBrowser and fixes a UI bug where a row would appear hovered (yellow) before becoming open (gray). It also reduces the number of times we render each list in the CellBrowser. Mouse support has been fixed in CellTable using similar code to what already existed in CellTree. Clicking on a natively focusable element in CellTable no longer steals focus out to the Cell.
Sorry about the big patch, but these changes are interrelated. I found a few of the bugs mentioned while working on the patch, and they have also been reported by users.
Review at http://gwt-code-reviews.appspot.com/1078801
Review by: sbrubaker@google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9203 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java b/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
index 79cf0a4..234e6bd 100644
--- a/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
+++ b/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
@@ -28,7 +28,6 @@
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.HasDataPresenter.ElementIterator;
import com.google.gwt.user.cellview.client.HasDataPresenter.LoadingState;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
@@ -75,30 +74,15 @@
return hasData.addHandler(handler, type);
}
- public boolean dependsOnSelection() {
- return hasData.dependsOnSelection();
- }
-
- public int getChildCount() {
- return hasData.getChildCount();
- }
-
- public ElementIterator getChildIterator() {
- return new HasDataPresenter.DefaultElementIterator(this,
- hasData.getChildContainer().getFirstChildElement());
- }
-
- public void onUpdateSelection() {
- hasData.onUpdateSelection();
- }
-
public void render(SafeHtmlBuilder sb, List<T> values, int start,
SelectionModel<? super T> selectionModel) {
hasData.renderRowValues(sb, values, start, selectionModel);
}
- public void replaceAllChildren(List<T> values, SafeHtml html) {
+ public void replaceAllChildren(List<T> values, SafeHtml html,
+ boolean stealFocus) {
// Removing elements can fire a blur event, which we ignore.
+ hasData.isFocused = hasData.isFocused || stealFocus;
wasFocused = hasData.isFocused;
hasData.isRefreshing = true;
hasData.replaceAllChildren(values, html);
@@ -106,8 +90,10 @@
fireValueChangeEvent();
}
- public void replaceChildren(List<T> values, int start, SafeHtml html) {
+ public void replaceChildren(List<T> values, int start, SafeHtml html,
+ boolean stealFocus) {
// Removing elements can fire a blur event, which we ignore.
+ hasData.isFocused = hasData.isFocused || stealFocus;
wasFocused = hasData.isFocused;
hasData.isRefreshing = true;
hasData.replaceChildren(values, start, html);
@@ -132,6 +118,7 @@
public void setKeyboardSelected(int index, boolean seleted,
boolean stealFocus) {
+ hasData.isFocused = hasData.isFocused || stealFocus;
hasData.setKeyboardSelected(index, seleted, stealFocus);
}
@@ -141,10 +128,6 @@
hasData.isRefreshing = false;
}
- public void setSelected(Element elem, boolean selected) {
- hasData.setSelected(elem, selected);
- }
-
/**
* Fire a value change event.
*/
@@ -321,7 +304,7 @@
*/
public T getDisplayedItem(int indexOnPage) {
checkRowBounds(indexOnPage);
- return presenter.getRowData().get(indexOnPage);
+ return presenter.getRowDataValue(indexOnPage);
}
/**
@@ -330,7 +313,12 @@
* @return a List of displayed items
*/
public List<T> getDisplayedItems() {
- return new ArrayList<T>(presenter.getRowData());
+ List<T> list = new ArrayList<T>();
+ int rowCount = presenter.getRowDataSize();
+ for (int i = 0; i < rowCount; i++) {
+ list.add(presenter.getRowDataValue(i));
+ }
+ return list;
}
public KeyboardPagingPolicy getKeyboardPagingPolicy() {
@@ -376,6 +364,7 @@
* @return the {@link Element} that contains the rendered row values
*/
public Element getRowContainer() {
+ presenter.flush();
return getChildContainer();
}
@@ -664,7 +653,8 @@
*/
protected Object getValueKey(T value) {
ProvidesKey<T> keyProvider = getKeyProvider();
- return keyProvider == null ? value : keyProvider.getKey(value);
+ return (keyProvider == null || value == null) ? value
+ : keyProvider.getKey(value);
}
/**
@@ -676,14 +666,13 @@
protected abstract boolean isKeyboardNavigationSuppressed();
/**
- * Checks that the row is within the correct bounds.
+ * Checks that the row is within bounds of the view.
*
* @param row row index to check
* @return true if within bounds, false if not
*/
protected boolean isRowWithinBounds(int row) {
- return row >= 0 && row < getChildCount()
- && row < presenter.getRowData().size();
+ return row >= 0 && row < presenter.getRowDataSize();
}
/**
@@ -714,7 +703,12 @@
/**
* Called when selection changes.
+ *
+ * @deprecated this method is never called by AbstractHasData, render the
+ * selected styles in
+ * {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
*/
+ @Deprecated
protected void onUpdateSelection() {
}
@@ -723,7 +717,7 @@
*
* @param sb the {@link SafeHtmlBuilder} to render into
* @param values the row values
- * @param start the start index of the values
+ * @param start the absolute start index of the values
* @param selectionModel the {@link SelectionModel}
*/
protected abstract void renderRowValues(SafeHtmlBuilder sb, List<T> values,
@@ -746,7 +740,7 @@
* should be appended.
*
* @param values the values of the new children
- * @param start the start index to be replaced
+ * @param start the start index to be replaced, relative to the page start
* @param html the HTML to convert
*/
protected void replaceChildren(List<T> values, int start, SafeHtml html) {
@@ -799,8 +793,14 @@
*
* @param elem the element to update
* @param selected true if selected, false if not
+ * @deprecated this method is never called by AbstractHasData, render the
+ * selected styles in
+ * {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
*/
- protected abstract void setSelected(Element elem, boolean selected);
+ @Deprecated
+ protected void setSelected(Element elem, boolean selected) {
+ // Never called.
+ }
/**
* Add a {@link ValueChangeHandler} that is called when the display values
@@ -815,47 +815,11 @@
return addHandler(handler, ValueChangeEvent.getType());
}
- /**
- * Return the number of child elements.
- */
- int getChildCount() {
- return getChildContainer().getChildCount();
- }
-
HasDataPresenter<T> getPresenter() {
return presenter;
}
/**
- * Get the first index of a displayed item according to its key.
- *
- * @param value the value
- * @return the index of the item, or -1 of not found
- */
- int indexOf(T value) {
- ProvidesKey<T> keyProvider = getKeyProvider();
- List<T> items = getDisplayedItems();
-
- // If no key provider is present, just compare the objets.
- if (keyProvider == null) {
- return items.indexOf(value);
- }
-
- // Compare the keys of each object.
- Object key = keyProvider.getKey(value);
- if (key == null) {
- return -1;
- }
- int itemCount = items.size();
- for (int i = 0; i < itemCount; i++) {
- if (key.equals(keyProvider.getKey(items.get(i)))) {
- return i;
- }
- }
- return -1;
- }
-
- /**
* Set the current loading state of the data.
*
* @param state the loading state
diff --git a/user/src/com/google/gwt/user/cellview/client/CellBasedWidgetImpl.java b/user/src/com/google/gwt/user/cellview/client/CellBasedWidgetImpl.java
index d8dde3a..2e179f8 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellBasedWidgetImpl.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellBasedWidgetImpl.java
@@ -17,10 +17,12 @@
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Element;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.Widget;
+import java.util.HashSet;
import java.util.Set;
/**
@@ -45,7 +47,31 @@
return impl;
}
+ /**
+ * The set of natively focusable elements.
+ */
+ private final Set<String> focusableTypes;
+
CellBasedWidgetImpl() {
+ focusableTypes = new HashSet<String>();
+ focusableTypes.add("select");
+ focusableTypes.add("input");
+ focusableTypes.add("textarea");
+ focusableTypes.add("option");
+ focusableTypes.add("button");
+ focusableTypes.add("label");
+ }
+
+ /**
+ * Check if an element is focusable. If an element is focusable, the cell
+ * widget should not steal focus from it.
+ *
+ * @param elem the element
+ * @return
+ */
+ public boolean isFocusable(Element elem) {
+ return focusableTypes.contains(elem.getTagName().toLowerCase())
+ || elem.getTabIndex() >= 0;
}
/**
diff --git a/user/src/com/google/gwt/user/cellview/client/CellBrowser.java b/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
index 8dcd61d..2ef939b 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
@@ -41,6 +41,7 @@
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
@@ -208,15 +209,20 @@
private Object focusedKey;
/**
- * The value of the currently focused item.
+ * A boolean indicating that this widget is no longer used.
*/
- private T focusedValue;
+ private boolean isDestroyed;
/**
* Indicates whether or not the focused value is open.
*/
private boolean isFocusedOpen;
+ /**
+ * Temporary element used to create elements from HTML.
+ */
+ private final Element tmpElem = Document.get().createDivElement();
+
public BrowserCellList(final Cell<T> cell, int level,
ProvidesKey<T> keyProvider) {
super(cell, cellListResources, keyProvider);
@@ -229,6 +235,17 @@
}
@Override
+ protected boolean isKeyboardNavigationSuppressed() {
+ /*
+ * Keyboard selection is never disabled in this list because we use it to
+ * track the open node, but we want to suppress keyboard navigation if the
+ * user disables it.
+ */
+ return KeyboardSelectionPolicy.DISABLED == CellBrowser.this.getKeyboardSelectionPolicy()
+ || super.isKeyboardNavigationSuppressed();
+ }
+
+ @Override
protected void onBrowserEvent2(Event event) {
super.onBrowserEvent2(event);
@@ -270,11 +287,9 @@
int end = start + length;
for (int i = start; i < end; i++) {
T value = values.get(i - start);
- Object key = getValueKey(value);
boolean isSelected = selectionModel == null ? false
: selectionModel.isSelected(value);
- boolean isOpen = (focusedKey == null || !isFocusedOpen) ? false
- : focusedKey.equals(key);
+ boolean isOpen = isOpen(i);
StringBuilder classesBuilder = new StringBuilder();
classesBuilder.append(i % 2 == 0 ? evenItem : oddItem);
if (isOpen) {
@@ -285,7 +300,7 @@
}
SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
- cell.render(value, null, cellBuilder);
+ cell.render(value, getValueKey(value), cellBuilder);
// Figure out which image to use.
SafeHtml image;
@@ -316,15 +331,54 @@
image, cellBuilder.toSafeHtml()));
}
}
+
+ // Update the child state.
+ updateChildState(this, true);
}
@Override
- void doKeyboardSelection(Event event, T value, int indexOnPage) {
- super.doKeyboardSelection(event, value, indexOnPage);
+ protected void setKeyboardSelected(int index, boolean selected,
+ boolean stealFocus) {
+ super.setKeyboardSelected(index, selected, stealFocus);
+ if (!isRowWithinBounds(index)) {
+ return;
+ }
- // Open the selected row. If keyboard selection updates the selection
- // model, this is a no-op.
- setChildState(this, value, true, true, true);
+ // Update the style.
+ Element elem = getRowElement(index);
+ T value = getPresenter().getRowDataValue(index);
+ boolean isOpen = selected && isOpen(index);
+ setStyleName(elem, style.cellBrowserOpenItem(), isOpen);
+
+ // Update the image.
+ SafeHtml image = null;
+ if (isOpen) {
+ image = openImageHtml;
+ } else if (getTreeViewModel().isLeaf(value)) {
+ image = LEAF_IMAGE;
+ } else {
+ image = closedImageHtml;
+ }
+ tmpElem.setInnerHTML(image.asString());
+ elem.replaceChild(tmpElem.getFirstChildElement(),
+ elem.getFirstChildElement());
+
+ // Update the open state.
+ updateChildState(this, true);
+ }
+
+ /**
+ * Check if the specified index is currently open. An index is open if it is
+ * the keyboard selected index, there is an associated keyboard selected
+ * value, and the value is not a leaf.
+ *
+ * @param index the index
+ * @return true if open, false if not
+ */
+ private boolean isOpen(int index) {
+ T value = getPresenter().getKeyboardSelectedRowValue();
+ return index == getKeyboardSelectedRow() && value != null
+ && !getTreeViewModel().isLeaf(value);
}
/**
@@ -338,14 +392,8 @@
// Move to the child node.
if (level < treeNodes.size() - 1) {
TreeNodeImpl<?> treeNode = treeNodes.get(level + 1);
- treeNode.display.setFocus(true);
-
- // Select the element.
- int selected = getKeyboardSelectedRow();
- if (isRowWithinBounds(selected)) {
- T value = getDisplayedItem(selected);
- setChildState(this, value, true, true, true);
- }
+ treeNode.display.getPresenter().setKeyboardSelectedRow(
+ treeNode.display.getKeyboardSelectedRow(), true);
}
}
@@ -415,7 +463,7 @@
public int getChildCount() {
assertNotDestroyed();
- return display.getChildCount();
+ return display.getPresenter().getRowDataSize();
}
public C getChildValue(int index) {
@@ -432,7 +480,7 @@
public TreeNodeImpl<?> getParent() {
assertNotDestroyed();
- return (display.level == 0) ? null : treeNodes.get(display.level - 1);
+ return getParentImpl();
}
public Object getValue() {
@@ -454,6 +502,16 @@
}
public boolean isDestroyed() {
+ if (nodeInfo != null) {
+ /*
+ * Flush the parent display because the user may have replaced this
+ * node, which would destroy it.
+ */
+ TreeNodeImpl<?> parent = getParentImpl();
+ if (parent != null && !parent.isDestroyed()) {
+ parent.display.getPresenter().flush();
+ }
+ }
return nodeInfo == null;
}
@@ -464,8 +522,22 @@
public TreeNode setChildOpen(int index, boolean open, boolean fireEvents) {
assertNotDestroyed();
checkChildBounds(index);
- return setChildState(display, getChildValue(index), open, fireEvents,
- true);
+ if (open) {
+ // Open the child node.
+ display.getPresenter().setKeyboardSelectedRow(index, false);
+ return updateChildState(display, fireEvents);
+ } else {
+ // Close the child node if it is currently open.
+ if (index == display.getKeyboardSelectedRow()) {
+ display.getPresenter().clearKeyboardSelectedRowValue();
+ updateChildState(display, fireEvents);
+ }
+ return null;
+ }
+ }
+
+ BrowserCellList<C> getDisplay() {
+ return display;
}
/**
@@ -507,6 +579,7 @@
* Unregister the list view and remove it from the widget.
*/
private void destroy() {
+ display.isDestroyed = true;
valueChangeHandler.removeHandler();
display.setSelectionModel(null);
nodeInfo.unsetDataDisplay();
@@ -520,8 +593,16 @@
* @return the index of the open item, or -1 if not found
*/
private int getOpenIndex() {
- return display.isFocusedOpen ? display.indexOf(display.focusedValue)
- : null;
+ return display.isFocusedOpen ? display.getKeyboardSelectedRow() : -1;
+ }
+
+ /**
+ * Get the parent node without checking if this node is destroyed.
+ *
+ * @return the parent node, or null if the node has no parent
+ */
+ private TreeNodeImpl<?> getParentImpl() {
+ return (display.level == 0) ? null : treeNodes.get(display.level - 1);
}
}
@@ -832,6 +913,12 @@
@Override
public void setKeyboardSelectionPolicy(KeyboardSelectionPolicy policy) {
super.setKeyboardSelectionPolicy(policy);
+
+ /*
+ * Set the policy on all lists. We use keyboard selection to track the open
+ * node, so we never actually disable keyboard selection on the lists.
+ */
+ policy = getKeyboardSelectionPolicyForLists();
for (TreeNodeImpl<?> treeNode : treeNodes) {
treeNode.display.setKeyboardSelectionPolicy(policy);
}
@@ -883,7 +970,7 @@
* @param nodeInfo the info about the node
* @param value the value of the open node
*/
- private <C> void appendTreeNode(final NodeInfo<C> nodeInfo, Object value) {
+ private <C> TreeNode appendTreeNode(final NodeInfo<C> nodeInfo, Object value) {
// Create the list view.
final int level = treeNodes.size();
final BrowserCellList<C> view = createDisplay(nodeInfo, level);
@@ -924,6 +1011,7 @@
// Scroll to the right.
animation.scrollToEnd();
+ return treeNode;
}
/**
@@ -939,7 +1027,10 @@
BrowserCellList<C> display = new BrowserCellList<C>(nodeInfo.getCell(),
level, nodeInfo.getProvidesKey());
display.setValueUpdater(nodeInfo.getValueUpdater());
- display.setKeyboardSelectionPolicy(getKeyboardSelectionPolicy());
+
+ // Set the keyboard selection policy, but never disable it.
+ KeyboardSelectionPolicy keyboardPolicy = getKeyboardSelectionPolicyForLists();
+ display.setKeyboardSelectionPolicy(keyboardPolicy);
return display;
}
@@ -958,8 +1049,21 @@
}
/**
+ * Get the {@link KeyboardSelectionPolicy} to apply to lists. We use keyboard
+ * selection to track the open node, so we never actually disable keyboard
+ * selection on the lists.
+ *
+ * @return the {@link KeyboardSelectionPolicy} to use on lists
+ */
+ private KeyboardSelectionPolicy getKeyboardSelectionPolicyForLists() {
+ KeyboardSelectionPolicy policy = getKeyboardSelectionPolicy();
+ return KeyboardSelectionPolicy.DISABLED == policy
+ ? KeyboardSelectionPolicy.ENABLED : policy;
+ }
+
+ /**
* Get the {@link SplitLayoutPanel} used to lay out the views.
- *
+ *
* @return the {@link SplitLayoutPanel}
*/
private SplitLayoutPanel getSplitLayoutPanel() {
@@ -967,86 +1071,6 @@
}
/**
- * Set the open state of a tree node.
- *
- * @param cellList the CellList that changed state.
- * @param value the value to open
- * @param open true to open, false to close
- * @param fireEvents true to fireEvents
- * @return the open {@link TreeNode}, or null if not opened
- */
- private <C> TreeNode setChildState(BrowserCellList<C> cellList, C value,
- boolean open, boolean fireEvents, boolean redraw) {
-
- // Get the key of the value to open.
- Object newKey = cellList.getValueKey(value);
-
- if (open) {
- if (newKey == null) {
- // Early exit if opening but the specified node has no key.
- return null;
- } else if (newKey.equals(cellList.focusedKey)) {
- // Early exit if opening but the specified node is already open.
- return cellList.isFocusedOpen ? treeNodes.get(cellList.level + 1)
- : null;
- }
-
- // Close the currently open node.
- if (cellList.focusedKey != null) {
- setChildState(cellList, cellList.focusedValue, false, fireEvents, false);
- }
-
- // Update the cell so it renders the styles correctly.
- cellList.focusedValue = value;
- cellList.focusedKey = cellList.getValueKey(value);
-
- // Add the child node.
- NodeInfo<?> childNodeInfo = isLeaf(value) ? null : getNodeInfo(value);
- if (childNodeInfo != null) {
- cellList.isFocusedOpen = true;
- appendTreeNode(childNodeInfo, value);
- } else {
- cellList.isFocusedOpen = false;
- }
-
- // Refresh the display to update the styles for this node.
- if (redraw) {
- treeNodes.get(cellList.level).display.redraw();
- }
-
- if (cellList.isFocusedOpen) {
- TreeNodeImpl<?> node = treeNodes.get(cellList.level + 1);
- if (fireEvents) {
- OpenEvent.fire(this, node);
- }
- return node.isDestroyed() ? null : node;
- }
- return null;
- } else {
- // Early exit if closing and the specified node or all nodes are closed.
- if (cellList.focusedKey == null || !cellList.focusedKey.equals(newKey)) {
- return null;
- }
-
- // Close the node.
- TreeNode closedNode = (cellList.isFocusedOpen && (treeNodes.size() > cellList.level + 1))
- ? treeNodes.get(cellList.level + 1) : null;
- trimToLevel(cellList.level);
-
- // Refresh the display to update the styles for this node.
- if (redraw) {
- treeNodes.get(cellList.level).display.redraw();
- }
-
- if (fireEvents && closedNode != null) {
- CloseEvent.fire(this, closedNode);
- }
- }
-
- return null;
- }
-
- /**
* Reduce the number of {@link HasData}s down to the specified level.
*
* @param level the level to trim to
@@ -1067,8 +1091,83 @@
if (level < treeNodes.size()) {
TreeNodeImpl<?> node = treeNodes.get(level);
node.display.focusedKey = null;
- node.display.focusedValue = null;
node.display.isFocusedOpen = false;
}
}
+
+ /**
+ * Update the state of a child node based on the keyboard selection of the
+ * specified {@link BrowserCellList}. This method will open/close child
+ * {@link TreeNode}s as needed.
+ *
+ * @param cellList the CellList that changed state.
+ * @param value the value to open
+ * @param open true to open, false to close
+ * @param fireEvents true to fireEvents
+ * @return the open {@link TreeNode}, or null if not opened
+ */
+ private <C> TreeNode updateChildState(BrowserCellList<C> cellList,
+ boolean fireEvents) {
+ /*
+ * Verify that the specified list is still in the browser. It possible for
+ * the list to receive deferred updates after it has been removed
+ */
+ if (cellList.isDestroyed) {
+ return null;
+ }
+
+ // Get the key of the value to open.
+ C newValue = cellList.getPresenter().getKeyboardSelectedRowValue();
+ Object newKey = cellList.getValueKey(newValue);
+
+ // Close the current open node.
+ TreeNode closedNode = null;
+ if (cellList.focusedKey != null && cellList.isFocusedOpen
+ && !cellList.focusedKey.equals(newKey)) {
+ // Get the node to close.
+ closedNode = (treeNodes.size() > cellList.level + 1)
+ ? treeNodes.get(cellList.level + 1) : null;
+
+ // Close the node.
+ trimToLevel(cellList.level);
+ }
+
+ // Open the new node.
+ TreeNode openNode = null;
+ boolean justOpenedNode = false;
+ if (newKey != null) {
+ if (newKey.equals(cellList.focusedKey)) {
+ // The node is already open.
+ openNode = cellList.isFocusedOpen ? treeNodes.get(cellList.level + 1)
+ : null;
+ } else {
+ // Add the child node.
+ cellList.focusedKey = newKey;
+ NodeInfo<?> childNodeInfo = isLeaf(newValue) ? null
+ : getNodeInfo(newValue);
+ if (childNodeInfo != null) {
+ cellList.isFocusedOpen = true;
+ justOpenedNode = true;
+ openNode = appendTreeNode(childNodeInfo, newValue);
+ }
+ }
+ }
+
+ /*
+ * Fire event. We fire events after updating the view in case user event
+ * handlers modify the open state of nodes, which would interrupt the
+ * process.
+ */
+ if (fireEvents) {
+ if (closedNode != null) {
+ CloseEvent.fire(this, closedNode);
+ }
+ if (openNode != null && justOpenedNode) {
+ OpenEvent.fire(this, openNode);
+ }
+ }
+
+ // Return the open node if it is still open.
+ return (openNode == null || openNode.isDestroyed()) ? null : openNode;
+ }
}
diff --git a/user/src/com/google/gwt/user/cellview/client/CellList.java b/user/src/com/google/gwt/user/cellview/client/CellList.java
index 2906315..37f7c8c 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellList.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellList.java
@@ -239,6 +239,7 @@
* page
*/
public Element getRowElement(int indexOnPage) {
+ getPresenter().flush();
checkRowBounds(indexOnPage);
if (childContainer.getChildCount() > indexOnPage) {
return childContainer.getChild(indexOnPage).cast();
@@ -335,8 +336,12 @@
@Override
protected Element getKeyboardSelectedElement() {
+ // Do not use getRowElement() because that will flush the presenter.
int rowIndex = getKeyboardSelectedRow();
- return isRowWithinBounds(rowIndex) ? getRowElement(rowIndex) : null;
+ if (childContainer.getChildCount() > rowIndex) {
+ return childContainer.getChild(rowIndex).cast();
+ }
+ return null;
}
@Override
@@ -360,13 +365,14 @@
if (!Element.is(eventTarget)) {
return;
}
- Element target = event.getEventTarget().cast();
+ final Element target = event.getEventTarget().cast();
// Forward the event to the cell.
String idxString = "";
- while ((target != null)
- && ((idxString = target.getAttribute("__idx")).length() == 0)) {
- target = target.getParentElement();
+ Element cellTarget = target;
+ while ((cellTarget != null)
+ && ((idxString = cellTarget.getAttribute("__idx")).length() == 0)) {
+ cellTarget = cellTarget.getParentElement();
}
if (idxString.length() > 0) {
// Select the item if the cell does not consume events. Selection occurs
@@ -382,16 +388,22 @@
}
// Get the cell parent before doing selection in case the list is redrawn.
- Element cellParent = getCellParent(target);
+ Element cellParent = getCellParent(cellTarget);
T value = getDisplayedItem(indexOnPage);
if (isMouseDown && !cell.handlesSelection()) {
doSelection(event, value, indexOnPage);
}
// Focus on the cell.
- if ("focus".equals(eventType) || isMouseDown) {
- isFocused = true;
- doKeyboardSelection(event, value, indexOnPage);
+ if (isMouseDown
+ && getPresenter().getKeyboardSelectedRowInView() != indexOnPage) {
+ /*
+ * If the selected element is natively focusable, then we do not want to
+ * steal focus away from it.
+ */
+ boolean isFocusable = CellBasedWidgetImpl.get().isFocusable(target);
+ isFocused = isFocused || isFocusable;
+ getPresenter().setKeyboardSelectedRow(indexOnPage, !isFocusable);
}
// Fire the event to the cell if the list has not been refreshed.
@@ -483,24 +495,17 @@
}
}
+ /**
+ * @deprecated this method is never called by AbstractHasData, render the
+ * selected styles in
+ * {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
+ */
@Override
+ @Deprecated
protected void setSelected(Element elem, boolean selected) {
setStyleName(elem, style.cellListSelectedItem(), selected);
}
- /**
- * Called when the user selects a cell with the mouse or tab key.
- *
- * @param event the event
- * @param value the value that is selected
- * @param indexOnPage the index on the page
- */
- void doKeyboardSelection(Event event, T value, int indexOnPage) {
- if (getPresenter().getKeyboardSelectedRow() != indexOnPage) {
- getPresenter().setKeyboardSelectedRow(indexOnPage, false);
- }
- }
-
@Override
void setLoadingState(LoadingState state) {
showOrHide(emptyMessageElem, state == LoadingState.EMPTY);
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTable.java b/user/src/com/google/gwt/user/cellview/client/CellTable.java
index 33e05e3..305c502 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTable.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTable.java
@@ -459,29 +459,6 @@
private int keyboardSelectedColumn = 0;
- /**
- * Indicates whether or not the scheduled redraw has been canceled.
- */
- private boolean redrawCancelled;
-
- /**
- * The command used to redraw the table after adding columns.
- */
- private final Scheduler.ScheduledCommand redrawCommand = new Scheduler.ScheduledCommand() {
- public void execute() {
- redrawScheduled = false;
- if (redrawCancelled) {
- redrawCancelled = false;
- return;
- }
- redraw();
- }
- };
-
- /**
- * Indicates whether or not a redraw is scheduled.
- */
- private boolean redrawScheduled;
private RowStyles<T> rowStyles;
private final Style style;
private final TableElement table;
@@ -653,7 +630,7 @@
}
CellBasedWidgetImpl.get().sinkEvents(this, consumedEvents);
- scheduleRedraw();
+ redraw();
}
/**
@@ -701,7 +678,7 @@
addColumn(col, new SafeHtmlHeader(headerHtml), new SafeHtmlHeader(
footerHtml));
}
-
+
/**
* Add a style name to the {@link TableColElement} at the specified index,
* creating it if necessary.
@@ -743,6 +720,7 @@
* current page
*/
public TableRowElement getRowElement(int row) {
+ getPresenter().flush();
checkRowBounds(row);
NodeList<TableRowElement> rows = tbody.getRows();
return rows.getLength() > row ? rows.getItem(row) : null;
@@ -805,7 +783,7 @@
}
// Redraw the table asynchronously.
- scheduleRedraw();
+ redraw();
// We don't unsink events because other handlers or user code may have sunk
// them intentionally.
@@ -867,9 +845,11 @@
@Override
protected Element getKeyboardSelectedElement() {
+ // Do not use getRowElement() because that will flush the presenter.
int rowIndex = getKeyboardSelectedRow();
- if (isRowWithinBounds(rowIndex) && columns.size() > 0) {
- TableRowElement tr = getRowElement(rowIndex);
+ NodeList<TableRowElement> rows = tbody.getRows();
+ if (rowIndex < rows.getLength() && columns.size() > 0) {
+ TableRowElement tr = rows.getItem(rowIndex);
TableCellElement td = tr.getCells().getItem(keyboardSelectedColumn);
return getCellParent(td);
}
@@ -900,7 +880,7 @@
if (!Element.is(eventTarget)) {
return;
}
- Element target = event.getEventTarget().cast();
+ final Element target = event.getEventTarget().cast();
// Ignore keydown events unless the cell is in edit mode
String eventType = event.getType();
@@ -937,13 +917,11 @@
if (section == thead) {
Header<?> header = headers.get(col);
if (header != null && cellConsumesEventType(header.getCell(), eventType)) {
-
header.onBrowserEvent(tableCell, event);
}
} else if (section == tfoot) {
Header<?> footer = footers.get(col);
if (footer != null && cellConsumesEventType(footer.getCell(), eventType)) {
-
footer.onBrowserEvent(tableCell, event);
}
} else if (section == tbody) {
@@ -963,16 +941,15 @@
setRowStyleName(hoveringRow, style.cellTableHoveredRow(),
style.cellTableHoveredRowCell(), false);
hoveringRow = null;
- } else if ("focus".equals(eventType) || isMouseDown) {
+ } else if (isMouseDown
+ && ((getPresenter().getKeyboardSelectedRowInView() != row)
+ || (keyboardSelectedColumn != col))) {
// Move keyboard focus. Since the user clicked, allow focus to go to a
// non-interactive column.
- isFocused = true;
- if (getPresenter().getKeyboardSelectedRow() != row
- || keyboardSelectedColumn != col) {
- deselectKeyboardRow(getKeyboardSelectedRow());
- keyboardSelectedColumn = col;
- getPresenter().setKeyboardSelectedRow(row, false);
- }
+ boolean isFocusable = CellBasedWidgetImpl.get().isFocusable(target);
+ isFocused = isFocused || isFocusable;
+ keyboardSelectedColumn = col;
+ getPresenter().setKeyboardSelectedRow(row, !isFocusable);
}
// Update selection. Selection occurs before firing the event to the cell
@@ -1004,25 +981,6 @@
}
@Override
- protected void onUpdateSelection() {
- // Refresh headers.
- for (Header<?> header : headers) {
- if (header != null && header.getCell().dependsOnSelection()) {
- createHeaders(false);
- break;
- }
- }
-
- // Refresh footers.
- for (Header<?> footer : footers) {
- if (footer != null && footer.getCell().dependsOnSelection()) {
- createHeaders(true);
- break;
- }
- }
- }
-
- @Override
protected void renderRowValues(SafeHtmlBuilder sb, List<T> values, int start,
SelectionModel<? super T> selectionModel) {
createHeadersAndFooters();
@@ -1117,10 +1075,6 @@
@Override
protected void replaceAllChildren(List<T> values, SafeHtml html) {
- // Cancel any pending redraw.
- if (redrawScheduled) {
- redrawCancelled = true;
- }
TABLE_IMPL.replaceAllRows(CellTable.this, tbody,
CellBasedWidgetImpl.get().processHtml(html));
}
@@ -1144,15 +1098,27 @@
}
TableRowElement tr = getRowElement(index);
- TableCellElement td = tr.getCells().getItem(keyboardSelectedColumn);
- final com.google.gwt.user.client.Element cellParent = getCellParent(td).cast();
- if (!selected || isFocused || stealFocus) {
- setRowStyleName(tr, style.cellTableKeyboardSelectedRow(),
- style.cellTableKeyboardSelectedRowCell(), selected);
- setStyleName(td, style.cellTableKeyboardSelectedCell(), selected);
+ String cellStyle = style.cellTableKeyboardSelectedCell();
+ boolean updatedSelection = !selected || isFocused || stealFocus;
+ setRowStyleName(tr, style.cellTableKeyboardSelectedRow(),
+ style.cellTableKeyboardSelectedRowCell(), selected);
+ NodeList<TableCellElement> cells = tr.getCells();
+ for (int i = 0; i < cells.getLength(); i++) {
+ TableCellElement td = cells.getItem(i);
+
+ // Update the selected style.
+ setStyleName(td, cellStyle, updatedSelection && selected
+ && i == keyboardSelectedColumn);
+
+ // Mark as focusable.
+ final com.google.gwt.user.client.Element cellParent = getCellParent(td).cast();
+ setFocusable(cellParent, selected && i == keyboardSelectedColumn);
}
- setFocusable(cellParent, selected);
+
+ // Move focus to the cell.
if (selected && stealFocus) {
+ TableCellElement td = tr.getCells().getItem(keyboardSelectedColumn);
+ final com.google.gwt.user.client.Element cellParent = getCellParent(td).cast();
CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
public void execute() {
cellParent.focus();
@@ -1161,7 +1127,13 @@
}
}
+ /**
+ * @deprecated this method is never called by AbstractHasData, render the
+ * selected styles in
+ * {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
+ */
@Override
+ @Deprecated
protected void setSelected(Element elem, boolean selected) {
TableRowElement tr = elem.cast();
setRowStyleName(tr, style.cellTableSelectedRow(),
@@ -1249,10 +1221,6 @@
createHeaders(true);
}
- private void deselectKeyboardRow(int row) {
- setKeyboardSelected(row, false, false);
- }
-
/**
* Get the {@link TableColElement} at the specified index, creating it if
* necessary.
@@ -1380,7 +1348,6 @@
if (nextColumn <= keyboardSelectedColumn) {
// Wrap to the next row.
if (presenter.hasKeyboardNext()) {
- deselectKeyboardRow(oldRow);
keyboardSelectedColumn = nextColumn;
presenter.keyboardNext();
event.preventDefault();
@@ -1388,9 +1355,8 @@
}
} else {
// Reselect the row to move the selected column.
- deselectKeyboardRow(oldRow);
keyboardSelectedColumn = nextColumn;
- setKeyboardSelected(oldRow, true, true);
+ getPresenter().setKeyboardSelectedRow(oldRow, true);
event.preventDefault();
return true;
}
@@ -1399,7 +1365,6 @@
if (prevColumn >= keyboardSelectedColumn) {
// Wrap to the previous row.
if (presenter.hasKeyboardPrev()) {
- deselectKeyboardRow(oldRow);
keyboardSelectedColumn = prevColumn;
presenter.keyboardPrev();
event.preventDefault();
@@ -1407,9 +1372,8 @@
}
} else {
// Reselect the row to move the selected column.
- deselectKeyboardRow(oldRow);
keyboardSelectedColumn = prevColumn;
- setKeyboardSelected(oldRow, true, true);
+ getPresenter().setKeyboardSelectedRow(oldRow, true);
event.preventDefault();
return true;
}
@@ -1436,17 +1400,6 @@
}
/**
- * Schedule a redraw for the end of the event loop.
- */
- private void scheduleRedraw() {
- redrawCancelled = false;
- if (!redrawScheduled) {
- redrawScheduled = true;
- Scheduler.get().scheduleFinally(redrawCommand);
- }
- }
-
- /**
* Show or hide the loading icon.
*
* @param visible true to show, false to hide.
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTree.java b/user/src/com/google/gwt/user/cellview/client/CellTree.java
index bbe2b4d..90b2ddd 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTree.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTree.java
@@ -18,7 +18,6 @@
import com.google.gwt.animation.client.Animation;
import com.google.gwt.core.client.GWT;
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.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Overflow;
@@ -521,6 +520,11 @@
*/
boolean isRefreshing;
+ /**
+ * The hidden root node in the tree. Visible for testing.
+ */
+ final CellTreeNodeView<?> rootNode;
+
private char accessKey = 0;
/**
@@ -554,11 +558,6 @@
private boolean isAnimationEnabled;
/**
- * The deferred command used to keyboard select a node.
- */
- private ScheduledCommand keyboardSelectCommand;
-
- /**
* The {@link CellTreeNodeView} whose children are currently being selected
* using the keyboard.
*/
@@ -580,11 +579,6 @@
private final SafeHtml openImageTopHtml;
/**
- * The hidden root node in the tree.
- */
- private final CellTreeNodeView<?> rootNode;
-
- /**
* The styles used by this widget.
*/
private final Style style;
@@ -731,11 +725,11 @@
}
}
- Element target = event.getEventTarget().cast();
+ final Element target = event.getEventTarget().cast();
ArrayList<Element> chain = new ArrayList<Element>();
collectElementChain(chain, getElement(), target);
- boolean isMouseDown = "mousedown".equals(eventType);
+ final boolean isMouseDown = "mousedown".equals(eventType);
final CellTreeNodeView<?> nodeView = findItemByChain(chain, 0, rootNode);
if (nodeView != null && nodeView != rootNode) {
if (isMouseDown) {
@@ -754,19 +748,14 @@
// Forward the event to the cell
if (nodeView.getSelectionElement().isOrHasChild(target)) {
// Move the keyboard focus to the clicked item.
- if ("focus".equals(eventType) || isMouseDown) {
- // Wait until any pending blur event has fired.
- final boolean targetsCellParent = nodeView.getCellParent().isOrHasChild(target);
- keyboardSelectCommand = new ScheduledCommand() {
- public void execute() {
- if (keyboardSelectCommand == this && !nodeView.isDestroyed()) {
- isFocused = true;
- keyboardSelectCommand = null;
- keyboardSelect(nodeView, !targetsCellParent);
- }
- }
- };
- Scheduler.get().scheduleDeferred(keyboardSelectCommand);
+ if (isMouseDown) {
+ /*
+ * If the selected element is natively focusable, then we do not want to
+ * steal focus away from it.
+ */
+ boolean isFocusable = CellBasedWidgetImpl.get().isFocusable(target);
+ isFocused = isFocused || isFocusable;
+ keyboardSelect(nodeView, !isFocusable);
}
nodeView.fireEventToCell(event);
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
index 2a6c71b..4b87828 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
@@ -36,7 +36,6 @@
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
-import com.google.gwt.user.cellview.client.HasDataPresenter.ElementIterator;
import com.google.gwt.user.cellview.client.HasDataPresenter.LoadingState;
import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.user.client.ui.UIObject;
@@ -103,22 +102,6 @@
return handlerManger.addHandler(type, handler);
}
- public boolean dependsOnSelection() {
- return cell.dependsOnSelection();
- }
-
- public int getChildCount() {
- return childContainer.getChildCount();
- }
-
- public ElementIterator getChildIterator() {
- return new HasDataPresenter.DefaultElementIterator(this,
- childContainer.getFirstChildElement());
- }
-
- public void onUpdateSelection() {
- }
-
public void render(SafeHtmlBuilder sb, List<C> values, int start,
SelectionModel<? super C> selectionModel) {
// Cache the style names that will be used for each child.
@@ -197,7 +180,8 @@
}
}
- public void replaceAllChildren(List<C> values, SafeHtml html) {
+ public void replaceAllChildren(List<C> values, SafeHtml html,
+ boolean stealFocus) {
// Hide the child container so we can animate it.
if (nodeView.tree.isAnimationEnabled()) {
nodeView.ensureAnimationFrame().getStyle().setDisplay(Display.NONE);
@@ -233,8 +217,10 @@
}
}
- public void replaceChildren(List<C> values, int start, SafeHtml html) {
- Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, 0);
+ public void replaceChildren(List<C> values, int start, SafeHtml html,
+ boolean stealFocus) {
+ Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values,
+ start);
nodeView.tree.isRefreshing = true;
Element newChildren = AbstractHasData.convertToElements(nodeView.tree,
@@ -243,7 +229,7 @@
newChildren, start, html);
nodeView.tree.isRefreshing = false;
- loadChildState(values, 0, savedViews);
+ loadChildState(values, start, savedViews);
}
public void resetFocus() {
@@ -263,11 +249,6 @@
showOrHide(nodeView.emptyMessageElem, state == LoadingState.EMPTY);
}
- public void setSelected(Element elem, boolean selected) {
- setStyleName(getSelectionElement(elem),
- nodeView.tree.getStyle().cellTreeSelectedItem(), selected);
- }
-
/**
* Reload the open children after rendering new items in this node.
*
@@ -283,7 +264,7 @@
ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey();
Element container = nodeView.ensureChildContainer();
- Element childElem = container.getFirstChildElement();
+ Element childElem = container.getChild(start).cast();
CellTreeNodeView<?> keyboardSelected = nodeView.tree.getKeyboardSelectedNode();
for (int i = start; i < end; i++) {
C childValue = values.get(i - start);
@@ -428,9 +409,13 @@
this.nodeView = nodeView;
cell = nodeInfo.getCell();
+ // Create a presenter.
presenter = new HasDataPresenter<C>(this, new View(
nodeView.ensureChildContainer()), pageSize, nodeInfo.getProvidesKey());
+ // Disable keyboard selection because it is handled by CellTree.
+ presenter.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
+
// Use a pager to update buttons.
presenter.addRowCountChangeHandler(new RowCountChangeEvent.Handler() {
public void onRowCountChange(RowCountChangeEvent event) {
@@ -529,12 +514,14 @@
public int getChildCount() {
assertNotDestroyed();
+ flush();
return nodeView.getChildCount();
}
public Object getChildValue(int index) {
assertNotDestroyed();
checkChildBounds(index);
+ flush();
return nodeView.getChildNode(index).value;
}
@@ -546,7 +533,7 @@
public TreeNode getParent() {
assertNotDestroyed();
- return nodeView.isRootNode() ? null : nodeView.parentNode.treeNode;
+ return getParentImpl();
}
public Object getValue() {
@@ -556,16 +543,28 @@
public boolean isChildLeaf(int index) {
assertNotDestroyed();
checkChildBounds(index);
+ flush();
return nodeView.getChildNode(index).isLeaf();
}
public boolean isChildOpen(int index) {
assertNotDestroyed();
checkChildBounds(index);
+ flush();
return nodeView.getChildNode(index).isOpen();
}
public boolean isDestroyed() {
+ if (!nodeView.isDestroyed) {
+ /*
+ * Flush the parent display because the user may have replaced this
+ * node, which would destroy it.
+ */
+ TreeNodeImpl parent = getParentImpl();
+ if (parent != null && !parent.isDestroyed()) {
+ parent.flush();
+ }
+ }
return nodeView.isDestroyed || !nodeView.isOpen();
}
@@ -600,6 +599,24 @@
throw new IndexOutOfBoundsException();
}
}
+
+ /**
+ * Flush pending changes in the view.
+ */
+ private void flush() {
+ if (nodeView.listView != null) {
+ nodeView.listView.presenter.flush();
+ }
+ }
+
+ /**
+ * Get the parent node without checking if this node is destroyed.
+ *
+ * @return the parent node, or null if the node has no parent
+ */
+ private TreeNodeImpl getParentImpl() {
+ return nodeView.isRootNode() ? null : nodeView.parentNode.treeNode;
+ }
}
/**
@@ -1173,6 +1190,10 @@
* @param stealFocus true to steal focus
*/
void setKeyboardSelected(boolean selected, boolean stealFocus) {
+ if (tree.isKeyboardSelectionDisabled()) {
+ return;
+ }
+
// Apply the selected style.
if (!selected || tree.isFocused || stealFocus) {
setKeyboardSelectedStyle(selected);
diff --git a/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java b/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
index e158c81..a130edd 100644
--- a/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
+++ b/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
@@ -1,12 +1,12 @@
/*
* 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
@@ -15,12 +15,15 @@
*/
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.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.HasKeyProvider;
import com.google.gwt.view.client.ProvidesKey;
@@ -34,8 +37,8 @@
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
-import java.util.NoSuchElementException;
import java.util.Set;
+import java.util.TreeSet;
/**
* <p>
@@ -50,63 +53,27 @@
* 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 {
/**
- * 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
*/
@@ -125,14 +92,14 @@
/**
* 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
@@ -141,38 +108,12 @@
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 start the absolute start index that is being rendered
* @param selectionModel the {@link SelectionModel}
*/
void render(SafeHtmlBuilder sb, List<T> values, int start,
@@ -180,23 +121,26 @@
/**
* 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);
+ 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
+ * @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);
+ 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
@@ -206,7 +150,7 @@
/**
* 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
@@ -215,18 +159,190 @@
/**
* 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>();
+
+ 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 boolean isRowCountExact() {
+ return rowCountIsExact;
+ }
/**
- * Update an element to reflect its selected state.
- *
- * @param elem the element to update
- * @param selected true if selected, false if not
+ * {@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>
*/
- void setSelected(Element elem, boolean selected);
+ public boolean isRowSelected(int index) {
+ return selectedRows.contains(index);
+ }
+ }
+
+ /**
+ * 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();
+
+ // 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 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);
}
/**
@@ -236,18 +352,31 @@
*/
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;
/**
- * The current keyboard selected row relative to page start. This value should
- * never be negative.
+ * A boolean indicating that we are in the process of resolving state.
*/
- private int keyboardSelectedRow = 0;
-
- /**
- * The last row value that was selected with the keyboard.
- */
- private T keyboardSelectedRowValue;
+ private boolean isResolvingState;
private KeyboardPagingPolicy keyboardPagingPolicy = KeyboardPagingPolicy.CHANGE_PAGE;
private KeyboardSelectionPolicy keyboardSelectionPolicy = KeyboardSelectionPolicy.ENABLED;
@@ -257,43 +386,42 @@
/**
* 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.
+ * set inner html. This is useful for apps that continuously refresh the view.
*/
private SafeHtml lastContents = null;
- private int pageSize;
- private int pageStart = 0;
+ /**
+ * The pending state of the presenter to be pushed to the view.
+ */
+ private PendingState<T> pendingState;
/**
- * Set to true when the page start changes, and we need to do a full refresh.
+ * The command used to resolve the pending state.
*/
- private boolean pageStartChangedSinceRender;
-
- private int rowCount = 0;
-
- private boolean rowCountIsExact;
+ private ScheduledCommand pendingStateCommand;
/**
- * The local cache of data in the view. The 0th index in the list corresponds
- * to the value at pageStart.
+ * 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 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 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
@@ -302,8 +430,8 @@
ProvidesKey<T> keyProvider) {
this.display = display;
this.view = view;
- this.pageSize = pageSize;
this.keyProvider = keyProvider;
+ this.state = new DefaultState<T>(pageSize);
}
public HandlerRegistration addRangeChangeHandler(
@@ -317,6 +445,15 @@
}
/**
+ * 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() {
@@ -336,13 +473,28 @@
}
/**
+ * 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(pageSize, rowCount - pageStart);
+ return Math.min(getPageSize(), getRowCount() - getPageStart());
}
public KeyboardPagingPolicy getKeyboardPagingPolicy() {
@@ -351,12 +503,34 @@
/**
* 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;
+ : 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() {
@@ -369,22 +543,30 @@
/**
* Get the overall data size.
- *
+ *
* @return the data size
*/
public int getRowCount() {
- return rowCount;
+ return getCurrentState().getRowCount();
}
/**
- * 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
+ * Get the size of the list of row data.
+ *
+ * @return the size of the row data
*/
- public List<T> getRowData() {
- return rowData;
+ public int getRowDataSize() {
+ return getCurrentState().getRowDataSize();
+ }
+
+ /**
+ * Get a value from the row data.
+ *
+ * @param the index of the row data
+ * @return the value at the specified index
+ */
+ public T getRowDataValue(int index) {
+ return getCurrentState().getRowDataValue(index);
}
public SelectionModel<? super T> getSelectionModel() {
@@ -395,21 +577,21 @@
* Return the range of data being displayed.
*/
public Range getVisibleRange() {
- return new Range(pageStart, pageSize);
+ 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 (keyboardSelectedRow < rowData.size() - 1) {
+ } else if (getKeyboardSelectedRow() < getRowDataSize() - 1) {
return true;
} else if (!keyboardPagingPolicy.isLimitedToRange()
- && (keyboardSelectedRow + pageStart < rowCount - 1 || !rowCountIsExact)) {
+ && (getKeyboardSelectedRow() + getPageStart() < getRowCount() - 1 || !isRowCountExact())) {
return true;
}
return false;
@@ -417,22 +599,33 @@
/**
* 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) {
+ } else if (getKeyboardSelectedRow() > 0) {
return true;
- } else if (!keyboardPagingPolicy.isLimitedToRange() && pageStart > 0) {
+ } 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;
+ }
+
public boolean isRowCountExact() {
- return rowCountIsExact;
+ return getCurrentState().isRowCountExact();
}
/**
@@ -440,7 +633,7 @@
*/
public void keyboardEnd() {
if (!keyboardPagingPolicy.isLimitedToRange()) {
- setKeyboardSelectedRow(rowCount - 1, true);
+ setKeyboardSelectedRow(getRowCount() - 1, true);
}
}
@@ -449,7 +642,7 @@
*/
public void keyboardHome() {
if (!keyboardPagingPolicy.isLimitedToRange()) {
- setKeyboardSelectedRow(-pageStart, true);
+ setKeyboardSelectedRow(-getPageStart(), true);
}
}
@@ -458,7 +651,7 @@
*/
public void keyboardNext() {
if (hasKeyboardNext()) {
- setKeyboardSelectedRow(keyboardSelectedRow + 1, true);
+ setKeyboardSelectedRow(getKeyboardSelectedRow() + 1, true);
}
}
@@ -468,9 +661,9 @@
public void keyboardNextPage() {
if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) {
// 0th index of next page.
- setKeyboardSelectedRow(pageSize, true);
+ setKeyboardSelectedRow(getPageSize(), true);
} else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) {
- setKeyboardSelectedRow(keyboardSelectedRow + PAGE_INCREMENT, true);
+ setKeyboardSelectedRow(getKeyboardSelectedRow() + PAGE_INCREMENT, true);
}
}
@@ -479,7 +672,7 @@
*/
public void keyboardPrev() {
if (hasKeyboardPrev()) {
- setKeyboardSelectedRow(keyboardSelectedRow - 1, true);
+ setKeyboardSelectedRow(getKeyboardSelectedRow() - 1, true);
}
}
@@ -489,9 +682,9 @@
public void keyboardPrevPage() {
if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) {
// 0th index of previous page.
- setKeyboardSelectedRow(-pageSize, true);
+ setKeyboardSelectedRow(-getPageSize(), true);
} else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) {
- setKeyboardSelectedRow(keyboardSelectedRow - PAGE_INCREMENT, true);
+ setKeyboardSelectedRow(getKeyboardSelectedRow() - PAGE_INCREMENT, true);
}
}
@@ -499,10 +692,11 @@
* Toggle selection of the current keyboard row in the {@link SelectionModel}.
*/
public void keyboardToggleSelect() {
+ int keyboardSelectedRow = getKeyboardSelectedRow();
if (KeyboardSelectionPolicy.ENABLED == keyboardSelectionPolicy
&& selectionModel != null && keyboardSelectedRow >= 0
- && keyboardSelectedRow < rowData.size()) {
- T value = rowData.get(keyboardSelectedRow);
+ && keyboardSelectedRow < getRowDataSize()) {
+ T value = getRowDataValue(keyboardSelectedRow);
if (value != null) {
selectionModel.setSelected(value, !selectionModel.isSelected(value));
}
@@ -514,7 +708,7 @@
*/
public void redraw() {
lastContents = null;
- setRowData(pageStart, rowData);
+ ensurePendingState().redrawRequired = true;
}
public void setKeyboardPagingPolicy(KeyboardPagingPolicy policy) {
@@ -526,7 +720,7 @@
/**
* Set the row index of the keyboard selected element.
- *
+ *
* @param index the row index
* @param stealFocus true to steal focus
*/
@@ -535,24 +729,16 @@
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 pageStart = getPageStart();
+ int pageSize = getPageSize();
+ int rowCount = getRowCount();
int absIndex = pageStart + index;
- if (absIndex < 0) {
- absIndex = 0;
- } else if (absIndex >= rowCount && rowCountIsExact) {
+ if (absIndex >= rowCount && isRowCountExact()) {
absIndex = rowCount - 1;
}
- index = absIndex - pageStart;
+ index = Math.max(0, absIndex) - pageStart;
if (keyboardPagingPolicy.isLimitedToRange()) {
index = Math.max(0, Math.min(index, pageSize - 1));
}
@@ -560,13 +746,15 @@
// Select the new index.
int newPageStart = pageStart;
int newPageSize = pageSize;
- keyboardSelectedRow = 0;
+ PendingState<T> pending = ensurePendingState();
+ pending.keyboardSelectedRow = 0;
+ pending.keyboardSelectedRowValue = null;
+ pending.keyboardSelectedRowChanged = true;
if (index >= 0 && index < pageSize) {
- keyboardSelectedRow = index;
- if (isBound) {
- selectKeyboardValue(index);
- }
- view.setKeyboardSelected(index, true, stealFocus);
+ 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.
@@ -607,8 +795,7 @@
// Update the range if it changed.
if (newPageStart != pageStart || newPageSize != pageSize) {
- deselectKeyboardValue();
- keyboardSelectedRow = index;
+ pending.keyboardSelectedRow = index;
setVisibleRange(new Range(newPageStart, newPageSize), false, false);
}
}
@@ -630,25 +817,17 @@
}
public void setRowCount(int count, boolean isExact) {
- if (count == this.rowCount && isExact == this.rowCountIsExact) {
+ if (count == getRowCount() && isExact == isRowCountExact()) {
return;
}
- this.rowCount = count;
- this.rowCountIsExact = isExact;
- updateLoadingState();
+ ensurePendingState().rowCount = count;
+ ensurePendingState().rowCountIsExact = isExact;
- // 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 cached data.
+ updateCachedData();
// Update the pager.
- RowCountChangeEvent.fire(display, count, rowCountIsExact);
+ RowCountChangeEvent.fire(display, count, isExact);
}
public void setRowData(int start, List<? extends T> values) {
@@ -656,7 +835,8 @@
int valuesEnd = start + valuesLength;
// Calculate the bounded start (inclusive) and end index (exclusive).
- int pageEnd = pageStart + pageSize;
+ int pageStart = getPageStart();
+ int pageEnd = getPageStart() + getPageSize();
int boundedStart = Math.max(start, pageStart);
int boundedEnd = Math.min(valuesEnd, pageEnd);
if (start != pageStart && boundedStart >= boundedEnd) {
@@ -665,115 +845,30 @@
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());
+ PendingState<T> pending = ensurePendingState();
+ int cacheOffset = Math.max(0, boundedStart - pageStart - getRowDataSize());
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.
- }
+ 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 < rowData.size()) {
- rowData.set(dataIndex, value);
+ if (dataIndex < getRowDataSize()) {
+ pending.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;
+ pending.rowData.add(value);
}
}
- // 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);
+ // Remember the range that has been replaced.
+ pending.replaceRange(boundedStart - cacheOffset, boundedEnd);
- // 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);
- }
+ // Fire a row count change event after updating the data.
+ if (valuesEnd > getRowCount()) {
+ setRowCount(valuesEnd, isRowCountExact());
}
}
@@ -803,55 +898,461 @@
if (selectionModel != null) {
selectionHandler = selectionModel.addSelectionChangeHandler(new SelectionChangeEvent.Handler() {
public void onSelectionChange(SelectionChangeEvent event) {
- updateSelection();
+ // Ensure that we resolve selection.
+ ensurePendingState();
}
});
}
// Update the current selection state based on the new model.
- updateSelection();
+ ensurePendingState();
}
/**
- * Deselect the keyboard selected value.
+ * 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
*/
- private void deselectKeyboardValue() {
- if (selectionModel != null && keyboardSelectedRowValue != null) {
- T curValue = keyboardSelectedRowValue;
- keyboardSelectedRow = 0;
- keyboardSelectedRowValue = null;
- selectionModel.setSelected(curValue, false);
+ 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();
+ }
+ }
+ };
+ Scheduler.get().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 : keyProvider.getKey(rowValue);
+ return (keyProvider == null || rowValue == null) ? rowValue
+ : keyProvider.getKey(rowValue);
}
/**
- * Select the value at the keyboard selected row.
- *
- * @param row the row index
+ * Resolve the pending state and push updates to the view.
*/
- private void selectKeyboardValue(int row) {
- if (selectionModel != null && row >= 0 && row < rowData.size()) {
- keyboardSelectedRowValue = rowData.get(row);
- if (keyboardSelectedRowValue != null) {
- selectionModel.setSelected(keyboardSelectedRowValue, true);
+ 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.
+ */
+ if (KeyboardSelectionPolicy.BOUND_TO_SELECTION == keyboardSelectionPolicy
+ && selectionModel != null) {
+ T oldValue = oldState.getRowDataSize() > 0
+ ? oldState.getRowDataValue(oldState.getKeyboardSelectedRow()) : null;
+ Object oldKey = getRowValueKey(oldValue);
+ T newValue = rowDataCount > 0
+ ? pending.getRowDataValue(pending.getKeyboardSelectedRow()) : null;
+ Object newKey = getRowValueKey(newValue);
+ if ((oldKey == null) ? newKey != null : !oldKey.equals(newKey)) {
+ // Deselect the old value.
+ if (oldValue != null && selectionModel.isSelected(oldValue)) {
+ selectionModel.setSelected(oldValue, false);
+ }
+
+ // Select the new value.
+ if (newValue != null && !selectionModel.isSelected(newValue)) {
+ selectionModel.setSelected(newValue, true);
+ }
+ }
+ }
+
+ // 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.
+ */
+ 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);
+ }
+ }
+
+ // We are done resolving state.
+ 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}
@@ -860,64 +1361,69 @@
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 1");
+ 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 (rowData.size() > increase) {
+ if (getRowDataSize() > increase) {
// Remove the data we no longer need.
for (int i = 0; i < increase; i++) {
- rowData.remove(0);
+ pending.rowData.remove(0);
}
} else {
// We have no overlapping data, so just clear it.
- rowData.clear();
+ pending.rowData.clear();
}
} else {
int decrease = pageStart - start;
- if ((rowData.size() > 0) && (decrease < pageSize)) {
+ if ((getRowDataSize() > 0) && (decrease < pageSize)) {
// Insert null data at the beginning.
for (int i = 0; i < decrease; i++) {
- rowData.add(0, null);
+ 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.
- rowData.clear();
+ pending.rowData.clear();
}
}
}
// Update the page start.
- pageStart = start;
- pageStartChangedSinceRender = true;
+ pending.pageStart = start;
}
// Update the page size.
final boolean pageSizeChanged = (pageSize != length);
if (pageSizeChanged) {
- pageSize = length;
+ ensurePendingState().pageSize = length;
}
// Clear the data.
if (clearData) {
- rowData.clear();
- selectedRows.clear();
+ ensurePendingState().rowData.clear();
}
- // Update the loading state.
- updateLoadingState();
-
- // Redraw with the existing data.
- if (pageStartChanged || clearData || updateCachedData()) {
- redraw();
- }
+ // Trim the row values if needed.
+ updateCachedData();
// Update the pager and data source if the range changed.
if (pageStartChanged || pageSizeChanged || forceRangeChangeEvent) {
@@ -927,30 +1433,25 @@
/**
* 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;
+ private void updateCachedData() {
+ int pageStart = getPageStart();
int expectedLastIndex = Math.max(0,
- Math.min(pageSize, rowCount - pageStart));
- int lastIndex = rowData.size() - 1;
+ Math.min(getPageSize(), getRowCount() - pageStart));
+ int lastIndex = getRowDataSize() - 1;
while (lastIndex >= expectedLastIndex) {
- rowData.remove(lastIndex);
- selectedRows.remove(lastIndex + pageStart);
+ ensurePendingState().rowData.remove(lastIndex);
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) {
+ int cacheSize = getRowDataSize();
+ int curPageSize = isRowCountExact() ? getCurrentPageSize() : getPageSize();
+ if (getRowCount() == 0 && isRowCountExact()) {
view.setLoadingState(LoadingState.EMPTY);
} else if (cacheSize >= curPageSize) {
view.setLoadingState(LoadingState.LOADED);
@@ -960,47 +1461,4 @@
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();
- }
- }
}
diff --git a/user/test/com/google/gwt/user/cellview/client/AbstractCellTreeTestBase.java b/user/test/com/google/gwt/user/cellview/client/AbstractCellTreeTestBase.java
index 17b1b35..8dbc539 100644
--- a/user/test/com/google/gwt/user/cellview/client/AbstractCellTreeTestBase.java
+++ b/user/test/com/google/gwt/user/cellview/client/AbstractCellTreeTestBase.java
@@ -17,6 +17,7 @@
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.TextCell;
+import com.google.gwt.core.client.Scheduler;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.OpenEvent;
@@ -24,7 +25,6 @@
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.ui.RootPanel;
-import com.google.gwt.view.client.AbstractDataProvider;
import com.google.gwt.view.client.ListDataProvider;
import com.google.gwt.view.client.ProvidesKey;
import com.google.gwt.view.client.TreeViewModel;
@@ -87,7 +87,7 @@
throw new IllegalArgumentException("Unrecognized value type");
}
- public AbstractDataProvider<String> getRootDataProvider() {
+ public ListDataProvider<String> getRootDataProvider() {
return rootDataProvider;
}
@@ -235,9 +235,15 @@
};
// Create a tree.
- new CellTree(model, null);
- assertEquals("Cell#render() should be called exactly thrice", 3,
- rendered.size());
+ createAbstractCellTree(model, null);
+ delayTestFinish(5000);
+ Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
+ public void execute() {
+ assertEquals("Cell#render() should be called exactly thrice", 3,
+ rendered.size());
+ finishTest();
+ }
+ });
}
/**
diff --git a/user/test/com/google/gwt/user/cellview/client/AbstractHasDataTestBase.java b/user/test/com/google/gwt/user/cellview/client/AbstractHasDataTestBase.java
index be504c6..b4ba250 100644
--- a/user/test/com/google/gwt/user/cellview/client/AbstractHasDataTestBase.java
+++ b/user/test/com/google/gwt/user/cellview/client/AbstractHasDataTestBase.java
@@ -127,6 +127,7 @@
ListDataProvider<String> provider = new ListDataProvider<String>(
createData(0, 10));
provider.addDataDisplay(display);
+ display.getPresenter().flush();
// Default tab index is 0.
assertEquals(0, display.getTabIndex());
@@ -139,6 +140,7 @@
// Push new data.
provider.refresh();
+ display.getPresenter().flush();
assertEquals(2, display.getTabIndex());
assertEquals(2, display.getKeyboardSelectedElement().getTabIndex());
}
diff --git a/user/test/com/google/gwt/user/cellview/client/CellBrowserTest.java b/user/test/com/google/gwt/user/cellview/client/CellBrowserTest.java
index 82bc7fe..11364e3 100644
--- a/user/test/com/google/gwt/user/cellview/client/CellBrowserTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/CellBrowserTest.java
@@ -17,6 +17,8 @@
import com.google.gwt.cell.client.NumberCell;
import com.google.gwt.cell.client.TextCell;
+import com.google.gwt.user.cellview.client.CellBrowser.BrowserCellList;
+import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
import com.google.gwt.view.client.ListDataProvider;
import com.google.gwt.view.client.TreeViewModel;
@@ -72,7 +74,7 @@
assertEquals(1, browser.treeNodes.size());
// Open a leaf node.
- rootNode.setChildOpen(1, true);
+ assertNull(rootNode.setChildOpen(1, true));
assertEquals(1, browser.treeNodes.size());
assertEquals(1, browser.treeNodes.get(0).getFocusedKey());
assertFalse(browser.treeNodes.get(0).isFocusedOpen());
@@ -80,7 +82,6 @@
// Close the leaf node.
rootNode.setChildOpen(1, false);
assertEquals(1, browser.treeNodes.size());
- assertNull(browser.treeNodes.get(0).getFocusedKey());
assertFalse(browser.treeNodes.get(0).isFocusedOpen());
}
@@ -117,6 +118,21 @@
assertTrue(browser.treeNodes.get(0).isFocusedOpen());
}
+ public void testSetKeyboardSelectionPolicyDisabled() {
+ CellBrowser browser = (CellBrowser) tree;
+
+ // Disable keyboard selection.
+ browser.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
+ assertEquals(KeyboardSelectionPolicy.DISABLED,
+ browser.getKeyboardSelectionPolicy());
+
+ // Verify that keyboard selection is enabled in the lists.
+ BrowserCellList<?> list = browser.treeNodes.get(0).getDisplay();
+ assertEquals(KeyboardSelectionPolicy.ENABLED,
+ list.getKeyboardSelectionPolicy());
+ assertTrue(list.isKeyboardNavigationSuppressed());
+ }
+
@Override
protected <T> CellBrowser createAbstractCellTree(TreeViewModel model,
T rootValue) {
diff --git a/user/test/com/google/gwt/user/cellview/client/CellListTest.java b/user/test/com/google/gwt/user/cellview/client/CellListTest.java
index be56b60..261d237 100644
--- a/user/test/com/google/gwt/user/cellview/client/CellListTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/CellListTest.java
@@ -22,6 +22,14 @@
*/
public class CellListTest extends AbstractHasDataTestBase {
+ public void testGetRowElement() {
+ CellList<String> list = createAbstractHasData();
+ list.setRowData(0, createData(0, 10));
+
+ // Ensure that calling getRowElement() flushes all pending changes.
+ assertNotNull(list.getRowElement(9));
+ }
+
@Override
protected CellList<String> createAbstractHasData() {
return new CellList<String>(new TextCell());
diff --git a/user/test/com/google/gwt/user/cellview/client/CellTableTest.java b/user/test/com/google/gwt/user/cellview/client/CellTableTest.java
index 4c42144..99a346d 100644
--- a/user/test/com/google/gwt/user/cellview/client/CellTableTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/CellTableTest.java
@@ -21,15 +21,55 @@
import com.google.gwt.dom.client.TableElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
+import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.user.cellview.client.CellTable.Resources;
import com.google.gwt.user.cellview.client.CellTable.Style;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Tests for {@link CellTable}.
*/
public class CellTableTest extends AbstractHasDataTestBase {
/**
+ * Test that calls to addColumn results in only one redraw.
+ */
+ public void testAddColumnSingleRedraw() {
+ final List<SafeHtml> replaceValues = new ArrayList<SafeHtml>();
+ CellTable<String> table = new CellTable<String>() {
+ @Override
+ protected void replaceAllChildren(List<String> values, SafeHtml html) {
+ replaceValues.add(html);
+ }
+ };
+ table.addColumn(new Column<String, String>(new TextCell()) {
+ @Override
+ public String getValue(String object) {
+ return object + "-3";
+ }
+ });
+ table.addColumn(new Column<String, String>(new TextCell()) {
+ @Override
+ public String getValue(String object) {
+ return object + "-4";
+ }
+ });
+ table.setRowData(0, createData(0, 10));
+ table.getPresenter().flush();
+ assertEquals(1, replaceValues.size());
+ }
+
+ public void testGetRowElement() {
+ CellTable<String> table = createAbstractHasData();
+ table.setRowData(0, createData(0, 10));
+
+ // Ensure that calling getRowElement() flushes all pending changes.
+ assertNotNull(table.getRowElement(9));
+ }
+
+ /**
* Test headers that span multiple columns.
*/
public void testMultiColumnHeader() {
@@ -45,6 +85,7 @@
// No header.
table.redraw();
+ table.getPresenter().flush();
assertEquals(0, getHeaderCount(table));
// Single column.
@@ -54,7 +95,7 @@
return null;
}
}, header);
- table.redraw();
+ table.getPresenter().flush();
assertEquals(1, getHeaderCount(table));
assertEquals(1, getHeaderElement(table, 0).getColSpan());
assertTrue(getHeaderElement(table, 0).getClassName().contains(styleHeader));
@@ -70,7 +111,7 @@
return null;
}
}, header);
- table.redraw();
+ table.getPresenter().flush();
assertEquals(1, getHeaderCount(table));
assertEquals(2, getHeaderElement(table, 0).getColSpan());
assertTrue(getHeaderElement(table, 0).getClassName().contains(styleHeader));
@@ -86,7 +127,7 @@
return null;
}
}, header);
- table.redraw();
+ table.getPresenter().flush();
assertEquals(1, getHeaderCount(table));
assertEquals(3, getHeaderElement(table, 0).getColSpan());
assertTrue(getHeaderElement(table, 0).getClassName().contains(styleHeader));
@@ -102,7 +143,7 @@
return null;
}
}, "New Header");
- table.redraw();
+ table.getPresenter().flush();
assertEquals(2, getHeaderCount(table));
assertEquals(3, getHeaderElement(table, 0).getColSpan());
assertTrue(getHeaderElement(table, 0).getClassName().contains(styleHeader));
@@ -126,7 +167,7 @@
return null;
}
}, header);
- table.redraw();
+ table.getPresenter().flush();
assertEquals(3, getHeaderCount(table));
assertEquals(3, getHeaderElement(table, 0).getColSpan());
assertTrue(getHeaderElement(table, 0).getClassName().contains(styleHeader));
diff --git a/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java
index 07f5886..e5b43fb 100644
--- a/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java
@@ -26,6 +26,41 @@
super(false);
}
+ /**
+ * Test that replacing a subset of children updates both the TreeNode value
+ * and the underlying DOM correctly.
+ */
+ public void testReplaceChildren() {
+ CellTree cellTree = (CellTree) tree;
+ TreeNode root = cellTree.getRootTreeNode();
+
+ // Open a couple of child nodes.
+ TreeNode a = root.setChildOpen(0, true);
+ TreeNode b = root.setChildOpen(1, true);
+ assertEquals("a", a.getValue());
+ assertEquals("ab", a.getChildValue(1));
+ assertEquals("b", b.getValue());
+ assertEquals("bc", b.getChildValue(2));
+
+ // Replace "b" with a "new" value.
+ model.getRootDataProvider().getList().set(1, "new");
+ model.getRootDataProvider().flush();
+ assertFalse(a.isDestroyed());
+ assertTrue(b.isDestroyed());
+ TreeNode newNode = root.setChildOpen(1, true);
+ assertEquals("a", a.getValue());
+ assertEquals("ab", a.getChildValue(1));
+ assertEquals("new", newNode.getValue());
+ assertEquals("newc", newNode.getChildValue(2));
+
+ // Check the underlying DOM values.
+ CellTreeNodeView<?> aImpl = cellTree.rootNode.getChildNode(0);
+ CellTreeNodeView<?> newNodeImpl = cellTree.rootNode.getChildNode(1);
+ assertEquals("a", aImpl.getCellParent().getInnerText());
+ assertEquals(10, aImpl.ensureChildContainer().getChildCount());
+ assertEquals("new", newNodeImpl.getCellParent().getInnerText());
+ }
+
public void testSetDefaultNodeSize() {
CellTree cellTree = (CellTree) tree;
TreeNode root = cellTree.getRootTreeNode();
diff --git a/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java b/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java
index 2b825f1..5f48c3d 100644
--- a/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java
@@ -1,12 +1,12 @@
/*
* 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
@@ -15,13 +15,13 @@
*/
package com.google.gwt.user.cellview.client;
-import com.google.gwt.dom.client.Element;
+import com.google.gwt.core.client.Scheduler;
import com.google.gwt.event.shared.EventHandler;
import com.google.gwt.event.shared.GwtEvent.Type;
import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
-import com.google.gwt.user.cellview.client.HasDataPresenter.ElementIterator;
import com.google.gwt.user.cellview.client.HasDataPresenter.LoadingState;
import com.google.gwt.user.cellview.client.HasDataPresenter.View;
import com.google.gwt.user.cellview.client.HasKeyboardPagingPolicy.KeyboardPagingPolicy;
@@ -33,82 +33,33 @@
import com.google.gwt.view.client.MockSelectionModel;
import com.google.gwt.view.client.Range;
import com.google.gwt.view.client.RangeChangeEvent;
+import com.google.gwt.view.client.SelectionChangeEvent;
import com.google.gwt.view.client.SelectionModel;
-
-import junit.framework.TestCase;
+import com.google.gwt.view.client.SingleSelectionModel;
import java.util.ArrayList;
-import java.util.HashSet;
import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Set;
+import java.util.TreeSet;
/**
* Tests for {@link HasDataPresenter}.
*/
-public class HasDataPresenterTest extends TestCase {
-
- /**
- * Mock iterator over DOM elements.
- */
- private static class MockElementIterator implements ElementIterator {
-
- private final int count;
- private int next = 0;
- private final MockView<?> view;
-
- public MockElementIterator(MockView<?> view, int count) {
- this.view = view;
- this.count = count;
- }
-
- public boolean hasNext() {
- return next < count;
- }
-
- public Element next() {
- if (!hasNext()) {
- throw new NoSuchElementException();
- }
- next++;
- return null;
- }
-
- 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 (next == 0) {
- throw new IllegalStateException();
- }
- view.setSelected(next - 1, selected);
- }
- }
+public class HasDataPresenterTest extends GWTTestCase {
/**
* A mock view used for testing.
- *
+ *
* @param <T> the data type
*/
private static class MockView<T> implements View<T> {
private int childCount;
- private boolean dependsOnSelection;
private List<Integer> keyboardSelectedRow = new ArrayList<Integer>();
private List<Boolean> keyboardSelectedRowState = new ArrayList<Boolean>();
- private SafeHtml lastHtml;
+ private final List<SafeHtml> lastHtml = new ArrayList<SafeHtml>();
private LoadingState loadingState;
- private boolean onUpdateSelectionFired;
private boolean replaceAllChildrenCalled;
private boolean replaceChildrenCalled;
- private Set<Integer> selectedRows = new HashSet<Integer>();
public <H extends EventHandler> HandlerRegistration addHandler(H handler,
Type<H> type) {
@@ -117,7 +68,7 @@
/**
* Assert the value of the oldest keyboard selected row and pop it.
- *
+ *
* @param row the row index
* @param selected true if selected, false if not
*/
@@ -137,22 +88,16 @@
public void assertLastHtml(String html) {
if (html == null) {
- assertNull(lastHtml);
+ assertTrue(lastHtml.isEmpty());
} else {
- assertEquals(html, lastHtml.asString());
+ assertEquals(html, lastHtml.remove(0).asString());
}
- lastHtml = null;
}
public void assertLoadingState(LoadingState expected) {
assertEquals(expected, loadingState);
}
- public void assertOnUpdateSelectionFired(boolean expected) {
- assertEquals(expected, onUpdateSelectionFired);
- onUpdateSelectionFired = false;
- }
-
public void assertReplaceAllChildrenCalled(boolean expected) {
assertEquals(expected, replaceAllChildrenCalled);
replaceAllChildrenCalled = false;
@@ -163,60 +108,33 @@
replaceChildrenCalled = false;
}
- /**
- * Assert that {@link #setSelected(int, boolean)} was called for the
- * specified rows.
- *
- * @param rows the rows
- */
- public void assertSelectedRows(Integer... rows) {
- assertEquals(rows.length, selectedRows.size());
- for (Integer row : rows) {
- assertTrue("Row " + row + "is not selected", selectedRows.contains(row));
- }
- }
-
- public boolean dependsOnSelection() {
- return dependsOnSelection;
- }
-
public int getChildCount() {
return childCount;
}
- public MockElementIterator getChildIterator() {
- return new MockElementIterator(this, 10);
- }
-
- public void onUpdateSelection() {
- onUpdateSelectionFired = true;
- }
-
public void render(SafeHtmlBuilder sb, List<T> values, int start,
SelectionModel<? super T> selectionModel) {
sb.appendHtmlConstant("start=").append(start);
sb.appendHtmlConstant(",size=").append(values.size());
}
- public void replaceAllChildren(List<T> values, SafeHtml html) {
+ public void replaceAllChildren(List<T> values, SafeHtml html,
+ boolean stealFocus) {
childCount = values.size();
replaceAllChildrenCalled = true;
- lastHtml = html;
+ lastHtml.add(html);
}
- public void replaceChildren(List<T> values, int start, SafeHtml html) {
+ public void replaceChildren(List<T> values, int start, SafeHtml html,
+ boolean stealFocus) {
childCount = Math.max(childCount, start + values.size());
replaceChildrenCalled = true;
- lastHtml = html;
+ lastHtml.add(html);
}
public void resetFocus() {
}
- public void setDependsOnSelection(boolean dependsOnSelection) {
- this.dependsOnSelection = dependsOnSelection;
- }
-
public void setKeyboardSelected(int index, boolean selected,
boolean stealFocus) {
keyboardSelectedRow.add(index);
@@ -226,19 +144,11 @@
public void setLoadingState(LoadingState state) {
this.loadingState = state;
}
+ }
- public void setSelected(Element elem, boolean selected) {
- // Not used in this mock.
- throw new UnsupportedOperationException();
- }
-
- protected void setSelected(int index, boolean selected) {
- if (selected) {
- selectedRows.add(index);
- } else {
- selectedRows.remove(index);
- }
- }
+ @Override
+ public String getModuleName() {
+ return "com.google.gwt.user.cellview.CellView";
}
public void testAddRowCountChangeHandler() {
@@ -318,16 +228,55 @@
handler.reset();
}
+ public void testCalculateModifiedRanges() {
+ HasData<String> listView = new MockHasData<String>();
+ MockView<String> view = new MockView<String>();
+ HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
+ view, 10, null);
+
+ TreeSet<Integer> rows = new TreeSet<Integer>();
+
+ // Empty set of rows.
+ assertListContains(presenter.calculateModifiedRanges(rows, 0, 10));
+
+ // One row in range.
+ rows.add(5);
+ assertListContains(presenter.calculateModifiedRanges(rows, 0, 10),
+ new Range(5, 1));
+
+ // One row not in range.
+ assertListContains(presenter.calculateModifiedRanges(rows, 6, 10));
+
+ // Consecutive rows (should return only one range).
+ rows.add(6);
+ rows.add(7);
+ rows.add(8);
+ assertListContains(presenter.calculateModifiedRanges(rows, 0, 10),
+ new Range(5, 4));
+
+ // Disjoint rows. Should return two ranges.
+ rows.add(10);
+ rows.add(11);
+ assertListContains(presenter.calculateModifiedRanges(rows, 0, 20),
+ new Range(5, 4), new Range(10, 2));
+
+ // Multiple gaps. The largest gap should be between the two ranges.
+ rows.add(15);
+ rows.add(17);
+ assertListContains(presenter.calculateModifiedRanges(rows, 0, 20),
+ new Range(5, 7), new Range(15, 3));
+ }
+
public void testClearSelectionModel() {
HasData<String> listView = new MockHasData<String>();
MockView<String> view = new MockView<String>();
- view.setDependsOnSelection(true);
HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
view, 10, null);
assertNull(presenter.getSelectionModel());
// Initialize some data.
populatePresenter(presenter);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=10");
@@ -336,19 +285,17 @@
SelectionModel<String> model = new MockSelectionModel<String>(null);
model.setSelected("test 0", true);
presenter.setSelectionModel(model);
- view.assertReplaceAllChildrenCalled(true);
- view.assertReplaceChildrenCalled(false);
- view.assertLastHtml("start=0,size=10");
- view.assertOnUpdateSelectionFired(true);
- view.assertSelectedRows();
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(false);
+ view.assertReplaceChildrenCalled(true);
+ view.assertLastHtml("start=0,size=1");
// Clear the selection model without updating the view.
presenter.clearSelectionModel();
+ presenter.flush();
view.assertReplaceAllChildrenCalled(false);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml(null);
- view.assertOnUpdateSelectionFired(false);
- view.assertSelectedRows();
}
public void testDefaults() {
@@ -363,6 +310,50 @@
assertEquals(new Range(0, 10), presenter.getVisibleRange());
}
+ /**
+ * Test that keyboard selection moves if its value moves.
+ */
+ public void testFindIndexOfBestMatch() {
+ HasData<String> listView = new MockHasData<String>();
+ MockView<String> view = new MockView<String>();
+ HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
+ view, 10, null);
+ populatePresenter(presenter);
+
+ // Select the second element.
+ presenter.setKeyboardSelectedRow(2, false);
+ presenter.flush();
+ assertEquals(2, presenter.getKeyboardSelectedRow());
+ assertEquals("test 2", presenter.getKeyboardSelectedRowValue());
+
+ // Shift the values by one.
+ presenter.setRowData(1, createData(0, 9));
+ presenter.flush();
+ assertEquals(3, presenter.getKeyboardSelectedRow());
+ assertEquals("test 2", presenter.getKeyboardSelectedRowValue());
+
+ // Replace the keyboard selected value.
+ presenter.setRowData(0, createData(100, 10));
+ presenter.flush();
+ assertEquals(0, presenter.getKeyboardSelectedRow());
+ assertEquals(null, presenter.getKeyboardSelectedRowValue());
+ }
+
+ public void testFlush() {
+ HasData<String> listView = new MockHasData<String>();
+ MockView<String> view = new MockView<String>();
+ HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
+ view, 10, null);
+
+ // Data should not be pushed to the view until flushed.
+ populatePresenter(presenter);
+ view.assertReplaceAllChildrenCalled(false);
+
+ // Now the data is pushed.
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(true);
+ }
+
public void testGetCurrentPageSize() {
HasData<String> listView = new MockHasData<String>();
MockView<String> view = new MockView<String>();
@@ -386,32 +377,39 @@
presenter.setRowCount(100, true);
presenter.setVisibleRange(new Range(50, 10));
populatePresenter(presenter);
+ presenter.flush();
presenter.setKeyboardPagingPolicy(KeyboardPagingPolicy.CHANGE_PAGE);
// keyboardPrev in middle.
presenter.setKeyboardSelectedRow(1, false);
+ presenter.flush();
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(1, true);
assertTrue(presenter.hasKeyboardPrev());
presenter.keyboardPrev();
+ presenter.flush();
assertEquals(0, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(1, false);
view.assertKeyboardSelectedRow(0, true);
- // keyboardPrev at beginning.
+ // keyboardPrev at beginning goes to previous page.
assertTrue(presenter.hasKeyboardPrev());
presenter.keyboardPrev();
- assertEquals(9, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(0, false);
- assertEquals(new Range(40, 10), presenter.getVisibleRange());
populatePresenter(presenter);
+ presenter.flush();
+ assertEquals(9, presenter.getKeyboardSelectedRow());
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
+ assertEquals(new Range(40, 10), presenter.getVisibleRange());
// keyboardNext in middle.
presenter.setKeyboardSelectedRow(8, false);
+ presenter.flush();
view.assertKeyboardSelectedRow(9, false);
view.assertKeyboardSelectedRow(8, true);
assertTrue(presenter.hasKeyboardNext());
presenter.keyboardNext();
+ presenter.flush();
assertEquals(9, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(8, false);
view.assertKeyboardSelectedRow(9, true);
@@ -419,53 +417,69 @@
// keyboardNext at end.
assertTrue(presenter.hasKeyboardNext());
presenter.keyboardNext();
- assertEquals(0, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(9, false);
- assertEquals(new Range(50, 10), presenter.getVisibleRange());
populatePresenter(presenter);
+ presenter.flush();
+ assertEquals(0, presenter.getKeyboardSelectedRow());
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
+ assertEquals(new Range(50, 10), presenter.getVisibleRange());
// keyboardPrevPage.
presenter.setKeyboardSelectedRow(5, false);
+ presenter.flush();
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(5, true);
presenter.keyboardPrevPage();
- assertEquals(0, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(5, false);
- assertEquals(new Range(40, 10), presenter.getVisibleRange());
populatePresenter(presenter);
+ presenter.flush();
+ assertEquals(0, presenter.getKeyboardSelectedRow());
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
+ assertEquals(new Range(40, 10), presenter.getVisibleRange());
// keyboardNextPage.
presenter.setKeyboardSelectedRow(5, false);
+ presenter.flush();
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(5, true);
presenter.keyboardNextPage();
- assertEquals(0, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(5, false);
- assertEquals(new Range(50, 10), presenter.getVisibleRange());
populatePresenter(presenter);
+ presenter.flush();
+ assertEquals(0, presenter.getKeyboardSelectedRow());
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
+ assertEquals(new Range(50, 10), presenter.getVisibleRange());
// keyboardHome.
presenter.keyboardHome();
- assertEquals(0, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(0, false);
- assertEquals(new Range(0, 10), presenter.getVisibleRange());
populatePresenter(presenter);
+ presenter.flush();
+ assertEquals(0, presenter.getKeyboardSelectedRow());
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
+ assertEquals(new Range(0, 10), presenter.getVisibleRange());
// keyboardPrev at first row.
assertFalse(presenter.hasKeyboardPrev());
presenter.keyboardPrev();
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(false);
view.assertKeyboardSelectedRowEmpty();
// keyboardEnd.
presenter.keyboardEnd();
- assertEquals(9, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(0, false);
- assertEquals(new Range(90, 10), presenter.getVisibleRange());
populatePresenter(presenter);
+ presenter.flush();
+ assertEquals(9, presenter.getKeyboardSelectedRow());
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
+ assertEquals(new Range(90, 10), presenter.getVisibleRange());
// keyboardNext at last row.
assertFalse(presenter.hasKeyboardNext());
presenter.keyboardNext();
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(false);
view.assertKeyboardSelectedRowEmpty();
}
@@ -476,14 +490,17 @@
view, 10, null);
presenter.setVisibleRange(new Range(50, 10));
populatePresenter(presenter);
+ presenter.flush();
presenter.setKeyboardPagingPolicy(KeyboardPagingPolicy.CURRENT_PAGE);
// keyboardPrev in middle.
presenter.setKeyboardSelectedRow(1, false);
+ presenter.flush();
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(1, true);
assertTrue(presenter.hasKeyboardPrev());
presenter.keyboardPrev();
+ presenter.flush();
assertEquals(0, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(1, false);
view.assertKeyboardSelectedRow(0, true);
@@ -491,15 +508,18 @@
// keyboardPrev at beginning.
assertFalse(presenter.hasKeyboardPrev());
presenter.keyboardPrev();
+ presenter.flush();
assertEquals(0, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRowEmpty();
// keyboardNext in middle.
presenter.setKeyboardSelectedRow(8, false);
+ presenter.flush();
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(8, true);
assertTrue(presenter.hasKeyboardNext());
presenter.keyboardNext();
+ presenter.flush();
assertEquals(9, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(8, false);
view.assertKeyboardSelectedRow(9, true);
@@ -507,23 +527,28 @@
// keyboardNext at end.
assertFalse(presenter.hasKeyboardNext());
presenter.keyboardNext();
+ presenter.flush();
assertEquals(9, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRowEmpty();
// keyboardPrevPage.
presenter.keyboardPrevPage();
+ presenter.flush();
view.assertKeyboardSelectedRowEmpty();
// keyboardNextPage.
presenter.keyboardNextPage();
+ presenter.flush();
view.assertKeyboardSelectedRowEmpty();
// keyboardHome.
presenter.keyboardHome();
+ presenter.flush();
view.assertKeyboardSelectedRowEmpty();
// keyboardEnd.
presenter.keyboardEnd();
+ presenter.flush();
view.assertKeyboardSelectedRowEmpty();
}
@@ -538,14 +563,17 @@
presenter.setRowCount(300, true);
presenter.setVisibleRange(new Range(pageStart, pageSize));
populatePresenter(presenter);
+ presenter.flush();
presenter.setKeyboardPagingPolicy(KeyboardPagingPolicy.INCREASE_RANGE);
// keyboardPrev in middle.
presenter.setKeyboardSelectedRow(1, false);
+ presenter.flush();
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(1, true);
assertTrue(presenter.hasKeyboardPrev());
presenter.keyboardPrev();
+ presenter.flush();
assertEquals(0, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(1, false);
view.assertKeyboardSelectedRow(0, true);
@@ -553,19 +581,23 @@
// keyboardPrev at beginning.
assertTrue(presenter.hasKeyboardPrev());
presenter.keyboardPrev();
- view.assertKeyboardSelectedRow(0, false);
+ populatePresenter(presenter);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
pageStart -= increment;
pageSize += increment;
assertEquals(increment - 1, presenter.getKeyboardSelectedRow());
assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
- populatePresenter(presenter);
// keyboardNext in middle.
presenter.setKeyboardSelectedRow(pageSize - 2, false);
+ presenter.flush();
view.assertKeyboardSelectedRow(increment - 1, false);
view.assertKeyboardSelectedRow(pageSize - 2, true);
assertTrue(presenter.hasKeyboardNext());
presenter.keyboardNext();
+ presenter.flush();
assertEquals(pageSize - 1, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(pageSize - 2, false);
view.assertKeyboardSelectedRow(pageSize - 1, true);
@@ -573,17 +605,21 @@
// keyboardNext at end.
assertTrue(presenter.hasKeyboardNext());
presenter.keyboardNext();
- view.assertKeyboardSelectedRow(pageSize - 1, false);
+ populatePresenter(presenter);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
pageSize += increment;
assertEquals(pageSize - increment, presenter.getKeyboardSelectedRow());
assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
- populatePresenter(presenter);
// keyboardPrevPage within range.
presenter.setKeyboardSelectedRow(increment, false);
+ presenter.flush();
view.assertKeyboardSelectedRow(pageSize - increment, false);
view.assertKeyboardSelectedRow(increment, true);
presenter.keyboardPrevPage();
+ presenter.flush();
assertEquals(0, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(increment, false);
view.assertKeyboardSelectedRow(0, true);
@@ -591,59 +627,123 @@
// keyboardPrevPage outside range.
presenter.keyboardPrevPage();
+ populatePresenter(presenter);
+ presenter.flush();
assertEquals(0, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(0, false);
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
pageStart -= increment;
pageSize += increment;
assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
- populatePresenter(presenter);
// keyboardNextPage inside range.
presenter.keyboardNextPage();
+ presenter.flush();
assertEquals(increment, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(increment, true);
assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
- populatePresenter(presenter);
// keyboardNextPage outside range.
presenter.setKeyboardSelectedRow(pageSize - 1, false);
+ presenter.flush();
view.assertKeyboardSelectedRow(increment, false);
view.assertKeyboardSelectedRow(pageSize - 1, true);
presenter.keyboardNextPage();
- view.assertKeyboardSelectedRow(pageSize - 1, false);
+ populatePresenter(presenter);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
pageSize += increment;
assertEquals(pageSize - 1, presenter.getKeyboardSelectedRow());
assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
- populatePresenter(presenter);
// keyboardHome.
presenter.keyboardHome();
- view.assertKeyboardSelectedRow(pageSize - 1, false);
+ populatePresenter(presenter);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
pageSize += pageStart;
pageStart = 0;
assertEquals(0, presenter.getKeyboardSelectedRow());
assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
- populatePresenter(presenter);
// keyboardPrev at first row.
assertFalse(presenter.hasKeyboardPrev());
presenter.keyboardPrev();
+ presenter.flush();
view.assertKeyboardSelectedRowEmpty();
// keyboardEnd.
presenter.keyboardEnd();
- view.assertKeyboardSelectedRow(0, false);
+ populatePresenter(presenter);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
assertEquals(299, presenter.getKeyboardSelectedRow());
assertEquals(new Range(0, 300), presenter.getVisibleRange());
- populatePresenter(presenter);
// keyboardNext at last row.
assertFalse(presenter.hasKeyboardNext());
presenter.keyboardNext();
+ presenter.flush();
view.assertKeyboardSelectedRowEmpty();
}
+ /**
+ * Test that we can detect an infinite loop caused by user code updating the
+ * presenter every time we try to resolve state.
+ */
+ public void testLoopDetection() {
+ HasData<String> listView = new MockHasData<String>();
+ final MockView<String> view = new MockView<String>();
+ final HasDataPresenter<String> presenter = new HasDataPresenter<String>(
+ listView, view, 10, null);
+ presenter.setSelectionModel(new SingleSelectionModel<String>() {
+ @Override
+ public boolean isSelected(String object) {
+ // This selection model triggers a selection change event every time it
+ // is accessed, which puts the presenter in a pending state.
+ SelectionChangeEvent.fire(this);
+ return super.isSelected(object);
+ }
+ });
+
+ populatePresenter(presenter);
+ try {
+ presenter.flush();
+ fail("Expected IllegalStateException because of infinite loop.");
+ } catch (IllegalStateException e) {
+ // Expected.
+ }
+ }
+
+ /**
+ * Test that pending command execute in a finally loop.
+ */
+ public void testPendingCommand() {
+ HasData<String> listView = new MockHasData<String>();
+ final MockView<String> view = new MockView<String>();
+ final HasDataPresenter<String> presenter = new HasDataPresenter<String>(
+ listView, view, 10, null);
+
+ // Data should not be pushed to the view until the pending command executes.
+ populatePresenter(presenter);
+ assertTrue(presenter.hasPendingState());
+ view.assertReplaceAllChildrenCalled(false);
+
+ // The pending command is scheduled. Wait for it to execute.
+ delayTestFinish(5000);
+ Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
+ public void execute() {
+ assertFalse(presenter.hasPendingState());
+ view.assertReplaceAllChildrenCalled(true);
+ finishTest();
+ }
+ });
+ }
+
public void testRedraw() {
HasData<String> listView = new MockHasData<String>();
MockView<String> view = new MockView<String>();
@@ -653,8 +753,9 @@
// Initialize some data.
presenter.setRowCount(10, true);
populatePresenter(presenter);
- assertEquals(10, presenter.getRowData().size());
- assertEquals("test 0", presenter.getRowData().get(0));
+ presenter.flush();
+ assertEquals(10, presenter.getRowDataSize());
+ assertEquals("test 0", presenter.getRowDataValue(0));
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=10");
@@ -662,6 +763,8 @@
// Redraw.
presenter.redraw();
+ view.assertReplaceAllChildrenCalled(false);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=10");
@@ -675,6 +778,7 @@
view, 10, null);
presenter.setVisibleRange(new Range(0, 10));
populatePresenter(presenter);
+ presenter.flush();
// The default is ENABLED.
assertEquals(KeyboardSelectionPolicy.ENABLED,
@@ -687,26 +791,31 @@
// Add a selection model.
MockSelectionModel<String> model = new MockSelectionModel<String>(null);
presenter.setSelectionModel(model);
+ presenter.flush();
assertEquals(0, model.getSelectedSet().size());
// Select an element.
presenter.setKeyboardSelectedRow(5, false);
+ presenter.flush();
assertEquals(1, model.getSelectedSet().size());
assertTrue(model.isSelected("test 5"));
// Select another element.
presenter.setKeyboardSelectedRow(9, false);
+ presenter.flush();
assertEquals(1, model.getSelectedSet().size());
assertTrue(model.isSelected("test 9"));
// Select an element on another page.
presenter.setKeyboardSelectedRow(11, false);
+ presenter.flush();
// Nothing is selected yet because we don't have data.
assertEquals(0, model.getSelectedSet().size());
populatePresenter(presenter);
+ presenter.flush();
// Once data is pushed, the selection model should be populated.
assertEquals(1, model.getSelectedSet().size());
- assertTrue(model.isSelected("test 11"));
+ assertTrue(model.isSelected("test 10"));
}
public void testSetKeyboardSelectedRowChangePage() {
@@ -716,6 +825,7 @@
view, 10, null);
presenter.setVisibleRange(new Range(10, 10));
populatePresenter(presenter);
+ presenter.flush();
// Default policy is CHANGE_PAGE.
assertEquals(KeyboardPagingPolicy.CHANGE_PAGE,
@@ -727,19 +837,28 @@
// Move to middle.
presenter.setKeyboardSelectedRow(1, false);
+ assertEquals("test 11", presenter.getKeyboardSelectedRowValue());
+ presenter.flush();
assertEquals(1, presenter.getKeyboardSelectedRow());
+ assertEquals("test 11", presenter.getKeyboardSelectedRowValue());
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(1, true);
// Move to same row (should not early out).
presenter.setKeyboardSelectedRow(1, false);
+ assertEquals("test 11", presenter.getKeyboardSelectedRowValue());
+ presenter.flush();
assertEquals(1, presenter.getKeyboardSelectedRow());
+ assertEquals("test 11", presenter.getKeyboardSelectedRowValue());
view.assertKeyboardSelectedRow(1, false);
view.assertKeyboardSelectedRow(1, true);
// Move to last row.
presenter.setKeyboardSelectedRow(9, false);
+ assertEquals("test 19", presenter.getKeyboardSelectedRowValue());
+ presenter.flush();
assertEquals(9, presenter.getKeyboardSelectedRow());
+ assertEquals("test 19", presenter.getKeyboardSelectedRowValue());
view.assertKeyboardSelectedRow(1, false);
view.assertKeyboardSelectedRow(9, true);
assertEquals(10, presenter.getVisibleRange().getStart());
@@ -747,19 +866,25 @@
// Move to next page.
presenter.setKeyboardSelectedRow(10, false);
+ populatePresenter(presenter);
+ assertNull(presenter.getKeyboardSelectedRowValue());
+ presenter.flush();
assertEquals(0, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(9, false);
- // Select is not fired because there is no row data yet.
+ assertEquals("test 20", presenter.getKeyboardSelectedRowValue());
+ view.assertReplaceAllChildrenCalled(true);
view.assertKeyboardSelectedRowEmpty();
assertEquals(20, presenter.getVisibleRange().getStart());
assertEquals(10, presenter.getVisibleRange().getLength());
- populatePresenter(presenter);
// Negative index.
presenter.setKeyboardSelectedRow(-1, false);
+ populatePresenter(presenter);
+ assertNull(presenter.getKeyboardSelectedRowValue());
+ presenter.flush();
assertEquals(9, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(0, false);
- // Select is not fired because there is no row data yet.
+ assertEquals("test 19", presenter.getKeyboardSelectedRowValue());
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
assertEquals(10, presenter.getVisibleRange().getStart());
assertEquals(10, presenter.getVisibleRange().getLength());
}
@@ -771,6 +896,7 @@
view, 10, null);
presenter.setVisibleRange(new Range(10, 10));
populatePresenter(presenter);
+ presenter.flush();
presenter.setKeyboardPagingPolicy(KeyboardPagingPolicy.CURRENT_PAGE);
// Default to row 0.
@@ -779,30 +905,35 @@
// Negative index (should remain at index 0).
presenter.setKeyboardSelectedRow(-1, false);
+ presenter.flush();
assertEquals(0, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(0, true);
// Move to middle.
presenter.setKeyboardSelectedRow(1, false);
+ presenter.flush();
assertEquals(1, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(1, true);
// Move to same row (should not early out).
presenter.setKeyboardSelectedRow(1, false);
+ presenter.flush();
assertEquals(1, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(1, false);
view.assertKeyboardSelectedRow(1, true);
// Move to last row.
presenter.setKeyboardSelectedRow(9, false);
+ presenter.flush();
assertEquals(9, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(1, false);
view.assertKeyboardSelectedRow(9, true);
// Move to next page (confined to page).
presenter.setKeyboardSelectedRow(10, false);
+ presenter.flush();
assertEquals(9, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(9, false);
view.assertKeyboardSelectedRow(9, true);
@@ -818,13 +949,17 @@
view, 10, null);
presenter.setVisibleRange(new Range(10, 10));
populatePresenter(presenter);
+ presenter.flush();
presenter.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
assertEquals(-1, presenter.getKeyboardSelectedRow());
+ assertNull(presenter.getKeyboardSelectedRowValue());
view.assertKeyboardSelectedRowEmpty();
presenter.setKeyboardSelectedRow(1, false);
+ presenter.flush();
assertEquals(-1, presenter.getKeyboardSelectedRow());
+ assertNull(presenter.getKeyboardSelectedRowValue());
view.assertKeyboardSelectedRowEmpty();
}
@@ -835,6 +970,7 @@
view, 10, null);
presenter.setVisibleRange(new Range(10, 10));
populatePresenter(presenter);
+ presenter.flush();
presenter.setKeyboardPagingPolicy(KeyboardPagingPolicy.INCREASE_RANGE);
int pageSize = presenter.getVisibleRange().getLength();
@@ -844,18 +980,21 @@
// Move to middle.
presenter.setKeyboardSelectedRow(1, false);
+ presenter.flush();
assertEquals(1, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(0, false);
view.assertKeyboardSelectedRow(1, true);
// Move to same row (should not early out).
presenter.setKeyboardSelectedRow(1, false);
+ presenter.flush();
assertEquals(1, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(1, false);
view.assertKeyboardSelectedRow(1, true);
// Move to last row.
presenter.setKeyboardSelectedRow(9, false);
+ presenter.flush();
assertEquals(9, presenter.getKeyboardSelectedRow());
view.assertKeyboardSelectedRow(1, false);
view.assertKeyboardSelectedRow(9, true);
@@ -864,20 +1003,22 @@
// Move to next page.
presenter.setKeyboardSelectedRow(10, false);
+ populatePresenter(presenter);
+ presenter.flush();
assertEquals(10, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(9, false);
- // Select is not fired because there is no row data yet.
+ view.assertReplaceAllChildrenCalled(true);
view.assertKeyboardSelectedRowEmpty();
assertEquals(10, presenter.getVisibleRange().getStart());
pageSize += HasDataPresenter.PAGE_INCREMENT;
assertEquals(pageSize, presenter.getVisibleRange().getLength());
- populatePresenter(presenter);
// Negative index near index 0.
presenter.setKeyboardSelectedRow(-1, false);
+ populatePresenter(presenter);
+ presenter.flush();
assertEquals(9, presenter.getKeyboardSelectedRow());
- view.assertKeyboardSelectedRow(10, false);
- // Select is not fired because there is no row data yet.
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertKeyboardSelectedRowEmpty();
assertEquals(0, presenter.getVisibleRange().getStart());
pageSize += 10;
assertEquals(pageSize, presenter.getVisibleRange().getLength());
@@ -894,6 +1035,7 @@
presenter.setRowCount(100, true);
assertEquals(100, presenter.getRowCount());
assertTrue(presenter.isRowCountExact());
+ presenter.flush();
view.assertLoadingState(LoadingState.LOADING);
// Set size to 0, but not exact. The state is loading until we know there is
@@ -901,12 +1043,14 @@
presenter.setRowCount(0, false);
assertEquals(0, presenter.getRowCount());
assertFalse(presenter.isRowCountExact());
+ presenter.flush();
view.assertLoadingState(LoadingState.LOADING);
// Set size to 0 and exact. Now we know the list is empty.
presenter.setRowCount(0, true);
assertEquals(0, presenter.getRowCount());
assertTrue(presenter.isRowCountExact());
+ presenter.flush();
view.assertLoadingState(LoadingState.EMPTY);
}
@@ -936,8 +1080,10 @@
presenter.setVisibleRange(new Range(0, 10));
assertEquals(new Range(0, 10), presenter.getVisibleRange());
populatePresenter(presenter);
- assertEquals(10, presenter.getRowData().size());
- assertEquals("test 0", presenter.getRowData().get(0));
+ presenter.flush();
+ assertEquals(10, presenter.getRowDataSize());
+ assertEquals("test 0", presenter.getRowDataValue(0));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=10");
@@ -948,7 +1094,8 @@
assertEquals(8, presenter.getRowCount());
assertTrue(presenter.isRowCountExact());
assertEquals(new Range(0, 10), presenter.getVisibleRange());
- assertEquals(8, presenter.getRowData().size());
+ assertEquals(8, presenter.getRowDataSize());
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=8");
@@ -961,13 +1108,15 @@
HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
view, 10, null);
presenter.setVisibleRange(new Range(5, 10));
+ presenter.flush();
+ view.assertLastHtml("start=5,size=0");
view.assertLoadingState(LoadingState.LOADING);
// Page range same as data range.
List<String> expectedData = createData(5, 10);
presenter.setRowData(5, createData(5, 10));
- assertEquals(10, presenter.getRowData().size());
- assertEquals(expectedData, presenter.getRowData());
+ assertPresenterRowData(expectedData, presenter);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=5,size=10");
@@ -978,8 +1127,8 @@
expectedData.set(2, "test 100");
expectedData.set(3, "test 101");
presenter.setRowData(7, createData(100, 2));
- assertEquals(10, presenter.getRowData().size());
- assertEquals(expectedData, presenter.getRowData());
+ assertPresenterRowData(expectedData, presenter);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(false);
view.assertReplaceChildrenCalled(true);
view.assertLastHtml("start=7,size=2");
@@ -990,8 +1139,8 @@
expectedData.set(0, "test 202");
expectedData.set(1, "test 203");
presenter.setRowData(3, createData(200, 4));
- assertEquals(10, presenter.getRowData().size());
- assertEquals(expectedData, presenter.getRowData());
+ assertPresenterRowData(expectedData, presenter);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(false);
view.assertReplaceChildrenCalled(true);
view.assertLastHtml("start=5,size=2");
@@ -1002,8 +1151,8 @@
expectedData.set(8, "test 300");
expectedData.set(9, "test 301");
presenter.setRowData(13, createData(300, 4));
- assertEquals(10, presenter.getRowData().size());
- assertEquals(expectedData, presenter.getRowData());
+ assertPresenterRowData(expectedData, presenter);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(false);
view.assertReplaceChildrenCalled(true);
view.assertLastHtml("start=13,size=2");
@@ -1013,8 +1162,8 @@
// Data range contains page range.
expectedData = createData(400, 20).subList(2, 12);
presenter.setRowData(3, createData(400, 20));
- assertEquals(10, presenter.getRowData().size());
- assertEquals(expectedData, presenter.getRowData());
+ assertPresenterRowData(expectedData, presenter);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=5,size=10");
@@ -1033,15 +1182,18 @@
// Set the initial data size.
presenter.setRowCount(10, true);
+ presenter.flush();
view.assertLoadingState(LoadingState.LOADING);
// Set the data within the range.
presenter.setRowData(0, createData(0, 10));
+ presenter.flush();
view.assertLoadingState(LoadingState.LOADED);
// Set the data past the range.
presenter.setRowData(5, createData(5, 10));
assertEquals(15, presenter.getRowCount());
+ presenter.flush();
view.assertLoadingState(LoadingState.LOADED);
}
@@ -1057,10 +1209,12 @@
// Set the initial data size.
presenter.setRowCount(10, true);
+ presenter.flush();
view.assertLoadingState(LoadingState.LOADING);
// Set an empty list of row values.
presenter.setRowData(0, createData(0, 0));
+ presenter.flush();
view.assertLoadingState(LoadingState.LOADING);
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
@@ -1072,13 +1226,15 @@
HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
view, 10, null);
presenter.setVisibleRange(new Range(5, 10));
+ presenter.flush();
+ view.assertLastHtml("start=5,size=0");
view.assertLoadingState(LoadingState.LOADING);
// Page range same as data range.
List<String> expectedData = createData(5, 10);
presenter.setRowData(5, createData(5, 10));
- assertEquals(10, presenter.getRowData().size());
- assertEquals(expectedData, presenter.getRowData());
+ assertPresenterRowData(expectedData, presenter);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=5,size=10");
@@ -1086,8 +1242,8 @@
// Data range past page end.
presenter.setRowData(15, createData(15, 5));
- assertEquals(10, presenter.getRowData().size());
- assertEquals(expectedData, presenter.getRowData());
+ assertPresenterRowData(expectedData, presenter);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(false);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml(null);
@@ -1095,8 +1251,8 @@
// Data range before page start.
presenter.setRowData(0, createData(0, 5));
- assertEquals(10, presenter.getRowData().size());
- assertEquals(expectedData, presenter.getRowData());
+ assertPresenterRowData(expectedData, presenter);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(false);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml(null);
@@ -1104,6 +1260,47 @@
}
/**
+ * Test that modifying more than 30% of the rows forces a full redraw.
+ */
+ public void testSetRowValuesRequiresRedraw() {
+ HasData<String> listView = new MockHasData<String>();
+ MockView<String> view = new MockView<String>();
+ HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
+ view, 100, null);
+
+ // Initialize 100% of the rows.
+ populatePresenter(presenter);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertReplaceChildrenCalled(false);
+
+ // Modify 30% of the rows.
+ presenter.setRowData(0, createData(0, 30));
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(false);
+ view.assertReplaceChildrenCalled(true);
+
+ // Modify 31% of the rows.
+ presenter.setRowData(0, createData(0, 31));
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertReplaceChildrenCalled(false);
+
+ /*
+ * Modify 4 rows in a 5 row table. This should NOT require a redraw because
+ * it is less than the minimum threshold.
+ */
+ presenter.setRowCount(5, true);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertReplaceChildrenCalled(false);
+ presenter.setRowData(0, createData(0, 4));
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(false);
+ view.assertReplaceChildrenCalled(true);
+ }
+
+ /**
* As an optimization, the presenter does not replace the rendered string if
* the rendered string is identical to the previously rendered string. This is
* useful for tables that refresh on an interval.
@@ -1118,6 +1315,7 @@
// Initialize some data.
presenter.setVisibleRange(new Range(0, 10));
presenter.setRowData(0, createData(0, 10));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=10");
@@ -1125,6 +1323,7 @@
// Set the same data over the entire range.
presenter.setRowData(0, createData(0, 10));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(false);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml(null);
@@ -1149,14 +1348,55 @@
expectedData.add(0, null);
presenter.setVisibleRange(new Range(0, 10));
presenter.setRowData(5, createData(5, 3));
- assertEquals(8, presenter.getRowData().size());
- assertEquals(expectedData, presenter.getRowData());
+ assertPresenterRowData(expectedData, presenter);
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=8");
view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
}
+ public void testSetSelectionModel() {
+ HasData<String> listView = new MockHasData<String>();
+ MockView<String> view = new MockView<String>();
+ HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
+ view, 10, null);
+ assertNull(presenter.getSelectionModel());
+
+ // Initialize some data.
+ presenter.setVisibleRange(new Range(0, 10));
+ populatePresenter(presenter);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(true);
+ view.assertReplaceChildrenCalled(false);
+ view.assertLastHtml("start=0,size=10");
+
+ // Set the selection model.
+ SelectionModel<String> model = new MockSelectionModel<String>(null);
+ model.setSelected("test 0", true);
+ presenter.setSelectionModel(model);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(false);
+ view.assertReplaceChildrenCalled(true);
+ view.assertLastHtml("start=0,size=1");
+
+ // Select something.
+ model.setSelected("test 2", true);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(false);
+ view.assertReplaceChildrenCalled(true);
+ view.assertLastHtml("start=2,size=1");
+
+ // Set selection model to null.
+ presenter.setSelectionModel(null);
+ presenter.flush();
+ view.assertReplaceAllChildrenCalled(false);
+ view.assertReplaceChildrenCalled(true);
+ view.assertLastHtml("start=0,size=1");
+ view.assertLastHtml("start=2,size=1");
+ view.assertLastHtml(null);
+ }
+
public void testSetVisibleRange() {
HasData<String> listView = new MockHasData<String>();
MockView<String> view = new MockView<String>();
@@ -1166,7 +1406,8 @@
// Set the range the first time.
presenter.setVisibleRange(new Range(0, 100));
assertEquals(new Range(0, 100), presenter.getVisibleRange());
- assertEquals(0, presenter.getRowData().size());
+ assertEquals(0, presenter.getRowDataSize());
+ presenter.flush();
view.assertReplaceAllChildrenCalled(false);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml(null);
@@ -1175,11 +1416,28 @@
// Set the range to the same value.
presenter.setVisibleRange(new Range(0, 100));
assertEquals(new Range(0, 100), presenter.getVisibleRange());
- assertEquals(0, presenter.getRowData().size());
+ assertEquals(0, presenter.getRowDataSize());
+ presenter.flush();
view.assertReplaceAllChildrenCalled(false);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml(null);
view.assertLoadingState(LoadingState.LOADING);
+
+ // Set the start to a negative value.
+ try {
+ presenter.setVisibleRange(new Range(-1, 100));
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
+
+ // Set the length to a negative value.
+ try {
+ presenter.setVisibleRange(new Range(0, -100));
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ // Expected.
+ }
}
public void testSetVisibleRangeAndClearDataDifferentRange() {
@@ -1200,7 +1458,8 @@
presenter.setVisibleRange(new Range(5, 10));
presenter.setRowData(5, createData(5, 10));
assertEquals(new Range(5, 10), presenter.getVisibleRange());
- assertEquals(10, presenter.getRowData().size());
+ assertEquals(10, presenter.getRowDataSize());
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=5,size=10");
@@ -1210,7 +1469,8 @@
// Set a different range.
presenter.setVisibleRangeAndClearData(new Range(0, 10), false);
assertEquals(new Range(0, 10), presenter.getVisibleRange());
- assertEquals(0, presenter.getRowData().size());
+ assertEquals(0, presenter.getRowDataSize());
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=0");
@@ -1235,7 +1495,8 @@
// Set some initial data.
presenter.setRowData(0, createData(0, 10));
assertEquals(new Range(0, 10), presenter.getVisibleRange());
- assertEquals(10, presenter.getRowData().size());
+ assertEquals(10, presenter.getRowDataSize());
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=10");
@@ -1245,7 +1506,8 @@
// Set the same range.
presenter.setVisibleRangeAndClearData(new Range(0, 10), false);
assertEquals(new Range(0, 10), presenter.getVisibleRange());
- assertEquals(0, presenter.getRowData().size());
+ assertEquals(0, presenter.getRowDataSize());
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=0");
@@ -1270,7 +1532,8 @@
// Set some initial data.
presenter.setRowData(0, createData(0, 10));
assertEquals(new Range(0, 10), presenter.getVisibleRange());
- assertEquals(10, presenter.getRowData().size());
+ assertEquals(10, presenter.getRowDataSize());
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=10");
@@ -1280,7 +1543,8 @@
// Set the same range.
presenter.setVisibleRangeAndClearData(new Range(0, 10), true);
assertEquals(new Range(0, 10), presenter.getVisibleRange());
- assertEquals(0, presenter.getRowData().size());
+ assertEquals(0, presenter.getRowDataSize());
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=0");
@@ -1298,8 +1562,9 @@
presenter.setVisibleRange(new Range(0, 10));
assertEquals(new Range(0, 10), presenter.getVisibleRange());
presenter.setRowData(0, createData(0, 10));
- assertEquals(10, presenter.getRowData().size());
- assertEquals("test 0", presenter.getRowData().get(0));
+ assertEquals(10, presenter.getRowDataSize());
+ assertEquals("test 0", presenter.getRowDataValue(0));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=10");
@@ -1308,8 +1573,9 @@
// Decrease the page size.
presenter.setVisibleRange(new Range(0, 8));
assertEquals(new Range(0, 8), presenter.getVisibleRange());
- assertEquals(8, presenter.getRowData().size());
- assertEquals("test 0", presenter.getRowData().get(0));
+ assertEquals(8, presenter.getRowDataSize());
+ assertEquals("test 0", presenter.getRowDataValue(0));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=8");
@@ -1326,8 +1592,9 @@
presenter.setVisibleRange(new Range(10, 30));
assertEquals(new Range(10, 30), presenter.getVisibleRange());
presenter.setRowData(10, createData(0, 10));
- assertEquals(10, presenter.getRowData().size());
- assertEquals("test 0", presenter.getRowData().get(0));
+ assertEquals(10, presenter.getRowDataSize());
+ assertEquals("test 0", presenter.getRowDataValue(0));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=10,size=10");
@@ -1336,10 +1603,11 @@
// Decrease the start index.
presenter.setVisibleRange(new Range(8, 30));
assertEquals(new Range(8, 30), presenter.getVisibleRange());
- assertEquals(12, presenter.getRowData().size());
- assertEquals(null, presenter.getRowData().get(0));
- assertEquals(null, presenter.getRowData().get(1));
- assertEquals("test 0", presenter.getRowData().get(2));
+ assertEquals(12, presenter.getRowDataSize());
+ assertEquals(null, presenter.getRowDataValue(0));
+ assertEquals(null, presenter.getRowDataValue(1));
+ assertEquals("test 0", presenter.getRowDataValue(2));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=8,size=12");
@@ -1356,8 +1624,9 @@
presenter.setVisibleRange(new Range(0, 10));
assertEquals(new Range(0, 10), presenter.getVisibleRange());
presenter.setRowData(0, createData(0, 10));
- assertEquals(10, presenter.getRowData().size());
- assertEquals("test 0", presenter.getRowData().get(0));
+ assertEquals(10, presenter.getRowDataSize());
+ assertEquals("test 0", presenter.getRowDataValue(0));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=10");
@@ -1366,8 +1635,9 @@
// Increase the page size.
presenter.setVisibleRange(new Range(0, 20));
assertEquals(new Range(0, 20), presenter.getVisibleRange());
- assertEquals(10, presenter.getRowData().size());
- assertEquals("test 0", presenter.getRowData().get(0));
+ assertEquals(10, presenter.getRowDataSize());
+ assertEquals("test 0", presenter.getRowDataValue(0));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(false);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml(null);
@@ -1384,8 +1654,9 @@
presenter.setVisibleRange(new Range(0, 20));
assertEquals(new Range(0, 20), presenter.getVisibleRange());
presenter.setRowData(0, createData(0, 10));
- assertEquals(10, presenter.getRowData().size());
- assertEquals("test 0", presenter.getRowData().get(0));
+ assertEquals(10, presenter.getRowDataSize());
+ assertEquals("test 0", presenter.getRowDataValue(0));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=0,size=10");
@@ -1394,8 +1665,9 @@
// Increase the start index.
presenter.setVisibleRange(new Range(2, 20));
assertEquals(new Range(2, 20), presenter.getVisibleRange());
- assertEquals(8, presenter.getRowData().size());
- assertEquals("test 2", presenter.getRowData().get(0));
+ assertEquals(8, presenter.getRowDataSize());
+ assertEquals("test 2", presenter.getRowDataValue(0));
+ presenter.flush();
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
view.assertLastHtml("start=2,size=8");
@@ -1417,98 +1689,64 @@
}
/**
- * If the cells depend on selection, the cells should be replaced.
+ * Test that the view is correctly updated if we move the page start back and
+ * forth in the same render loop.
*/
- public void testSetSelectionModelDependOnSelection() {
+ public void testSetVisibleRangeResetPageStart() {
HasData<String> listView = new MockHasData<String>();
MockView<String> view = new MockView<String>();
- view.setDependsOnSelection(true);
HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
view, 10, null);
- assertNull(presenter.getSelectionModel());
- // Initialize some data.
- presenter.setVisibleRange(new Range(0, 10));
+ // Initialize the view.
populatePresenter(presenter);
+ presenter.flush();
+ view.assertLastHtml("start=0,size=10");
view.assertReplaceAllChildrenCalled(true);
view.assertReplaceChildrenCalled(false);
- view.assertLastHtml("start=0,size=10");
- // Set the selection model.
- SelectionModel<String> model = new MockSelectionModel<String>(null);
- model.setSelected("test 0", true);
- presenter.setSelectionModel(model);
- view.assertReplaceAllChildrenCalled(true);
- view.assertReplaceChildrenCalled(false);
- view.assertLastHtml("start=0,size=10");
- view.assertOnUpdateSelectionFired(true);
- view.assertSelectedRows();
-
- // Select something.
- model.setSelected("test 2", true);
- view.assertReplaceAllChildrenCalled(true);
- view.assertReplaceChildrenCalled(false);
- view.assertLastHtml("start=0,size=10");
- view.assertOnUpdateSelectionFired(true);
- view.assertSelectedRows();
-
- // Set selection model to null.
- presenter.setSelectionModel(null);
- view.assertReplaceAllChildrenCalled(true);
- view.assertReplaceChildrenCalled(false);
- view.assertLastHtml("start=0,size=10");
- view.assertOnUpdateSelectionFired(true);
- view.assertSelectedRows();
+ // Move pageStart to 2, then back to 0.
+ presenter.setVisibleRange(new Range(2, 8));
+ presenter.setVisibleRange(new Range(0, 10));
+ presenter.flush();
+ view.assertLastHtml("start=0,size=2");
+ view.assertReplaceAllChildrenCalled(false);
+ view.assertReplaceChildrenCalled(true);
}
/**
- * If the cells do not depend on selection, the view should be told to update
- * the cell container element.
+ * Assert that the expected List of values matches the row data in the
+ * specified {@link HasDataPresenter}.
+ *
+ * @param <T> the data type
+ * @param expected the expected values
+ * @param presenter the presenter
*/
- public void testSetSelectionModelDoesNotDependOnSelection() {
- HasData<String> listView = new MockHasData<String>();
- MockView<String> view = new MockView<String>();
- HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
- view, 10, null);
- assertNull(presenter.getSelectionModel());
+ private <T> void assertPresenterRowData(List<T> expected,
+ HasDataPresenter<T> presenter) {
+ assertEquals(expected.size(), presenter.getRowDataSize());
+ for (int i = 0; i < expected.size(); i++) {
+ assertEquals(expected.get(i), presenter.getRowDataValue(i));
+ }
+ }
- // Initialize some data.
- presenter.setVisibleRange(new Range(0, 10));
- populatePresenter(presenter);
- view.assertReplaceAllChildrenCalled(true);
- view.assertReplaceChildrenCalled(false);
- view.assertLastHtml("start=0,size=10");
-
- // Set the selection model.
- SelectionModel<String> model = new MockSelectionModel<String>(null);
- model.setSelected("test 0", true);
- presenter.setSelectionModel(model);
- view.assertReplaceAllChildrenCalled(false);
- view.assertReplaceChildrenCalled(false);
- view.assertLastHtml(null);
- view.assertOnUpdateSelectionFired(true);
- view.assertSelectedRows(0);
-
- // Select something.
- model.setSelected("test 2", true);
- view.assertReplaceAllChildrenCalled(false);
- view.assertReplaceChildrenCalled(false);
- view.assertLastHtml(null);
- view.assertOnUpdateSelectionFired(true);
- view.assertSelectedRows(0, 2);
-
- // Set selection model to null.
- presenter.setSelectionModel(null);
- view.assertReplaceAllChildrenCalled(false);
- view.assertReplaceChildrenCalled(false);
- view.assertLastHtml(null);
- view.assertOnUpdateSelectionFired(true);
- view.assertSelectedRows();
+ /**
+ * Assert that the specified set contains specified values in order.
+ *
+ * @param <T> the data type
+ * @param list the list to check
+ * @param values the expected values
+ */
+ private <T> void assertListContains(List<T> list, T... values) {
+ assertEquals(values.length, list.size());
+ for (int i = 0; i < values.length; i++) {
+ assertEquals(values[i], list.get(i));
+ }
}
/**
* Create a list of data for testing.
- *
+ *
* @param start the start index
* @param length the length
* @return a list of data
@@ -1523,7 +1761,7 @@
/**
* Populate the entire range of a presenter.
- *
+ *
* @param presenter the presenter
*/
private void populatePresenter(HasDataPresenter<String> presenter) {