From: Henrik Paul Date: Tue, 17 Dec 2013 09:08:25 +0000 (+0200) Subject: Grid supports data set changes (#12645) X-Git-Tag: 7.2.0.beta1~108^2~21 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=7670a020c7447d7ae2fe2c3e1fb1fa966b138e65;p=vaadin-framework.git Grid supports data set changes (#12645) Change-Id: I5ceb52dea079f48b0065c1b2dbdc35b30fe8c4ee --- diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java index ff8847ea44..127eb80696 100644 --- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java +++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java @@ -22,7 +22,7 @@ import java.util.List; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.vaadin.client.Profiler; -import com.vaadin.client.ui.grid.Range; +import com.vaadin.shared.ui.grid.Range; /** * Base implementation for data sources that fetch data from a remote system. @@ -238,4 +238,86 @@ public abstract class AbstractRemoteDataSource implements DataSource { Profiler.leave("AbstractRemoteDataSource.setRowData"); } + + /** + * Informs this data source that the server has removed data. + * + * @param firstRowIndex + * the index of the first removed row + * @param count + * the number of removed rows, starting from + * firstRowIndex + */ + protected void removeRowData(int firstRowIndex, int count) { + Profiler.enter("AbstractRemoteDataSource.removeRowData"); + + // pack the cached data + for (int i = 0; i < count; i++) { + Integer oldIndex = Integer.valueOf(firstRowIndex + count + i); + if (rowCache.containsKey(oldIndex)) { + Integer newIndex = Integer.valueOf(firstRowIndex + i); + rowCache.put(newIndex, rowCache.remove(oldIndex)); + } + } + + Range removedRange = Range.withLength(firstRowIndex, count); + if (removedRange.intersects(cached)) { + Range[] partitions = cached.partitionWith(removedRange); + Range remainsBefore = partitions[0]; + Range transposedRemainsAfter = partitions[2].offsetBy(-removedRange + .length()); + cached = remainsBefore.combineWith(transposedRemainsAfter); + } + estimatedSize -= count; + dataChangeHandler.dataRemoved(firstRowIndex, count); + checkCacheCoverage(); + + Profiler.leave("AbstractRemoteDataSource.removeRowData"); + } + + /** + * Informs this data source that new data has been inserted from the server. + * + * @param firstRowIndex + * the destination index of the new row data + * @param count + * the number of rows inserted + */ + protected void insertRowData(int firstRowIndex, int count) { + Profiler.enter("AbstractRemoteDataSource.insertRowData"); + + if (cached.contains(firstRowIndex)) { + int oldCacheEnd = cached.getEnd(); + /* + * We need to invalidate the cache from the inserted row onwards, + * since the cache wants to be a contiguous range. It doesn't + * support holes. + * + * If holes were supported, we could shift the higher part of + * "cached" and leave a hole the size of "count" in the middle. + */ + cached = cached.splitAt(firstRowIndex)[0]; + + for (int i = firstRowIndex; i < oldCacheEnd; i++) { + rowCache.remove(Integer.valueOf(i)); + } + } + + else if (firstRowIndex < cached.getStart()) { + Range oldCached = cached; + cached = cached.offsetBy(count); + + for (int i = 0; i < rowCache.size(); i++) { + Integer oldIndex = Integer.valueOf(oldCached.getEnd() - i); + Integer newIndex = Integer.valueOf(cached.getEnd() - i); + rowCache.put(newIndex, rowCache.remove(oldIndex)); + } + } + + estimatedSize += count; + dataChangeHandler.dataAdded(firstRowIndex, count); + checkCacheCoverage(); + + Profiler.leave("AbstractRemoteDataSource.insertRowData"); + } } diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java index 1785fc62c2..4d22c10197 100644 --- a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java +++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java @@ -56,6 +56,16 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector { public void setRowData(int firstRow, List rows) { dataSource.setRowData(firstRow, rows); } + + @Override + public void removeRowData(int firstRow, int count) { + dataSource.removeRowData(firstRow, count); + } + + @Override + public void insertRowData(int firstRow, int count) { + dataSource.insertRowData(firstRow, count); + } }); } diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java index 20a187e1a5..a395038890 100644 --- a/client/src/com/vaadin/client/ui/grid/Escalator.java +++ b/client/src/com/vaadin/client/ui/grid/Escalator.java @@ -35,12 +35,12 @@ import com.google.gwt.dom.client.NodeList; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.UIObject; import com.google.gwt.user.client.ui.Widget; -import com.google.web.bindery.event.shared.HandlerRegistration; import com.vaadin.client.Profiler; import com.vaadin.client.Util; import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle; @@ -50,6 +50,7 @@ import com.vaadin.client.ui.grid.PositionFunction.TranslatePosition; import com.vaadin.client.ui.grid.PositionFunction.WebkitTranslate3DPosition; import com.vaadin.client.ui.grid.ScrollbarBundle.HorizontalScrollbarBundle; import com.vaadin.client.ui.grid.ScrollbarBundle.VerticalScrollbarBundle; +import com.vaadin.shared.ui.grid.Range; import com.vaadin.shared.util.SharedUtil; /*- @@ -1633,6 +1634,8 @@ public class Escalator extends Widget { return; } + boolean rowsWereMoved = false; + final int topRowPos = getRowTop(visualRowOrder.getFirst()); // TODO [[mpixscroll]] final int scrollTop = tBodyScrollTop; @@ -1655,6 +1658,8 @@ public class Escalator extends Widget { final int logicalRowIndex = scrollTop / ROW_HEIGHT_PX; moveAndUpdateEscalatorRows(Range.between(start, end), 0, logicalRowIndex); + + rowsWereMoved = (rowsToMove != 0); } else if (viewportOffset + ROW_HEIGHT_PX <= 0) { @@ -1723,9 +1728,13 @@ public class Escalator extends Widget { .get(1)) - 1; moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex); } + + rowsWereMoved = (rowsToMove != 0); } - fireRowVisibilityChangeEvent(); + if (rowsWereMoved) { + fireRowVisibilityChangeEvent(); + } } @Override @@ -1805,6 +1814,8 @@ public class Escalator extends Widget { setRowPosition(tr, 0, rowTop); rowTop += ROW_HEIGHT_PX; } + + fireRowVisibilityChangeEvent(); } return addedRows; } @@ -1919,8 +1930,6 @@ public class Escalator extends Widget { newRowTop += ROW_HEIGHT_PX; } } - - fireRowVisibilityChangeEvent(); } /** @@ -3181,9 +3190,15 @@ public class Escalator extends Widget { */ @Override public void setHeight(final String height) { + final int escalatorRowsBefore = body.visualRowOrder.size(); + super.setHeight(height != null && !height.isEmpty() ? height : DEFAULT_HEIGHT); recalculateElementSizes(); + + if (escalatorRowsBefore != body.visualRowOrder.size()) { + fireRowVisibilityChangeEvent(); + } } /** @@ -3437,26 +3452,30 @@ public class Escalator extends Widget { * Adds an event handler that gets notified when the range of visible rows * changes e.g. because of scrolling. * - * @param rowVisibilityChangeHadler + * @param rowVisibilityChangeHandler * the event handler * @return a handler registration for the added handler */ public HandlerRegistration addRowVisibilityChangeHandler( - RowVisibilityChangeHandler rowVisibilityChangeHadler) { - return addHandler(rowVisibilityChangeHadler, + RowVisibilityChangeHandler rowVisibilityChangeHandler) { + return addHandler(rowVisibilityChangeHandler, RowVisibilityChangeEvent.TYPE); } private void fireRowVisibilityChangeEvent() { - int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder - .getFirst()); - int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder - .getLast()) + 1; + if (!body.visualRowOrder.isEmpty()) { + int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder + .getFirst()); + int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder + .getLast()) + 1; - int visibleRowCount = visibleRangeEnd - visibleRangeStart; + int visibleRowCount = visibleRangeEnd - visibleRangeStart; - fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, - visibleRowCount)); + fireEvent(new RowVisibilityChangeEvent(visibleRangeStart, + visibleRowCount)); + } else { + fireEvent(new RowVisibilityChangeEvent(0, 0)); + } } /** diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java index 2dbb0275cd..7f8ab408a9 100644 --- a/client/src/com/vaadin/client/ui/grid/Grid.java +++ b/client/src/com/vaadin/client/ui/grid/Grid.java @@ -21,6 +21,7 @@ import java.util.List; import com.google.gwt.core.shared.GWT; import com.google.gwt.dom.client.Element; +import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.HasVisibility; import com.google.gwt.user.client.ui.Widget; @@ -73,6 +74,10 @@ public class Grid extends Composite { */ private final List> columns = new ArrayList>(); + /** + * The datasource currently in use. Note: it is null + * on initialization, but not after that. + */ private DataSource dataSource; /** @@ -1211,4 +1216,14 @@ public class Grid extends Composite { public GridColumn getLastFrozenColumn() { return lastFrozenColumn; } + + public HandlerRegistration addRowVisibilityChangeHandler( + RowVisibilityChangeHandler handler) { + /* + * Reusing Escalator's RowVisibilityChangeHandler, since a scroll + * concept is too abstract. e.g. the event needs to be re-sent when the + * widget is resized. + */ + return escalator.addRowVisibilityChangeHandler(handler); + } } diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java index ffe1444942..f04326c7e6 100644 --- a/client/src/com/vaadin/client/ui/grid/GridConnector.java +++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java @@ -30,6 +30,7 @@ import com.vaadin.shared.ui.Connect; import com.vaadin.shared.ui.grid.ColumnGroupRowState; import com.vaadin.shared.ui.grid.ColumnGroupState; import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; /** @@ -82,6 +83,21 @@ public class GridConnector extends AbstractComponentConnector { return (GridState) super.getState(); } + @Override + protected void init() { + super.init(); + getWidget().addRowVisibilityChangeHandler( + new RowVisibilityChangeHandler() { + @Override + public void onRowVisibilityChange( + RowVisibilityChangeEvent event) { + getRpcProxy(GridServerRpc.class).setVisibleRows( + event.getFirstVisibleRow(), + event.getVisibleRowCount()); + } + }); + } + @Override public void onStateChanged(StateChangeEvent stateChangeEvent) { super.onStateChanged(stateChangeEvent); diff --git a/client/src/com/vaadin/client/ui/grid/Range.java b/client/src/com/vaadin/client/ui/grid/Range.java deleted file mode 100644 index 634a182421..0000000000 --- a/client/src/com/vaadin/client/ui/grid/Range.java +++ /dev/null @@ -1,378 +0,0 @@ -/* - * Copyright 2000-2013 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package com.vaadin.client.ui.grid; - -/** - * An immutable representation of a range, marked by start and end points. - *

- * The range is treated as inclusive at the start, and exclusive at the end. - * I.e. the range [0..1[ has the length 1, and represents one integer: 0. - *

- * The range is considered {@link #isEmpty() empty} if the start is the same as - * the end. - * - * @since 7.2 - * @author Vaadin Ltd - */ -public final class Range { - private final int start; - private final int end; - - /** - * Creates a range object representing a single integer. - * - * @param integer - * the number to represent as a range - * @return the range represented by integer - */ - public static Range withOnly(final int integer) { - return new Range(integer, integer + 1); - } - - /** - * Creates a range between two integers. - *

- * The range start is inclusive and the end is exclusive. - * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4, - * but not 5. - * - * @param start - * the start of the the range, inclusive - * @param end - * the end of the range, exclusive - * @return a range representing [start..end[ - * @throws IllegalArgumentException - * if start > end - */ - public static Range between(final int start, final int end) - throws IllegalArgumentException { - return new Range(start, end); - } - - /** - * Creates a range from a start point, with a given length. - * - * @param start - * the first integer to include in the range - * @param length - * the length of the resulting range - * @return a range starting from start, with - * length number of integers following - * @throws IllegalArgumentException - * if length < 0 - */ - public static Range withLength(final int start, final int length) - throws IllegalArgumentException { - if (length < 0) { - /* - * The constructor of Range will throw an exception if start > - * start+length (i.e. if length is negative). We're throwing the - * same exception type, just with a more descriptive message. - */ - throw new IllegalArgumentException("length must not be negative"); - } - return new Range(start, start + length); - } - - /** - * Creates a new range between two numbers: [start..end[. - * - * @param start - * the start integer, inclusive - * @param end - * the end integer, exclusive - * @throws IllegalArgumentException - * if start > end - */ - private Range(final int start, final int end) - throws IllegalArgumentException { - if (start > end) { - throw new IllegalArgumentException( - "start must not be greater than end"); - } - - this.start = start; - this.end = end; - } - - /** - * Returns the inclusive start point of this range. - * - * @return the start point of this range - */ - public int getStart() { - return start; - } - - /** - * Returns the exclusive end point of this range. - * - * @return the end point of this range - */ - public int getEnd() { - return end; - } - - /** - * The number of integers contained in the range. - * - * @return the number of integers contained in the range - */ - public int length() { - return getEnd() - getStart(); - } - - /** - * Checks whether the range has no elements between the start and end. - * - * @return true iff the range contains no elements. - */ - public boolean isEmpty() { - return getStart() >= getEnd(); - } - - /** - * Checks whether this range and another range are at least partially - * covering the same values. - * - * @param other - * the other range to check against - * @return true if this and other intersect - */ - public boolean intersects(final Range other) { - return getStart() < other.getEnd() && other.getStart() < getEnd(); - } - - /** - * Checks whether an integer is found within this range. - * - * @param integer - * an integer to test for presence in this range - * @return true iff integer is in this range - */ - public boolean contains(final int integer) { - return getStart() <= integer && integer < getEnd(); - } - - /** - * Checks whether this range is a subset of another range. - * - * @return true iff other completely wraps this - * range - */ - public boolean isSubsetOf(final Range other) { - return other.getStart() <= getStart() && getEnd() <= other.getEnd(); - } - - /** - * Overlay this range with another one, and partition the ranges according - * to how they position relative to each other. - *

- * The three partitions are returned as a three-element Range array: - *

    - *
  • Elements in this range that occur before elements in - * other. - *
  • Elements that are shared between the two ranges. - *
  • Elements in this range that occur after elements in - * other. - *
- * - * @param other - * the other range to act as delimiters. - * @return a three-element Range array of partitions depicting the elements - * before (index 0), shared/inside (index 1) and after (index 2). - */ - public Range[] partitionWith(final Range other) { - final Range[] splitBefore = splitAt(other.getStart()); - final Range rangeBefore = splitBefore[0]; - final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd()); - final Range rangeInside = splitAfter[0]; - final Range rangeAfter = splitAfter[1]; - return new Range[] { rangeBefore, rangeInside, rangeAfter }; - } - - /** - * Get a range that is based on this one, but offset by a number. - * - * @param offset - * the number to offset by - * @return a copy of this range, offset by offset - */ - public Range offsetBy(final int offset) { - if (offset == 0) { - return this; - } else { - return new Range(start + offset, end + offset); - } - } - - @Override - public String toString() { - return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd() - + "[" + (isEmpty() ? " (empty)" : ""); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + end; - result = prime * result + start; - return result; - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final Range other = (Range) obj; - if (end != other.end) { - return false; - } - if (start != other.start) { - return false; - } - return true; - } - - /** - * Checks whether this range starts before the start of another range. - * - * @param other - * the other range to compare against - * @return true iff this range starts before the - * other - */ - public boolean startsBefore(final Range other) { - return getStart() < other.getStart(); - } - - /** - * Checks whether this range ends before the start of another range. - * - * @param other - * the other range to compare against - * @return true iff this range ends before the - * other - */ - public boolean endsBefore(final Range other) { - return getEnd() <= other.getStart(); - } - - /** - * Checks whether this range ends after the end of another range. - * - * @param other - * the other range to compare against - * @return true iff this range ends after the - * other - */ - public boolean endsAfter(final Range other) { - return getEnd() > other.getEnd(); - } - - /** - * Checks whether this range starts after the end of another range. - * - * @param other - * the other range to compare against - * @return true iff this range starts after the - * other - */ - public boolean startsAfter(final Range other) { - return getStart() >= other.getEnd(); - } - - /** - * Split the range into two at a certain integer. - *

- * Example: [5..10[.splitAt(7) == [5..7[, [7..10[ - * - * @param integer - * the integer at which to split the range into two - * @return an array of two ranges, with [start..integer[ in the - * first element, and [integer..end[ in the second - * element. - *

- * If {@code integer} is less than {@code start}, [empty, - * {@code this} ] is returned. if integer is equal to - * or greater than {@code end}, [{@code this}, empty] is returned - * instead. - */ - public Range[] splitAt(final int integer) { - if (integer < start) { - return new Range[] { Range.withLength(start, 0), this }; - } else if (integer >= end) { - return new Range[] { this, Range.withLength(end, 0) }; - } else { - return new Range[] { new Range(start, integer), - new Range(integer, end) }; - } - } - - /** - * Split the range into two after a certain number of integers into the - * range. - *

- * Calling this method is equivalent to calling - * {@link #splitAt(int) splitAt}({@link #getStart()}+length); - *

- * Example: - * [5..10[.splitAtFromStart(2) == [5..7[, [7..10[ - * - * @param length - * the length at which to split this range into two - * @return an array of two ranges, having the length-first - * elements of this range, and the second range having the rest. If - * length ≤ 0, the first element will be empty, and - * the second element will be this range. If length - * ≥ {@link #length()}, the first element will be this range, - * and the second element will be empty. - */ - public Range[] splitAtFromStart(final int length) { - return splitAt(getStart() + length); - } - - /** - * Combines two ranges to create a range containing all values in both - * ranges, provided there are no gaps between the ranges. - * - * @param other - * the range to combine with this range - * - * @return the combined range - * - * @throws IllegalArgumentException - * if the two ranges aren't connected - */ - public Range combineWith(Range other) throws IllegalArgumentException { - if (getStart() > other.getEnd() || other.getStart() > getEnd()) { - throw new IllegalArgumentException("There is a gap between " + this - + " and " + other); - } - - return Range.between(Math.min(getStart(), other.getStart()), - Math.max(getEnd(), other.getEnd())); - } -} diff --git a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java index 3cbc6351b1..e97bb339e4 100644 --- a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java +++ b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java @@ -21,6 +21,8 @@ import static org.junit.Assert.assertTrue; import org.junit.Test; +import com.vaadin.shared.ui.grid.Range; + @SuppressWarnings("static-method") public class PartitioningTest { diff --git a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java b/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java deleted file mode 100644 index d73b0fb02f..0000000000 --- a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright 2000-2013 Vaadin Ltd. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.vaadin.client.ui.grid; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - -@SuppressWarnings("static-method") -public class RangeTest { - - @Test(expected = IllegalArgumentException.class) - public void startAfterEndTest() { - Range.between(10, 9); - } - - @Test(expected = IllegalArgumentException.class) - public void negativeLengthTest() { - Range.withLength(10, -1); - } - - @Test - public void constructorEquivalenceTest() { - assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11)); - assertEquals("[10,20[ == 10, length 10", Range.between(10, 20), - Range.withLength(10, 10)); - assertEquals("10 == 10, length 1", Range.withOnly(10), - Range.withLength(10, 1)); - } - - @Test - public void boundsTest() { - { - final Range range = Range.between(0, 10); - assertEquals("between(0, 10) start", 0, range.getStart()); - assertEquals("between(0, 10) end", 10, range.getEnd()); - } - - { - final Range single = Range.withOnly(10); - assertEquals("withOnly(10) start", 10, single.getStart()); - assertEquals("withOnly(10) end", 11, single.getEnd()); - } - - { - final Range length = Range.withLength(10, 5); - assertEquals("withLength(10, 5) start", 10, length.getStart()); - assertEquals("withLength(10, 5) end", 15, length.getEnd()); - } - } - - @Test - @SuppressWarnings("boxing") - public void equalsTest() { - final Range range1 = Range.between(0, 10); - final Range range2 = Range.withLength(0, 11); - - assertTrue("null", !range1.equals(null)); - assertTrue("reflexive", range1.equals(range1)); - assertEquals("symmetric", range1.equals(range2), range2.equals(range1)); - } - - @Test - public void containsTest() { - final int start = 0; - final int end = 10; - final Range range = Range.between(start, end); - - assertTrue("start should be contained", range.contains(start)); - assertTrue("start-1 should not be contained", - !range.contains(start - 1)); - assertTrue("end should not be contained", !range.contains(end)); - assertTrue("end-1 should be contained", range.contains(end - 1)); - - assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5)); - assertTrue("empty range does not contain 5", !Range.between(5, 5) - .contains(5)); - } - - @Test - public void emptyTest() { - assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty()); - assertTrue("Range of length 0 should be empty", Range.withLength(0, 0) - .isEmpty()); - - assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty()); - assertTrue("Range of length 1 should not be empty", - !Range.withLength(0, 1).isEmpty()); - } - - @Test - public void splitTest() { - final Range startRange = Range.between(0, 10); - final Range[] splitRanges = startRange.splitAt(5); - assertEquals("[0..10[ split at 5, lower", Range.between(0, 5), - splitRanges[0]); - assertEquals("[0..10[ split at 5, upper", Range.between(5, 10), - splitRanges[1]); - } - - @Test - public void split_valueBefore() { - Range range = Range.between(10, 20); - Range[] splitRanges = range.splitAt(5); - - assertEquals(Range.between(10, 10), splitRanges[0]); - assertEquals(range, splitRanges[1]); - } - - @Test - public void split_valueAfter() { - Range range = Range.between(10, 20); - Range[] splitRanges = range.splitAt(25); - - assertEquals(range, splitRanges[0]); - assertEquals(Range.between(20, 20), splitRanges[1]); - } - - @Test - public void emptySplitTest() { - final Range range = Range.between(5, 10); - final Range[] split1 = range.splitAt(0); - assertTrue("split1, [0]", split1[0].isEmpty()); - assertEquals("split1, [1]", range, split1[1]); - - final Range[] split2 = range.splitAt(15); - assertEquals("split2, [0]", range, split2[0]); - assertTrue("split2, [1]", split2[1].isEmpty()); - } - - @Test - public void lengthTest() { - assertEquals("withLength length", 5, Range.withLength(10, 5).length()); - assertEquals("between length", 5, Range.between(10, 15).length()); - assertEquals("withOnly 10 length", 1, Range.withOnly(10).length()); - } - - @Test - public void intersectsTest() { - assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10) - .intersects(Range.between(5, 15))); - assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10) - .intersects(Range.between(10, 20))); - } - - @Test - public void intersects_emptyInside() { - assertTrue("[5..5[ does intersect with [0..10[", Range.between(5, 5) - .intersects(Range.between(0, 10))); - assertTrue("[0..10[ does intersect with [5..5[", Range.between(0, 10) - .intersects(Range.between(5, 5))); - } - - @Test - public void intersects_emptyOutside() { - assertTrue("[15..15[ does not intersect with [0..10[", - !Range.between(15, 15).intersects(Range.between(0, 10))); - assertTrue("[0..10[ does not intersect with [15..15[", - !Range.between(0, 10).intersects(Range.between(15, 15))); - } - - @Test - public void subsetTest() { - assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10) - .isSubsetOf(Range.between(0, 20))); - - final Range range = Range.between(0, 10); - assertTrue("range is subset of self", range.isSubsetOf(range)); - - assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10) - .isSubsetOf(Range.between(5, 15))); - } - - @Test - public void offsetTest() { - assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5)); - } - - @Test - public void rangeStartsBeforeTest() { - final Range former = Range.between(0, 5); - final Range latter = Range.between(1, 5); - assertTrue("former should starts before latter", - former.startsBefore(latter)); - assertTrue("latter shouldn't start before latter", - !latter.startsBefore(former)); - - assertTrue("no overlap allowed", - !Range.between(0, 5).startsBefore(Range.between(0, 10))); - } - - @Test - public void rangeStartsAfterTest() { - final Range former = Range.between(0, 5); - final Range latter = Range.between(5, 10); - assertTrue("latter should start after former", - latter.startsAfter(former)); - assertTrue("former shouldn't start after latter", - !former.startsAfter(latter)); - - assertTrue("no overlap allowed", - !Range.between(5, 10).startsAfter(Range.between(0, 6))); - } - - @Test - public void rangeEndsBeforeTest() { - final Range former = Range.between(0, 5); - final Range latter = Range.between(5, 10); - assertTrue("latter should end before former", former.endsBefore(latter)); - assertTrue("former shouldn't end before latter", - !latter.endsBefore(former)); - - assertTrue("no overlap allowed", - !Range.between(5, 10).endsBefore(Range.between(9, 15))); - } - - @Test - public void rangeEndsAfterTest() { - final Range former = Range.between(1, 5); - final Range latter = Range.between(1, 6); - assertTrue("latter should end after former", latter.endsAfter(former)); - assertTrue("former shouldn't end after latter", - !former.endsAfter(latter)); - - assertTrue("no overlap allowed", - !Range.between(0, 10).endsAfter(Range.between(5, 10))); - } - - @Test(expected = IllegalArgumentException.class) - public void combine_notOverlappingFirstSmaller() { - Range.between(0, 10).combineWith(Range.between(11, 20)); - } - - @Test(expected = IllegalArgumentException.class) - public void combine_notOverlappingSecondLarger() { - Range.between(11, 20).combineWith(Range.between(0, 10)); - } - - @Test(expected = IllegalArgumentException.class) - public void combine_firstEmptyNotOverlapping() { - Range.between(15, 15).combineWith(Range.between(0, 10)); - } - - @Test(expected = IllegalArgumentException.class) - public void combine_secondEmptyNotOverlapping() { - Range.between(0, 10).combineWith(Range.between(15, 15)); - } - - @Test - public void combine_barelyOverlapping() { - Range r1 = Range.between(0, 10); - Range r2 = Range.between(10, 20); - - // Test both ways, should give the same result - Range combined1 = r1.combineWith(r2); - Range combined2 = r2.combineWith(r1); - assertEquals(combined1, combined2); - - assertEquals(0, combined1.getStart()); - assertEquals(20, combined1.getEnd()); - } - - @Test - public void combine_subRange() { - Range r1 = Range.between(0, 10); - Range r2 = Range.between(2, 8); - - // Test both ways, should give the same result - Range combined1 = r1.combineWith(r2); - Range combined2 = r2.combineWith(r1); - assertEquals(combined1, combined2); - - assertEquals(r1, combined1); - } - - @Test - public void combine_intersecting() { - Range r1 = Range.between(0, 10); - Range r2 = Range.between(5, 15); - - // Test both ways, should give the same result - Range combined1 = r1.combineWith(r2); - Range combined2 = r2.combineWith(r1); - assertEquals(combined1, combined2); - - assertEquals(0, combined1.getStart()); - assertEquals(15, combined1.getEnd()); - - } - - @Test - public void combine_emptyInside() { - Range r1 = Range.between(0, 10); - Range r2 = Range.between(5, 5); - - // Test both ways, should give the same result - Range combined1 = r1.combineWith(r2); - Range combined2 = r2.combineWith(r1); - assertEquals(combined1, combined2); - - assertEquals(r1, combined1); - } - -} diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java index 48f03b98c0..b22e6a209b 100644 --- a/server/src/com/vaadin/data/RpcDataProviderExtension.java +++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java @@ -18,6 +18,7 @@ package com.vaadin.data; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; import com.vaadin.data.Container.Indexed; @@ -67,22 +68,24 @@ public class RpcDataProviderExtension extends AbstractExtension { Collection propertyIds = container.getContainerPropertyIds(); List rows = new ArrayList(itemIds.size()); for (Object itemId : itemIds) { - Item item = container.getItem(itemId); - String[] row = new String[propertyIds.size()]; - - int i = 0; - for (Object propertyId : propertyIds) { - Object value = item.getItemProperty(propertyId).getValue(); - String stringValue = String.valueOf(value); - row[i++] = stringValue; - } - - rows.add(row); + rows.add(getRowData(propertyIds, itemId)); } - getRpcProxy(DataProviderRpc.class).setRowData(firstRow, rows); } + private String[] getRowData(Collection propertyIds, Object itemId) { + Item item = container.getItem(itemId); + String[] row = new String[propertyIds.size()]; + + int i = 0; + for (Object propertyId : propertyIds) { + Object value = item.getItemProperty(propertyId).getValue(); + String stringValue = String.valueOf(value); + row[i++] = stringValue; + } + return row; + } + @Override protected DataProviderState getState() { return (DataProviderState) super.getState(); @@ -98,4 +101,48 @@ public class RpcDataProviderExtension extends AbstractExtension { super.extend(component); } + /** + * Informs the client side that new rows have been inserted into the data + * source. + * + * @param index + * the index at which new rows have been inserted + * @param count + * the number of rows inserted at index + */ + public void insertRowData(int index, int count) { + getState().containerSize += count; + getRpcProxy(DataProviderRpc.class).insertRowData(index, count); + } + + /** + * Informs the client side that rows have been removed from the data source. + * + * @param firstIndex + * the index of the first row removed + * @param count + * the number of rows removed + */ + public void removeRowData(int firstIndex, int count) { + getState().containerSize -= count; + getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count); + } + + /** + * Informs the client side that data of a row has been modified in the data + * source. + * + * @param index + * the index of the row that was updated + */ + public void updateRowData(int index) { + /* + * TODO: ignore duplicate requests for the same index during the same + * roundtrip. + */ + Object itemId = container.getIdByIndex(index); + String[] row = getRowData(container.getContainerPropertyIds(), itemId); + getRpcProxy(DataProviderRpc.class).setRowData(index, + Collections.singletonList(row)); + } } diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java index 1fb0692104..08685874c1 100644 --- a/server/src/com/vaadin/ui/components/grid/Grid.java +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -26,15 +26,28 @@ import java.util.List; import java.util.Map; import com.vaadin.data.Container; +import com.vaadin.data.Container.Indexed.ItemAddEvent; +import com.vaadin.data.Container.Indexed.ItemRemoveEvent; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.Container.ItemSetChangeListener; +import com.vaadin.data.Container.ItemSetChangeNotifier; import com.vaadin.data.Container.PropertySetChangeEvent; import com.vaadin.data.Container.PropertySetChangeListener; import com.vaadin.data.Container.PropertySetChangeNotifier; +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.Property.ValueChangeListener; +import com.vaadin.data.Property.ValueChangeNotifier; import com.vaadin.data.RpcDataProviderExtension; import com.vaadin.server.KeyMapper; import com.vaadin.shared.ui.grid.ColumnGroupRowState; import com.vaadin.shared.ui.grid.GridColumnState; +import com.vaadin.shared.ui.grid.GridServerRpc; import com.vaadin.shared.ui.grid.GridState; +import com.vaadin.shared.ui.grid.Range; import com.vaadin.ui.AbstractComponent; +import com.vaadin.ui.Component; /** * Data grid component @@ -56,6 +69,282 @@ import com.vaadin.ui.AbstractComponent; */ public class Grid extends AbstractComponent { + /** + * A helper class that handles the client-side Escalator logic relating to + * making sure that whatever is currently visible to the user, is properly + * initialized and otherwise handled on the server side (as far as + * requried). + *

+ * This bookeeping includes, but is not limited to: + *

    + *
  • listening to the currently visible {@link Property Properties'} value + * changes on the server side and sending those back to the client; and + *
  • attaching and detaching {@link Component Components} from the Vaadin + * Component hierarchy. + *
+ */ + private final class ActiveRowHandler { + /** + * A map from itemId to the value change listener used for all of its + * properties + */ + private final Map valueChangeListeners = new HashMap(); + + /** + * The currently active range. Practically, it's the range of row + * indices being displayed currently. + */ + private Range activeRange = Range.withLength(0, 0); + + /** + * A hook for making sure that appropriate data is "active". All other + * rows should be "inactive". + *

+ * "Active" can mean different things in different contexts. For + * example, only the Properties in the active range need + * ValueChangeListeners. Also, whenever a row with a Component becomes + * active, it needs to be attached (and conversely, when inactive, it + * needs to be detached). + * + * @param firstActiveRow + * the first active row + * @param activeRowCount + * the number of active rows + */ + public void setActiveRows(int firstActiveRow, int activeRowCount) { + + final Range newActiveRange = Range.withLength(firstActiveRow, + activeRowCount); + + // TODO [[Components]] attach and detach components + + /*- + * Example + * + * New Range: [3, 4, 5, 6, 7] + * Old Range: [1, 2, 3, 4, 5] + * Result: [1, 2][3, 4, 5] [] + */ + final Range[] depractionPartition = activeRange + .partitionWith(newActiveRange); + removeValueChangeListeners(depractionPartition[0]); + removeValueChangeListeners(depractionPartition[2]); + + /*- + * Example + * + * Old Range: [1, 2, 3, 4, 5] + * New Range: [3, 4, 5, 6, 7] + * Result: [] [3, 4, 5][6, 7] + */ + final Range[] activationPartition = newActiveRange + .partitionWith(activeRange); + addValueChangeListeners(activationPartition[0]); + addValueChangeListeners(activationPartition[2]); + + activeRange = newActiveRange; + } + + private void addValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + + if (valueChangeListeners.containsKey(itemId)) { + /* + * This might occur when items are removed from above the + * viewport, the escalator scrolls up to compensate, but the + * same items remain in the view: It looks as if one row was + * scrolled, when in fact the whole viewport was shifted up. + */ + continue; + } + + GridValueChangeListener listener = new GridValueChangeListener( + itemId); + valueChangeListeners.put(itemId, listener); + + for (final Object propertyId : item.getItemPropertyIds()) { + final Property property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + private void removeValueChangeListeners(Range range) { + for (int i = range.getStart(); i < range.getEnd(); i++) { + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .remove(itemId); + + if (listener != null) { + for (final Object propertyId : item.getItemPropertyIds()) { + final Property property = item + .getItemProperty(propertyId); + + /* + * Because listener != null, we can be certain that this + * property is a ValueChangeNotifier: It wouldn't be + * inserted in addValueChangeListeners if the property + * wasn't a suitable type. I.e. No need for "instanceof" + * check. + */ + ((ValueChangeNotifier) property) + .removeValueChangeListener(listener); + } + } + } + } + + public void clear() { + removeValueChangeListeners(activeRange); + /* + * we're doing an assert for emptiness there (instead of a + * carte-blanche ".clear()"), to be absolutely sure that everything + * is cleaned up properly, and that we have no dangling listeners. + */ + assert valueChangeListeners.isEmpty() : "GridValueChangeListeners are leaking"; + + activeRange = Range.withLength(0, 0); + } + + /** + * Manages removed properties in active rows. + * + * @param removedPropertyIds + * the property ids that have been removed from the container + */ + public void propertiesRemoved(Collection removedPropertyIds) { + /* + * no-op, for now. + * + * The Container should be responsible for cleaning out any + * ValueChangeListeners from removed Properties. Components will + * benefit from this, however. + */ + } + + /** + * Manages added properties in active rows. + * + * @param addedPropertyIds + * the property ids that have been added to the container + */ + public void propertiesAdded(Collection addedPropertyIds) { + for (int i = activeRange.getStart(); i < activeRange.getEnd(); i++) { + final Object itemId = datasource.getIdByIndex(i); + final Item item = datasource.getItem(itemId); + final GridValueChangeListener listener = valueChangeListeners + .get(itemId); + assert (listener != null) : "a listener should've been pre-made by addValueChangeListeners"; + + for (final Object propertyId : addedPropertyIds) { + final Property property = item + .getItemProperty(propertyId); + if (property instanceof ValueChangeNotifier) { + ((ValueChangeNotifier) property) + .addValueChangeListener(listener); + } + } + } + } + + /** + * Handles the insertion of rows. + *

+ * This method's responsibilities are to: + *

    + *
  • shift the internal bookkeeping by count if the + * insertion happens above currently active range + *
  • ignore rows inserted below the currently active range + *
  • shift (and deactivate) rows pushed out of view + *
  • activate rows that are inserted in the current viewport + *
+ * + * @param firstIndex + * the index of the first inserted rows + * @param count + * the number of rows inserted at firstIndex + */ + public void insertRows(int firstIndex, int count) { + if (firstIndex < activeRange.getStart()) { + activeRange = activeRange.offsetBy(count); + } else if (firstIndex < activeRange.getEnd()) { + final Range deprecatedRange = Range.withLength( + activeRange.getEnd(), count); + removeValueChangeListeners(deprecatedRange); + + final Range freshRange = Range.between(firstIndex, count); + addValueChangeListeners(freshRange); + } else { + // out of view, noop + } + } + + /** + * Removes a single item by its id. + * + * @param itemId + * the id of the removed id. Note: this item does + * not exist anymore in the datasource + */ + public void removeItemId(Object itemId) { + final GridValueChangeListener removedListener = valueChangeListeners + .remove(itemId); + if (removedListener != null) { + /* + * We removed an item from somewhere in the visible range, so we + * make the active range shorter. The empty hole will be filled + * by the client-side code when it asks for more information. + */ + activeRange = Range.withLength(activeRange.getStart(), + activeRange.length() - 1); + } + } + } + + /** + * A class to listen to changes in property values in the Container added + * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies + * the data source to update the client-side representation of the modified + * item. + *

+ * One instance of this class can (and should) be reused for all the + * properties in an item, since this class will inform that the entire row + * needs to be re-evaluated (in contrast to a property-based change + * management) + *

+ * Since there's no Container-wide possibility to listen to any kind of + * value changes, an instance of this class needs to be attached to each and + * every Item's Property in the container. + * + * @see Grid#addValueChangeListener(Container, Object, Object) + * @see Grid#valueChangeListeners + */ + private class GridValueChangeListener implements ValueChangeListener { + private final Object itemId; + + public GridValueChangeListener(Object itemId) { + /* + * Using an assert instead of an exception throw, just to optimize + * prematurely + */ + assert itemId != null : "null itemId not accepted"; + this.itemId = itemId; + } + + @Override + public void valueChange(ValueChangeEvent event) { + datasourceExtension.updateRowData(datasource.indexOfId(itemId)); + } + } + /** * The data source attached to the grid */ @@ -98,13 +387,17 @@ public class Grid extends AbstractComponent { columnKeys.remove(columnId); getState().columns.remove(column.getState()); } + activeRowHandler.propertiesRemoved(removedColumns); // Add new columns + HashSet addedPropertyIds = new HashSet(); for (Object propertyId : properties) { if (!columns.containsKey(propertyId)) { appendColumn(propertyId); + addedPropertyIds.add(propertyId); } } + activeRowHandler.propertiesAdded(addedPropertyIds); Object frozenPropertyId = columnKeys .get(getState(false).lastFrozenColumnId); @@ -114,8 +407,53 @@ public class Grid extends AbstractComponent { } }; + private ItemSetChangeListener itemListener = new ItemSetChangeListener() { + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + + if (event instanceof ItemAddEvent) { + ItemAddEvent addEvent = (ItemAddEvent) event; + int firstIndex = addEvent.getFirstIndex(); + int count = addEvent.getAddedItemsCount(); + datasourceExtension.insertRowData(firstIndex, count); + activeRowHandler.insertRows(firstIndex, count); + } + + else if (event instanceof ItemRemoveEvent) { + ItemRemoveEvent removeEvent = (ItemRemoveEvent) event; + int firstIndex = removeEvent.getFirstIndex(); + int count = removeEvent.getRemovedItemsCount(); + datasourceExtension.removeRowData(firstIndex, count); + + /* + * Unfortunately, there's no sane way of getting the rest of the + * removed itemIds. + * + * Fortunately, the only time _currently_ an event with more + * than one removed item seems to be when calling + * AbstractInMemoryContainer.removeAllElements(). Otherwise, + * it's only removing one item at a time. + * + * We _could_ have a backup of all the itemIds, and compare to + * that one, but we really really don't want to go there. + */ + activeRowHandler.removeItemId(removeEvent.getFirstItemId()); + } + + else { + // TODO no diff info available, redraw everything + throw new UnsupportedOperationException("bare " + + "ItemSetChangeEvents are currently " + + "not supported, use a container that " + + "uses AddItemEvents and RemoveItemEvents."); + } + } + }; + private RpcDataProviderExtension datasourceExtension; + private final ActiveRowHandler activeRowHandler = new ActiveRowHandler(); + /** * Creates a new Grid using the given datasource. * @@ -124,6 +462,14 @@ public class Grid extends AbstractComponent { */ public Grid(Container.Indexed datasource) { setContainerDatasource(datasource); + + registerRpc(new GridServerRpc() { + @Override + public void setVisibleRows(int firstVisibleRow, int visibleRowCount) { + activeRowHandler + .setActiveRows(firstVisibleRow, visibleRowCount); + } + }); } /** @@ -143,11 +489,16 @@ public class Grid extends AbstractComponent { return; } - // Remove old listener + // Remove old listeners if (datasource instanceof PropertySetChangeNotifier) { ((PropertySetChangeNotifier) datasource) .removePropertySetChangeListener(propertyListener); } + if (datasource instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) datasource) + .removeItemSetChangeListener(itemListener); + } + activeRowHandler.clear(); if (datasourceExtension != null) { removeExtension(datasourceExtension); @@ -162,6 +513,15 @@ public class Grid extends AbstractComponent { ((PropertySetChangeNotifier) datasource) .addPropertySetChangeListener(propertyListener); } + if (datasource instanceof ItemSetChangeNotifier) { + ((ItemSetChangeNotifier) datasource) + .addItemSetChangeListener(itemListener); + } + /* + * activeRowHandler will be updated by the client-side request that + * occurs on container change - no need to actively re-insert any + * ValueChangeListeners at this point. + */ getState().columns.clear(); setLastFrozenPropertyId(null); diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java index 7d82ecc342..79e3f17f8d 100644 --- a/shared/src/com/vaadin/shared/data/DataProviderRpc.java +++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java @@ -37,4 +37,25 @@ public interface DataProviderRpc extends ClientRpc { * the updated row data */ public void setRowData(int firstRowIndex, List rowData); + + /** + * Informs the client to remove row data. + * + * @param firstRowIndex + * the index of the first removed row + * @param count + * the number of rows removed from firstRowIndex and + * onwards + */ + public void removeRowData(int firstRowIndex, int count); + + /** + * Informs the client to insert new row data. + * + * @param firstRowIndex + * the index of the first new row + * @param count + * the number of rows inserted at firstRowIndex + */ + public void insertRowData(int firstRowIndex, int count); } diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java new file mode 100644 index 0000000000..db0a31ed2c --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import com.vaadin.shared.communication.ServerRpc; + +/** + * TODO + * + * @since 7.2 + * @author Vaadin Ltd + */ +public interface GridServerRpc extends ServerRpc { + + /** + * TODO + * + * @param firstVisibleRow + * the index of the first visible row + * @param visibleRowCount + * the number of rows visible, counted from + * firstVisibleRow + */ + void setVisibleRows(int firstVisibleRow, int visibleRowCount); + +} diff --git a/shared/src/com/vaadin/shared/ui/grid/Range.java b/shared/src/com/vaadin/shared/ui/grid/Range.java new file mode 100644 index 0000000000..3114a79c82 --- /dev/null +++ b/shared/src/com/vaadin/shared/ui/grid/Range.java @@ -0,0 +1,378 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.vaadin.shared.ui.grid; + +/** + * An immutable representation of a range, marked by start and end points. + *

+ * The range is treated as inclusive at the start, and exclusive at the end. + * I.e. the range [0..1[ has the length 1, and represents one integer: 0. + *

+ * The range is considered {@link #isEmpty() empty} if the start is the same as + * the end. + * + * @since 7.2 + * @author Vaadin Ltd + */ +public final class Range { + private final int start; + private final int end; + + /** + * Creates a range object representing a single integer. + * + * @param integer + * the number to represent as a range + * @return the range represented by integer + */ + public static Range withOnly(final int integer) { + return new Range(integer, integer + 1); + } + + /** + * Creates a range between two integers. + *

+ * The range start is inclusive and the end is exclusive. + * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4, + * but not 5. + * + * @param start + * the start of the the range, inclusive + * @param end + * the end of the range, exclusive + * @return a range representing [start..end[ + * @throws IllegalArgumentException + * if start > end + */ + public static Range between(final int start, final int end) + throws IllegalArgumentException { + return new Range(start, end); + } + + /** + * Creates a range from a start point, with a given length. + * + * @param start + * the first integer to include in the range + * @param length + * the length of the resulting range + * @return a range starting from start, with + * length number of integers following + * @throws IllegalArgumentException + * if length < 0 + */ + public static Range withLength(final int start, final int length) + throws IllegalArgumentException { + if (length < 0) { + /* + * The constructor of Range will throw an exception if start > + * start+length (i.e. if length is negative). We're throwing the + * same exception type, just with a more descriptive message. + */ + throw new IllegalArgumentException("length must not be negative"); + } + return new Range(start, start + length); + } + + /** + * Creates a new range between two numbers: [start..end[. + * + * @param start + * the start integer, inclusive + * @param end + * the end integer, exclusive + * @throws IllegalArgumentException + * if start > end + */ + private Range(final int start, final int end) + throws IllegalArgumentException { + if (start > end) { + throw new IllegalArgumentException( + "start must not be greater than end"); + } + + this.start = start; + this.end = end; + } + + /** + * Returns the inclusive start point of this range. + * + * @return the start point of this range + */ + public int getStart() { + return start; + } + + /** + * Returns the exclusive end point of this range. + * + * @return the end point of this range + */ + public int getEnd() { + return end; + } + + /** + * The number of integers contained in the range. + * + * @return the number of integers contained in the range + */ + public int length() { + return getEnd() - getStart(); + } + + /** + * Checks whether the range has no elements between the start and end. + * + * @return true iff the range contains no elements. + */ + public boolean isEmpty() { + return getStart() >= getEnd(); + } + + /** + * Checks whether this range and another range are at least partially + * covering the same values. + * + * @param other + * the other range to check against + * @return true if this and other intersect + */ + public boolean intersects(final Range other) { + return getStart() < other.getEnd() && other.getStart() < getEnd(); + } + + /** + * Checks whether an integer is found within this range. + * + * @param integer + * an integer to test for presence in this range + * @return true iff integer is in this range + */ + public boolean contains(final int integer) { + return getStart() <= integer && integer < getEnd(); + } + + /** + * Checks whether this range is a subset of another range. + * + * @return true iff other completely wraps this + * range + */ + public boolean isSubsetOf(final Range other) { + return other.getStart() <= getStart() && getEnd() <= other.getEnd(); + } + + /** + * Overlay this range with another one, and partition the ranges according + * to how they position relative to each other. + *

+ * The three partitions are returned as a three-element Range array: + *

    + *
  • Elements in this range that occur before elements in + * other. + *
  • Elements that are shared between the two ranges. + *
  • Elements in this range that occur after elements in + * other. + *
+ * + * @param other + * the other range to act as delimiters. + * @return a three-element Range array of partitions depicting the elements + * before (index 0), shared/inside (index 1) and after (index 2). + */ + public Range[] partitionWith(final Range other) { + final Range[] splitBefore = splitAt(other.getStart()); + final Range rangeBefore = splitBefore[0]; + final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd()); + final Range rangeInside = splitAfter[0]; + final Range rangeAfter = splitAfter[1]; + return new Range[] { rangeBefore, rangeInside, rangeAfter }; + } + + /** + * Get a range that is based on this one, but offset by a number. + * + * @param offset + * the number to offset by + * @return a copy of this range, offset by offset + */ + public Range offsetBy(final int offset) { + if (offset == 0) { + return this; + } else { + return new Range(start + offset, end + offset); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd() + + "[" + (isEmpty() ? " (empty)" : ""); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + end; + result = prime * result + start; + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Range other = (Range) obj; + if (end != other.end) { + return false; + } + if (start != other.start) { + return false; + } + return true; + } + + /** + * Checks whether this range starts before the start of another range. + * + * @param other + * the other range to compare against + * @return true iff this range starts before the + * other + */ + public boolean startsBefore(final Range other) { + return getStart() < other.getStart(); + } + + /** + * Checks whether this range ends before the start of another range. + * + * @param other + * the other range to compare against + * @return true iff this range ends before the + * other + */ + public boolean endsBefore(final Range other) { + return getEnd() <= other.getStart(); + } + + /** + * Checks whether this range ends after the end of another range. + * + * @param other + * the other range to compare against + * @return true iff this range ends after the + * other + */ + public boolean endsAfter(final Range other) { + return getEnd() > other.getEnd(); + } + + /** + * Checks whether this range starts after the end of another range. + * + * @param other + * the other range to compare against + * @return true iff this range starts after the + * other + */ + public boolean startsAfter(final Range other) { + return getStart() >= other.getEnd(); + } + + /** + * Split the range into two at a certain integer. + *

+ * Example: [5..10[.splitAt(7) == [5..7[, [7..10[ + * + * @param integer + * the integer at which to split the range into two + * @return an array of two ranges, with [start..integer[ in the + * first element, and [integer..end[ in the second + * element. + *

+ * If {@code integer} is less than {@code start}, [empty, + * {@code this} ] is returned. if integer is equal to + * or greater than {@code end}, [{@code this}, empty] is returned + * instead. + */ + public Range[] splitAt(final int integer) { + if (integer < start) { + return new Range[] { Range.withLength(start, 0), this }; + } else if (integer >= end) { + return new Range[] { this, Range.withLength(end, 0) }; + } else { + return new Range[] { new Range(start, integer), + new Range(integer, end) }; + } + } + + /** + * Split the range into two after a certain number of integers into the + * range. + *

+ * Calling this method is equivalent to calling + * {@link #splitAt(int) splitAt}({@link #getStart()}+length); + *

+ * Example: + * [5..10[.splitAtFromStart(2) == [5..7[, [7..10[ + * + * @param length + * the length at which to split this range into two + * @return an array of two ranges, having the length-first + * elements of this range, and the second range having the rest. If + * length ≤ 0, the first element will be empty, and + * the second element will be this range. If length + * ≥ {@link #length()}, the first element will be this range, + * and the second element will be empty. + */ + public Range[] splitAtFromStart(final int length) { + return splitAt(getStart() + length); + } + + /** + * Combines two ranges to create a range containing all values in both + * ranges, provided there are no gaps between the ranges. + * + * @param other + * the range to combine with this range + * + * @return the combined range + * + * @throws IllegalArgumentException + * if the two ranges aren't connected + */ + public Range combineWith(Range other) throws IllegalArgumentException { + if (getStart() > other.getEnd() || other.getStart() > getEnd()) { + throw new IllegalArgumentException("There is a gap between " + this + + " and " + other); + } + + return Range.between(Math.min(getStart(), other.getStart()), + Math.max(getEnd(), other.getEnd())); + } +} diff --git a/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java new file mode 100644 index 0000000000..b042cee509 --- /dev/null +++ b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java @@ -0,0 +1,318 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.shared.ui.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +@SuppressWarnings("static-method") +public class RangeTest { + + @Test(expected = IllegalArgumentException.class) + public void startAfterEndTest() { + Range.between(10, 9); + } + + @Test(expected = IllegalArgumentException.class) + public void negativeLengthTest() { + Range.withLength(10, -1); + } + + @Test + public void constructorEquivalenceTest() { + assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11)); + assertEquals("[10,20[ == 10, length 10", Range.between(10, 20), + Range.withLength(10, 10)); + assertEquals("10 == 10, length 1", Range.withOnly(10), + Range.withLength(10, 1)); + } + + @Test + public void boundsTest() { + { + final Range range = Range.between(0, 10); + assertEquals("between(0, 10) start", 0, range.getStart()); + assertEquals("between(0, 10) end", 10, range.getEnd()); + } + + { + final Range single = Range.withOnly(10); + assertEquals("withOnly(10) start", 10, single.getStart()); + assertEquals("withOnly(10) end", 11, single.getEnd()); + } + + { + final Range length = Range.withLength(10, 5); + assertEquals("withLength(10, 5) start", 10, length.getStart()); + assertEquals("withLength(10, 5) end", 15, length.getEnd()); + } + } + + @Test + @SuppressWarnings("boxing") + public void equalsTest() { + final Range range1 = Range.between(0, 10); + final Range range2 = Range.withLength(0, 11); + + assertTrue("null", !range1.equals(null)); + assertTrue("reflexive", range1.equals(range1)); + assertEquals("symmetric", range1.equals(range2), range2.equals(range1)); + } + + @Test + public void containsTest() { + final int start = 0; + final int end = 10; + final Range range = Range.between(start, end); + + assertTrue("start should be contained", range.contains(start)); + assertTrue("start-1 should not be contained", + !range.contains(start - 1)); + assertTrue("end should not be contained", !range.contains(end)); + assertTrue("end-1 should be contained", range.contains(end - 1)); + + assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5)); + assertTrue("empty range does not contain 5", !Range.between(5, 5) + .contains(5)); + } + + @Test + public void emptyTest() { + assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty()); + assertTrue("Range of length 0 should be empty", Range.withLength(0, 0) + .isEmpty()); + + assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty()); + assertTrue("Range of length 1 should not be empty", + !Range.withLength(0, 1).isEmpty()); + } + + @Test + public void splitTest() { + final Range startRange = Range.between(0, 10); + final Range[] splitRanges = startRange.splitAt(5); + assertEquals("[0..10[ split at 5, lower", Range.between(0, 5), + splitRanges[0]); + assertEquals("[0..10[ split at 5, upper", Range.between(5, 10), + splitRanges[1]); + } + + @Test + public void split_valueBefore() { + Range range = Range.between(10, 20); + Range[] splitRanges = range.splitAt(5); + + assertEquals(Range.between(10, 10), splitRanges[0]); + assertEquals(range, splitRanges[1]); + } + + @Test + public void split_valueAfter() { + Range range = Range.between(10, 20); + Range[] splitRanges = range.splitAt(25); + + assertEquals(range, splitRanges[0]); + assertEquals(Range.between(20, 20), splitRanges[1]); + } + + @Test + public void emptySplitTest() { + final Range range = Range.between(5, 10); + final Range[] split1 = range.splitAt(0); + assertTrue("split1, [0]", split1[0].isEmpty()); + assertEquals("split1, [1]", range, split1[1]); + + final Range[] split2 = range.splitAt(15); + assertEquals("split2, [0]", range, split2[0]); + assertTrue("split2, [1]", split2[1].isEmpty()); + } + + @Test + public void lengthTest() { + assertEquals("withLength length", 5, Range.withLength(10, 5).length()); + assertEquals("between length", 5, Range.between(10, 15).length()); + assertEquals("withOnly 10 length", 1, Range.withOnly(10).length()); + } + + @Test + public void intersectsTest() { + assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10) + .intersects(Range.between(5, 15))); + assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10) + .intersects(Range.between(10, 20))); + } + + @Test + public void intersects_emptyInside() { + assertTrue("[5..5[ does intersect with [0..10[", Range.between(5, 5) + .intersects(Range.between(0, 10))); + assertTrue("[0..10[ does intersect with [5..5[", Range.between(0, 10) + .intersects(Range.between(5, 5))); + } + + @Test + public void intersects_emptyOutside() { + assertTrue("[15..15[ does not intersect with [0..10[", + !Range.between(15, 15).intersects(Range.between(0, 10))); + assertTrue("[0..10[ does not intersect with [15..15[", + !Range.between(0, 10).intersects(Range.between(15, 15))); + } + + @Test + public void subsetTest() { + assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10) + .isSubsetOf(Range.between(0, 20))); + + final Range range = Range.between(0, 10); + assertTrue("range is subset of self", range.isSubsetOf(range)); + + assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10) + .isSubsetOf(Range.between(5, 15))); + } + + @Test + public void offsetTest() { + assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5)); + } + + @Test + public void rangeStartsBeforeTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(1, 5); + assertTrue("former should starts before latter", + former.startsBefore(latter)); + assertTrue("latter shouldn't start before latter", + !latter.startsBefore(former)); + + assertTrue("no overlap allowed", + !Range.between(0, 5).startsBefore(Range.between(0, 10))); + } + + @Test + public void rangeStartsAfterTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(5, 10); + assertTrue("latter should start after former", + latter.startsAfter(former)); + assertTrue("former shouldn't start after latter", + !former.startsAfter(latter)); + + assertTrue("no overlap allowed", + !Range.between(5, 10).startsAfter(Range.between(0, 6))); + } + + @Test + public void rangeEndsBeforeTest() { + final Range former = Range.between(0, 5); + final Range latter = Range.between(5, 10); + assertTrue("latter should end before former", former.endsBefore(latter)); + assertTrue("former shouldn't end before latter", + !latter.endsBefore(former)); + + assertTrue("no overlap allowed", + !Range.between(5, 10).endsBefore(Range.between(9, 15))); + } + + @Test + public void rangeEndsAfterTest() { + final Range former = Range.between(1, 5); + final Range latter = Range.between(1, 6); + assertTrue("latter should end after former", latter.endsAfter(former)); + assertTrue("former shouldn't end after latter", + !former.endsAfter(latter)); + + assertTrue("no overlap allowed", + !Range.between(0, 10).endsAfter(Range.between(5, 10))); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_notOverlappingFirstSmaller() { + Range.between(0, 10).combineWith(Range.between(11, 20)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_notOverlappingSecondLarger() { + Range.between(11, 20).combineWith(Range.between(0, 10)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_firstEmptyNotOverlapping() { + Range.between(15, 15).combineWith(Range.between(0, 10)); + } + + @Test(expected = IllegalArgumentException.class) + public void combine_secondEmptyNotOverlapping() { + Range.between(0, 10).combineWith(Range.between(15, 15)); + } + + @Test + public void combine_barelyOverlapping() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(10, 20); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(0, combined1.getStart()); + assertEquals(20, combined1.getEnd()); + } + + @Test + public void combine_subRange() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(2, 8); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(r1, combined1); + } + + @Test + public void combine_intersecting() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(5, 15); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(0, combined1.getStart()); + assertEquals(15, combined1.getEnd()); + + } + + @Test + public void combine_emptyInside() { + Range r1 = Range.between(0, 10); + Range r2 = Range.between(5, 5); + + // Test both ways, should give the same result + Range combined1 = r1.combineWith(r2); + Range combined2 = r2.combineWith(r1); + assertEquals(combined1, combined2); + + assertEquals(r1, combined1); + } + +} diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java index 82b2d7a4e8..c28feb8d10 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java @@ -40,20 +40,22 @@ public class GridBasicFeatures extends AbstractComponentTest { private final int ROWS = 1000; + private IndexedContainer ds; + @Override protected Grid constructComponent() { // Build data source - IndexedContainer ds = new IndexedContainer(); + ds = new IndexedContainer(); for (int col = 0; col < COLUMNS; col++) { - ds.addContainerProperty("Column" + col, String.class, ""); + ds.addContainerProperty(getColumnProperty(col), String.class, ""); } for (int row = 0; row < ROWS; row++) { Item item = ds.addItem(Integer.valueOf(row)); for (int col = 0; col < COLUMNS; col++) { - item.getItemProperty("Column" + col).setValue( + item.getItemProperty(getColumnProperty(col)).setValue( "(" + row + ", " + col + ")"); } } @@ -63,7 +65,8 @@ public class GridBasicFeatures extends AbstractComponentTest { // Add footer values (header values are automatically created) for (int col = 0; col < COLUMNS; col++) { - grid.getColumn("Column" + col).setFooterCaption("Footer " + col); + grid.getColumn(getColumnProperty(col)).setFooterCaption( + "Footer " + col); } // Set varying column widths @@ -81,6 +84,8 @@ public class GridBasicFeatures extends AbstractComponentTest { createColumnGroupActions(); + createRowActions(); + return grid; } @@ -131,9 +136,9 @@ public class GridBasicFeatures extends AbstractComponentTest { createCategory("Columns", null); for (int c = 0; c < COLUMNS; c++) { - createCategory("Column" + c, "Columns"); + createCategory(getColumnProperty(c), "Columns"); - createBooleanAction("Visible", "Column" + c, true, + createBooleanAction("Visible", getColumnProperty(c), true, new Command() { @Override @@ -148,7 +153,7 @@ public class GridBasicFeatures extends AbstractComponentTest { } }, c); - createClickAction("Remove", "Column" + c, + createClickAction("Remove", getColumnProperty(c), new Command() { @Override @@ -158,7 +163,7 @@ public class GridBasicFeatures extends AbstractComponentTest { } }, null, c); - createClickAction("Freeze", "Column" + c, + createClickAction("Freeze", getColumnProperty(c), new Command() { @Override @@ -167,7 +172,7 @@ public class GridBasicFeatures extends AbstractComponentTest { } }, null, c); - createCategory("Column" + c + " Width", "Column" + c); + createCategory("Column" + c + " Width", getColumnProperty(c)); createClickAction("Auto", "Column" + c + " Width", new Command() { @@ -203,6 +208,10 @@ public class GridBasicFeatures extends AbstractComponentTest { } } + private static String getColumnProperty(int c) { + return "Column" + c; + } + protected void createColumnGroupActions() { createCategory("Column groups", null); @@ -269,6 +278,58 @@ public class GridBasicFeatures extends AbstractComponentTest { } + protected void createRowActions() { + createCategory("Body rows", null); + + createClickAction("Add first row", "Body rows", + new Command() { + @Override + public void execute(Grid c, String value, Object data) { + Item item = ds.addItemAt(0, new Object()); + for (int i = 0; i < COLUMNS; i++) { + item.getItemProperty(getColumnProperty(i)) + .setValue("newcell: " + i); + } + } + }, null); + + createClickAction("Remove first row", "Body rows", + new Command() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + ds.removeItem(firstItemId); + } + }, null); + + createClickAction("Modify first row (getItemProperty)", "Body rows", + new Command() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + Item item = ds.getItem(firstItemId); + for (int i = 0; i < COLUMNS; i++) { + item.getItemProperty(getColumnProperty(i)) + .setValue("modified: " + i); + } + } + }, null); + + createClickAction("Modify first row (getContainerProperty)", + "Body rows", new Command() { + @Override + public void execute(Grid c, String value, Object data) { + Object firstItemId = ds.getIdByIndex(0); + for (Object containerPropertyId : ds + .getContainerPropertyIds()) { + ds.getContainerProperty(firstItemId, + containerPropertyId).setValue( + "modified: " + containerPropertyId); + } + } + }, null); + } + @Override protected Integer getTicketNumber() { return 12829; diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java index 8beee46156..bc43f2be98 100644 --- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java +++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java @@ -22,6 +22,7 @@ import java.util.List; import org.junit.Test; import org.openqa.selenium.By; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.interactions.Actions; @@ -253,6 +254,55 @@ public class GridBasicFeaturesTest extends MultiBrowserTest { assertPrimaryStylename("v-grid"); } + /** + * Test that the current view is updated when a server-side container change + * occurs (without scrolling back and forth) + */ + @Test + public void testItemSetChangeEvent() throws Exception { + openTestURL(); + + final By newRow = By.xpath("//td[text()='newcell: 0']"); + + assertTrue("Unexpected initial state", !elementIsFound(newRow)); + + selectMenuPath("Component", "Body rows", "Add first row"); + assertTrue("Add row failed", elementIsFound(newRow)); + + selectMenuPath("Component", "Body rows", "Remove first row"); + assertTrue("Remove row failed", !elementIsFound(newRow)); + } + + /** + * Test that the current view is updated when a property's value is reflect + * to the client, when the value is modified server-side. + */ + @Test + public void testPropertyValueChangeEvent() throws Exception { + openTestURL(); + + assertEquals("Unexpected cell initial state", "(0, 0)", + getBodyCellByRowAndColumn(1, 1).getText()); + + selectMenuPath("Component", "Body rows", + "Modify first row (getItemProperty)"); + assertEquals("(First) modification with getItemProperty failed", + "modified: 0", getBodyCellByRowAndColumn(1, 1).getText()); + + selectMenuPath("Component", "Body rows", + "Modify first row (getContainerProperty)"); + assertEquals("(Second) modification with getItemProperty failed", + "modified: Column0", getBodyCellByRowAndColumn(1, 1).getText()); + } + + private boolean elementIsFound(By locator) { + try { + return driver.findElement(locator) != null; + } catch (NoSuchElementException e) { + return false; + } + } + private void assertPrimaryStylename(String stylename) { assertTrue(getGridElement().getAttribute("class").contains(stylename));