]> source.dussan.org Git - vaadin-framework.git/commitdiff
Grid supports data set changes (#12645)
authorHenrik Paul <henrik@vaadin.com>
Tue, 17 Dec 2013 09:08:25 +0000 (11:08 +0200)
committerHenrik Paul <henrik@vaadin.com>
Tue, 17 Dec 2013 09:08:25 +0000 (11:08 +0200)
Change-Id: I5ceb52dea079f48b0065c1b2dbdc35b30fe8c4ee

16 files changed:
client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
client/src/com/vaadin/client/data/RpcDataSourceConnector.java
client/src/com/vaadin/client/ui/grid/Escalator.java
client/src/com/vaadin/client/ui/grid/Grid.java
client/src/com/vaadin/client/ui/grid/GridConnector.java
client/src/com/vaadin/client/ui/grid/Range.java [deleted file]
client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
client/tests/src/com/vaadin/client/ui/grid/RangeTest.java [deleted file]
server/src/com/vaadin/data/RpcDataProviderExtension.java
server/src/com/vaadin/ui/components/grid/Grid.java
shared/src/com/vaadin/shared/data/DataProviderRpc.java
shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java [new file with mode: 0644]
shared/src/com/vaadin/shared/ui/grid/Range.java [new file with mode: 0644]
shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java [new file with mode: 0644]
uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java

index ff8847ea446bd56e858678640cba1537a1077727..127eb80696c90ed972a80c381f556933decf2c19 100644 (file)
@@ -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<T> implements DataSource<T> {
 
         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
+     *            <code>firstRowIndex</code>
+     */
+    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");
+    }
 }
index 1785fc62c2540dcc919995465c02ae6bdca7695c..4d22c10197927532d1f4eb8457e4432ff9d1b1b5 100644 (file)
@@ -56,6 +56,16 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector {
             public void setRowData(int firstRow, List<String[]> 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);
+            }
         });
     }
 
index 20a187e1a53223204ad671717ddc42c2421f87c8..a395038890580e25f7aca982b673e25b3cfa7f78 100644 (file)
@@ -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));
+        }
     }
 
     /**
index 2dbb0275cd22d65a796a2131b3e67b4130706e0c..7f8ab408a95e941856e086f6bbf48408c2820a16 100644 (file)
@@ -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<T> extends Composite {
      */
     private final List<GridColumn<?, T>> columns = new ArrayList<GridColumn<?, T>>();
 
+    /**
+     * The datasource currently in use. <em>Note:</em> it is <code>null</code>
+     * on initialization, but not after that.
+     */
     private DataSource<T> dataSource;
 
     /**
@@ -1211,4 +1216,14 @@ public class Grid<T> extends Composite {
     public GridColumn<?, T> 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);
+    }
 }
index ffe1444942daad1c01677772980fa742d050859c..f04326c7e63265de3acfbac12de987596639eaa4 100644 (file)
@@ -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 (file)
index 634a182..0000000
+++ /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.
- * <p>
- * 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.
- * <p>
- * 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 <code>integer</code>
-     */
-    public static Range withOnly(final int integer) {
-        return new Range(integer, integer + 1);
-    }
-
-    /**
-     * Creates a range between two integers.
-     * <p>
-     * The range start is <em>inclusive</em> and the end is <em>exclusive</em>.
-     * 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 <code>[start..end[</code>
-     * @throws IllegalArgumentException
-     *             if <code>start &gt; end</code>
-     */
-    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 <code>start</code>, with
-     *         <code>length</code> number of integers following
-     * @throws IllegalArgumentException
-     *             if length &lt; 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: <code>[start..end[</code>.
-     * 
-     * @param start
-     *            the start integer, inclusive
-     * @param end
-     *            the end integer, exclusive
-     * @throws IllegalArgumentException
-     *             if <code>start &gt; end</code>
-     */
-    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 <em>inclusive</em> start point of this range.
-     * 
-     * @return the start point of this range
-     */
-    public int getStart() {
-        return start;
-    }
-
-    /**
-     * Returns the <em>exclusive</em> 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 <code>true</code> 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 <code>true</code> if this and <code>other</code> 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 <code>true</code> iff <code>integer</code> 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 <code>true</code> iff <code>other</code> 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.
-     * <p>
-     * The three partitions are returned as a three-element Range array:
-     * <ul>
-     * <li>Elements in this range that occur before elements in
-     * <code>other</code>.
-     * <li>Elements that are shared between the two ranges.
-     * <li>Elements in this range that occur after elements in
-     * <code>other</code>.
-     * </ul>
-     * 
-     * @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 <code>offset</code>
-     */
-    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 <code>true</code> iff this range starts before the
-     *         <code>other</code>
-     */
-    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 <code>true</code> iff this range ends before the
-     *         <code>other</code>
-     */
-    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 <code>true</code> iff this range ends after the
-     *         <code>other</code>
-     */
-    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 <code>true</code> iff this range starts after the
-     *         <code>other</code>
-     */
-    public boolean startsAfter(final Range other) {
-        return getStart() >= other.getEnd();
-    }
-
-    /**
-     * Split the range into two at a certain integer.
-     * <p>
-     * <em>Example:</em> <code>[5..10[.splitAt(7) == [5..7[, [7..10[</code>
-     * 
-     * @param integer
-     *            the integer at which to split the range into two
-     * @return an array of two ranges, with <code>[start..integer[</code> in the
-     *         first element, and <code>[integer..end[</code> in the second
-     *         element.
-     *         <p>
-     *         If {@code integer} is less than {@code start}, [empty,
-     *         {@code this} ] is returned. if <code>integer</code> 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.
-     * <p>
-     * Calling this method is equivalent to calling
-     * <code>{@link #splitAt(int) splitAt}({@link #getStart()}+length);</code>
-     * <p>
-     * <em>Example:</em>
-     * <code>[5..10[.splitAtFromStart(2) == [5..7[, [7..10[</code>
-     * 
-     * @param length
-     *            the length at which to split this range into two
-     * @return an array of two ranges, having the <code>length</code>-first
-     *         elements of this range, and the second range having the rest. If
-     *         <code>length</code> &leq; 0, the first element will be empty, and
-     *         the second element will be this range. If <code>length</code>
-     *         &geq; {@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()));
-    }
-}
index 3cbc6351b164bf0de6da34e415290ce8ec3e5eba..e97bb339e4c22f90d0105eccf5cf5d2749211552 100644 (file)
@@ -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 (file)
index d73b0fb..0000000
+++ /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);
-    }
-
-}
index 48f03b98c0577c3a4f8281cdcdef242fb035b100..b22e6a209b1f82b11be96f5a5bcde3c9123897ff 100644 (file)
@@ -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<String[]> rows = new ArrayList<String[]>(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 <code>index</code>
+     */
+    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));
+    }
 }
