From 1c1506ef0447b1d979a6adb4d812ae9858f00b67 Mon Sep 17 00:00:00 2001
From: John Ahlroos
+ * A column header is a single cell header on top of each column reserved
+ * for a specific header for that column. The column header can be set by
+ * {@link GridColumn#setHeaderCaption(String)} and column headers cannot be
+ * merged with other column headers.
+ *
+ * All column headers occupy the first header row of the grid. If you do not
+ * wish to show the column headers in the grid you should hide the row by
+ * setting visibility of the header row to
+ * If you want to merge the column headers into groups you can use
+ * {@link ColumnGroupRow}s to group columns together and give them a common
+ * header. See {@link #addColumnGroupRow()} for details.
+ *
+ * The header row is by default visible.
+ *
+ * A column footer is a single cell footer below of each column reserved for
+ * a specific footer for that column. The column footer can be set by
+ * {@link GridColumn#setFooterCaption(String)} and column footers cannot be
+ * merged with other column footers.
+ *
+ * All column footers occupy the first footer row of the grid. If you do not
+ * wish to show the column footers in the grid you should hide the row by
+ * setting visibility of the footer row to
+ * If you want to merge the column footers into groups you can use
+ * {@link ColumnGroupRow}s to group columns together and give them a common
+ * footer. See {@link #addColumnGroupRow()} for details.
+ *
+ * The footer row is by default hidden.
+ *
+ * Column group rows are rendered in the header and footer of the grid.
+ * Column group rows are made up of column groups which groups together
+ * columns for adding a common auxiliary header or footer for the columns.
+ * true
if header is visible
+ */
+ public boolean isHeaderVisible() {
+ return headerVisible;
+ }
+
+ /**
+ * Sets the header visible for the row.
+ *
+ * @param visible
+ * should the header be shown
+ */
+ public void setHeaderVisible(boolean visible) {
+ headerVisible = visible;
+ grid.refreshHeader();
+ }
+
+ /**
+ * Is the footer visible for the row.
+ *
+ * @return true
if footer is visible
+ */
+ public boolean isFooterVisible() {
+ return footerVisible;
+ }
+
+ /**
+ * Sets the footer visible for the row.
+ *
+ * @param visible
+ * should the footer be shown
+ */
+ public void setFooterVisible(boolean visible) {
+ footerVisible = visible;
+ grid.refreshFooter();
+ }
+
+ /**
+ * Iterates all the column groups and checks if the columns alread has been
+ * added to a group.
+ */
+ private boolean isColumnGrouped(GridColumn column) {
+ for (ColumnGroup group : groups) {
+ if (group.getColumns().contains(column)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java
index 3c4e2d6e13..67f14301f0 100644
--- a/client/src/com/vaadin/client/ui/grid/Grid.java
+++ b/client/src/com/vaadin/client/ui/grid/Grid.java
@@ -55,7 +55,7 @@ import com.vaadin.shared.util.SharedUtil;
public class Gridtrue
to show the column in the grid
+ * true
if the column should be displayed in the
+ * grid
*/
public void setVisible(boolean visible) {
if (this.visible == visible) {
@@ -206,8 +223,9 @@ public class Gridtrue
if the row should be visible
+ */
+ public abstract boolean isRowVisible(ColumnGroupRow row);
+
+ /**
+ * Should the first row be visible
+ *
+ * @return true
if the first row should be visible
+ */
+ public abstract boolean firstRowIsVisible();
+
+ @Override
+ public void updateCells(Row row, Listtrue
if we refreshing the header, else assumed
+ * the footer
*/
- private void refreshHeader() {
- RowContainer header = escalator.getHeader();
- if (isHeaderVisible() && header.getRowCount() > 0) {
- header.refreshRows(0, header.getRowCount());
+ private void refreshRowContainer(RowContainer rows,
+ boolean firstRowIsVisible, boolean isHeader) {
+
+ // Count needed rows
+ int totalRows = firstRowIsVisible ? 1 : 0;
+ for (ColumnGroupRow row : columnGroupRows) {
+ if (isHeader ? row.isHeaderVisible() : row.isFooterVisible()) {
+ totalRows++;
+ }
+ }
+
+ // Add or Remove rows on demand
+ int rowDiff = totalRows - rows.getRowCount();
+ if (rowDiff > 0) {
+ rows.insertRows(0, rowDiff);
+ } else if (rowDiff < 0) {
+ rows.removeRows(0, -rowDiff);
+ }
+
+ // Refresh all the rows
+ if (rows.getRowCount() > 0) {
+ rows.refreshRows(0, rows.getRowCount());
}
}
/**
- * Refreshes all footer rows.
+ * Refreshes all header rows
*/
- private void refreshFooter() {
- RowContainer footer = escalator.getFooter();
- if (isFooterVisible() && footer.getRowCount() > 0) {
- footer.refreshRows(0, footer.getRowCount());
- }
+ void refreshHeader() {
+ refreshRowContainer(escalator.getHeader(), isColumnHeadersVisible(),
+ true);
+ }
+
+ /**
+ * Refreshes all footer rows
+ */
+ void refreshFooter() {
+ refreshRowContainer(escalator.getFooter(), isColumnFootersVisible(),
+ false);
}
/**
@@ -388,71 +573,200 @@ public class Gridfalse
.
+ * true
if header rows should be visible
*/
- public void setHeaderVisible(boolean visible) {
- if (visible == isHeaderVisible()) {
+ public void setColumnHeadersVisible(boolean visible) {
+ if (visible == isColumnHeadersVisible()) {
return;
}
-
- RowContainer header = escalator.getHeader();
-
- // TODO Should support multiple headers
- if (visible) {
- header.insertRows(0, 1);
- } else {
- header.removeRows(0, 1);
- }
+ columnHeadersVisible = visible;
+ refreshHeader();
}
/**
- * Are the header row(s) visible?
+ * Are the column headers visible
*
- * @return true
if the header is visible
+ * @return true
if they are visible
*/
- public boolean isHeaderVisible() {
- return escalator.getHeader().getRowCount() > 0;
+ public boolean isColumnHeadersVisible() {
+ return columnHeadersVisible;
}
/**
- * Sets the footer row(s) visible.
+ * Set the column footers visible.
+ *
+ * false
.
+ * true
if the footer row should be visible
*/
- public void setFooterVisible(boolean visible) {
- if (visible == isFooterVisible()) {
+ public void setColumnFootersVisible(boolean visible) {
+ if (visible == isColumnFootersVisible()) {
return;
}
+ this.columnFootersVisible = visible;
+ refreshFooter();
+ }
- RowContainer footer = escalator.getFooter();
+ /**
+ * Are the column footers visible
+ *
+ * @return true
if they are visible
+ *
+ */
+ public boolean isColumnFootersVisible() {
+ return columnFootersVisible;
+ }
- // TODO Should support multiple footers
- if (visible) {
- footer.insertRows(0, 1);
- } else {
- footer.removeRows(0, 1);
- }
+ /**
+ * Adds a new column group row to the grid.
+ *
+ *
+ * // Add a new column group row to the grid
+ * ColumnGroupRow row = grid.addColumnGroupRow();
+ *
+ * // Group "Column1" and "Column2" together to form a header in the row
+ * ColumnGroup column12 = row.addGroup("Column1", "Column2");
+ *
+ * // Set a common header for "Column1" and "Column2"
+ * column12.setHeader("Column 1&2");
+ *
+ * // Set a common footer for "Column1" and "Column2"
+ * column12.setFooter("Column 1&2");
+ *
+ *
+ * @return a column group row instance you can use to add column groups
+ */
+ public ColumnGroupRow addColumnGroupRow() {
+ ColumnGroupRow row = new ColumnGroupRow(this);
+ columnGroupRows.add(row);
+ refreshHeader();
+ refreshFooter();
+ return row;
+ }
+
+ /**
+ * Adds a new column group row to the grid at a specific index.
+ *
+ * @see #addColumnGroupRow() {@link Grid#addColumnGroupRow()} for example
+ * usage
+ *
+ * @param rowIndex
+ * the index where the column group row should be added
+ * @return a column group row instance you can use to add column groups
+ */
+ public ColumnGroupRow addColumnGroupRow(int rowIndex) {
+ ColumnGroupRow row = new ColumnGroupRow(this);
+ columnGroupRows.add(rowIndex, row);
+ refreshHeader();
+ refreshFooter();
+ return row;
}
/**
- * Are the footer row(s) visible?
+ * Removes a column group row
*
- * @return true
if the footer is visible
+ * @param row
+ * The row to remove
*/
- public boolean isFooterVisible() {
- return escalator.getFooter().getRowCount() > 0;
+ public void removeColumnGroupRow(ColumnGroupRow row) {
+ columnGroupRows.remove(row);
+ refreshHeader();
+ refreshFooter();
+ }
+
+ /**
+ * Get the column group rows
+ *
+ * @return a unmodifiable list of column group rows
+ *
+ */
+ public Listnull
if not
+ * found.
+ */
+ private static ColumnGroup getGroupForColumn(ColumnGroupRow row,
+ GridColumn column) {
+ for (ColumnGroup group : row.getGroups()) {
+ List
+ * Example usage: + * + *
+ * // Add a new column group row to the grid + * ColumnGroupRow row = grid.addColumnGroupRow(); + * + * // Group "Column1" and "Column2" together to form a header in the row + * ColumnGroup column12 = row.addGroup("Column1", "Column2"); + * + * // Set a common header for "Column1" and "Column2" + * column12.setHeader("Column 1&2"); + *+ * + * + * + * @return a column group instance you can use to add column groups + */ + public ColumnGroupRow addColumnGroupRow() { + ColumnGroupRowState state = new ColumnGroupRowState(); + ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys); + columnGroupRows.add(row); + getState().columnGroupRows.add(state); + return row; + } + + /** + * Adds a new column group to the grid at a specific index + * + * @param rowIndex + * the index of the row + * @return a column group instance you can use to add column groups + */ + public ColumnGroupRow addColumnGroupRow(int rowIndex) { + ColumnGroupRowState state = new ColumnGroupRowState(); + ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys); + columnGroupRows.add(rowIndex, row); + getState().columnGroupRows.add(rowIndex, state); + return row; + } + + /** + * Removes a column group. + * + * @param row + * the row to remove + */ + public void removeColumnGroupRow(ColumnGroupRow row) { + columnGroupRows.remove(row); + getState().columnGroupRows.remove(row.getState()); + } + + /** + * Gets the column group rows. + * + * @return an unmodifiable list of column group rows + */ + public List
null
if not found
+ * @return the column with the id or null
if not found
*/
GridColumn getColumnByColumnId(String columnId) {
- Object propertyId = columnKeys.get(columnId);
+ Object propertyId = getPropertyIdByColumnId(columnId);
return getColumn(propertyId);
}
+ /**
+ * Used internally by the {@link Grid} to get a property id by referencing
+ * the columns generated state id.
+ *
+ * @param columnId
+ * The state id of the column
+ * @return The column instance or null if not found
+ */
+ Object getPropertyIdByColumnId(String columnId) {
+ return columnKeys.get(columnId);
+ }
+
@Override
protected GridState getState() {
return (GridState) super.getState();
@@ -241,7 +337,7 @@ public class Grid extends AbstractComponent {
* @param datasourcePropertyId
* The property id of a property in the datasource
*/
- protected GridColumn appendColumn(Object datasourcePropertyId) {
+ private GridColumn appendColumn(Object datasourcePropertyId) {
if (datasourcePropertyId == null) {
throw new IllegalArgumentException("Property id cannot be null");
}
diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java
index 505919b3cf..dde0669238 100644
--- a/server/src/com/vaadin/ui/components/grid/GridColumn.java
+++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java
@@ -16,6 +16,8 @@
package com.vaadin.ui.components.grid;
+import java.io.Serializable;
+
import com.vaadin.shared.ui.grid.GridColumnState;
/**
@@ -25,10 +27,10 @@ import com.vaadin.shared.ui.grid.GridColumnState;
* @since 7.2
* @author Vaadin Ltd
*/
-public class GridColumn {
+public class GridColumn implements Serializable {
/**
- * The shared state of the column
+ * The state of the column shared to the client
*/
private final GridColumnState state;
@@ -138,9 +140,16 @@ public class GridColumn {
* the new pixel width of the column
* @throws IllegalStateException
* if the column is no longer attached to any grid
+ * @throws IllegalArgumentException
+ * thrown if pixel width is less than zero
*/
- public void setWidth(int pixelWidth) throws IllegalStateException {
+ public void setWidth(int pixelWidth) throws IllegalStateException,
+ IllegalArgumentException {
checkColumnIsAttached();
+ if (pixelWidth < 0) {
+ throw new IllegalArgumentException(
+ "Pixel width should be greated than 0");
+ }
state.width = pixelWidth;
grid.markAsDirty();
}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
index 5989d537b4..85864160a8 100644
--- a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
@@ -32,6 +32,8 @@ import com.vaadin.data.util.IndexedContainer;
import com.vaadin.server.KeyMapper;
import com.vaadin.shared.ui.grid.GridColumnState;
import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.ui.components.grid.ColumnGroup;
+import com.vaadin.ui.components.grid.ColumnGroupRow;
import com.vaadin.ui.components.grid.Grid;
import com.vaadin.ui.components.grid.GridColumn;
@@ -110,9 +112,15 @@ public class GridColumns {
assertEquals(100, column.getWidth());
assertEquals(column.getWidth(), getColumnState("column1").width);
- column.setWidth(-1);
- assertEquals(-1, column.getWidth());
- assertEquals(-1, getColumnState("column1").width);
+ try {
+ column.setWidth(-1);
+ fail("Setting width to -1 should throw exception");
+ } catch (IllegalArgumentException iae) {
+
+ }
+
+ assertEquals(100, column.getWidth());
+ assertEquals(100, getColumnState("column1").width);
}
@Test
@@ -126,6 +134,7 @@ public class GridColumns {
try {
column.setHeaderCaption("asd");
+
fail("Succeeded in modifying a detached column");
} catch (IllegalStateException ise) {
// Detached state should throw exception
@@ -157,7 +166,7 @@ public class GridColumns {
}
@Test
- public void testAddingColumn() {
+ public void testAddingColumn() throws Exception {
grid.getContainerDatasource().addContainerProperty("columnX",
String.class, "");
GridColumn column = grid.getColumn("columnX");
@@ -165,33 +174,72 @@ public class GridColumns {
}
@Test
- public void testHeaderVisiblility() {
+ public void testHeaderVisiblility() throws Exception {
- assertTrue(grid.isHeaderVisible());
- assertTrue(state.headerVisible);
+ assertTrue(grid.isColumnHeadersVisible());
+ assertTrue(state.columnHeadersVisible);
- grid.setHeaderVisible(false);
- assertFalse(grid.isHeaderVisible());
- assertFalse(state.headerVisible);
+ grid.setColumnHeadersVisible(false);
+ assertFalse(grid.isColumnHeadersVisible());
+ assertFalse(state.columnHeadersVisible);
- grid.setHeaderVisible(true);
- assertTrue(grid.isHeaderVisible());
- assertTrue(state.headerVisible);
+ grid.setColumnHeadersVisible(true);
+ assertTrue(grid.isColumnHeadersVisible());
+ assertTrue(state.columnHeadersVisible);
}
@Test
- public void testFooterVisibility() {
+ public void testFooterVisibility() throws Exception {
+
+ assertFalse(grid.isColumnFootersVisible());
+ assertFalse(state.columnFootersVisible);
- assertTrue(grid.isFooterVisible());
- assertTrue(state.footerVisible);
+ grid.setColumnFootersVisible(false);
+ assertFalse(grid.isColumnFootersVisible());
+ assertFalse(state.columnFootersVisible);
- grid.setFooterVisible(false);
- assertFalse(grid.isFooterVisible());
- assertFalse(state.footerVisible);
+ grid.setColumnFootersVisible(true);
+ assertTrue(grid.isColumnFootersVisible());
+ assertTrue(state.columnFootersVisible);
+ }
- grid.setFooterVisible(true);
- assertTrue(grid.isFooterVisible());
- assertTrue(state.footerVisible);
+ @Test
+ public void testColumnGroups() throws Exception {
+
+ // Add a new row
+ ColumnGroupRow row = grid.addColumnGroupRow();
+ assertTrue(state.columnGroupRows.size() == 1);
+
+ // Add a group by property id
+ ColumnGroup columns12 = row.addGroup("column1", "column2");
+ assertTrue(state.columnGroupRows.get(0).groups.size() == 1);
+
+ // Set header of column
+ columns12.setHeaderCaption("Column12");
+ assertEquals("Column12",
+ state.columnGroupRows.get(0).groups.get(0).header);
+
+ // Set footer of column
+ columns12.setFooterCaption("Footer12");
+ assertEquals("Footer12",
+ state.columnGroupRows.get(0).groups.get(0).footer);
+
+ // Add another group by column instance
+ ColumnGroup columns34 = row.addGroup(grid.getColumn("column3"),
+ grid.getColumn("column4"));
+ assertTrue(state.columnGroupRows.get(0).groups.size() == 2);
+
+ // add another group row
+ ColumnGroupRow row2 = grid.addColumnGroupRow();
+ assertTrue(state.columnGroupRows.size() == 2);
+
+ // add a group by combining the two previous groups
+ ColumnGroup columns1234 = row2.addGroup(columns12, columns34);
+ assertTrue(columns1234.getColumns().size() == 4);
+
+ // Insert a group as the second group
+ ColumnGroupRow newRow2 = grid.addColumnGroupRow(1);
+ assertTrue(state.columnGroupRows.size() == 3);
}
private GridColumnState getColumnState(Object propertyId) {
diff --git a/shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java
new file mode 100644
index 0000000000..a8e0f87457
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java
@@ -0,0 +1,46 @@
+/*
+ * 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 java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * The column group row data shared between the server and client
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class ColumnGroupRowState implements Serializable {
+
+ /**
+ * The groups that has been added to the row
+ */
+ public ListfirstRowIndex
+ */
+ protected void setRowData(int firstRowIndex, List
+ * This method triggers lazy loading of data if necessary. The change
+ * handler registered using {@link #setDataChangeHandler(DataChangeHandler)}
+ * is informed when new data has been loaded.
+ *
+ * @param firstRowIndex
+ * the index of the first needed row
+ * @param numberOfRows
+ * the number of needed rows
+ */
+ public void ensureAvailability(int firstRowIndex, int numberOfRows);
+
+ /**
+ * Retrieves the data for the row at the given index. If the row data is not
+ * available, returns null
.
+ *
+ * This method does not trigger loading of unavailable data.
+ * {@link #ensureAvailability(int, int)} should be used to signal what data
+ * will be needed.
+ *
+ * @param rowIndex
+ * the index of the row to retrieve data for
+ * @return data for the row; or
+ * All columns up to and including the given column will be frozen in place
+ * when the grid is scrolled sideways.
+ *
+ * @param lastFrozenColumn
+ * the rightmost column to freeze, or
+ * Note: Most usually, this method returns the very value set with
+ * {@link #setLastFrozenColumn(GridColumn)}. This value, however, can be
+ * reset to
+ * All columns up to and including the given column will be frozen in place
+ * when the grid is scrolled sideways.
+ *
+ * @param lastFrozenColumn
+ * the rightmost column to freeze, or
+ * All columns up to and including the indicated property will be frozen in
+ * place when the grid is scrolled sideways.
+ *
+ * Note: If the container used by this grid supports a propertyId
+ *
+ * Note: Most often, this method returns the very value set with
+ * {@link #setLastFrozenPropertyId(Object)}. This value, however, can be
+ * reset to
- * The range is treated as inclusive at the start, and exclusive at the end.
- * I.e. the range [0..1[ has the length 1, and represents one integer: 0.
- *
- * The range is considered {@link #isEmpty() empty} if the start is the same as
- * the end.
- *
- * @since 7.2
- * @author Vaadin Ltd
- */
-public final class Range {
- private final int start;
- private final int end;
-
- /**
- * Creates a range object representing a single integer.
- *
- * @param integer
- * the number to represent as a range
- * @return the range represented by
- * The range start is inclusive and the end is exclusive.
- * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4,
- * but not 5.
- *
- * @param start
- * the start of the the range, inclusive
- * @param end
- * the end of the range, exclusive
- * @return a range representing
- * The three partitions are returned as a three-element Range array:
- *
- * Example:
- * If {@code integer} is less than {@code start}, [empty,
- * {@code this} ] is returned. if
- * Calling this method is equivalent to calling
- *
- * Example:
- *
+ * This bookeeping includes, but is not limited to:
+ *
+ * "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
+ * This method's responsibilities are to:
+ *
+ * One instance of this class can (and should) be reused for all the
+ * properties in an item, since this class will inform that the entire row
+ * needs to be re-evaluated (in contrast to a property-based change
+ * management)
+ *
+ * Since there's no Container-wide possibility to listen to any kind of
+ * value changes, an instance of this class needs to be attached to each and
+ * every Item's Property in the container.
+ *
+ * @see Grid#addValueChangeListener(Container, Object, Object)
+ * @see Grid#valueChangeListeners
+ */
+ private class GridValueChangeListener implements ValueChangeListener {
+ private final Object itemId;
+
+ public GridValueChangeListener(Object itemId) {
+ /*
+ * Using an assert instead of an exception throw, just to optimize
+ * prematurely
+ */
+ assert itemId != null : "null itemId not accepted";
+ this.itemId = itemId;
+ }
+
+ @Override
+ public void valueChange(ValueChangeEvent event) {
+ datasourceExtension.updateRowData(datasource.indexOfId(itemId));
+ }
+ }
+
/**
* The data source attached to the grid
*/
@@ -98,13 +387,17 @@ public class Grid extends AbstractComponent {
columnKeys.remove(columnId);
getState().columns.remove(column.getState());
}
+ activeRowHandler.propertiesRemoved(removedColumns);
// Add new columns
+ HashSet
+ * The range is treated as inclusive at the start, and exclusive at the end.
+ * I.e. the range [0..1[ has the length 1, and represents one integer: 0.
+ *
+ * The range is considered {@link #isEmpty() empty} if the start is the same as
+ * the end.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public final class Range {
+ private final int start;
+ private final int end;
+
+ /**
+ * Creates a range object representing a single integer.
+ *
+ * @param integer
+ * the number to represent as a range
+ * @return the range represented by
+ * The range start is inclusive and the end is exclusive.
+ * So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4,
+ * but not 5.
+ *
+ * @param start
+ * the start of the the range, inclusive
+ * @param end
+ * the end of the range, exclusive
+ * @return a range representing
+ * The three partitions are returned as a three-element Range array:
+ *
+ * Example:
+ * If {@code integer} is less than {@code start}, [empty,
+ * {@code this} ] is returned. if
+ * Calling this method is equivalent to calling
+ *
+ * Example:
+ * null
if no data is available
+ */
+ public T getRow(int rowIndex);
+
+ /**
+ * Returns the current best guess for the number of rows in the container.
+ *
+ * @return the current estimation of the container size
+ */
+ public int getEstimatedSize();
+
+ /**
+ * Sets a data change handler to inform when data is updated, added or
+ * removed.
+ *
+ * @param dataChangeHandler
+ * the data change handler
+ */
+ public void setDataChangeHandler(DataChangeHandler dataChangeHandler);
+
+}
diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
new file mode 100644
index 0000000000..1785fc62c2
--- /dev/null
+++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
@@ -0,0 +1,71 @@
+/*
+ * 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.data;
+
+import java.util.List;
+
+import com.vaadin.client.ServerConnector;
+import com.vaadin.client.extensions.AbstractExtensionConnector;
+import com.vaadin.client.ui.grid.GridConnector;
+import com.vaadin.shared.data.DataProviderRpc;
+import com.vaadin.shared.data.DataProviderState;
+import com.vaadin.shared.data.DataRequestRpc;
+import com.vaadin.shared.ui.Connect;
+
+/**
+ * Connects a Vaadin server-side container data source to a Grid. This is
+ * currently implemented as an Extension hardcoded to support a specific
+ * connector type. This will be changed once framework support for something
+ * more flexible has been implemented.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.data.RpcDataProviderExtension.class)
+public class RpcDataSourceConnector extends AbstractExtensionConnector {
+
+ private final AbstractRemoteDataSourcedataSource
is null
+ */
+ public void setDataSource(DataSourcenull
to not
+ * have any columns frozen
+ * @throws IllegalArgumentException
+ * if {@code lastFrozenColumn} is not a column from this grid
+ */
+ public void setLastFrozenColumn(GridColumnnull
if the column is removed from this grid.
+ *
+ * @return the rightmost frozen column in the grid, or null
if
+ * no columns are frozen.
+ */
+ public GridColumnnull
to not
+ * have any columns frozen
+ * @throws IllegalArgumentException
+ * if {@code lastFrozenColumn} is not a column from this grid
+ */
+ void setLastFrozenColumn(GridColumn lastFrozenColumn) {
+ /*
+ * TODO: If and when Grid supports column reordering or insertion of
+ * columns before other columns, make sure to mention that adding
+ * columns before lastFrozenColumn will change the frozen column count
+ */
+
+ if (lastFrozenColumn == null) {
+ getState().lastFrozenColumnId = null;
+ } else if (columns.containsValue(lastFrozenColumn)) {
+ getState().lastFrozenColumnId = lastFrozenColumn.getState().id;
+ } else {
+ throw new IllegalArgumentException(
+ "The given column isn't attached to this grid");
+ }
+ }
+
+ /**
+ * Sets (or unsets) the rightmost frozen column in the grid.
+ * null
, it can never be defined as the last frozen column, as
+ * a null
parameter will always reset the frozen columns in
+ * Grid.
+ *
+ * @param propertyId
+ * the property id corresponding to the column that should be the
+ * last frozen column, or null
to not have any
+ * columns frozen.
+ * @throws IllegalArgumentException
+ * if {@code lastFrozenColumn} is not a column from this grid
+ */
+ public void setLastFrozenPropertyId(Object propertyId) {
+ final GridColumn column;
+ if (propertyId == null) {
+ column = null;
+ } else {
+ column = getColumn(propertyId);
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "property id does not exist.");
+ }
+ }
+ setLastFrozenColumn(column);
+ }
+
+ /**
+ * Gets the rightmost frozen column in the grid.
+ * null
if the column is detached from this grid.
+ *
+ * @return the rightmost frozen column in the grid, or null
if
+ * no columns are frozen.
+ */
+ public Object getLastFrozenPropertyId() {
+ return columnKeys.get(getState().lastFrozenColumnId);
+ }
}
diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java
index dde0669238..8dae9428e5 100644
--- a/server/src/com/vaadin/ui/components/grid/GridColumn.java
+++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java
@@ -192,4 +192,16 @@ public class GridColumn implements Serializable {
throw new IllegalStateException("Column no longer exists.");
}
}
+
+ /**
+ * Sets this column as the last frozen column in its grid.
+ *
+ * @throws IllegalArgumentException
+ * if the column is no longer attached to any grid
+ * @see Grid#setLastFrozenColumn(GridColumn)
+ */
+ public void setLastFrozenColumn() {
+ checkColumnIsAttached();
+ grid.setLastFrozenColumn(this);
+ }
}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
index 85864160a8..c129db0264 100644
--- a/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
@@ -242,6 +242,20 @@ public class GridColumns {
assertTrue(state.columnGroupRows.size() == 3);
}
+ @Test
+ public void testFrozenColumnByPropertyId() {
+ assertNull("Grid should not start with a frozen column",
+ grid.getLastFrozenPropertyId());
+
+ Object propertyId = grid.getContainerDatasource()
+ .getContainerPropertyIds().iterator().next();
+ grid.setLastFrozenPropertyId(propertyId);
+ assertEquals(propertyId, grid.getLastFrozenPropertyId());
+
+ grid.getContainerDatasource().removeContainerProperty(propertyId);
+ assertNull(grid.getLastFrozenPropertyId());
+ }
+
private GridColumnState getColumnState(Object propertyId) {
String columnId = columnIdMapper.key(propertyId);
for (GridColumnState columnState : state.columns) {
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java
index d1167f3d4f..93e602a539 100644
--- a/shared/src/com/vaadin/shared/ui/grid/GridState.java
+++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java
@@ -53,4 +53,12 @@ public class GridState extends AbstractComponentState {
* The column groups added to the grid
*/
public ListfirstRowIndex
+ */
+ protected void removeRowData(int firstRowIndex, int count) {
+ Profiler.enter("AbstractRemoteDataSource.removeRowData");
+
+ // pack the cached data
+ for (int i = 0; i < count; i++) {
+ Integer oldIndex = Integer.valueOf(firstRowIndex + count + i);
+ if (rowCache.containsKey(oldIndex)) {
+ Integer newIndex = Integer.valueOf(firstRowIndex + i);
+ rowCache.put(newIndex, rowCache.remove(oldIndex));
+ }
+ }
+
+ Range removedRange = Range.withLength(firstRowIndex, count);
+ if (removedRange.intersects(cached)) {
+ Range[] partitions = cached.partitionWith(removedRange);
+ Range remainsBefore = partitions[0];
+ Range transposedRemainsAfter = partitions[2].offsetBy(-removedRange
+ .length());
+ cached = remainsBefore.combineWith(transposedRemainsAfter);
+ }
+ estimatedSize -= count;
+ dataChangeHandler.dataRemoved(firstRowIndex, count);
+ checkCacheCoverage();
+
+ Profiler.leave("AbstractRemoteDataSource.removeRowData");
+ }
+
+ /**
+ * Informs this data source that new data has been inserted from the server.
+ *
+ * @param firstRowIndex
+ * the destination index of the new row data
+ * @param count
+ * the number of rows inserted
+ */
+ protected void insertRowData(int firstRowIndex, int count) {
+ Profiler.enter("AbstractRemoteDataSource.insertRowData");
+
+ if (cached.contains(firstRowIndex)) {
+ int oldCacheEnd = cached.getEnd();
+ /*
+ * We need to invalidate the cache from the inserted row onwards,
+ * since the cache wants to be a contiguous range. It doesn't
+ * support holes.
+ *
+ * If holes were supported, we could shift the higher part of
+ * "cached" and leave a hole the size of "count" in the middle.
+ */
+ cached = cached.splitAt(firstRowIndex)[0];
+
+ for (int i = firstRowIndex; i < oldCacheEnd; i++) {
+ rowCache.remove(Integer.valueOf(i));
+ }
+ }
+
+ else if (firstRowIndex < cached.getStart()) {
+ Range oldCached = cached;
+ cached = cached.offsetBy(count);
+
+ for (int i = 0; i < rowCache.size(); i++) {
+ Integer oldIndex = Integer.valueOf(oldCached.getEnd() - i);
+ Integer newIndex = Integer.valueOf(cached.getEnd() - i);
+ rowCache.put(newIndex, rowCache.remove(oldIndex));
+ }
+ }
+
+ estimatedSize += count;
+ dataChangeHandler.dataAdded(firstRowIndex, count);
+ checkCacheCoverage();
+
+ Profiler.leave("AbstractRemoteDataSource.insertRowData");
+ }
}
diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
index 1785fc62c2..4d22c10197 100644
--- a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
+++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
@@ -56,6 +56,16 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector {
public void setRowData(int firstRow, Listnull
+ * on initialization, but not after that.
+ */
private DataSourceinteger
- */
- public static Range withOnly(final int integer) {
- return new Range(integer, integer + 1);
- }
-
- /**
- * Creates a range between two integers.
- * [start..end[
- * @throws IllegalArgumentException
- * if start > end
- */
- public static Range between(final int start, final int end)
- throws IllegalArgumentException {
- return new Range(start, end);
- }
-
- /**
- * Creates a range from a start point, with a given length.
- *
- * @param start
- * the first integer to include in the range
- * @param length
- * the length of the resulting range
- * @return a range starting from start
, with
- * length
number of integers following
- * @throws IllegalArgumentException
- * if length < 0
- */
- public static Range withLength(final int start, final int length)
- throws IllegalArgumentException {
- if (length < 0) {
- /*
- * The constructor of Range will throw an exception if start >
- * start+length (i.e. if length is negative). We're throwing the
- * same exception type, just with a more descriptive message.
- */
- throw new IllegalArgumentException("length must not be negative");
- }
- return new Range(start, start + length);
- }
-
- /**
- * Creates a new range between two numbers: [start..end[
.
- *
- * @param start
- * the start integer, inclusive
- * @param end
- * the end integer, exclusive
- * @throws IllegalArgumentException
- * if start > end
- */
- private Range(final int start, final int end)
- throws IllegalArgumentException {
- if (start > end) {
- throw new IllegalArgumentException(
- "start must not be greater than end");
- }
-
- this.start = start;
- this.end = end;
- }
-
- /**
- * Returns the inclusive start point of this range.
- *
- * @return the start point of this range
- */
- public int getStart() {
- return start;
- }
-
- /**
- * Returns the exclusive end point of this range.
- *
- * @return the end point of this range
- */
- public int getEnd() {
- return end;
- }
-
- /**
- * The number of integers contained in the range.
- *
- * @return the number of integers contained in the range
- */
- public int length() {
- return getEnd() - getStart();
- }
-
- /**
- * Checks whether the range has no elements between the start and end.
- *
- * @return true
iff the range contains no elements.
- */
- public boolean isEmpty() {
- return getStart() >= getEnd();
- }
-
- /**
- * Checks whether this range and another range are at least partially
- * covering the same values.
- *
- * @param other
- * the other range to check against
- * @return true
if this and other
intersect
- */
- public boolean intersects(final Range other) {
- return getStart() < other.getEnd() && other.getStart() < getEnd();
- }
-
- /**
- * Checks whether an integer is found within this range.
- *
- * @param integer
- * an integer to test for presence in this range
- * @return true
iff integer
is in this range
- */
- public boolean contains(final int integer) {
- return getStart() <= integer && integer < getEnd();
- }
-
- /**
- * Checks whether this range is a subset of another range.
- *
- * @return true
iff other
completely wraps this
- * range
- */
- public boolean isSubsetOf(final Range other) {
- return other.getStart() <= getStart() && getEnd() <= other.getEnd();
- }
-
- /**
- * Overlay this range with another one, and partition the ranges according
- * to how they position relative to each other.
- *
- *
- *
- * @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 other
.
- * other
.
- * offset
- */
- public Range offsetBy(final int offset) {
- if (offset == 0) {
- return this;
- } else {
- return new Range(start + offset, end + offset);
- }
- }
-
- @Override
- public String toString() {
- return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd()
- + "[" + (isEmpty() ? " (empty)" : "");
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + end;
- result = prime * result + start;
- return result;
- }
-
- @Override
- public boolean equals(final Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- final Range other = (Range) obj;
- if (end != other.end) {
- return false;
- }
- if (start != other.start) {
- return false;
- }
- return true;
- }
-
- /**
- * Checks whether this range starts before the start of another range.
- *
- * @param other
- * the other range to compare against
- * @return true
iff this range starts before the
- * other
- */
- public boolean startsBefore(final Range other) {
- return getStart() < other.getStart();
- }
-
- /**
- * Checks whether this range ends before the start of another range.
- *
- * @param other
- * the other range to compare against
- * @return true
iff this range ends before the
- * other
- */
- public boolean endsBefore(final Range other) {
- return getEnd() <= other.getStart();
- }
-
- /**
- * Checks whether this range ends after the end of another range.
- *
- * @param other
- * the other range to compare against
- * @return true
iff this range ends after the
- * other
- */
- public boolean endsAfter(final Range other) {
- return getEnd() > other.getEnd();
- }
-
- /**
- * Checks whether this range starts after the end of another range.
- *
- * @param other
- * the other range to compare against
- * @return true
iff this range starts after the
- * other
- */
- public boolean startsAfter(final Range other) {
- return getStart() >= other.getEnd();
- }
-
- /**
- * Split the range into two at a certain integer.
- * [5..10[.splitAt(7) == [5..7[, [7..10[
- *
- * @param integer
- * the integer at which to split the range into two
- * @return an array of two ranges, with [start..integer[
in the
- * first element, and [integer..end[
in the second
- * element.
- * integer
is equal to
- * or greater than {@code end}, [{@code this}, empty] is returned
- * instead.
- */
- public Range[] splitAt(final int integer) {
- if (integer < start) {
- return new Range[] { Range.withLength(start, 0), this };
- } else if (integer >= end) {
- return new Range[] { this, Range.withLength(end, 0) };
- } else {
- return new Range[] { new Range(start, integer),
- new Range(integer, end) };
- }
- }
-
- /**
- * Split the range into two after a certain number of integers into the
- * range.
- * {@link #splitAt(int) splitAt}({@link #getStart()}+length);
- * [5..10[.splitAtFromStart(2) == [5..7[, [7..10[
- *
- * @param length
- * the length at which to split this range into two
- * @return an array of two ranges, having the length
-first
- * elements of this range, and the second range having the rest. If
- * length
≤ 0, the first element will be empty, and
- * the second element will be this range. If length
- * ≥ {@link #length()}, the first element will be this range,
- * and the second element will be empty.
- */
- public Range[] splitAtFromStart(final int length) {
- return splitAt(getStart() + length);
- }
-
- /**
- * Combines two ranges to create a range containing all values in both
- * ranges, provided there are no gaps between the ranges.
- *
- * @param other
- * the range to combine with this range
- *
- * @return the combined range
- *
- * @throws IllegalArgumentException
- * if the two ranges aren't connected
- */
- public Range combineWith(Range other) throws IllegalArgumentException {
- if (getStart() > other.getEnd() || other.getStart() > getEnd()) {
- throw new IllegalArgumentException("There is a gap between " + this
- + " and " + other);
- }
-
- return Range.between(Math.min(getStart(), other.getStart()),
- Math.max(getEnd(), other.getEnd()));
- }
-}
diff --git a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
index 3cbc6351b1..e97bb339e4 100644
--- a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
+++ b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
@@ -21,6 +21,8 @@ import static org.junit.Assert.assertTrue;
import org.junit.Test;
+import com.vaadin.shared.ui.grid.Range;
+
@SuppressWarnings("static-method")
public class PartitioningTest {
diff --git a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java b/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java
deleted file mode 100644
index d73b0fb02f..0000000000
--- a/client/tests/src/com/vaadin/client/ui/grid/RangeTest.java
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- * Copyright 2000-2013 Vaadin Ltd.
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-package com.vaadin.client.ui.grid;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import org.junit.Test;
-
-@SuppressWarnings("static-method")
-public class RangeTest {
-
- @Test(expected = IllegalArgumentException.class)
- public void startAfterEndTest() {
- Range.between(10, 9);
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void negativeLengthTest() {
- Range.withLength(10, -1);
- }
-
- @Test
- public void constructorEquivalenceTest() {
- assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11));
- assertEquals("[10,20[ == 10, length 10", Range.between(10, 20),
- Range.withLength(10, 10));
- assertEquals("10 == 10, length 1", Range.withOnly(10),
- Range.withLength(10, 1));
- }
-
- @Test
- public void boundsTest() {
- {
- final Range range = Range.between(0, 10);
- assertEquals("between(0, 10) start", 0, range.getStart());
- assertEquals("between(0, 10) end", 10, range.getEnd());
- }
-
- {
- final Range single = Range.withOnly(10);
- assertEquals("withOnly(10) start", 10, single.getStart());
- assertEquals("withOnly(10) end", 11, single.getEnd());
- }
-
- {
- final Range length = Range.withLength(10, 5);
- assertEquals("withLength(10, 5) start", 10, length.getStart());
- assertEquals("withLength(10, 5) end", 15, length.getEnd());
- }
- }
-
- @Test
- @SuppressWarnings("boxing")
- public void equalsTest() {
- final Range range1 = Range.between(0, 10);
- final Range range2 = Range.withLength(0, 11);
-
- assertTrue("null", !range1.equals(null));
- assertTrue("reflexive", range1.equals(range1));
- assertEquals("symmetric", range1.equals(range2), range2.equals(range1));
- }
-
- @Test
- public void containsTest() {
- final int start = 0;
- final int end = 10;
- final Range range = Range.between(start, end);
-
- assertTrue("start should be contained", range.contains(start));
- assertTrue("start-1 should not be contained",
- !range.contains(start - 1));
- assertTrue("end should not be contained", !range.contains(end));
- assertTrue("end-1 should be contained", range.contains(end - 1));
-
- assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5));
- assertTrue("empty range does not contain 5", !Range.between(5, 5)
- .contains(5));
- }
-
- @Test
- public void emptyTest() {
- assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty());
- assertTrue("Range of length 0 should be empty", Range.withLength(0, 0)
- .isEmpty());
-
- assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty());
- assertTrue("Range of length 1 should not be empty",
- !Range.withLength(0, 1).isEmpty());
- }
-
- @Test
- public void splitTest() {
- final Range startRange = Range.between(0, 10);
- final Range[] splitRanges = startRange.splitAt(5);
- assertEquals("[0..10[ split at 5, lower", Range.between(0, 5),
- splitRanges[0]);
- assertEquals("[0..10[ split at 5, upper", Range.between(5, 10),
- splitRanges[1]);
- }
-
- @Test
- public void split_valueBefore() {
- Range range = Range.between(10, 20);
- Range[] splitRanges = range.splitAt(5);
-
- assertEquals(Range.between(10, 10), splitRanges[0]);
- assertEquals(range, splitRanges[1]);
- }
-
- @Test
- public void split_valueAfter() {
- Range range = Range.between(10, 20);
- Range[] splitRanges = range.splitAt(25);
-
- assertEquals(range, splitRanges[0]);
- assertEquals(Range.between(20, 20), splitRanges[1]);
- }
-
- @Test
- public void emptySplitTest() {
- final Range range = Range.between(5, 10);
- final Range[] split1 = range.splitAt(0);
- assertTrue("split1, [0]", split1[0].isEmpty());
- assertEquals("split1, [1]", range, split1[1]);
-
- final Range[] split2 = range.splitAt(15);
- assertEquals("split2, [0]", range, split2[0]);
- assertTrue("split2, [1]", split2[1].isEmpty());
- }
-
- @Test
- public void lengthTest() {
- assertEquals("withLength length", 5, Range.withLength(10, 5).length());
- assertEquals("between length", 5, Range.between(10, 15).length());
- assertEquals("withOnly 10 length", 1, Range.withOnly(10).length());
- }
-
- @Test
- public void intersectsTest() {
- assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10)
- .intersects(Range.between(5, 15)));
- assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10)
- .intersects(Range.between(10, 20)));
- }
-
- @Test
- public void intersects_emptyInside() {
- assertTrue("[5..5[ does intersect with [0..10[", Range.between(5, 5)
- .intersects(Range.between(0, 10)));
- assertTrue("[0..10[ does intersect with [5..5[", Range.between(0, 10)
- .intersects(Range.between(5, 5)));
- }
-
- @Test
- public void intersects_emptyOutside() {
- assertTrue("[15..15[ does not intersect with [0..10[",
- !Range.between(15, 15).intersects(Range.between(0, 10)));
- assertTrue("[0..10[ does not intersect with [15..15[",
- !Range.between(0, 10).intersects(Range.between(15, 15)));
- }
-
- @Test
- public void subsetTest() {
- assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10)
- .isSubsetOf(Range.between(0, 20)));
-
- final Range range = Range.between(0, 10);
- assertTrue("range is subset of self", range.isSubsetOf(range));
-
- assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10)
- .isSubsetOf(Range.between(5, 15)));
- }
-
- @Test
- public void offsetTest() {
- assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5));
- }
-
- @Test
- public void rangeStartsBeforeTest() {
- final Range former = Range.between(0, 5);
- final Range latter = Range.between(1, 5);
- assertTrue("former should starts before latter",
- former.startsBefore(latter));
- assertTrue("latter shouldn't start before latter",
- !latter.startsBefore(former));
-
- assertTrue("no overlap allowed",
- !Range.between(0, 5).startsBefore(Range.between(0, 10)));
- }
-
- @Test
- public void rangeStartsAfterTest() {
- final Range former = Range.between(0, 5);
- final Range latter = Range.between(5, 10);
- assertTrue("latter should start after former",
- latter.startsAfter(former));
- assertTrue("former shouldn't start after latter",
- !former.startsAfter(latter));
-
- assertTrue("no overlap allowed",
- !Range.between(5, 10).startsAfter(Range.between(0, 6)));
- }
-
- @Test
- public void rangeEndsBeforeTest() {
- final Range former = Range.between(0, 5);
- final Range latter = Range.between(5, 10);
- assertTrue("latter should end before former", former.endsBefore(latter));
- assertTrue("former shouldn't end before latter",
- !latter.endsBefore(former));
-
- assertTrue("no overlap allowed",
- !Range.between(5, 10).endsBefore(Range.between(9, 15)));
- }
-
- @Test
- public void rangeEndsAfterTest() {
- final Range former = Range.between(1, 5);
- final Range latter = Range.between(1, 6);
- assertTrue("latter should end after former", latter.endsAfter(former));
- assertTrue("former shouldn't end after latter",
- !former.endsAfter(latter));
-
- assertTrue("no overlap allowed",
- !Range.between(0, 10).endsAfter(Range.between(5, 10)));
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void combine_notOverlappingFirstSmaller() {
- Range.between(0, 10).combineWith(Range.between(11, 20));
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void combine_notOverlappingSecondLarger() {
- Range.between(11, 20).combineWith(Range.between(0, 10));
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void combine_firstEmptyNotOverlapping() {
- Range.between(15, 15).combineWith(Range.between(0, 10));
- }
-
- @Test(expected = IllegalArgumentException.class)
- public void combine_secondEmptyNotOverlapping() {
- Range.between(0, 10).combineWith(Range.between(15, 15));
- }
-
- @Test
- public void combine_barelyOverlapping() {
- Range r1 = Range.between(0, 10);
- Range r2 = Range.between(10, 20);
-
- // Test both ways, should give the same result
- Range combined1 = r1.combineWith(r2);
- Range combined2 = r2.combineWith(r1);
- assertEquals(combined1, combined2);
-
- assertEquals(0, combined1.getStart());
- assertEquals(20, combined1.getEnd());
- }
-
- @Test
- public void combine_subRange() {
- Range r1 = Range.between(0, 10);
- Range r2 = Range.between(2, 8);
-
- // Test both ways, should give the same result
- Range combined1 = r1.combineWith(r2);
- Range combined2 = r2.combineWith(r1);
- assertEquals(combined1, combined2);
-
- assertEquals(r1, combined1);
- }
-
- @Test
- public void combine_intersecting() {
- Range r1 = Range.between(0, 10);
- Range r2 = Range.between(5, 15);
-
- // Test both ways, should give the same result
- Range combined1 = r1.combineWith(r2);
- Range combined2 = r2.combineWith(r1);
- assertEquals(combined1, combined2);
-
- assertEquals(0, combined1.getStart());
- assertEquals(15, combined1.getEnd());
-
- }
-
- @Test
- public void combine_emptyInside() {
- Range r1 = Range.between(0, 10);
- Range r2 = Range.between(5, 5);
-
- // Test both ways, should give the same result
- Range combined1 = r1.combineWith(r2);
- Range combined2 = r2.combineWith(r1);
- assertEquals(combined1, combined2);
-
- assertEquals(r1, combined1);
- }
-
-}
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java
index 48f03b98c0..b22e6a209b 100644
--- a/server/src/com/vaadin/data/RpcDataProviderExtension.java
+++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java
@@ -18,6 +18,7 @@ package com.vaadin.data;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import com.vaadin.data.Container.Indexed;
@@ -67,22 +68,24 @@ public class RpcDataProviderExtension extends AbstractExtension {
Collection> propertyIds = container.getContainerPropertyIds();
Listindex
+ */
+ public void insertRowData(int index, int count) {
+ getState().containerSize += count;
+ getRpcProxy(DataProviderRpc.class).insertRowData(index, count);
+ }
+
+ /**
+ * Informs the client side that rows have been removed from the data source.
+ *
+ * @param firstIndex
+ * the index of the first row removed
+ * @param count
+ * the number of rows removed
+ */
+ public void removeRowData(int firstIndex, int count) {
+ getState().containerSize -= count;
+ getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count);
+ }
+
+ /**
+ * Informs the client side that data of a row has been modified in the data
+ * source.
+ *
+ * @param index
+ * the index of the row that was updated
+ */
+ public void updateRowData(int index) {
+ /*
+ * TODO: ignore duplicate requests for the same index during the same
+ * roundtrip.
+ */
+ Object itemId = container.getIdByIndex(index);
+ String[] row = getRowData(container.getContainerPropertyIds(), itemId);
+ getRpcProxy(DataProviderRpc.class).setRowData(index,
+ Collections.singletonList(row));
+ }
}
diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java
index 1fb0692104..08685874c1 100644
--- a/server/src/com/vaadin/ui/components/grid/Grid.java
+++ b/server/src/com/vaadin/ui/components/grid/Grid.java
@@ -26,15 +26,28 @@ import java.util.List;
import java.util.Map;
import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
+import com.vaadin.data.Container.ItemSetChangeNotifier;
import com.vaadin.data.Container.PropertySetChangeEvent;
import com.vaadin.data.Container.PropertySetChangeListener;
import com.vaadin.data.Container.PropertySetChangeNotifier;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.Property.ValueChangeEvent;
+import com.vaadin.data.Property.ValueChangeListener;
+import com.vaadin.data.Property.ValueChangeNotifier;
import com.vaadin.data.RpcDataProviderExtension;
import com.vaadin.server.KeyMapper;
import com.vaadin.shared.ui.grid.ColumnGroupRowState;
import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridServerRpc;
import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.Range;
import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.Component;
/**
* Data grid component
@@ -56,6 +69,282 @@ import com.vaadin.ui.AbstractComponent;
*/
public class Grid extends AbstractComponent {
+ /**
+ * A helper class that handles the client-side Escalator logic relating to
+ * making sure that whatever is currently visible to the user, is properly
+ * initialized and otherwise handled on the server side (as far as
+ * requried).
+ *
+ *
+ */
+ private final class ActiveRowHandler {
+ /**
+ * A map from itemId to the value change listener used for all of its
+ * properties
+ */
+ private final Map
+ *
+ *
+ * @param firstIndex
+ * the index of the first inserted rows
+ * @param count
+ * the number of rows inserted at count
if the
+ * insertion happens above currently active range
+ * firstIndex
+ */
+ public void insertRows(int firstIndex, int count) {
+ if (firstIndex < activeRange.getStart()) {
+ activeRange = activeRange.offsetBy(count);
+ } else if (firstIndex < activeRange.getEnd()) {
+ final Range deprecatedRange = Range.withLength(
+ activeRange.getEnd(), count);
+ removeValueChangeListeners(deprecatedRange);
+
+ final Range freshRange = Range.between(firstIndex, count);
+ addValueChangeListeners(freshRange);
+ } else {
+ // out of view, noop
+ }
+ }
+
+ /**
+ * Removes a single item by its id.
+ *
+ * @param itemId
+ * the id of the removed id. Note: this item does
+ * not exist anymore in the datasource
+ */
+ public void removeItemId(Object itemId) {
+ final GridValueChangeListener removedListener = valueChangeListeners
+ .remove(itemId);
+ if (removedListener != null) {
+ /*
+ * We removed an item from somewhere in the visible range, so we
+ * make the active range shorter. The empty hole will be filled
+ * by the client-side code when it asks for more information.
+ */
+ activeRange = Range.withLength(activeRange.getStart(),
+ activeRange.length() - 1);
+ }
+ }
+ }
+
+ /**
+ * A class to listen to changes in property values in the Container added
+ * with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies
+ * the data source to update the client-side representation of the modified
+ * item.
+ * firstRowIndex
and
+ * onwards
+ */
+ public void removeRowData(int firstRowIndex, int count);
+
+ /**
+ * Informs the client to insert new row data.
+ *
+ * @param firstRowIndex
+ * the index of the first new row
+ * @param count
+ * the number of rows inserted at firstRowIndex
+ */
+ public void insertRowData(int firstRowIndex, int count);
}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
new file mode 100644
index 0000000000..db0a31ed2c
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.shared.ui.grid;
+
+import com.vaadin.shared.communication.ServerRpc;
+
+/**
+ * TODO
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface GridServerRpc extends ServerRpc {
+
+ /**
+ * TODO
+ *
+ * @param firstVisibleRow
+ * the index of the first visible row
+ * @param visibleRowCount
+ * the number of rows visible, counted from
+ * firstVisibleRow
+ */
+ void setVisibleRows(int firstVisibleRow, int visibleRowCount);
+
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/Range.java b/shared/src/com/vaadin/shared/ui/grid/Range.java
new file mode 100644
index 0000000000..3114a79c82
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/Range.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.shared.ui.grid;
+
+/**
+ * An immutable representation of a range, marked by start and end points.
+ * integer
+ */
+ public static Range withOnly(final int integer) {
+ return new Range(integer, integer + 1);
+ }
+
+ /**
+ * Creates a range between two integers.
+ * [start..end[
+ * @throws IllegalArgumentException
+ * if start > end
+ */
+ public static Range between(final int start, final int end)
+ throws IllegalArgumentException {
+ return new Range(start, end);
+ }
+
+ /**
+ * Creates a range from a start point, with a given length.
+ *
+ * @param start
+ * the first integer to include in the range
+ * @param length
+ * the length of the resulting range
+ * @return a range starting from start
, with
+ * length
number of integers following
+ * @throws IllegalArgumentException
+ * if length < 0
+ */
+ public static Range withLength(final int start, final int length)
+ throws IllegalArgumentException {
+ if (length < 0) {
+ /*
+ * The constructor of Range will throw an exception if start >
+ * start+length (i.e. if length is negative). We're throwing the
+ * same exception type, just with a more descriptive message.
+ */
+ throw new IllegalArgumentException("length must not be negative");
+ }
+ return new Range(start, start + length);
+ }
+
+ /**
+ * Creates a new range between two numbers: [start..end[
.
+ *
+ * @param start
+ * the start integer, inclusive
+ * @param end
+ * the end integer, exclusive
+ * @throws IllegalArgumentException
+ * if start > end
+ */
+ private Range(final int start, final int end)
+ throws IllegalArgumentException {
+ if (start > end) {
+ throw new IllegalArgumentException(
+ "start must not be greater than end");
+ }
+
+ this.start = start;
+ this.end = end;
+ }
+
+ /**
+ * Returns the inclusive start point of this range.
+ *
+ * @return the start point of this range
+ */
+ public int getStart() {
+ return start;
+ }
+
+ /**
+ * Returns the exclusive end point of this range.
+ *
+ * @return the end point of this range
+ */
+ public int getEnd() {
+ return end;
+ }
+
+ /**
+ * The number of integers contained in the range.
+ *
+ * @return the number of integers contained in the range
+ */
+ public int length() {
+ return getEnd() - getStart();
+ }
+
+ /**
+ * Checks whether the range has no elements between the start and end.
+ *
+ * @return true
iff the range contains no elements.
+ */
+ public boolean isEmpty() {
+ return getStart() >= getEnd();
+ }
+
+ /**
+ * Checks whether this range and another range are at least partially
+ * covering the same values.
+ *
+ * @param other
+ * the other range to check against
+ * @return true
if this and other
intersect
+ */
+ public boolean intersects(final Range other) {
+ return getStart() < other.getEnd() && other.getStart() < getEnd();
+ }
+
+ /**
+ * Checks whether an integer is found within this range.
+ *
+ * @param integer
+ * an integer to test for presence in this range
+ * @return true
iff integer
is in this range
+ */
+ public boolean contains(final int integer) {
+ return getStart() <= integer && integer < getEnd();
+ }
+
+ /**
+ * Checks whether this range is a subset of another range.
+ *
+ * @return true
iff other
completely wraps this
+ * range
+ */
+ public boolean isSubsetOf(final Range other) {
+ return other.getStart() <= getStart() && getEnd() <= other.getEnd();
+ }
+
+ /**
+ * Overlay this range with another one, and partition the ranges according
+ * to how they position relative to each other.
+ *
+ *
+ *
+ * @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 other
.
+ * other
.
+ * offset
+ */
+ public Range offsetBy(final int offset) {
+ if (offset == 0) {
+ return this;
+ } else {
+ return new Range(start + offset, end + offset);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd()
+ + "[" + (isEmpty() ? " (empty)" : "");
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + end;
+ result = prime * result + start;
+ return result;
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final Range other = (Range) obj;
+ if (end != other.end) {
+ return false;
+ }
+ if (start != other.start) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks whether this range starts before the start of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return true
iff this range starts before the
+ * other
+ */
+ public boolean startsBefore(final Range other) {
+ return getStart() < other.getStart();
+ }
+
+ /**
+ * Checks whether this range ends before the start of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return true
iff this range ends before the
+ * other
+ */
+ public boolean endsBefore(final Range other) {
+ return getEnd() <= other.getStart();
+ }
+
+ /**
+ * Checks whether this range ends after the end of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return true
iff this range ends after the
+ * other
+ */
+ public boolean endsAfter(final Range other) {
+ return getEnd() > other.getEnd();
+ }
+
+ /**
+ * Checks whether this range starts after the end of another range.
+ *
+ * @param other
+ * the other range to compare against
+ * @return true
iff this range starts after the
+ * other
+ */
+ public boolean startsAfter(final Range other) {
+ return getStart() >= other.getEnd();
+ }
+
+ /**
+ * Split the range into two at a certain integer.
+ * [5..10[.splitAt(7) == [5..7[, [7..10[
+ *
+ * @param integer
+ * the integer at which to split the range into two
+ * @return an array of two ranges, with [start..integer[
in the
+ * first element, and [integer..end[
in the second
+ * element.
+ * integer
is equal to
+ * or greater than {@code end}, [{@code this}, empty] is returned
+ * instead.
+ */
+ public Range[] splitAt(final int integer) {
+ if (integer < start) {
+ return new Range[] { Range.withLength(start, 0), this };
+ } else if (integer >= end) {
+ return new Range[] { this, Range.withLength(end, 0) };
+ } else {
+ return new Range[] { new Range(start, integer),
+ new Range(integer, end) };
+ }
+ }
+
+ /**
+ * Split the range into two after a certain number of integers into the
+ * range.
+ * {@link #splitAt(int) splitAt}({@link #getStart()}+length);
+ * [5..10[.splitAtFromStart(2) == [5..7[, [7..10[
+ *
+ * @param length
+ * the length at which to split this range into two
+ * @return an array of two ranges, having the length
-first
+ * elements of this range, and the second range having the rest. If
+ * length
≤ 0, the first element will be empty, and
+ * the second element will be this range. If length
+ * ≥ {@link #length()}, the first element will be this range,
+ * and the second element will be empty.
+ */
+ public Range[] splitAtFromStart(final int length) {
+ return splitAt(getStart() + length);
+ }
+
+ /**
+ * Combines two ranges to create a range containing all values in both
+ * ranges, provided there are no gaps between the ranges.
+ *
+ * @param other
+ * the range to combine with this range
+ *
+ * @return the combined range
+ *
+ * @throws IllegalArgumentException
+ * if the two ranges aren't connected
+ */
+ public Range combineWith(Range other) throws IllegalArgumentException {
+ if (getStart() > other.getEnd() || other.getStart() > getEnd()) {
+ throw new IllegalArgumentException("There is a gap between " + this
+ + " and " + other);
+ }
+
+ return Range.between(Math.min(getStart(), other.getStart()),
+ Math.max(getEnd(), other.getEnd()));
+ }
+}
diff --git a/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java
new file mode 100644
index 0000000000..b042cee509
--- /dev/null
+++ b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.shared.ui.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+@SuppressWarnings("static-method")
+public class RangeTest {
+
+ @Test(expected = IllegalArgumentException.class)
+ public void startAfterEndTest() {
+ Range.between(10, 9);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void negativeLengthTest() {
+ Range.withLength(10, -1);
+ }
+
+ @Test
+ public void constructorEquivalenceTest() {
+ assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11));
+ assertEquals("[10,20[ == 10, length 10", Range.between(10, 20),
+ Range.withLength(10, 10));
+ assertEquals("10 == 10, length 1", Range.withOnly(10),
+ Range.withLength(10, 1));
+ }
+
+ @Test
+ public void boundsTest() {
+ {
+ final Range range = Range.between(0, 10);
+ assertEquals("between(0, 10) start", 0, range.getStart());
+ assertEquals("between(0, 10) end", 10, range.getEnd());
+ }
+
+ {
+ final Range single = Range.withOnly(10);
+ assertEquals("withOnly(10) start", 10, single.getStart());
+ assertEquals("withOnly(10) end", 11, single.getEnd());
+ }
+
+ {
+ final Range length = Range.withLength(10, 5);
+ assertEquals("withLength(10, 5) start", 10, length.getStart());
+ assertEquals("withLength(10, 5) end", 15, length.getEnd());
+ }
+ }
+
+ @Test
+ @SuppressWarnings("boxing")
+ public void equalsTest() {
+ final Range range1 = Range.between(0, 10);
+ final Range range2 = Range.withLength(0, 11);
+
+ assertTrue("null", !range1.equals(null));
+ assertTrue("reflexive", range1.equals(range1));
+ assertEquals("symmetric", range1.equals(range2), range2.equals(range1));
+ }
+
+ @Test
+ public void containsTest() {
+ final int start = 0;
+ final int end = 10;
+ final Range range = Range.between(start, end);
+
+ assertTrue("start should be contained", range.contains(start));
+ assertTrue("start-1 should not be contained",
+ !range.contains(start - 1));
+ assertTrue("end should not be contained", !range.contains(end));
+ assertTrue("end-1 should be contained", range.contains(end - 1));
+
+ assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5));
+ assertTrue("empty range does not contain 5", !Range.between(5, 5)
+ .contains(5));
+ }
+
+ @Test
+ public void emptyTest() {
+ assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty());
+ assertTrue("Range of length 0 should be empty", Range.withLength(0, 0)
+ .isEmpty());
+
+ assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty());
+ assertTrue("Range of length 1 should not be empty",
+ !Range.withLength(0, 1).isEmpty());
+ }
+
+ @Test
+ public void splitTest() {
+ final Range startRange = Range.between(0, 10);
+ final Range[] splitRanges = startRange.splitAt(5);
+ assertEquals("[0..10[ split at 5, lower", Range.between(0, 5),
+ splitRanges[0]);
+ assertEquals("[0..10[ split at 5, upper", Range.between(5, 10),
+ splitRanges[1]);
+ }
+
+ @Test
+ public void split_valueBefore() {
+ Range range = Range.between(10, 20);
+ Range[] splitRanges = range.splitAt(5);
+
+ assertEquals(Range.between(10, 10), splitRanges[0]);
+ assertEquals(range, splitRanges[1]);
+ }
+
+ @Test
+ public void split_valueAfter() {
+ Range range = Range.between(10, 20);
+ Range[] splitRanges = range.splitAt(25);
+
+ assertEquals(range, splitRanges[0]);
+ assertEquals(Range.between(20, 20), splitRanges[1]);
+ }
+
+ @Test
+ public void emptySplitTest() {
+ final Range range = Range.between(5, 10);
+ final Range[] split1 = range.splitAt(0);
+ assertTrue("split1, [0]", split1[0].isEmpty());
+ assertEquals("split1, [1]", range, split1[1]);
+
+ final Range[] split2 = range.splitAt(15);
+ assertEquals("split2, [0]", range, split2[0]);
+ assertTrue("split2, [1]", split2[1].isEmpty());
+ }
+
+ @Test
+ public void lengthTest() {
+ assertEquals("withLength length", 5, Range.withLength(10, 5).length());
+ assertEquals("between length", 5, Range.between(10, 15).length());
+ assertEquals("withOnly 10 length", 1, Range.withOnly(10).length());
+ }
+
+ @Test
+ public void intersectsTest() {
+ assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10)
+ .intersects(Range.between(5, 15)));
+ assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10)
+ .intersects(Range.between(10, 20)));
+ }
+
+ @Test
+ public void intersects_emptyInside() {
+ assertTrue("[5..5[ does intersect with [0..10[", Range.between(5, 5)
+ .intersects(Range.between(0, 10)));
+ assertTrue("[0..10[ does intersect with [5..5[", Range.between(0, 10)
+ .intersects(Range.between(5, 5)));
+ }
+
+ @Test
+ public void intersects_emptyOutside() {
+ assertTrue("[15..15[ does not intersect with [0..10[",
+ !Range.between(15, 15).intersects(Range.between(0, 10)));
+ assertTrue("[0..10[ does not intersect with [15..15[",
+ !Range.between(0, 10).intersects(Range.between(15, 15)));
+ }
+
+ @Test
+ public void subsetTest() {
+ assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10)
+ .isSubsetOf(Range.between(0, 20)));
+
+ final Range range = Range.between(0, 10);
+ assertTrue("range is subset of self", range.isSubsetOf(range));
+
+ assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10)
+ .isSubsetOf(Range.between(5, 15)));
+ }
+
+ @Test
+ public void offsetTest() {
+ assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5));
+ }
+
+ @Test
+ public void rangeStartsBeforeTest() {
+ final Range former = Range.between(0, 5);
+ final Range latter = Range.between(1, 5);
+ assertTrue("former should starts before latter",
+ former.startsBefore(latter));
+ assertTrue("latter shouldn't start before latter",
+ !latter.startsBefore(former));
+
+ assertTrue("no overlap allowed",
+ !Range.between(0, 5).startsBefore(Range.between(0, 10)));
+ }
+
+ @Test
+ public void rangeStartsAfterTest() {
+ final Range former = Range.between(0, 5);
+ final Range latter = Range.between(5, 10);
+ assertTrue("latter should start after former",
+ latter.startsAfter(former));
+ assertTrue("former shouldn't start after latter",
+ !former.startsAfter(latter));
+
+ assertTrue("no overlap allowed",
+ !Range.between(5, 10).startsAfter(Range.between(0, 6)));
+ }
+
+ @Test
+ public void rangeEndsBeforeTest() {
+ final Range former = Range.between(0, 5);
+ final Range latter = Range.between(5, 10);
+ assertTrue("latter should end before former", former.endsBefore(latter));
+ assertTrue("former shouldn't end before latter",
+ !latter.endsBefore(former));
+
+ assertTrue("no overlap allowed",
+ !Range.between(5, 10).endsBefore(Range.between(9, 15)));
+ }
+
+ @Test
+ public void rangeEndsAfterTest() {
+ final Range former = Range.between(1, 5);
+ final Range latter = Range.between(1, 6);
+ assertTrue("latter should end after former", latter.endsAfter(former));
+ assertTrue("former shouldn't end after latter",
+ !former.endsAfter(latter));
+
+ assertTrue("no overlap allowed",
+ !Range.between(0, 10).endsAfter(Range.between(5, 10)));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void combine_notOverlappingFirstSmaller() {
+ Range.between(0, 10).combineWith(Range.between(11, 20));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void combine_notOverlappingSecondLarger() {
+ Range.between(11, 20).combineWith(Range.between(0, 10));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void combine_firstEmptyNotOverlapping() {
+ Range.between(15, 15).combineWith(Range.between(0, 10));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void combine_secondEmptyNotOverlapping() {
+ Range.between(0, 10).combineWith(Range.between(15, 15));
+ }
+
+ @Test
+ public void combine_barelyOverlapping() {
+ Range r1 = Range.between(0, 10);
+ Range r2 = Range.between(10, 20);
+
+ // Test both ways, should give the same result
+ Range combined1 = r1.combineWith(r2);
+ Range combined2 = r2.combineWith(r1);
+ assertEquals(combined1, combined2);
+
+ assertEquals(0, combined1.getStart());
+ assertEquals(20, combined1.getEnd());
+ }
+
+ @Test
+ public void combine_subRange() {
+ Range r1 = Range.between(0, 10);
+ Range r2 = Range.between(2, 8);
+
+ // Test both ways, should give the same result
+ Range combined1 = r1.combineWith(r2);
+ Range combined2 = r2.combineWith(r1);
+ assertEquals(combined1, combined2);
+
+ assertEquals(r1, combined1);
+ }
+
+ @Test
+ public void combine_intersecting() {
+ Range r1 = Range.between(0, 10);
+ Range r2 = Range.between(5, 15);
+
+ // Test both ways, should give the same result
+ Range combined1 = r1.combineWith(r2);
+ Range combined2 = r2.combineWith(r1);
+ assertEquals(combined1, combined2);
+
+ assertEquals(0, combined1.getStart());
+ assertEquals(15, combined1.getEnd());
+
+ }
+
+ @Test
+ public void combine_emptyInside() {
+ Range r1 = Range.between(0, 10);
+ Range r2 = Range.between(5, 5);
+
+ // Test both ways, should give the same result
+ Range combined1 = r1.combineWith(r2);
+ Range combined2 = r2.combineWith(r1);
+ assertEquals(combined1, combined2);
+
+ assertEquals(r1, combined1);
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
index 82b2d7a4e8..c28feb8d10 100644
--- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
+++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
@@ -40,20 +40,22 @@ public class GridBasicFeatures extends AbstractComponentTest