index 1fb0692104bd3202e41d3395b29d669415727f72..08685874c148c11c82e5847b8299e1eec4e0aec9 100644 (file)
@@ -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).
+     * <p>
+     * This bookeeping includes, but is not limited to:
+     * <ul>
+     * <li>listening to the currently visible {@link Property Properties'} value
+     * changes on the server side and sending those back to the client; and
+     * <li>attaching and detaching {@link Component Components} from the Vaadin
+     * Component hierarchy.
+     * </ul>
+     */
+    private final class ActiveRowHandler {
+        /**
+         * A map from itemId to the value change listener used for all of its
+         * properties
+         */
+        private final Map<Object, GridValueChangeListener> valueChangeListeners = new HashMap<Object, GridValueChangeListener>();
+
+        /**
+         * 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".
+         * <p>
+         * "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<Object> 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<Object> 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.
+         * <p>
+         * This method's responsibilities are to:
+         * <ul>
+         * <li>shift the internal bookkeeping by <code>count</code> if the
+         * insertion happens above currently active range
+         * <li>ignore rows inserted below the currently active range
+         * <li>shift (and deactivate) rows pushed out of view
+         * <li>activate rows that are inserted in the current viewport
+         * </ul>
+         * 
+         * @param firstIndex
+         *            the index of the first inserted rows
+         * @param count
+         *            the number of rows inserted at <code>firstIndex</code>
+         */
+        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. <em>Note:</em> 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.
+     * <p>
+     * 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)
+     * <p>
+     * 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<Object> addedPropertyIds = new HashSet<Object>();
             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);
index 7d82ecc34246a8623c31e7e8ddf53b6b5a184793..79e3f17f8db7b96cdb37be2a6c005572f0d72b04 100644 (file)
@@ -37,4 +37,25 @@ public interface DataProviderRpc extends ClientRpc {
      *            the updated row data
      */
     public void setRowData(int firstRowIndex, List<String[]> 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 <code>firstRowIndex</code> 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 <code>firstRowIndex</code>
+     */
+    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 (file)
index 0000000..db0a31e
--- /dev/null
@@ -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
+     *            <code>firstVisibleRow</code>
+     */
+    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 (file)
index 0000000..3114a79
--- /dev/null
@@ -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.
+ * <p>
+ * 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.
+ * <p>
+ * 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 <code>integer</code>
+     */
+    public static Range withOnly(final int integer) {
+        return new Range(integer, integer + 1);
+    }
+
+    /**
+     * Creates a range between two integers.
+     * <p>
+     * The range start is <em>inclusive</em> and the end is <em>exclusive</em>.
+     * 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 <code>[start..end[</code>
+     * @throws IllegalArgumentException
+     *             if <code>start &gt; end</code>
+     */
+    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 <code>start</code>, with
+     *         <code>length</code> number of integers following
+     * @throws IllegalArgumentException
+     *             if length &lt; 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: <code>[start..end[</code>.
+     * 
+     * @param start
+     *            the start integer, inclusive
+     * @param end
+     *            the end integer, exclusive
+     * @throws IllegalArgumentException
+     *             if <code>start &gt; end</code>
+     */
+    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 <em>inclusive</em> start point of this range.
+     * 
+     * @return the start point of this range
+     */
+    public int getStart() {
+        return start;
+    }
+
+    /**
+     * Returns the <em>exclusive</em> 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 <code>true</code> 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 <code>true</code> if this and <code>other</code> 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 <code>true</code> iff <code>integer</code> 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 <code>true</code> iff <code>other</code> 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.
+     * <p>
+     * The three partitions are returned as a three-element Range array:
+     * <ul>
+     * <li>Elements in this range that occur before elements in
+     * <code>other</code>.
+     * <li>Elements that are shared between the two ranges.
+     * <li>Elements in this range that occur after elements in
+     * <code>other</code>.
+     * </ul>
+     * 
+     * @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 <code>offset</code>
+     */
+    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 <code>true</code> iff this range starts before the
+     *         <code>other</code>
+     */
+    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 <code>true</code> iff this range ends before the
+     *         <code>other</code>
+     */
+    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 <code>true</code> iff this range ends after the
+     *         <code>other</code>
+     */
+    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 <code>true</code> iff this range starts after the
+     *         <code>other</code>
+     */
+    public boolean startsAfter(final Range other) {
+        return getStart() >= other.getEnd();
+    }
+
+    /**
+     * Split the range into two at a certain integer.
+     * <p>
+     * <em>Example:</em> <code>[5..10[.splitAt(7) == [5..7[, [7..10[</code>
+     * 
+     * @param integer
+     *            the integer at which to split the range into two
+     * @return an array of two ranges, with <code>[start..integer[</code> in the
+     *         first element, and <code>[integer..end[</code> in the second
+     *         element.
+     *         <p>
+     *         If {@code integer} is less than {@code start}, [empty,
+     *         {@code this} ] is returned. if <code>integer</code> 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.
+     * <p>
+     * Calling this method is equivalent to calling
+     * <code>{@link #splitAt(int) splitAt}({@link #getStart()}+length);</code>
+     * <p>
+     * <em>Example:</em>
+     * <code>[5..10[.splitAtFromStart(2) == [5..7[, [7..10[</code>
+     * 
+     * @param length
+     *            the length at which to split this range into two
+     * @return an array of two ranges, having the <code>length</code>-first
+     *         elements of this range, and the second range having the rest. If
+     *         <code>length</code> &leq; 0, the first element will be empty, and
+     *         the second element will be this range. If <code>length</code>
+     *         &geq; {@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 (file)
index 0000000..b042cee
--- /dev/null
@@ -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);
+    }
+
+}
index 82b2d7a4e88757636641d2454c84542c071cf0ac..c28feb8d1011437cf56bef84d98b0b38d79e6929 100644 (file)
@@ -40,20 +40,22 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
 
     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<Grid> {
 
         // 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<Grid> {
 
         createColumnGroupActions();
 
+        createRowActions();
+
         return grid;
     }
 
@@ -131,9 +136,9 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
         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<Grid, Boolean>() {
 
                         @Override
@@ -148,7 +153,7 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
                         }
                     }, c);
 
-            createClickAction("Remove", "Column" + c,
+            createClickAction("Remove", getColumnProperty(c),
                     new Command<Grid, String>() {
 
                         @Override
@@ -158,7 +163,7 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
                         }
                     }, null, c);
 
-            createClickAction("Freeze", "Column" + c,
+            createClickAction("Freeze", getColumnProperty(c),
                     new Command<Grid, String>() {
 
                         @Override
@@ -167,7 +172,7 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
                         }
                     }, null, c);
 
-            createCategory("Column" + c + " Width", "Column" + c);
+            createCategory("Column" + c + " Width", getColumnProperty(c));
 
             createClickAction("Auto", "Column" + c + " Width",
                     new Command<Grid, Integer>() {
@@ -203,6 +208,10 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
         }
     }
 
+    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<Grid> {
 
     }
 
+    protected void createRowActions() {
+        createCategory("Body rows", null);
+
+        createClickAction("Add first row", "Body rows",
+                new Command<Grid, String>() {
+                    @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<Grid, String>() {
+                    @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<Grid, String>() {
+                    @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<Grid, String>() {
+                    @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;
index 8beee461563faa2f36f73b6a8089940fe107e47a..bc43f2be984cbd2913e5f9260ff04b1851b608cf 100644 (file)
@@ -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));