summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--WebContent/VAADIN/themes/base/base.scss4
-rw-r--r--WebContent/VAADIN/themes/base/escalator/escalator.scss110
-rw-r--r--WebContent/VAADIN/themes/base/grid/grid.scss3
-rw-r--r--client/src/com/vaadin/client/data/AbstractRemoteDataSource.java323
-rw-r--r--client/src/com/vaadin/client/data/DataChangeHandler.java59
-rw-r--r--client/src/com/vaadin/client/data/DataSource.java76
-rw-r--r--client/src/com/vaadin/client/data/RpcDataSourceConnector.java81
-rw-r--r--client/src/com/vaadin/client/ui/grid/Cell.java75
-rw-r--r--client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java146
-rw-r--r--client/src/com/vaadin/client/ui/grid/ColumnGroup.java184
-rw-r--r--client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java243
-rw-r--r--client/src/com/vaadin/client/ui/grid/Escalator.java3660
-rw-r--r--client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java66
-rw-r--r--client/src/com/vaadin/client/ui/grid/FlyweightCell.java205
-rw-r--r--client/src/com/vaadin/client/ui/grid/FlyweightRow.java217
-rw-r--r--client/src/com/vaadin/client/ui/grid/Grid.java1318
-rw-r--r--client/src/com/vaadin/client/ui/grid/GridColumn.java54
-rw-r--r--client/src/com/vaadin/client/ui/grid/GridConnector.java277
-rw-r--r--client/src/com/vaadin/client/ui/grid/PositionFunction.java118
-rw-r--r--client/src/com/vaadin/client/ui/grid/Renderer.java43
-rw-r--r--client/src/com/vaadin/client/ui/grid/Row.java55
-rw-r--r--client/src/com/vaadin/client/ui/grid/RowContainer.java126
-rw-r--r--client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java90
-rw-r--r--client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java38
-rw-r--r--client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java403
-rw-r--r--client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java357
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java94
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java42
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java64
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java33
-rw-r--r--client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java178
-rw-r--r--client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java104
-rw-r--r--server/src/com/vaadin/data/RpcDataProviderExtension.java148
-rw-r--r--server/src/com/vaadin/ui/components/grid/ColumnGroup.java165
-rw-r--r--server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java303
-rw-r--r--server/src/com/vaadin/ui/components/grid/Grid.java861
-rw-r--r--server/src/com/vaadin/ui/components/grid/GridColumn.java216
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java265
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java228
-rw-r--r--shared/src/com/vaadin/shared/data/DataProviderRpc.java61
-rw-r--r--shared/src/com/vaadin/shared/data/DataProviderState.java32
-rw-r--r--shared/src/com/vaadin/shared/data/DataRequestRpc.java38
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java46
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java45
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java53
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridColumnState.java55
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridConstants.java33
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java39
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridState.java64
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/Range.java378
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java55
-rw-r--r--shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java318
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/BasicEscalator.html176
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java304
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java343
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java373
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java111
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridScrolling.java115
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java46
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java127
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java29
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java216
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java92
63 files changed, 14181 insertions, 0 deletions
diff --git a/WebContent/VAADIN/themes/base/base.scss b/WebContent/VAADIN/themes/base/base.scss
index fd3c5d067d..58274edacb 100644
--- a/WebContent/VAADIN/themes/base/base.scss
+++ b/WebContent/VAADIN/themes/base/base.scss
@@ -15,8 +15,10 @@
@import "inlinedatefield/inlinedatefield.scss";
@import "dragwrapper/dragwrapper.scss";
@import "embedded/embedded.scss";
+@import "escalator/escalator.scss";
@import "fonts/fonts.scss";
@import "formlayout/formlayout.scss";
+@import "grid/grid.scss";
@import "gridlayout/gridlayout.scss";
@import "label/label.scss";
@import "link/link.scss";
@@ -90,7 +92,9 @@ $line-height: normal;
@include base-inline-datefield;
@include base-dragwrapper;
@include base-embedded;
+ @include base-escalator;
@include base-formlayout;
+ @include base-grid;
@include base-gridlayout;
@include base-label;
@include base-link;
diff --git a/WebContent/VAADIN/themes/base/escalator/escalator.scss b/WebContent/VAADIN/themes/base/escalator/escalator.scss
new file mode 100644
index 0000000000..21424bd456
--- /dev/null
+++ b/WebContent/VAADIN/themes/base/escalator/escalator.scss
@@ -0,0 +1,110 @@
+@mixin base-escalator($primaryStyleName : v-escalator) {
+
+$background-color: white;
+$border-color: #aaa;
+
+.#{$primaryStyleName} {
+ position: relative;
+ background-color: $background-color;
+}
+
+.#{$primaryStyleName}-scroller {
+ position: absolute;
+ overflow: auto;
+ z-index: 20;
+}
+
+.#{$primaryStyleName}-scroller-horizontal {
+ left: 0; /* Left position adjusted to align with frozen columns */
+ right: 0;
+ bottom: 0;
+ overflow-y: hidden;
+ -ms-overflow-y: hidden;
+}
+
+.#{$primaryStyleName}-scroller-vertical {
+ right: 0;
+ top: 0; /* this will be overridden by code, but it's a good default behavior */
+ bottom: 0; /* this will be overridden by code, but it's a good default behavior */
+ overflow-x: hidden;
+ -ms-overflow-x: hidden;
+}
+
+.#{$primaryStyleName}-tablewrapper {
+ position: absolute;
+ overflow: hidden;
+}
+
+.#{$primaryStyleName}-tablewrapper > table {
+ border-spacing: 0;
+ table-layout: fixed;
+ width: inherit; /* a decent default fallback */
+}
+
+.#{$primaryStyleName}-header,
+.#{$primaryStyleName}-body,
+.#{$primaryStyleName}-footer {
+ position: absolute;
+ left: 0;
+ width: inherit;
+ z-index: 10;
+}
+
+.#{$primaryStyleName}-header { top: 0; }
+.#{$primaryStyleName}-footer { bottom: 0; }
+
+.#{$primaryStyleName}-body {
+ z-index: 0;
+ top: 0;
+
+ .#{$primaryStyleName}-row {
+ position: absolute;
+ top: 0;
+ left: 0;
+ }
+}
+
+.#{$primaryStyleName}-row {
+ display: block;
+
+ .v-ie8 & {
+ /* IE8 doesn't let table rows be longer than body only with display block. Moar hax. */
+ float: left;
+ clear: left;
+
+ /*
+ * The inline style of margin-top from the <tbody> to offset the header's dimension is,
+ * for some strange reason, inherited into each contained <tr>.
+ * We need to cancel it:
+ */
+ margin-top: 0;
+ }
+
+ > td, > th {
+ /* IE8 likes the bgcolor here instead of on the row */
+ background-color: $background-color;
+ }
+}
+
+
+.#{$primaryStyleName}-row {
+ width: inherit;
+}
+
+.#{$primaryStyleName}-cell {
+ display: block;
+ float: left;
+ border: 1px solid $border-color;
+ padding: 2px;
+ white-space: nowrap;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ overflow:hidden;
+}
+
+.#{$primaryStyleName}-cell.frozen {
+ position: relative;
+ z-index: 0;
+}
+
+} \ No newline at end of file
diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss
new file mode 100644
index 0000000000..9f7a2d8664
--- /dev/null
+++ b/WebContent/VAADIN/themes/base/grid/grid.scss
@@ -0,0 +1,3 @@
+@mixin base-grid($primaryStyleName : v-grid) {
+ @include base-escalator($primaryStyleName);
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
new file mode 100644
index 0000000000..127eb80696
--- /dev/null
+++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
@@ -0,0 +1,323 @@
+/*
+ * 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.HashMap;
+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.shared.ui.grid.Range;
+
+/**
+ * Base implementation for data sources that fetch data from a remote system.
+ * This class takes care of caching data and communicating with the data source
+ * user. An implementation of this class should override
+ * {@link #requestRows(int, int)} to trigger asynchronously loading of data.
+ * When data is received from the server, new row data should be passed to
+ * {@link #setRowData(int, List)}. {@link #setEstimatedSize(int)} should be used
+ * based on estimations of how many rows are available.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type
+ */
+public abstract class AbstractRemoteDataSource<T> implements DataSource<T> {
+
+ private boolean requestPending = false;
+
+ private boolean coverageCheckPending = false;
+
+ private Range requestedAvailability = Range.between(0, 0);
+
+ private Range cached = Range.between(0, 0);
+
+ private final HashMap<Integer, T> rowCache = new HashMap<Integer, T>();
+
+ private DataChangeHandler dataChangeHandler;
+
+ private int estimatedSize;
+
+ private final ScheduledCommand coverageChecker = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ coverageCheckPending = false;
+ checkCacheCoverage();
+ }
+ };
+
+ /**
+ * Sets the estimated number of rows in the data source.
+ *
+ * @param estimatedSize
+ * the estimated number of available rows
+ */
+ protected void setEstimatedSize(int estimatedSize) {
+ // TODO update dataChangeHandler if size changes
+ this.estimatedSize = estimatedSize;
+ }
+
+ private void ensureCoverageCheck() {
+ if (!coverageCheckPending) {
+ coverageCheckPending = true;
+ Scheduler.get().scheduleDeferred(coverageChecker);
+ }
+ }
+
+ @Override
+ public void ensureAvailability(int firstRowIndex, int numberOfRows) {
+ requestedAvailability = Range.withLength(firstRowIndex, numberOfRows);
+
+ /*
+ * Don't request any data right away since the data might be included in
+ * a message that has been received but not yet fully processed.
+ */
+ ensureCoverageCheck();
+ }
+
+ private void checkCacheCoverage() {
+ if (requestPending) {
+ // Anyone clearing requestPending should run this method again
+ return;
+ }
+
+ Profiler.enter("AbstractRemoteDataSource.checkCacheCoverage");
+
+ if (!requestedAvailability.intersects(cached) || cached.isEmpty()) {
+ /*
+ * Simple case: no overlap between cached data and needed data.
+ * Clear the cache and request new data
+ */
+ rowCache.clear();
+ cached = Range.between(0, 0);
+
+ handleMissingRows(requestedAvailability);
+ } else {
+ discardStaleCacheEntries();
+
+ // Might need more rows -> request them
+ Range[] availabilityPartition = requestedAvailability
+ .partitionWith(cached);
+ handleMissingRows(availabilityPartition[0]);
+ handleMissingRows(availabilityPartition[2]);
+ }
+
+ Profiler.leave("AbstractRemoteDataSource.checkCacheCoverage");
+ }
+
+ private void discardStaleCacheEntries() {
+ Range[] cacheParition = cached.partitionWith(requestedAvailability);
+ dropFromCache(cacheParition[0]);
+ cached = cacheParition[1];
+ dropFromCache(cacheParition[2]);
+ }
+
+ private void dropFromCache(Range range) {
+ for (int i = range.getStart(); i < range.getEnd(); i++) {
+ rowCache.remove(Integer.valueOf(i));
+ }
+ }
+
+ private void handleMissingRows(Range range) {
+ if (range.isEmpty()) {
+ return;
+ }
+ requestPending = true;
+ requestRows(range.getStart(), range.length());
+ }
+
+ /**
+ * Triggers fetching rows from the remote data source.
+ * {@link #setRowData(int, List)} should be invoked with data for the
+ * requested rows when they have been received.
+ *
+ * @param firstRowIndex
+ * the index of the first row to fetch
+ * @param numberOfRows
+ * the number of rows to fetch
+ */
+ protected abstract void requestRows(int firstRowIndex, int numberOfRows);
+
+ @Override
+ public int getEstimatedSize() {
+ return estimatedSize;
+ }
+
+ @Override
+ public T getRow(int rowIndex) {
+ return rowCache.get(Integer.valueOf(rowIndex));
+ }
+
+ @Override
+ public void setDataChangeHandler(DataChangeHandler dataChangeHandler) {
+ this.dataChangeHandler = dataChangeHandler;
+
+ if (dataChangeHandler != null && !cached.isEmpty()) {
+ // Push currently cached data to the implementation
+ dataChangeHandler.dataUpdated(cached.getStart(), cached.length());
+ }
+ }
+
+ /**
+ * Informs this data source that updated data has been sent from the server.
+ *
+ * @param firstRowIndex
+ * the index of the first received row
+ * @param rowData
+ * a list of rows, starting from <code>firstRowIndex</code>
+ */
+ protected void setRowData(int firstRowIndex, List<T> rowData) {
+ requestPending = false;
+
+ Profiler.enter("AbstractRemoteDataSource.setRowData");
+
+ Range received = Range.withLength(firstRowIndex, rowData.size());
+
+ Range[] partition = received.partitionWith(requestedAvailability);
+
+ Range newUsefulData = partition[1];
+ if (!newUsefulData.isEmpty()) {
+ // Update the parts that are actually inside
+ for (int i = newUsefulData.getStart(); i < newUsefulData.getEnd(); i++) {
+ rowCache.put(Integer.valueOf(i), rowData.get(i - firstRowIndex));
+ }
+
+ if (dataChangeHandler != null) {
+ Profiler.enter("AbstractRemoteDataSource.setRowData notify dataChangeHandler");
+ dataChangeHandler.dataUpdated(newUsefulData.getStart(),
+ newUsefulData.length());
+ Profiler.leave("AbstractRemoteDataSource.setRowData notify dataChangeHandler");
+ }
+
+ // Potentially extend the range
+ if (cached.isEmpty()) {
+ cached = newUsefulData;
+ } else {
+ discardStaleCacheEntries();
+
+ /*
+ * everything might've become stale so we need to re-check for
+ * emptiness.
+ */
+ if (!cached.isEmpty()) {
+ cached = cached.combineWith(newUsefulData);
+ } else {
+ cached = newUsefulData;
+ }
+ }
+ }
+
+ if (!partition[0].isEmpty() || !partition[2].isEmpty()) {
+ /*
+ * FIXME
+ *
+ * Got data that we might need in a moment if the container is
+ * updated before the widget settings. Support for this will be
+ * implemented later on.
+ */
+ }
+
+ // Eventually check whether all needed rows are now available
+ ensureCoverageCheck();
+
+ 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");
+ }
+}
diff --git a/client/src/com/vaadin/client/data/DataChangeHandler.java b/client/src/com/vaadin/client/data/DataChangeHandler.java
new file mode 100644
index 0000000000..4c4cc7656d
--- /dev/null
+++ b/client/src/com/vaadin/client/data/DataChangeHandler.java
@@ -0,0 +1,59 @@
+/*
+ * 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;
+
+/**
+ * Callback interface used by {@link DataSource} to inform its user about
+ * updates to the data.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface DataChangeHandler {
+ /**
+ * Called when the contents of the data source has changed. If the number of
+ * rows has changed or if rows have been moved around,
+ * {@link #dataAdded(int, int)} or {@link #dataRemoved(int, int)} should
+ * ideally be used instead.
+ *
+ * @param firstRowIndex
+ * the index of the first changed row
+ * @param numberOfRows
+ * the number of changed rows
+ */
+ public void dataUpdated(int firstRowIndex, int numberOfRows);
+
+ /**
+ * Called when rows have been removed from the data source.
+ *
+ * @param firstRowIndex
+ * the index that the first removed row had prior to removal
+ * @param numberOfRows
+ * the number of removed rows
+ */
+ public void dataRemoved(int firstRowIndex, int numberOfRows);
+
+ /**
+ * Called when the new rows have been added to the container.
+ *
+ * @param firstRowIndex
+ * the index of the first added row
+ * @param numberOfRows
+ * the number of added rows
+ */
+ public void dataAdded(int firstRowIndex, int numberOfRows);
+}
diff --git a/client/src/com/vaadin/client/data/DataSource.java b/client/src/com/vaadin/client/data/DataSource.java
new file mode 100644
index 0000000000..9179b6d03d
--- /dev/null
+++ b/client/src/com/vaadin/client/data/DataSource.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+/**
+ * Source of data for widgets showing lazily loaded data based on indexable
+ * items (e.g. rows) of a specified type. The data source is a lazy view into a
+ * larger data set.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type
+ */
+public interface DataSource<T> {
+ /**
+ * Informs the data source that data for the given range is needed. A data
+ * source only has one active region at a time, so calling this method
+ * discards the previously set range.
+ * <p>
+ * 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 <code>null</code>.
+ * <p>
+ * 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 <code>null</code> 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..4d22c10197
--- /dev/null
+++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
@@ -0,0 +1,81 @@
+/*
+ * 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 AbstractRemoteDataSource<String[]> dataSource = new AbstractRemoteDataSource<String[]>() {
+ @Override
+ protected void requestRows(int firstRowIndex, int numberOfRows) {
+ getRpcProxy(DataRequestRpc.class).requestRows(firstRowIndex,
+ numberOfRows);
+ }
+ };
+
+ @Override
+ protected void extend(ServerConnector target) {
+ dataSource.setEstimatedSize(getState().containerSize);
+ ((GridConnector) target).getWidget().setDataSource(dataSource);
+
+ registerRpc(DataProviderRpc.class, new DataProviderRpc() {
+ @Override
+ 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);
+ }
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.vaadin.client.ui.AbstractConnector#getState()
+ */
+ @Override
+ public DataProviderState getState() {
+ return (DataProviderState) super.getState();
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/Cell.java b/client/src/com/vaadin/client/ui/grid/Cell.java
new file mode 100644
index 0000000000..3d42f082a6
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Cell.java
@@ -0,0 +1,75 @@
+/*
+ * 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 com.google.gwt.dom.client.Element;
+import com.google.gwt.user.client.ui.HasOneWidget;
+
+/**
+ * A representation of a single cell.
+ * <p>
+ * A Cell instance will be provided to the {@link EscalatorUpdater} responsible
+ * for rendering the cells in a certain {@link RowContainer}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface Cell extends HasOneWidget {
+
+ /**
+ * Gets the index of the row this cell is in.
+ *
+ * @return the index of the row this cell is in
+ */
+ public int getRow();
+
+ /**
+ * Gets the index of the column this cell is in.
+ *
+ * @return the index of the column this cell is in
+ */
+ public int getColumn();
+
+ /**
+ * Gets the root element for this cell. The {@link EscalatorUpdater} may
+ * update the class names of the element, add inline styles and freely
+ * modify the contents.
+ * <p>
+ * Avoid modifying the dimensions, positioning or colspan of the cell
+ * element.
+ *
+ * @return The root element for this cell. Never <code>null</code>.
+ */
+ public Element getElement();
+
+ /**
+ * Sets the column span of the cell.
+ * <p>
+ * This will overwrite any possible "colspan" attribute in the current
+ * element (i.e. the object returned by {@link #getElement()}). This will
+ * also handle internal bookkeeping, skip the rendering of any affected
+ * adjacent cells, and make sure that the current cell's dimensions are
+ * handled correctly.
+ *
+ * @param numberOfCells
+ * the number of cells to span to the right, or <code>1</code> to
+ * unset any column spans
+ * @throws IllegalArgumentException
+ * if <code>numberOfCells &lt; 1</code>
+ */
+ public void setColSpan(int numberOfCells) throws IllegalArgumentException;
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java
new file mode 100644
index 0000000000..64104164cd
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java
@@ -0,0 +1,146 @@
+/*
+ * 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;
+
+/**
+ * A representation of the columns in an instance of {@link Escalator}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see Escalator#getColumnConfiguration()
+ */
+public interface ColumnConfiguration {
+
+ /**
+ * Removes columns at a certain index.
+ * <p>
+ * If any of the removed columns were frozen, the number of frozen columns
+ * will be reduced by the number of the removed columns that were frozen.
+ *
+ * @param index
+ * the index of the first column to be removed
+ * @param numberOfColumns
+ * the number of rows to remove, starting from the index
+ * @throws IndexOutOfBoundsException
+ * if any integer in the range
+ * <code>[index..(index+numberOfColumns)]</code> is not an
+ * existing column index.
+ * @throws IllegalArgumentException
+ * if <code>numberOfColumns</code> is less than 1.
+ */
+ public void removeColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Adds columns at a certain index.
+ * <p>
+ * The new columns will be inserted between the column at the index, and the
+ * column before (an index of 0 means that the columns are inserted at the
+ * beginning). Therefore, the columns at the index and afterwards will be
+ * moved to the right.
+ * <p>
+ * The contents of the inserted columns will be queried from the respective
+ * cell renderers in the header, body and footer.
+ * <p>
+ * If there are frozen columns and the first added column is to the left of
+ * the last frozen column, the number of frozen columns will be increased by
+ * the number of inserted columns.
+ * <p>
+ * <em>Note:</em> Only the contents of the inserted columns will be
+ * rendered. If inserting new columns affects the contents of existing
+ * columns, {@link RowContainer#refreshRows(int, int)} needs to be called as
+ * appropriate.
+ *
+ * @param index
+ * the index of the column before which new columns are inserted,
+ * or {@link #getColumnCount()} to add new columns at the end
+ * @param numberOfColumns
+ * the number of columns to insert after the <code>index</code>
+ * @throws IndexOutOfBoundsException
+ * if <code>index</code> is not an integer in the range
+ * <code>[0..{@link #getColumnCount()}]</code>
+ * @throws IllegalArgumentException
+ * if {@code numberOfColumns} is less than 1.
+ */
+ public void insertColumns(int index, int numberOfColumns)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Returns the number of columns in the escalator.
+ *
+ * @return the number of columns in the escalator
+ */
+ public int getColumnCount();
+
+ /**
+ * Sets the number of leftmost columns that are not affected by horizontal
+ * scrolling.
+ *
+ * @param count
+ * the number of columns to freeze
+ *
+ * @throws IllegalArgumentException
+ * if the column count is &lt; 0 or &gt; the number of columns
+ *
+ */
+ public void setFrozenColumnCount(int count) throws IllegalArgumentException;
+
+ /**
+ * Get the number of leftmost columns that are not affected by horizontal
+ * scrolling.
+ *
+ * @return the number of frozen columns
+ */
+ public int getFrozenColumnCount();
+
+ /**
+ * Sets (or unsets) an explicit width for a column.
+ *
+ * @param index
+ * the index of the column for which to set a width
+ * @param px
+ * the number of pixels the indicated column should be, or a
+ * negative number to let the escalator decide
+ * @throws IllegalArgumentException
+ * if <code>index</code> is not a valid column index
+ */
+ public void setColumnWidth(int index, int px)
+ throws IllegalArgumentException;
+
+ /**
+ * Returns the user-defined width of a column.
+ *
+ * @param index
+ * the index of the column for which to retrieve the width
+ * @return the column's width in pixels, or a negative number if the width
+ * is implicitly decided by the escalator
+ * @throws IllegalArgumentException
+ * if <code>index</code> is not a valid column index
+ */
+ public int getColumnWidth(int index) throws IllegalArgumentException;
+
+ /**
+ * Returns the actual width of a column.
+ *
+ * @param index
+ * the index of the column for which to retrieve the width
+ * @return the column's actual width in pixels
+ * @throws IllegalArgumentException
+ * if <code>index</code> is not a valid column index
+ */
+ public int getColumnWidthActual(int index) throws IllegalArgumentException;
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/ColumnGroup.java b/client/src/com/vaadin/client/ui/grid/ColumnGroup.java
new file mode 100644
index 0000000000..e48656bc6b
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/ColumnGroup.java
@@ -0,0 +1,184 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.client.ui.grid.renderers.TextRenderer;
+
+/**
+ * Column groups are used to group columns together for adding common auxiliary
+ * headers and footers. Columns groups are added to {@link ColumnGroupRow
+ * ColumnGroupRows}.
+ *
+ * @param <T>
+ * The row type of the grid. The row type is the POJO type from where
+ * the data is retrieved into the column cells.
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class ColumnGroup<T> {
+
+ /**
+ * The text shown in the header
+ */
+ private String header;
+
+ /**
+ * The text shown in the footer
+ */
+ private String footer;
+
+ /**
+ * Renders the header cells for the column group
+ */
+ private Renderer<String> headerRenderer = new TextRenderer();
+
+ /**
+ * Renders the footer cells for the column group
+ */
+ private Renderer<String> footerRenderer = new TextRenderer();
+
+ /**
+ * The columns included in the group when also accounting for subgroup
+ * columns
+ */
+ private final List<GridColumn<?, T>> columns;
+
+ /**
+ * The grid associated with the column group
+ */
+ private final Grid<T> grid;
+
+ /**
+ * Constructs a new column group
+ */
+ ColumnGroup(Grid<T> grid, Collection<GridColumn<?, T>> columns) {
+ if (columns == null) {
+ throw new IllegalArgumentException(
+ "columns cannot be null. Pass an empty list instead.");
+ }
+ this.grid = grid;
+ this.columns = Collections
+ .unmodifiableList(new ArrayList<GridColumn<?, T>>(columns));
+ }
+
+ /**
+ * Gets the header text.
+ *
+ * @return the header text
+ */
+ public String getHeaderCaption() {
+ return header;
+ }
+
+ /**
+ * Sets the text shown in the header.
+ *
+ * @param header
+ * the header to set
+ */
+ public void setHeaderCaption(String header) {
+ this.header = header;
+ grid.refreshHeader();
+ }
+
+ /**
+ * Gets the text shown in the footer.
+ *
+ * @return the text in the footer
+ */
+ public String getFooterCaption() {
+ return footer;
+ }
+
+ /**
+ * Sets the text displayed in the footer.
+ *
+ * @param footer
+ * the footer to set
+ */
+ public void setFooterCaption(String footer) {
+ this.footer = footer;
+ grid.refreshFooter();
+ }
+
+ /**
+ * Returns all column in this group. It includes the subgroups columns as
+ * well.
+ *
+ * @return unmodifiable list of columns
+ */
+ public List<GridColumn<?, T>> getColumns() {
+ return columns;
+ }
+
+ /**
+ * Returns the renderer used for rendering the header cells
+ *
+ * @return a renderer that renders header cells
+ */
+ public Renderer<String> getHeaderRenderer() {
+ return headerRenderer;
+ }
+
+ /**
+ * Sets the renderer that renders header cells.
+ *
+ * @param renderer
+ * The renderer to use for rendering header cells. Must not be
+ * null.
+ * @throws IllegalArgumentException
+ * thrown when renderer is null
+ */
+ public void setHeaderRenderer(Renderer<String> renderer) {
+ if (renderer == null) {
+ throw new IllegalArgumentException("Renderer cannot be null.");
+ }
+ this.headerRenderer = renderer;
+ grid.refreshHeader();
+ }
+
+ /**
+ * Returns the renderer used for rendering the footer cells
+ *
+ * @return a renderer that renders footer cells
+ */
+ public Renderer<String> getFooterRenderer() {
+ return footerRenderer;
+ }
+
+ /**
+ * Sets the renderer that renders footer cells.
+ *
+ * @param renderer
+ * The renderer to use for rendering footer cells. Must not be
+ * null.
+ * @throws IllegalArgumentException
+ * thrown when renderer is null
+ */
+ public void setFooterRenderer(Renderer<String> renderer) {
+ if (renderer == null) {
+ throw new IllegalArgumentException("Renderer cannot be null.");
+ }
+ this.footerRenderer = renderer;
+ grid.refreshFooter();
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java b/client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java
new file mode 100644
index 0000000000..ebe4db508c
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java
@@ -0,0 +1,243 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * A column group row represents an auxiliary header or footer row added to the
+ * grid. A column group row includes column groups that group columns together.
+ *
+ * @param <T>
+ * Row type
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class ColumnGroupRow<T> {
+
+ /**
+ * The column groups in this row
+ */
+ private List<ColumnGroup<T>> groups = new ArrayList<ColumnGroup<T>>();
+
+ /**
+ * The grid associated with the column row
+ */
+ private final Grid<T> grid;
+
+ /**
+ * Is the header shown
+ */
+ private boolean headerVisible = true;
+
+ /**
+ * Is the footer shown
+ */
+ private boolean footerVisible = false;
+
+ /**
+ * Constructs a new column group row
+ *
+ * @param grid
+ * Grid associated with this column
+ *
+ */
+ ColumnGroupRow(Grid<T> grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Add a new group to the row by using column instances.
+ *
+ * @param columns
+ * The columns that should belong to the group
+ * @return a column group representing the collection of columns added to
+ * the group.
+ */
+ public ColumnGroup<T> addGroup(GridColumn<?, T>... columns)
+ throws IllegalArgumentException {
+
+ for (GridColumn<?, T> column : columns) {
+ if (isColumnGrouped(column)) {
+ throw new IllegalArgumentException("Column "
+ + String.valueOf(column.getHeaderCaption())
+ + " already belongs to another group.");
+ }
+ }
+
+ validateNewGroupProperties(Arrays.asList(columns));
+
+ ColumnGroup<T> group = new ColumnGroup<T>(grid, Arrays.asList(columns));
+ groups.add(group);
+ grid.refreshHeader();
+ grid.refreshFooter();
+ return group;
+ }
+
+ private void validateNewGroupProperties(Collection<GridColumn<?, T>> columns) {
+
+ int rowIndex = grid.getColumnGroupRows().indexOf(this);
+ int parentRowIndex = rowIndex - 1;
+
+ // Get the parent row of this row.
+ ColumnGroupRow<T> parentRow = null;
+ if (parentRowIndex > -1) {
+ parentRow = grid.getColumnGroupRows().get(parentRowIndex);
+ }
+
+ if (parentRow == null) {
+ // A parentless row is always valid and is usually the first row
+ // added to the grid
+ return;
+ }
+
+ for (GridColumn<?, T> column : columns) {
+ if (parentRow.hasColumnBeenGrouped(column)) {
+ /*
+ * If a property has been grouped in the parent row then all of
+ * the properties in the parent group also needs to be included
+ * in the child group for the groups to be valid
+ */
+ ColumnGroup parentGroup = parentRow.getGroupForColumn(column);
+ if (!columns.containsAll(parentGroup.getColumns())) {
+ throw new IllegalArgumentException(
+ "Grouped properties overlaps previous grouping bounderies");
+ }
+ }
+ }
+ }
+
+ private boolean hasColumnBeenGrouped(GridColumn<?, T> column) {
+ return getGroupForColumn(column) != null;
+ }
+
+ private ColumnGroup<T> getGroupForColumn(GridColumn<?, T> column) {
+ for (ColumnGroup<T> group : groups) {
+ if (group.getColumns().contains(column)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Add a new group to the row by using other already greated groups
+ *
+ * @param groups
+ * The subgroups of the group.
+ * @return a column group representing the collection of columns added to
+ * the group.
+ *
+ */
+ public ColumnGroup<T> addGroup(ColumnGroup<T>... groups)
+ throws IllegalArgumentException {
+ assert groups != null : "groups cannot be null";
+
+ Set<GridColumn<?, T>> columns = new HashSet<GridColumn<?, T>>();
+ for (ColumnGroup<T> group : groups) {
+ columns.addAll(group.getColumns());
+ }
+
+ validateNewGroupProperties(columns);
+
+ ColumnGroup<T> group = new ColumnGroup<T>(grid, columns);
+ this.groups.add(group);
+ grid.refreshHeader();
+ grid.refreshFooter();
+ return group;
+ }
+
+ /**
+ * Removes a group from the row.
+ *
+ * @param group
+ * The group to remove
+ */
+ public void removeGroup(ColumnGroup<T> group) {
+ groups.remove(group);
+ grid.refreshHeader();
+ grid.refreshFooter();
+ }
+
+ /**
+ * Get the groups in the row
+ *
+ * @return unmodifiable list of groups in this row
+ */
+ public List<ColumnGroup<T>> getGroups() {
+ return Collections.unmodifiableList(groups);
+ }
+
+ /**
+ * Is the header visible for the row.
+ *
+ * @return <code>true</code> 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 <code>true</code> 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<?, T> column) {
+ for (ColumnGroup<T> group : groups) {
+ if (group.getColumns().contains(column)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java
new file mode 100644
index 0000000000..77a8c2dbd9
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Escalator.java
@@ -0,0 +1,3660 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import com.google.gwt.animation.client.AnimationScheduler;
+import com.google.gwt.animation.client.AnimationScheduler.AnimationCallback;
+import com.google.gwt.animation.client.AnimationScheduler.AnimationHandle;
+import com.google.gwt.core.client.Duration;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+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.Window;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.Profiler;
+import com.vaadin.client.Util;
+import com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle;
+import com.vaadin.client.ui.grid.PositionFunction.AbsolutePosition;
+import com.vaadin.client.ui.grid.PositionFunction.Translate3DPosition;
+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.ui.grid.ScrollDestination;
+import com.vaadin.shared.util.SharedUtil;
+
+/*-
+
+ Maintenance Notes! Reading these might save your day.
+
+
+ == Row Container Structure
+
+ AbstractRowContainer
+ |-- AbstractStaticRowContainer
+ | |-- HeaderRowContainer
+ | `-- FooterContainer
+ `-- BodyRowContainer
+
+ AbstractRowContainer is intended to contain all common logic
+ between RowContainers. It manages the bookkeeping of row
+ count, makes sure that all individual cells are rendered
+ the same way, and so on.
+
+ AbstractStaticRowContainer has some special logic that is
+ required by all RowContainers that don't scroll (hence the
+ word "static"). HeaderRowContainer and FooterRowContainer
+ are pretty thin special cases of a StaticRowContainer
+ (mostly relating to positioning of the root element).
+
+ BodyRowContainer could also be split into an additional
+ "AbstractScrollingRowContainer", but I felt that no more
+ inner classes were needed. So it contains both logic
+ required for making things scroll about, and equivalent
+ special cases for layouting, as are found in
+ Header/FooterRowContainers.
+
+
+ == The Three Indices
+
+ Each RowContainer can be thought to have three levels of
+ indices for any given displayed row (but the distinction
+ matters primarily for the BodyRowContainer, because of the
+ way it scrolls through data):
+
+ - Logical index
+ - Physical (or DOM) index
+ - Visual index
+
+ LOGICAL INDEX is the index that is linked to the data
+ source. If you want your data source to represent a SQL
+ database with 10 000 rows, the 7 000:th row in the SQL has a
+ logical index of 6 999, since the index is 0-based (unless
+ that data source does some funky logic).
+
+ PHYSICAL INDEX is the index for a row that you see in a
+ browser's DOM inspector. If your row is the second <tr>
+ element within a <tbody> tag, it has a physical index of 1
+ (because of 0-based indices). In Header and
+ FooterRowContainers, you are safe to assume that the logical
+ index is the same as the physical index. But because the
+ BodyRowContainer never displays large data sources entirely
+ in the DOM, a physical index usually has no apparent direct
+ relationship with its logical index.
+
+ VISUAL INDEX is the index relating to the order that you
+ see a row in, in the browser, as it is rendered. The
+ topmost row is 0, the second is 1, and so on. The visual
+ index is similar to the physical index in the sense that
+ Header and FooterRowContainers can assume a 1:1
+ relationship between visual index and logical index. And
+ again, BodyRowContainer has no such relationship. The
+ body's visual index has additionally no apparent
+ relationship with its physical index. Because the <tr> tags
+ are reused in the body and visually repositioned with CSS
+ as the user scrolls, the relationship between physical
+ index and visual index is quickly broken. You can get an
+ element's visual index via the field
+ BodyRowContainer.visualRowOrder.
+
+ */
+
+/**
+ * A workaround-class for GWT and JSNI.
+ * <p>
+ * GWT is unable to handle some method calls to Java methods in inner-classes
+ * from within JSNI blocks. Having that inner class implement a non-inner-class
+ * (or interface), makes it possible for JSNI to indirectly refer to the inner
+ * class, by invoking methods and fields in the non-inner-class.
+ *
+ * @see Escalator.Scroller
+ */
+abstract class JsniWorkaround {
+ /**
+ * A JavaScript function that handles the scroll DOM event, and passes it on
+ * to Java code.
+ *
+ * @see #createScrollListenerFunction(Escalator)
+ * @see Escalator#onScroll(double,double)
+ * @see Escalator.Scroller#onScroll(double, double)
+ */
+ protected final JavaScriptObject scrollListenerFunction;
+
+ /**
+ * A JavaScript function that handles the mousewheel DOM event, and passes
+ * it on to Java code.
+ *
+ * @see #createMousewheelListenerFunction(Escalator)
+ * @see Escalator#onScroll(double,double)
+ * @see Escalator.Scroller#onScroll(double, double)
+ */
+ protected final JavaScriptObject mousewheelListenerFunction;
+
+ /**
+ * A JavaScript function that handles the touch start DOM event, and passes
+ * it on to Java code.
+ *
+ * @see TouchHandlerBundle#touchStart(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
+ */
+ protected JavaScriptObject touchStartFunction;
+
+ /**
+ * A JavaScript function that handles the touch move DOM event, and passes
+ * it on to Java code.
+ *
+ * @see TouchHandlerBundle#touchMove(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
+ */
+ protected JavaScriptObject touchMoveFunction;
+
+ /**
+ * A JavaScript function that handles the touch end and cancel DOM events,
+ * and passes them on to Java code.
+ *
+ * @see TouchHandlerBundle#touchEnd(Escalator.JsniUtil.TouchHandlerBundle.CustomTouchEvent)
+ */
+ protected JavaScriptObject touchEndFunction;
+
+ protected JsniWorkaround(final Escalator escalator) {
+ scrollListenerFunction = createScrollListenerFunction(escalator);
+ mousewheelListenerFunction = createMousewheelListenerFunction(escalator);
+
+ final TouchHandlerBundle bundle = new TouchHandlerBundle(escalator);
+ touchStartFunction = bundle.getTouchStartHandler();
+ touchMoveFunction = bundle.getTouchMoveHandler();
+ touchEndFunction = bundle.getTouchEndHandler();
+ }
+
+ /**
+ * A method that constructs the JavaScript function that will be stored into
+ * {@link #scrollListenerFunction}.
+ *
+ * @param esc
+ * a reference to the current instance of {@link Escalator}
+ * @see Escalator#onScroll(double,double)
+ */
+ protected abstract JavaScriptObject createScrollListenerFunction(
+ Escalator esc);
+
+ /**
+ * A method that constructs the JavaScript function that will be stored into
+ * {@link #mousewheelListenerFunction}.
+ *
+ * @param esc
+ * a reference to the current instance of {@link Escalator}
+ * @see Escalator#onScroll(double,double)
+ */
+ protected abstract JavaScriptObject createMousewheelListenerFunction(
+ Escalator esc);
+}
+
+/**
+ * A low-level table-like widget that features a scrolling virtual viewport and
+ * lazily generated rows.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class Escalator extends Widget {
+
+ // todo comments legend
+ /*
+ * [[optimize]]: There's an opportunity to rewrite the code in such a way
+ * that it _might_ perform better (rememeber to measure, implement,
+ * re-measure)
+ */
+ /*
+ * [[rowheight]]: This code will require alterations that are relevant for
+ * being able to support variable row heights. NOTE: these bits can most
+ * often also be identified by searching for code reading the ROW_HEIGHT_PX
+ * constant.
+ */
+ /*
+ * [[API]]: Implementing this suggestion would require a change in the
+ * public API. These suggestions usually don't come lightly.
+ */
+ /*
+ * [[mpixscroll]]: This code will require alterations that are relevant for
+ * supporting the scrolling through more pixels than some browsers normally
+ * would support. (i.e. when we support more than "a million" pixels in the
+ * escalator DOM). NOTE: these bits can most often also be identified by
+ * searching for code that call scrollElem.getScrollTop();.
+ */
+
+ /**
+ * A utility class that contains utility methods that are usually called
+ * from JSNI.
+ * <p>
+ * The methods are moved in this class to minimize the amount of JSNI code
+ * as much as feasible.
+ */
+ static class JsniUtil {
+ public static class TouchHandlerBundle {
+
+ /**
+ * A <a href=
+ * "http://www.gwtproject.org/doc/latest/DevGuideCodingBasicsOverlay.html"
+ * >JavaScriptObject overlay</a> for the <a
+ * href="http://www.w3.org/TR/touch-events/">JavaScript
+ * TouchEvent</a> object.
+ * <p>
+ * This needs to be used in the touch event handlers, since GWT's
+ * {@link com.google.gwt.event.dom.client.TouchEvent TouchEvent}
+ * can't be cast from the JSNI call, and the
+ * {@link com.google.gwt.dom.client.NativeEvent NativeEvent} isn't
+ * properly populated with the correct values.
+ */
+ private final static class CustomTouchEvent extends
+ JavaScriptObject {
+ protected CustomTouchEvent() {
+ }
+
+ public native NativeEvent getNativeEvent()
+ /*-{
+ return this;
+ }-*/;
+
+ public native int getPageX()
+ /*-{
+ return this.targetTouches[0].pageX;
+ }-*/;
+
+ public native int getPageY()
+ /*-{
+ return this.targetTouches[0].pageY;
+ }-*/;
+ }
+
+ private double touches = 0;
+ private int lastX = 0;
+ private int lastY = 0;
+ private double lastTime = 0;
+ private boolean snappedScrollEnabled = true;
+ private double deltaX = 0;
+ private double deltaY = 0;
+
+ private final Escalator escalator;
+ private CustomTouchEvent latestTouchMoveEvent;
+ private AnimationCallback mover = new AnimationCallback() {
+ @Override
+ public void execute(double timestamp) {
+ if (touches != 1) {
+ return;
+ }
+
+ final int x = latestTouchMoveEvent.getPageX();
+ final int y = latestTouchMoveEvent.getPageY();
+ deltaX = x - lastX;
+ deltaY = y - lastY;
+ lastX = x;
+ lastY = y;
+ lastTime = timestamp;
+
+ // snap the scroll to the major axes, at first.
+ if (snappedScrollEnabled) {
+ final double oldDeltaX = deltaX;
+ final double oldDeltaY = deltaY;
+
+ /*
+ * Scrolling snaps to 40 degrees vs. flick scroll's 30
+ * degrees, since slow movements have poor resolution -
+ * it's easy to interpret a slight angle as a steep
+ * angle, since the sample rate is "unnecessarily" high.
+ * 40 simply felt better than 30.
+ */
+ final double[] snapped = Escalator.snapDeltas(deltaX,
+ deltaY, RATIO_OF_40_DEGREES);
+ deltaX = snapped[0];
+ deltaY = snapped[1];
+
+ /*
+ * if the snap failed once, let's follow the pointer
+ * from now on.
+ */
+ if (oldDeltaX != 0 && deltaX == oldDeltaX
+ && oldDeltaY != 0 && deltaY == oldDeltaY) {
+ snappedScrollEnabled = false;
+ }
+ }
+
+ moveScrollFromEvent(escalator, -deltaX, -deltaY,
+ latestTouchMoveEvent.getNativeEvent());
+ }
+ };
+ private AnimationHandle animationHandle;
+
+ public TouchHandlerBundle(final Escalator escalator) {
+ this.escalator = escalator;
+ }
+
+ public native JavaScriptObject getTouchStartHandler()
+ /*-{
+ // we need to store "this", since it won't be preserved on call.
+ var self = this;
+ return $entry(function (e) {
+ self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchStart(*)(e);
+ });
+ }-*/;
+
+ public native JavaScriptObject getTouchMoveHandler()
+ /*-{
+ // we need to store "this", since it won't be preserved on call.
+ var self = this;
+ return $entry(function (e) {
+ self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchMove(*)(e);
+ });
+ }-*/;
+
+ public native JavaScriptObject getTouchEndHandler()
+ /*-{
+ // we need to store "this", since it won't be preserved on call.
+ var self = this;
+ return $entry(function (e) {
+ self.@com.vaadin.client.ui.grid.Escalator.JsniUtil.TouchHandlerBundle::touchEnd(*)(e);
+ });
+ }-*/;
+
+ public void touchStart(final CustomTouchEvent event) {
+ touches++;
+ if (touches != 1) {
+ return;
+ }
+
+ escalator.scroller.cancelFlickScroll();
+
+ lastX = event.getPageX();
+ lastY = event.getPageY();
+
+ snappedScrollEnabled = true;
+ }
+
+ public void touchMove(final CustomTouchEvent event) {
+ /*
+ * since we only use the getPageX/Y, and calculate the diff
+ * within the handler, we don't need to calculate any
+ * intermediate deltas.
+ */
+ latestTouchMoveEvent = event;
+
+ if (animationHandle != null) {
+ animationHandle.cancel();
+ }
+ animationHandle = AnimationScheduler.get()
+ .requestAnimationFrame(mover, escalator.bodyElem);
+ event.getNativeEvent().preventDefault();
+ mover.execute(Duration.currentTimeMillis());
+ }
+
+ public void touchEnd(@SuppressWarnings("unused")
+ final CustomTouchEvent event) {
+ touches--;
+
+ if (touches == 0) {
+ escalator.scroller.handleFlickScroll(deltaX, deltaY,
+ lastTime);
+ }
+ }
+ }
+
+ public static void moveScrollFromEvent(final Escalator escalator,
+ final double deltaX, final double deltaY,
+ final NativeEvent event) {
+
+ if (!Double.isNaN(deltaX)) {
+ escalator.horizontalScrollbar.setScrollPosByDelta((int) deltaX);
+ }
+
+ if (!Double.isNaN(deltaY)) {
+ escalator.verticalScrollbar.setScrollPosByDelta((int) deltaY);
+ }
+
+ /*
+ * TODO: only prevent if not scrolled to end/bottom. Or no? UX team
+ * needs to decide.
+ */
+ final boolean warrantedYScroll = deltaY != 0
+ && escalator.verticalScrollbar.showsScrollHandle();
+ final boolean warrantedXScroll = deltaX != 0
+ && escalator.horizontalScrollbar.showsScrollHandle();
+ if (warrantedYScroll || warrantedXScroll) {
+ event.preventDefault();
+ }
+ }
+ }
+
+ /**
+ * The animation callback that handles the animation of a touch-scrolling
+ * flick with inertia.
+ */
+ private class FlickScrollAnimator implements AnimationCallback {
+ private static final double MIN_MAGNITUDE = 0.005;
+ private static final double MAX_SPEED = 7;
+
+ private double velX;
+ private double velY;
+ private double prevTime = 0;
+ private int millisLeft;
+ private double xFric;
+ private double yFric;
+
+ private boolean cancelled = false;
+
+ /**
+ * Creates a new animation callback to handle touch-scrolling flick with
+ * inertia.
+ *
+ * @param deltaX
+ * the last scrolling delta in the x-axis in a touchmove
+ * @param deltaY
+ * the last scrolling delta in the y-axis in a touchmove
+ * @param lastTime
+ * the timestamp of the last touchmove
+ */
+ public FlickScrollAnimator(final double deltaX, final double deltaY,
+ final double lastTime) {
+ final double currentTimeMillis = Duration.currentTimeMillis();
+ velX = Math.max(Math.min(deltaX / (currentTimeMillis - lastTime),
+ MAX_SPEED), -MAX_SPEED);
+ velY = Math.max(Math.min(deltaY / (currentTimeMillis - lastTime),
+ MAX_SPEED), -MAX_SPEED);
+ prevTime = lastTime;
+
+ /*
+ * If we're scrolling mainly in one of the four major directions,
+ * and only a teeny bit to any other side, snap the scroll to that
+ * major direction instead.
+ */
+ final double[] snapDeltas = Escalator.snapDeltas(velX, velY,
+ RATIO_OF_30_DEGREES);
+ velX = snapDeltas[0];
+ velY = snapDeltas[1];
+
+ if (velX * velX + velY * velY > MIN_MAGNITUDE) {
+ millisLeft = 1500;
+ xFric = velX / millisLeft;
+ yFric = velY / millisLeft;
+ } else {
+ millisLeft = 0;
+ }
+
+ }
+
+ @Override
+ public void execute(final double timestamp) {
+ if (millisLeft <= 0 || cancelled) {
+ scroller.currentFlickScroller = null;
+ return;
+ }
+
+ final int lastLeft = tBodyScrollLeft;
+ final int lastTop = tBodyScrollTop;
+
+ final double timeDiff = timestamp - prevTime;
+ setScrollLeft((int) (tBodyScrollLeft - velX * timeDiff));
+ velX -= xFric * timeDiff;
+
+ setScrollTop(tBodyScrollTop - velY * timeDiff);
+ velY -= yFric * timeDiff;
+
+ cancelBecauseOfEdgeOrCornerMaybe(lastLeft, lastTop);
+
+ prevTime = timestamp;
+ millisLeft -= timeDiff;
+ AnimationScheduler.get().requestAnimationFrame(this);
+ }
+
+ private void cancelBecauseOfEdgeOrCornerMaybe(final int lastLeft,
+ final int lastTop) {
+ if (lastLeft == horizontalScrollbar.getScrollPos()
+ && lastTop == verticalScrollbar.getScrollPos()) {
+ cancel();
+ }
+ }
+
+ public void cancel() {
+ cancelled = true;
+ }
+ }
+
+ private static final int ROW_HEIGHT_PX = 20;
+
+ /**
+ * ScrollDestination case-specific handling logic.
+ */
+ private static double getScrollPos(final ScrollDestination destination,
+ final double targetStartPx, final double targetEndPx,
+ final double viewportStartPx, final double viewportEndPx,
+ final int padding) {
+
+ final double viewportLength = viewportEndPx - viewportStartPx;
+
+ switch (destination) {
+
+ /*
+ * Scroll as little as possible to show the target element. If the
+ * element fits into view, this works as START or END depending on the
+ * current scroll position. If the element does not fit into view, this
+ * works as START.
+ */
+ case ANY: {
+ final double startScrollPos = targetStartPx - padding;
+ final double endScrollPos = targetEndPx + padding - viewportLength;
+
+ if (startScrollPos < viewportStartPx) {
+ return startScrollPos;
+ } else if (targetEndPx + padding > viewportEndPx) {
+ return endScrollPos;
+ } else {
+ // NOOP, it's already visible
+ return viewportStartPx;
+ }
+ }
+
+ /*
+ * Scrolls so that the element is shown at the end of the viewport. The
+ * viewport will, however, not scroll before its first element.
+ */
+ case END: {
+ return targetEndPx + padding - viewportLength;
+ }
+
+ /*
+ * Scrolls so that the element is shown in the middle of the viewport.
+ * The viewport will, however, not scroll beyond its contents, given
+ * more elements than what the viewport is able to show at once. Under
+ * no circumstances will the viewport scroll before its first element.
+ */
+ case MIDDLE: {
+ final double targetMiddle = targetStartPx
+ + (targetEndPx - targetStartPx) / 2;
+ return targetMiddle - viewportLength / 2;
+ }
+
+ /*
+ * Scrolls so that the element is shown at the start of the viewport.
+ * The viewport will, however, not scroll beyond its contents.
+ */
+ case START: {
+ return targetStartPx - padding;
+ }
+
+ /*
+ * Throw an error if we're here. This can only mean that
+ * ScrollDestination has been carelessly amended..
+ */
+ default: {
+ throw new IllegalArgumentException(
+ "Internal: ScrollDestination has been modified, "
+ + "but Escalator.getScrollPos has not been updated "
+ + "to match new values.");
+ }
+ }
+
+ }
+
+ /** An inner class that handles all logic related to scrolling. */
+ private class Scroller extends JsniWorkaround {
+ private double lastScrollTop = 0;
+ private double lastScrollLeft = 0;
+ /**
+ * The current flick scroll animator. This is <code>null</code> if the
+ * view isn't animating a flick scroll at the moment.
+ */
+ private FlickScrollAnimator currentFlickScroller;
+
+ public Scroller() {
+ super(Escalator.this);
+ }
+
+ @Override
+ protected native JavaScriptObject createScrollListenerFunction(
+ Escalator esc)
+ /*-{
+ var vScroll = esc.@com.vaadin.client.ui.grid.Escalator::verticalScrollbar;
+ var vScrollElem = vScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::getElement()();
+
+ var hScroll = esc.@com.vaadin.client.ui.grid.Escalator::horizontalScrollbar;
+ var hScrollElem = hScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::getElement()();
+
+ return $entry(function(e) {
+ var target = e.target || e.srcElement; // IE8 uses e.scrElement
+
+ // in case the scroll event was native (i.e. scrollbars were dragged, or
+ // the scrollTop/Left was manually modified), the bundles have old cache
+ // values. We need to make sure that the caches are kept up to date.
+ if (target === vScrollElem) {
+ vScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::updateScrollPosFromDom()();
+ } else if (target === hScrollElem) {
+ hScroll.@com.vaadin.client.ui.grid.ScrollbarBundle::updateScrollPosFromDom()();
+ } else {
+ $wnd.console.error("unexpected scroll target: "+target);
+ }
+
+ esc.@com.vaadin.client.ui.grid.Escalator::onScroll()();
+ });
+ }-*/;
+
+ @Override
+ protected native JavaScriptObject createMousewheelListenerFunction(
+ Escalator esc)
+ /*-{
+ return $entry(function(e) {
+ var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
+ var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
+
+ // IE8 has only delta y
+ if (isNaN(deltaY)) {
+ deltaY = -0.5*e.wheelDelta;
+ }
+
+ @com.vaadin.client.ui.grid.Escalator.JsniUtil::moveScrollFromEvent(*)(esc, deltaX, deltaY, e);
+ });
+ }-*/;
+
+ /**
+ * Recalculates the virtual viewport represented by the scrollbars, so
+ * that the sizes of the scroll handles appear correct in the browser
+ */
+ public void recalculateScrollbarsForVirtualViewport() {
+ int scrollContentHeight = ROW_HEIGHT_PX * body.getRowCount();
+ int scrollContentWidth = columnConfiguration.calculateRowWidth();
+
+ double tableWrapperHeight = height;
+ double tableWrapperWidth = width;
+
+ boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
+ - header.height - footer.height;
+ boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth;
+
+ // One dimension got scrollbars, but not the other. Recheck time!
+ if (verticalScrollNeeded != horizontalScrollNeeded) {
+ if (!verticalScrollNeeded && horizontalScrollNeeded) {
+ verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
+ - header.height
+ - footer.height
+ - horizontalScrollbar.getScrollbarThickness();
+ } else {
+ horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth
+ - verticalScrollbar.getScrollbarThickness();
+ }
+ }
+
+ // let's fix the table wrapper size, since it's now stable.
+ if (verticalScrollNeeded) {
+ tableWrapperWidth -= verticalScrollbar.getScrollbarThickness();
+ }
+ if (horizontalScrollNeeded) {
+ tableWrapperHeight -= horizontalScrollbar
+ .getScrollbarThickness();
+ }
+ tableWrapper.getStyle().setHeight(tableWrapperHeight, Unit.PX);
+ tableWrapper.getStyle().setWidth(tableWrapperWidth, Unit.PX);
+
+ verticalScrollbar.setOffsetSize((int) (tableWrapperHeight
+ - footer.height - header.height));
+ verticalScrollbar.setScrollSize(scrollContentHeight);
+
+ /*
+ * If decreasing the amount of frozen columns, and scrolled to the
+ * right, the scroll position might reset. So we need to remember
+ * the scroll position, and re-apply it once the scrollbar size has
+ * been adjusted.
+ */
+ int prevScrollPos = horizontalScrollbar.getScrollPos();
+
+ int unfrozenPixels = columnConfiguration
+ .getCalculatedColumnsWidth(Range.between(
+ columnConfiguration.getFrozenColumnCount(),
+ columnConfiguration.getColumnCount()));
+ int frozenPixels = scrollContentWidth - unfrozenPixels;
+ double hScrollOffsetWidth = tableWrapperWidth - frozenPixels;
+ horizontalScrollbar.setOffsetSize((int) hScrollOffsetWidth);
+ horizontalScrollbar.setScrollSize(unfrozenPixels);
+ horizontalScrollbar.getElement().getStyle()
+ .setLeft(frozenPixels, Unit.PX);
+ horizontalScrollbar.setScrollPos(prevScrollPos);
+ }
+
+ /**
+ * Logical scrolling event handler for the entire widget.
+ *
+ * @param scrollLeft
+ * the current number of pixels that the user has scrolled
+ * from left
+ * @param scrollTop
+ * the current number of pixels that the user has scrolled
+ * from the top
+ */
+ public void onScroll() {
+ if (internalScrollEventCalls > 0) {
+ internalScrollEventCalls--;
+ return;
+ }
+
+ final int scrollLeft = horizontalScrollbar.getScrollPos();
+ final int scrollTop = verticalScrollbar.getScrollPos();
+
+ if (lastScrollLeft != scrollLeft) {
+ for (int i = 0; i < columnConfiguration.frozenColumns; i++) {
+ header.updateFreezePosition(i, scrollLeft);
+ body.updateFreezePosition(i, scrollLeft);
+ footer.updateFreezePosition(i, scrollLeft);
+ }
+
+ position.set(headElem, -scrollLeft, 0);
+
+ /*
+ * TODO [[optimize]]: cache this value in case the instanceof
+ * check has undesirable overhead. This could also be a
+ * candidate for some deferred binding magic so that e.g.
+ * AbsolutePosition is not even considered in permutations that
+ * we know support something better. That would let the compiler
+ * completely remove the entire condition since it knows that
+ * the if will never be true.
+ */
+ if (position instanceof AbsolutePosition) {
+ /*
+ * we don't want to put "top: 0" on the footer, since it'll
+ * render wrong, as we already have
+ * "bottom: $footer-height".
+ */
+ footElem.getStyle().setLeft(-scrollLeft, Unit.PX);
+ } else {
+ position.set(footElem, -scrollLeft, 0);
+ }
+
+ lastScrollLeft = scrollLeft;
+ }
+
+ body.setBodyScrollPosition(scrollLeft, scrollTop);
+
+ lastScrollTop = scrollTop;
+ body.updateEscalatorRowsOnScroll();
+ /*
+ * TODO [[optimize]]: Might avoid a reflow by first calculating new
+ * scrolltop and scrolleft, then doing the escalator magic based on
+ * those numbers and only updating the positions after that.
+ */
+ }
+
+ public native void attachScrollListener(Element element)
+ /*
+ * Attaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ element.addEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
+ } else {
+ element.attachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
+ }
+ }-*/;
+
+ public native void detachScrollListener(Element element)
+ /*
+ * Attaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ element.removeEventListener("scroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
+ } else {
+ element.detachEvent("onscroll", this.@com.vaadin.client.ui.grid.JsniWorkaround::scrollListenerFunction);
+ }
+ }-*/;
+
+ public native void attachMousewheelListener(Element element)
+ /*
+ * Attaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ // firefox likes "wheel", while others use "mousewheel"
+ var eventName = element.onwheel===undefined?"mousewheel":"wheel";
+ element.addEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
+ } else {
+ // IE8
+ element.attachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
+ }
+ }-*/;
+
+ public native void detachMousewheelListener(Element element)
+ /*
+ * Detaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ // firefox likes "wheel", while others use "mousewheel"
+ var eventName = element.onwheel===undefined?"mousewheel":"wheel";
+ element.removeEventListener(eventName, this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
+ } else {
+ // IE8
+ element.detachEvent("onmousewheel", this.@com.vaadin.client.ui.grid.JsniWorkaround::mousewheelListenerFunction);
+ }
+ }-*/;
+
+ public native void attachTouchListeners(Element element)
+ /*
+ * Detaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.addEventListener) {
+ element.addEventListener("touchstart", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchStartFunction);
+ element.addEventListener("touchmove", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchMoveFunction);
+ element.addEventListener("touchend", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
+ element.addEventListener("touchcancel", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
+ } else {
+ // this would be IE8, but we don't support it with touch
+ }
+ }-*/;
+
+ public native void detachTouchListeners(Element element)
+ /*
+ * Detaching events with JSNI instead of the GWT event mechanism because
+ * GWT didn't provide enough details in events, or triggering the event
+ * handlers with GWT bindings was unsuccessful. Maybe, with more time
+ * and skill, it could be done with better success. JavaScript overlay
+ * types might work. This might also get rid of the JsniWorkaround
+ * class.
+ */
+ /*-{
+ if (element.removeEventListener) {
+ element.removeEventListener("touchstart", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchStartFunction);
+ element.removeEventListener("touchmove", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchMoveFunction);
+ element.removeEventListener("touchend", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
+ element.removeEventListener("touchcancel", this.@com.vaadin.client.ui.grid.JsniWorkaround::touchEndFunction);
+ } else {
+ // this would be IE8, but we don't support it with touch
+ }
+ }-*/;
+
+ private void cancelFlickScroll() {
+ if (currentFlickScroller != null) {
+ currentFlickScroller.cancel();
+ }
+ }
+
+ /**
+ * Handles a touch-based flick scroll.
+ *
+ * @param deltaX
+ * the last scrolling delta in the x-axis in a touchmove
+ * @param deltaY
+ * the last scrolling delta in the y-axis in a touchmove
+ * @param lastTime
+ * the timestamp of the last touchmove
+ */
+ public void handleFlickScroll(double deltaX, double deltaY,
+ double lastTime) {
+ currentFlickScroller = new FlickScrollAnimator(deltaX, deltaY,
+ lastTime);
+ AnimationScheduler.get()
+ .requestAnimationFrame(currentFlickScroller);
+ }
+
+ public void scrollToColumn(final int columnIndex,
+ final ScrollDestination destination, final int padding) {
+ assert columnIndex >= columnConfiguration.frozenColumns : "Can't scroll to a frozen column";
+
+ /*
+ * To cope with frozen columns, we just pretend those columns are
+ * not there at all when calculating the position of the target
+ * column and the boundaries of the viewport. The resulting
+ * scrollLeft will be correct without compensation since the DOM
+ * structure effectively means that scrollLeft also ignores the
+ * frozen columns.
+ */
+ final int frozenPixels = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(0,
+ columnConfiguration.frozenColumns));
+
+ final int targetStartPx = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(0, columnIndex))
+ - frozenPixels;
+ final int targetEndPx = targetStartPx
+ + columnConfiguration.getColumnWidthActual(columnIndex);
+
+ final int viewportStartPx = getScrollLeft();
+ int viewportEndPx = viewportStartPx + getElement().getOffsetWidth()
+ - frozenPixels;
+ if (verticalScrollbar.showsScrollHandle()) {
+ viewportEndPx -= Util.getNativeScrollbarSize();
+ }
+
+ final double scrollLeft = getScrollPos(destination, targetStartPx,
+ targetEndPx, viewportStartPx, viewportEndPx, padding);
+
+ /*
+ * note that it doesn't matter if the scroll would go beyond the
+ * content, since the browser will adjust for that, and everything
+ * fall into line accordingly.
+ */
+ setScrollLeft((int) scrollLeft);
+ }
+
+ public void scrollToRow(final int rowIndex,
+ final ScrollDestination destination, final int padding) {
+ // TODO [[rowheight]]
+ final int targetStartPx = ROW_HEIGHT_PX * rowIndex;
+ final int targetEndPx = targetStartPx + ROW_HEIGHT_PX;
+
+ final double viewportStartPx = getScrollTop();
+ final double viewportEndPx = viewportStartPx
+ + body.calculateHeight();
+
+ final double scrollTop = getScrollPos(destination, targetStartPx,
+ targetEndPx, viewportStartPx, viewportEndPx, padding);
+
+ /*
+ * note that it doesn't matter if the scroll would go beyond the
+ * content, since the browser will adjust for that, and everything
+ * falls into line accordingly.
+ */
+ setScrollTop(scrollTop);
+ }
+ }
+
+ private abstract class AbstractRowContainer implements RowContainer {
+ private EscalatorUpdater updater = EscalatorUpdater.NULL;
+
+ private int rows;
+
+ /**
+ * The table section element ({@code <thead>}, {@code <tbody>} or
+ * {@code <tfoot>}) the rows (i.e. {@code <tr>} tags) are contained in.
+ */
+ protected final Element root;
+
+ /** The height of the combined rows in the DOM. */
+ protected double height = -1;
+
+ /**
+ * The primary style name of the escalator. Most commonly provided by
+ * Escalator as "v-escalator".
+ */
+ private String primaryStyleName = null;
+
+ public AbstractRowContainer(final Element rowContainerElement) {
+ root = rowContainerElement;
+ }
+
+ /**
+ * Informs the row container that the height of its respective table
+ * section has changed.
+ * <p>
+ * These calculations might affect some layouting logic, such as the
+ * body is being offset by the footer, the footer needs to be readjusted
+ * according to its height, and so on.
+ * <p>
+ * A table section is either header, body or footer.
+ *
+ */
+ protected void sectionHeightCalculated() {
+ // Does nothing by default. Override to catch the "event".
+ }
+
+ /**
+ * Gets the tag name of an element to represent a cell in a row.
+ * <p>
+ * Usually {@code "th"} or {@code "td"}.
+ * <p>
+ * <em>Note:</em> To actually <em>create</em> such an element, use
+ * {@link #createCellElement()} instead.
+ *
+ * @return the tag name for the element to represent cells as
+ * @see #createCellElement()
+ */
+ protected abstract String getCellElementTagName();
+
+ @Override
+ public EscalatorUpdater getEscalatorUpdater() {
+ return updater;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for rows or columns
+ * when this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void setEscalatorUpdater(final EscalatorUpdater escalatorUpdater) {
+ if (escalatorUpdater == null) {
+ throw new IllegalArgumentException(
+ "escalator updater cannot be null");
+ }
+
+ updater = escalatorUpdater;
+
+ if (hasColumnAndRowData() && getRowCount() > 0) {
+ refreshRows(0, getRowCount());
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there are no rows in the DOM when
+ * this method is called.
+ *
+ * @see #hasSomethingInDom()
+ */
+ @Override
+ public void removeRows(final int index, final int numberOfRows) {
+ assertArgumentsAreValidAndWithinRange(index, numberOfRows);
+
+ rows -= numberOfRows;
+
+ if (!isAttached()) {
+ return;
+ }
+
+ if (hasSomethingInDom()) {
+ paintRemoveRows(index, numberOfRows);
+ }
+ }
+
+ protected abstract void paintRemoveRows(final int index,
+ final int numberOfRows);
+
+ private void assertArgumentsAreValidAndWithinRange(final int index,
+ final int numberOfRows) throws IllegalArgumentException,
+ IndexOutOfBoundsException {
+ if (numberOfRows < 1) {
+ throw new IllegalArgumentException(
+ "Number of rows must be 1 or greater (was "
+ + numberOfRows + ")");
+ }
+
+ if (index < 0 || index + numberOfRows > getRowCount()) {
+ throw new IndexOutOfBoundsException("The given "
+ + "row range (" + index + ".." + (index + numberOfRows)
+ + ") was outside of the current number of rows ("
+ + getRowCount() + ")");
+ }
+ }
+
+ @Override
+ public int getRowCount() {
+ return rows;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for columns when
+ * this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void insertRows(final int index, final int numberOfRows) {
+ if (index < 0 || index > getRowCount()) {
+ throw new IndexOutOfBoundsException("The given index (" + index
+ + ") was outside of the current number of rows (0.."
+ + getRowCount() + ")");
+ }
+
+ if (numberOfRows < 1) {
+ throw new IllegalArgumentException(
+ "Number of rows must be 1 or greater (was "
+ + numberOfRows + ")");
+ }
+
+ rows += numberOfRows;
+
+ /*
+ * only add items in the DOM if the widget itself is attached to the
+ * DOM. We can't calculate sizes otherwise.
+ */
+ if (isAttached()) {
+ paintInsertRows(index, numberOfRows);
+ }
+ }
+
+ /**
+ * Actually add rows into the DOM, now that everything can be
+ * calculated.
+ *
+ * @param visualIndex
+ * the DOM index to add rows into
+ * @param numberOfRows
+ * the number of rows to insert
+ * @return a list of the added row elements
+ */
+ protected List<Element> paintInsertRows(final int visualIndex,
+ final int numberOfRows) {
+ assert isAttached() : "Can't paint rows if Escalator is not attached";
+
+ final List<Element> addedRows = new ArrayList<Element>();
+
+ if (numberOfRows < 1) {
+ return addedRows;
+ }
+
+ Node referenceRow;
+ if (root.getChildCount() != 0 && visualIndex != 0) {
+ // get the row node we're inserting stuff after
+ referenceRow = root.getChild(visualIndex - 1);
+ } else {
+ // index is 0, so just prepend.
+ referenceRow = null;
+ }
+
+ for (int row = visualIndex; row < visualIndex + numberOfRows; row++) {
+ final int rowHeight = ROW_HEIGHT_PX;
+ final Element tr = DOM.createTR();
+ addedRows.add(tr);
+ tr.addClassName(getStylePrimaryName() + "-row");
+ referenceRow = insertAfterReferenceAndUpdateIt(root, tr,
+ referenceRow);
+
+ for (int col = 0; col < columnConfiguration.getColumnCount(); col++) {
+ final int colWidth = columnConfiguration
+ .getColumnWidthActual(col);
+ final Element cellElem = createCellElement(rowHeight,
+ colWidth);
+ tr.appendChild(cellElem);
+
+ // Set stylename and position if new cell is frozen
+ if (col < columnConfiguration.frozenColumns) {
+ cellElem.addClassName("frozen");
+ position.set(cellElem, scroller.lastScrollLeft, 0);
+ }
+ }
+
+ refreshRow(tr, row);
+ }
+ reapplyRowWidths();
+
+ recalculateSectionHeight();
+
+ return addedRows;
+ }
+
+ private Node insertAfterReferenceAndUpdateIt(final Element parent,
+ final Element elem, final Node referenceNode) {
+ if (referenceNode != null) {
+ parent.insertAfter(elem, referenceNode);
+ } else {
+ /*
+ * referencenode being null means we have offset 0, i.e. make it
+ * the first row
+ */
+ /*
+ * TODO [[optimize]]: Is insertFirst or append faster for an
+ * empty root?
+ */
+ parent.insertFirst(elem);
+ }
+ return elem;
+ }
+
+ protected void recalculateSectionHeight() {
+ Profiler.enter("Escalator.AbstractRowContainer.recalculateSectionHeight");
+ final double newHeight = root.getChildCount() * ROW_HEIGHT_PX;
+ if (newHeight != height) {
+ height = newHeight;
+ sectionHeightCalculated();
+ }
+ Profiler.leave("Escalator.AbstractRowContainer.recalculateSectionHeight");
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for columns when
+ * this method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void refreshRows(final int index, final int numberOfRows) {
+ Profiler.enter("Escalator.AbstractRowContainer.refreshRows");
+
+ assertArgumentsAreValidAndWithinRange(index, numberOfRows);
+
+ if (!isAttached()) {
+ return;
+ }
+
+ /*
+ * TODO [[rowheight]]: even if no rows are evaluated in the current
+ * viewport, the heights of some unrendered rows might change in a
+ * refresh. This would cause the scrollbar to be adjusted (in
+ * scrollHeight and/or scrollTop). Do we want to take this into
+ * account?
+ */
+ if (hasColumnAndRowData()) {
+ /*
+ * TODO [[rowheight]]: nudge rows down with
+ * refreshRowPositions() as needed
+ */
+ for (int row = index; row < index + numberOfRows; row++) {
+ final Node tr = getTrByVisualIndex(row);
+ refreshRow(tr, row);
+ }
+ }
+
+ Profiler.leave("Escalator.AbstractRowContainer.refreshRows");
+ }
+
+ void refreshRow(final Node tr, final int logicalRowIndex) {
+ flyweightRow.setup((Element) tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+ updater.updateCells(flyweightRow, flyweightRow.getCells());
+
+ /*
+ * the "assert" guarantees that this code is run only during
+ * development/debugging.
+ */
+ assert flyweightRow.teardown();
+ }
+
+ /**
+ * Create and setup an empty cell element.
+ *
+ * @param width
+ * the width of the cell, in pixels
+ * @param height
+ * the height of the cell, in pixels
+ *
+ * @return a set-up empty cell element
+ */
+ @SuppressWarnings("hiding")
+ public Element createCellElement(final int height, final int width) {
+ final Element cellElem = DOM.createElement(getCellElementTagName());
+ cellElem.getStyle().setHeight(height, Unit.PX);
+ cellElem.getStyle().setWidth(width, Unit.PX);
+ cellElem.addClassName(getStylePrimaryName() + "-cell");
+ return cellElem;
+ }
+
+ /**
+ * Gets the child element that is visually at a certain index
+ *
+ * @param index
+ * the index of the element to retrieve
+ * @return the element at position {@code index}
+ * @throws IndexOutOfBoundsException
+ * if {@code index} is not valid within {@link #root}
+ */
+ abstract protected Element getTrByVisualIndex(int index)
+ throws IndexOutOfBoundsException;
+
+ abstract protected int getTopVisualRowLogicalIndex();
+
+ protected void paintRemoveColumns(final int offset,
+ final int numberOfColumns,
+ final List<ColumnConfigurationImpl.Column> removedColumns) {
+ final NodeList<Node> childNodes = root.getChildNodes();
+ for (int visualRowIndex = 0; visualRowIndex < childNodes
+ .getLength(); visualRowIndex++) {
+ final Node tr = childNodes.getItem(visualRowIndex);
+
+ for (int column = 0; column < numberOfColumns; column++) {
+ Element cellElement = tr.getChild(offset).cast();
+ detachPossibleWidgetFromCell(cellElement);
+ cellElement.removeFromParent();
+ }
+ }
+ reapplyRowWidths();
+
+ final int firstRemovedColumnLeft = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(0, offset));
+ final boolean columnsWereRemovedFromLeftOfTheViewport = scroller.lastScrollLeft > firstRemovedColumnLeft;
+
+ if (columnsWereRemovedFromLeftOfTheViewport) {
+ int removedColumnsPxAmount = 0;
+ for (ColumnConfigurationImpl.Column removedColumn : removedColumns) {
+ removedColumnsPxAmount += removedColumn
+ .getCalculatedWidth();
+ }
+ final int leftByDiff = (int) (scroller.lastScrollLeft - removedColumnsPxAmount);
+ final int newScrollLeft = Math.max(firstRemovedColumnLeft,
+ leftByDiff);
+ horizontalScrollbar.setScrollPos(newScrollLeft);
+ }
+
+ // this needs to be after the scroll position adjustment above.
+ scroller.recalculateScrollbarsForVirtualViewport();
+
+ /*
+ * Because we might remove columns where affected by colspans, it's
+ * easiest to simply redraw everything when columns are modified.
+ *
+ * Yes, this is a TODO [[optimize]].
+ */
+ if (getRowCount() > 0
+ && getColumnConfiguration().getColumnCount() > 0) {
+ refreshRows(0, getRowCount());
+ }
+ }
+
+ void detachPossibleWidgetFromCell(Node cellNode) {
+ // Detach possible widget
+ Widget widget = getWidgetFromCell(cellNode);
+ if (widget != null) {
+ // Orphan.
+ setParent(widget, null);
+
+ // Physical detach.
+ cellNode.removeChild(widget.getElement());
+ }
+ }
+
+ protected void paintInsertColumns(final int offset,
+ final int numberOfColumns, boolean frozen) {
+ final NodeList<Node> childNodes = root.getChildNodes();
+
+ for (int row = 0; row < childNodes.getLength(); row++) {
+ final int rowHeight = ROW_HEIGHT_PX;
+ final Element tr = getTrByVisualIndex(row);
+
+ Node referenceCell;
+ if (offset != 0) {
+ referenceCell = tr.getChild(offset - 1);
+ } else {
+ referenceCell = null;
+ }
+
+ for (int col = offset; col < offset + numberOfColumns; col++) {
+ final int colWidth = columnConfiguration
+ .getColumnWidthActual(col);
+ final Element cellElem = createCellElement(rowHeight,
+ colWidth);
+ referenceCell = insertAfterReferenceAndUpdateIt(tr,
+ cellElem, referenceCell);
+ }
+ }
+ reapplyRowWidths();
+
+ if (frozen) {
+ for (int col = offset; col < offset + numberOfColumns; col++) {
+ setColumnFrozen(col, true);
+ }
+ }
+
+ // this needs to be before the scrollbar adjustment.
+ scroller.recalculateScrollbarsForVirtualViewport();
+
+ int pixelsToInsertedColumn = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(0, offset));
+ final boolean columnsWereAddedToTheLeftOfViewport = scroller.lastScrollLeft > pixelsToInsertedColumn;
+
+ if (columnsWereAddedToTheLeftOfViewport) {
+ int insertedColumnsWidth = columnConfiguration
+ .getCalculatedColumnsWidth(Range.withLength(offset,
+ numberOfColumns));
+ horizontalScrollbar
+ .setScrollPos((int) (scroller.lastScrollLeft + insertedColumnsWidth));
+ }
+
+ /*
+ * Because we might insert columns where affected by colspans, it's
+ * easiest to simply redraw everything when columns are modified.
+ *
+ * Yes, this is a TODO [[optimize]].
+ */
+ if (getRowCount() > 0
+ && getColumnConfiguration().getColumnCount() > 1) {
+ refreshRows(0, getRowCount());
+ }
+ }
+
+ public void setColumnFrozen(int column, boolean frozen) {
+ final NodeList<Node> childNodes = root.getChildNodes();
+
+ for (int row = 0; row < childNodes.getLength(); row++) {
+ final Element tr = childNodes.getItem(row).cast();
+
+ Element cell = (Element) tr.getChild(column);
+ if (frozen) {
+ cell.addClassName("frozen");
+ } else {
+ cell.removeClassName("frozen");
+ position.reset(cell);
+ }
+ }
+
+ if (frozen) {
+ updateFreezePosition(column, scroller.lastScrollLeft);
+ }
+ }
+
+ public void updateFreezePosition(int column, double scrollLeft) {
+ final NodeList<Node> childNodes = root.getChildNodes();
+
+ for (int row = 0; row < childNodes.getLength(); row++) {
+ final Element tr = childNodes.getItem(row).cast();
+
+ Element cell = (Element) tr.getChild(column);
+ position.set(cell, scrollLeft, 0);
+ }
+ }
+
+ /**
+ * Iterates through all the cells in a column and returns the width of
+ * the widest element in this RowContainer.
+ *
+ * @param index
+ * the index of the column to inspect
+ * @return the pixel width of the widest element in the indicated column
+ */
+ public int calculateMaxColWidth(int index) {
+ Element row = root.getFirstChildElement();
+ int maxWidth = 0;
+ while (row != null) {
+ final Element cell = (Element) row.getChild(index);
+ final boolean isVisible = !cell.getStyle().getDisplay()
+ .equals(Display.NONE.getCssName());
+ if (isVisible) {
+ maxWidth = Math.max(maxWidth, cell.getScrollWidth());
+ }
+ row = row.getNextSiblingElement();
+ }
+ return maxWidth;
+ }
+
+ /**
+ * Reapplies all the cells' widths according to the calculated widths in
+ * the column configuration.
+ */
+ public void reapplyColumnWidths() {
+ Element row = root.getFirstChildElement();
+ while (row != null) {
+ Element cell = row.getFirstChildElement();
+ int columnIndex = 0;
+ while (cell != null) {
+ @SuppressWarnings("hiding")
+ final int width = getCalculatedColumnWidthWithColspan(cell,
+ columnIndex);
+
+ /*
+ * TODO Should Escalator implement ProvidesResize at some
+ * point, this is where we need to do that.
+ */
+ cell.getStyle().setWidth(width, Unit.PX);
+
+ cell = cell.getNextSiblingElement();
+ columnIndex++;
+ }
+ row = row.getNextSiblingElement();
+ }
+
+ reapplyRowWidths();
+ }
+
+ private int getCalculatedColumnWidthWithColspan(final Element cell,
+ final int columnIndex) {
+ final int colspan = cell.getPropertyInt(FlyweightCell.COLSPAN_ATTR);
+ Range spannedColumns = Range.withLength(columnIndex, colspan);
+
+ /*
+ * Since browsers don't explode with overflowing colspans, escalator
+ * shouldn't either.
+ */
+ if (spannedColumns.getEnd() > columnConfiguration.getColumnCount()) {
+ spannedColumns = Range.between(columnIndex,
+ columnConfiguration.getColumnCount());
+ }
+ return columnConfiguration
+ .getCalculatedColumnsWidth(spannedColumns);
+ }
+
+ /**
+ * Applies the total length of the columns to each row element.
+ * <p>
+ * <em>Note:</em> In contrast to {@link #reapplyColumnWidths()}, this
+ * method only modifies the width of the {@code <tr>} element, not the
+ * cells within.
+ */
+ protected void reapplyRowWidths() {
+ int rowWidth = columnConfiguration.calculateRowWidth();
+
+ com.google.gwt.dom.client.Element row = root.getFirstChildElement();
+ while (row != null) {
+ row.getStyle().setWidth(rowWidth, Unit.PX);
+ row = row.getNextSiblingElement();
+ }
+ }
+
+ /**
+ * The primary style name for the container.
+ *
+ * @param primaryStyleName
+ * the style name to use as prefix for all row and cell style
+ * names.
+ */
+ protected void setStylePrimaryName(String primaryStyleName) {
+ String oldStyle = getStylePrimaryName();
+ if (SharedUtil.equals(oldStyle, primaryStyleName)) {
+ return;
+ }
+
+ this.primaryStyleName = primaryStyleName;
+
+ // Update already rendered rows and cells
+ Node row = root.getFirstChild();
+ while (row != null) {
+ Element rowElement = row.cast();
+ UIObject.setStylePrimaryName(rowElement, primaryStyleName
+ + "-row");
+ Node cell = row.getFirstChild();
+ while (cell != null) {
+ Element cellElement = cell.cast();
+ UIObject.setStylePrimaryName(cellElement, primaryStyleName
+ + "-cell");
+ cell = cell.getNextSibling();
+ }
+ row = row.getNextSibling();
+ }
+ }
+
+ /**
+ * Returns the primary style name of the container.
+ *
+ * @return The primary style name or <code>null</code> if not set.
+ */
+ protected String getStylePrimaryName() {
+ return primaryStyleName;
+ }
+ }
+
+ private abstract class AbstractStaticRowContainer extends
+ AbstractRowContainer {
+ public AbstractStaticRowContainer(final Element headElement) {
+ super(headElement);
+ }
+
+ @Override
+ protected void paintRemoveRows(final int index, final int numberOfRows) {
+ for (int i = index; i < index + numberOfRows; i++) {
+ final Element tr = (Element) root.getChild(index);
+ for (int c = 0; c < tr.getChildCount(); c++) {
+ detachPossibleWidgetFromCell((Element) tr.getChild(c)
+ .cast());
+ }
+ tr.removeFromParent();
+ }
+ recalculateSectionHeight();
+ }
+
+ @Override
+ protected Element getTrByVisualIndex(final int index)
+ throws IndexOutOfBoundsException {
+ if (index >= 0 && index < root.getChildCount()) {
+ return (Element) root.getChild(index);
+ } else {
+ throw new IndexOutOfBoundsException("No such visual index: "
+ + index);
+ }
+ }
+
+ @Override
+ protected int getTopVisualRowLogicalIndex() {
+ return 0;
+ }
+
+ @Override
+ public void insertRows(int index, int numberOfRows) {
+ super.insertRows(index, numberOfRows);
+ recalculateElementSizes();
+ }
+
+ @Override
+ public void removeRows(int index, int numberOfRows) {
+ super.removeRows(index, numberOfRows);
+ recalculateElementSizes();
+ }
+ }
+
+ private class HeaderRowContainer extends AbstractStaticRowContainer {
+ public HeaderRowContainer(final Element headElement) {
+ super(headElement);
+ }
+
+ @Override
+ protected void sectionHeightCalculated() {
+ bodyElem.getStyle().setMarginTop(height, Unit.PX);
+ verticalScrollbar.getElement().getStyle().setTop(height, Unit.PX);
+ }
+
+ @Override
+ protected String getCellElementTagName() {
+ return "th";
+ }
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ UIObject.setStylePrimaryName(root, primaryStyleName + "-header");
+ }
+ }
+
+ private class FooterRowContainer extends AbstractStaticRowContainer {
+ public FooterRowContainer(final Element footElement) {
+ super(footElement);
+ }
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ UIObject.setStylePrimaryName(root, primaryStyleName + "-footer");
+ }
+
+ @Override
+ protected String getCellElementTagName() {
+ return "td";
+ }
+ }
+
+ private class BodyRowContainer extends AbstractRowContainer {
+ /*
+ * TODO [[optimize]]: check whether a native JsArray might be faster
+ * than LinkedList
+ */
+ /**
+ * The order in which row elements are rendered visually in the browser,
+ * with the help of CSS tricks. Usually has nothing to do with the DOM
+ * order.
+ */
+ private final LinkedList<Element> visualRowOrder = new LinkedList<Element>();
+
+ /**
+ * Don't use this field directly, because it will not take proper care
+ * of all the bookkeeping required.
+ *
+ * @deprecated Use {@link #setRowPosition(Element, int, int)} and
+ * {@link #getRowTop(Element)} instead.
+ */
+ @Deprecated
+ private final Map<Element, Integer> rowTopPosMap = new HashMap<Element, Integer>();
+
+ public BodyRowContainer(final Element bodyElement) {
+ super(bodyElement);
+ }
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ UIObject.setStylePrimaryName(root, primaryStyleName + "-body");
+ }
+
+ public void updateEscalatorRowsOnScroll() {
+ if (visualRowOrder.isEmpty()) {
+ return;
+ }
+
+ boolean rowsWereMoved = false;
+
+ final int topRowPos = getRowTop(visualRowOrder.getFirst());
+ // TODO [[mpixscroll]]
+ final int scrollTop = tBodyScrollTop;
+ final int viewportOffset = topRowPos - scrollTop;
+
+ /*
+ * TODO [[optimize]] this if-else can most probably be refactored
+ * into a neater block of code
+ */
+
+ if (viewportOffset > 0) {
+ // there's empty room on top
+
+ int rowsToMove = (int) Math.ceil((double) viewportOffset
+ / (double) ROW_HEIGHT_PX);
+ rowsToMove = Math.min(rowsToMove, root.getChildCount());
+
+ final int end = root.getChildCount();
+ final int start = end - rowsToMove;
+ final int logicalRowIndex = scrollTop / ROW_HEIGHT_PX;
+ moveAndUpdateEscalatorRows(Range.between(start, end), 0,
+ logicalRowIndex);
+
+ rowsWereMoved = (rowsToMove != 0);
+ }
+
+ else if (viewportOffset + ROW_HEIGHT_PX <= 0) {
+ /*
+ * the viewport has been scrolled more than the topmost visual
+ * row.
+ */
+
+ /*
+ * Using the fact that integer division has implicit
+ * floor-function to our advantage here.
+ */
+ int rowsToMove = Math.abs(viewportOffset / ROW_HEIGHT_PX);
+ rowsToMove = Math.min(rowsToMove, root.getChildCount());
+
+ int logicalRowIndex;
+ if (rowsToMove < root.getChildCount()) {
+ /*
+ * We scroll so little that we can just keep adding the rows
+ * below the current escalator
+ */
+ logicalRowIndex = getLogicalRowIndex(visualRowOrder
+ .getLast()) + 1;
+ } else {
+ /*
+ * Since we're moving all escalator rows, we need to
+ * calculate the first logical row index from the scroll
+ * position.
+ */
+ logicalRowIndex = scrollTop / ROW_HEIGHT_PX;
+ }
+
+ /*
+ * Since we're moving the viewport downwards, the visual index
+ * is always at the bottom. Note: Due to how
+ * moveAndUpdateEscalatorRows works, this will work out even if
+ * we move all the rows, and try to place them "at the end".
+ */
+ final int targetVisualIndex = root.getChildCount();
+
+ // make sure that we don't move rows over the data boundary
+ boolean aRowWasLeftBehind = false;
+ if (logicalRowIndex + rowsToMove > getRowCount()) {
+ /*
+ * TODO [[rowheight]]: with constant row heights, there's
+ * always exactly one row that will be moved beyond the data
+ * source, when viewport is scrolled to the end. This,
+ * however, isn't guaranteed anymore once row heights start
+ * varying.
+ */
+ rowsToMove--;
+ aRowWasLeftBehind = true;
+ }
+
+ moveAndUpdateEscalatorRows(Range.between(0, rowsToMove),
+ targetVisualIndex, logicalRowIndex);
+
+ if (aRowWasLeftBehind) {
+ /*
+ * To keep visualRowOrder as a spatially contiguous block of
+ * rows, let's make sure that the one row we didn't move
+ * visually still stays with the pack.
+ */
+ final Range strayRow = Range.withOnly(0);
+ final int topLogicalIndex = getLogicalRowIndex(visualRowOrder
+ .get(1)) - 1;
+ moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
+ }
+
+ rowsWereMoved = (rowsToMove != 0);
+ }
+
+ if (rowsWereMoved) {
+ fireRowVisibilityChangeEvent();
+ }
+ }
+
+ @Override
+ protected List<Element> paintInsertRows(final int index,
+ final int numberOfRows) {
+ if (numberOfRows == 0) {
+ return Collections.emptyList();
+ }
+
+ /*
+ * TODO: this method should probably only add physical rows, and not
+ * populate them - let everything be populated as appropriate by the
+ * logic that follows.
+ *
+ * This also would lead to the fact that paintInsertRows wouldn't
+ * need to return anything.
+ */
+ final List<Element> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
+ index, numberOfRows);
+
+ /*
+ * insertRows will always change the number of rows - update the
+ * scrollbar sizes.
+ */
+ scroller.recalculateScrollbarsForVirtualViewport();
+
+ final boolean addedRowsAboveCurrentViewport = index * ROW_HEIGHT_PX < getScrollTop();
+ final boolean addedRowsBelowCurrentViewport = index * ROW_HEIGHT_PX > getScrollTop()
+ + calculateHeight();
+
+ if (addedRowsAboveCurrentViewport) {
+ /*
+ * We need to tweak the virtual viewport (scroll handle
+ * positions, table "scroll position" and row locations), but
+ * without re-evaluating any rows.
+ */
+
+ final int yDelta = numberOfRows * ROW_HEIGHT_PX;
+ adjustScrollPosIgnoreEvents(yDelta);
+ }
+
+ else if (addedRowsBelowCurrentViewport) {
+ // NOOP, we already recalculated scrollbars.
+ }
+
+ else { // some rows were added inside the current viewport
+
+ final int unupdatedLogicalStart = index + addedRows.size();
+ final int visualOffset = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+
+ /*
+ * At this point, we have added new escalator rows, if so
+ * needed.
+ *
+ * If more rows were added than the new escalator rows can
+ * account for, we need to start to spin the escalator to update
+ * the remaining rows aswell.
+ */
+ final int rowsStillNeeded = numberOfRows - addedRows.size();
+ final Range unupdatedVisual = convertToVisual(Range.withLength(
+ unupdatedLogicalStart, rowsStillNeeded));
+ final int end = root.getChildCount();
+ final int start = end - unupdatedVisual.length();
+ final int visualTargetIndex = unupdatedLogicalStart
+ - visualOffset;
+ moveAndUpdateEscalatorRows(Range.between(start, end),
+ visualTargetIndex, unupdatedLogicalStart);
+
+ // move the surrounding rows to their correct places.
+ int rowTop = (unupdatedLogicalStart + (end - start))
+ * ROW_HEIGHT_PX;
+ final ListIterator<Element> i = visualRowOrder
+ .listIterator(visualTargetIndex + (end - start));
+ while (i.hasNext()) {
+ final Element tr = i.next();
+ setRowPosition(tr, 0, rowTop);
+ rowTop += ROW_HEIGHT_PX;
+ }
+
+ fireRowVisibilityChangeEvent();
+ }
+ return addedRows;
+ }
+
+ /**
+ * Move escalator rows around, and make sure everything gets
+ * appropriately repositioned and repainted.
+ *
+ * @param visualSourceRange
+ * the range of rows to move to a new place
+ * @param visualTargetIndex
+ * the visual index where the rows will be placed to
+ * @param logicalTargetIndex
+ * the logical index to be assigned to the first moved row
+ * @throws IllegalArgumentException
+ * if any of <code>visualSourceRange.getStart()</code>,
+ * <code>visualTargetIndex</code> or
+ * <code>logicalTargetIndex</code> is a negative number; or
+ * if <code>visualTargetInfo</code> is greater than the
+ * number of escalator rows.
+ */
+ private void moveAndUpdateEscalatorRows(final Range visualSourceRange,
+ final int visualTargetIndex, final int logicalTargetIndex)
+ throws IllegalArgumentException {
+
+ if (visualSourceRange.isEmpty()) {
+ return;
+ }
+
+ if (visualSourceRange.getStart() < 0) {
+ throw new IllegalArgumentException(
+ "Logical source start must be 0 or greater (was "
+ + visualSourceRange.getStart() + ")");
+ } else if (logicalTargetIndex < 0) {
+ throw new IllegalArgumentException(
+ "Logical target must be 0 or greater");
+ } else if (visualTargetIndex < 0) {
+ throw new IllegalArgumentException(
+ "Visual target must be 0 or greater");
+ } else if (visualTargetIndex > root.getChildCount()) {
+ throw new IllegalArgumentException(
+ "Visual target must not be greater than the number of escalator rows");
+ } else if (logicalTargetIndex + visualSourceRange.length() > getRowCount()) {
+ final int logicalEndIndex = logicalTargetIndex
+ + visualSourceRange.length() - 1;
+ throw new IllegalArgumentException(
+ "Logical target leads to rows outside of the data range ("
+ + logicalTargetIndex + ".." + logicalEndIndex
+ + ")");
+ }
+
+ /*
+ * Since we move a range into another range, the indices might move
+ * about. Having 10 rows, if we move 0..1 to index 10 (to the end of
+ * the collection), the target range will end up being 8..9, instead
+ * of 10..11.
+ *
+ * This applies only if we move elements forward in the collection,
+ * not backward.
+ */
+ final int adjustedVisualTargetIndex;
+ if (visualSourceRange.getStart() < visualTargetIndex) {
+ adjustedVisualTargetIndex = visualTargetIndex
+ - visualSourceRange.length();
+ } else {
+ adjustedVisualTargetIndex = visualTargetIndex;
+ }
+
+ if (visualSourceRange.getStart() != adjustedVisualTargetIndex) {
+
+ /*
+ * Reorder the rows to their correct places within
+ * visualRowOrder (unless rows are moved back to their original
+ * places)
+ */
+
+ /*
+ * TODO [[optimize]]: move whichever set is smaller: the ones
+ * explicitly moved, or the others. So, with 10 escalator rows,
+ * if we are asked to move idx[0..8] to the end of the list,
+ * it's faster to just move idx[9] to the beginning.
+ */
+
+ final List<Element> removedRows = new ArrayList<Element>(
+ visualSourceRange.length());
+ for (int i = 0; i < visualSourceRange.length(); i++) {
+ final Element tr = visualRowOrder.remove(visualSourceRange
+ .getStart());
+ removedRows.add(tr);
+ }
+ visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
+ }
+
+ { // Refresh the contents of the affected rows
+ final ListIterator<Element> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex
+ + visualSourceRange.length(); logicalIndex++) {
+ final Element tr = iter.next();
+ refreshRow(tr, logicalIndex);
+ }
+ }
+
+ { // Reposition the rows that were moved
+ int newRowTop = logicalTargetIndex * ROW_HEIGHT_PX;
+
+ final ListIterator<Element> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ for (int i = 0; i < visualSourceRange.length(); i++) {
+ final Element tr = iter.next();
+ setRowPosition(tr, 0, newRowTop);
+ newRowTop += ROW_HEIGHT_PX;
+ }
+ }
+ }
+
+ /**
+ * Adjust the scroll position without having the scroll handler have any
+ * side-effects.
+ * <p>
+ * <em>Note:</em> {@link Scroller#onScroll(double, double)}
+ * <em>will</em> be triggered, but it not do anything, with the help of
+ * {@link Escalator#internalScrollEventCalls}.
+ *
+ * @param yDelta
+ * the delta of pixels to scrolls. A positive value moves the
+ * viewport downwards, while a negative value moves the
+ * viewport upwards
+ */
+ public void adjustScrollPosIgnoreEvents(final int yDelta) {
+ if (yDelta == 0) {
+ return;
+ }
+
+ internalScrollEventCalls++;
+ verticalScrollbar.setScrollPosByDelta(yDelta);
+
+ final int snappedYDelta = yDelta - yDelta % ROW_HEIGHT_PX;
+ for (final Element tr : visualRowOrder) {
+ setRowPosition(tr, 0, getRowTop(tr) + snappedYDelta);
+ }
+ setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta);
+ }
+
+ private void setRowPosition(final Element tr, final int x, final int y) {
+ position.set(tr, x, y);
+ rowTopPosMap.put(tr, y);
+ }
+
+ private int getRowTop(final Element tr) {
+ return rowTopPosMap.get(tr);
+ }
+
+ /**
+ * Adds new physical escalator rows to the DOM at the given index if
+ * there's still a need for more escalator rows.
+ * <p>
+ * If Escalator already is at (or beyond) max capacity, this method does
+ * nothing to the DOM.
+ *
+ * @param index
+ * the index at which to add new escalator rows.
+ * <em>Note:</em>It is assumed that the index is both the
+ * visual index and the logical index.
+ * @param numberOfRows
+ * the number of rows to add at <code>index</code>
+ * @return a list of the added rows
+ */
+ private List<Element> fillAndPopulateEscalatorRowsIfNeeded(
+ final int index, final int numberOfRows) {
+
+ final int escalatorRowsStillFit = getMaxEscalatorRowCapacity()
+ - root.getChildCount();
+ final int escalatorRowsNeeded = Math.min(numberOfRows,
+ escalatorRowsStillFit);
+
+ if (escalatorRowsNeeded > 0) {
+
+ final List<Element> addedRows = super.paintInsertRows(index,
+ escalatorRowsNeeded);
+ visualRowOrder.addAll(index, addedRows);
+
+ /*
+ * We need to figure out the top positions for the rows we just
+ * added.
+ */
+ for (int i = 0; i < addedRows.size(); i++) {
+ setRowPosition(addedRows.get(i), 0, (index + i)
+ * ROW_HEIGHT_PX);
+ }
+
+ /* Move the other rows away from above the added escalator rows */
+ for (int i = index + addedRows.size(); i < visualRowOrder
+ .size(); i++) {
+ final Element tr = visualRowOrder.get(i);
+ setRowPosition(tr, 0, i * ROW_HEIGHT_PX);
+ }
+
+ return addedRows;
+ } else {
+ return new ArrayList<Element>();
+ }
+ }
+
+ private int getMaxEscalatorRowCapacity() {
+ final int maxEscalatorRowCapacity = (int) Math
+ .ceil(calculateHeight() / ROW_HEIGHT_PX) + 1;
+ /*
+ * maxEscalatorRowCapacity can become negative if the headers and
+ * footers start to overlap. This is a crazy situation, but Vaadin
+ * blinks the components a lot, so it's feasible.
+ */
+ return Math.max(0, maxEscalatorRowCapacity);
+ }
+
+ @Override
+ protected void paintRemoveRows(final int index, final int numberOfRows) {
+
+ final Range viewportRange = Range.withLength(
+ getLogicalRowIndex(visualRowOrder.getFirst()),
+ visualRowOrder.size());
+
+ final Range removedRowsRange = Range
+ .withLength(index, numberOfRows);
+
+ final Range[] partitions = removedRowsRange
+ .partitionWith(viewportRange);
+ final Range removedAbove = partitions[0];
+ final Range removedLogicalInside = partitions[1];
+ final Range removedVisualInside = convertToVisual(removedLogicalInside);
+
+ /*
+ * TODO: extract the following if-block to a separate method. I'll
+ * leave this be inlined for now, to make linediff-based code
+ * reviewing easier. Probably will be moved in the following patch
+ * set.
+ */
+
+ /*
+ * Adjust scroll position in one of two scenarios:
+ *
+ * 1) Rows were removed above. Then we just need to adjust the
+ * scrollbar by the height of the removed rows.
+ *
+ * 2) There are no logical rows above, and at least the first (if
+ * not more) visual row is removed. Then we need to snap the scroll
+ * position to the first visible row (i.e. reset scroll position to
+ * absolute 0)
+ *
+ * The logic is optimized in such a way that the
+ * adjustScrollPosIgnoreEvents is called only once, to avoid extra
+ * reflows, and thus the code might seem a bit obscure.
+ */
+ final boolean firstVisualRowIsRemoved = !removedVisualInside
+ .isEmpty() && removedVisualInside.getStart() == 0;
+
+ if (!removedAbove.isEmpty() || firstVisualRowIsRemoved) {
+ // TODO [[rowheight]]
+ final int yDelta = removedAbove.length() * ROW_HEIGHT_PX;
+ final int firstLogicalRowHeight = ROW_HEIGHT_PX;
+ final boolean removalScrollsToShowFirstLogicalRow = verticalScrollbar
+ .getScrollPos() - yDelta < firstLogicalRowHeight;
+
+ if (removedVisualInside.isEmpty()
+ && (!removalScrollsToShowFirstLogicalRow || !firstVisualRowIsRemoved)) {
+ /*
+ * rows were removed from above the viewport, so all we need
+ * to do is to adjust the scroll position to account for the
+ * removed rows
+ */
+ adjustScrollPosIgnoreEvents(-yDelta);
+ } else if (removalScrollsToShowFirstLogicalRow) {
+ /*
+ * It seems like we've removed all rows from above, and also
+ * into the current viewport. This means we'll need to even
+ * out the scroll position to exactly 0 (i.e. adjust by the
+ * current negative scrolltop, presto!), so that it isn't
+ * aligned funnily
+ */
+ adjustScrollPosIgnoreEvents(-verticalScrollbar
+ .getScrollPos());
+ }
+ }
+
+ // ranges evaluated, let's do things.
+ if (!removedVisualInside.isEmpty()) {
+ int escalatorRowCount = bodyElem.getChildCount();
+
+ /*
+ * If we're left with less rows than the number of escalators,
+ * remove the unused ones.
+ */
+ final int escalatorRowsToRemove = escalatorRowCount
+ - getRowCount();
+ if (escalatorRowsToRemove > 0) {
+ for (int i = 0; i < escalatorRowsToRemove; i++) {
+ final Element tr = visualRowOrder
+ .remove(removedVisualInside.getStart());
+ for (int c = 0; c < tr.getChildCount(); c++) {
+ detachPossibleWidgetFromCell((Element) tr.getChild(
+ c).cast());
+ }
+ tr.removeFromParent();
+ rowTopPosMap.remove(tr);
+ }
+ escalatorRowCount -= escalatorRowsToRemove;
+
+ /*
+ * Because we're removing escalator rows, we don't have
+ * anything to scroll by. Let's make sure the viewport is
+ * scrolled to top, to render any rows possibly left above.
+ */
+ body.setBodyScrollPosition(tBodyScrollLeft, 0);
+
+ /*
+ * We might have removed some rows from the middle, so let's
+ * make sure we're not left with any holes. Also remember:
+ * visualIndex == logicalIndex applies now.
+ */
+ final int dirtyRowsStart = removedLogicalInside.getStart();
+ for (int i = dirtyRowsStart; i < escalatorRowCount; i++) {
+ final Element tr = visualRowOrder.get(i);
+ setRowPosition(tr, 0, i * ROW_HEIGHT_PX);
+ }
+
+ /*
+ * this is how many rows appeared into the viewport from
+ * below
+ */
+ final int rowsToUpdateDataOn = numberOfRows
+ - escalatorRowsToRemove;
+ final int start = Math.max(0, escalatorRowCount
+ - rowsToUpdateDataOn);
+ final int end = escalatorRowCount;
+ for (int i = start; i < end; i++) {
+ final Element tr = visualRowOrder.get(i);
+ refreshRow(tr, i);
+ }
+ }
+
+ else {
+ // No escalator rows need to be removed.
+
+ /*
+ * Two things (or a combination thereof) can happen:
+ *
+ * 1) We're scrolled to the bottom, the last rows are
+ * removed. SOLUTION: moveAndUpdateEscalatorRows the
+ * bottommost rows, and place them at the top to be
+ * refreshed.
+ *
+ * 2) We're scrolled somewhere in the middle, arbitrary rows
+ * are removed. SOLUTION: moveAndUpdateEscalatorRows the
+ * removed rows, and place them at the bottom to be
+ * refreshed.
+ *
+ * Since a combination can also happen, we need to handle
+ * this in a smart way, all while avoiding
+ * double-refreshing.
+ */
+
+ final int contentBottom = getRowCount() * ROW_HEIGHT_PX;
+ final int viewportBottom = (int) (tBodyScrollTop + calculateHeight());
+ if (viewportBottom <= contentBottom) {
+ /*
+ * We're in the middle of the row container, everything
+ * is added to the bottom
+ */
+ paintRemoveRowsAtMiddle(removedLogicalInside,
+ removedVisualInside, 0);
+ }
+
+ else if (contentBottom + (numberOfRows * ROW_HEIGHT_PX)
+ - viewportBottom < ROW_HEIGHT_PX) {
+ /*
+ * We're at the end of the row container, everything is
+ * added to the top.
+ */
+ paintRemoveRowsAtBottom(removedLogicalInside,
+ removedVisualInside);
+ }
+
+ else {
+ /*
+ * We're in a combination, where we need to both scroll
+ * up AND show new rows at the bottom.
+ *
+ * Example: Scrolled down to show the second to last
+ * row. Remove two. Viewport scrolls up, revealing the
+ * row above row. The last element collapses up and into
+ * view.
+ *
+ * Reminder: this use case handles only the case when
+ * there are enough escalator rows to still render a
+ * full view. I.e. all escalator rows will _always_ be
+ * populated
+ */
+ /*-
+ * 1 1 |1| <- newly rendered
+ * |2| |2| |2|
+ * |3| ==> |*| ==> |5| <- newly rendered
+ * |4| |*|
+ * 5 5
+ *
+ * 1 1 |1| <- newly rendered
+ * |2| |*| |4|
+ * |3| ==> |*| ==> |5| <- newly rendered
+ * |4| |4|
+ * 5 5
+ */
+
+ /*
+ * STEP 1:
+ *
+ * reorganize deprecated escalator rows to bottom, but
+ * don't re-render anything yet
+ */
+ /*-
+ * 1 1 1
+ * |2| |*| |4|
+ * |3| ==> |*| ==> |*|
+ * |4| |4| |*|
+ * 5 5 5
+ */
+ int newTop = getRowTop(visualRowOrder
+ .get(removedVisualInside.getStart()));
+ for (int i = 0; i < removedVisualInside.length(); i++) {
+ final Element tr = visualRowOrder
+ .remove(removedVisualInside.getStart());
+ visualRowOrder.addLast(tr);
+ }
+
+ for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) {
+ final Element tr = visualRowOrder.get(i);
+ setRowPosition(tr, 0, newTop);
+ newTop += ROW_HEIGHT_PX;
+ }
+
+ /*
+ * STEP 2:
+ *
+ * manually scroll
+ */
+ /*-
+ * 1 |1| <-- newly rendered (by scrolling)
+ * |4| |4|
+ * |*| ==> |*|
+ * |*|
+ * 5 5
+ */
+ final double newScrollTop = contentBottom
+ - calculateHeight();
+ setScrollTop(newScrollTop);
+ /*
+ * Manually call the scroll handler, so we get immediate
+ * effects in the escalator.
+ */
+ scroller.onScroll();
+ internalScrollEventCalls++;
+
+ /*
+ * Move the bottommost (n+1:th) escalator row to top,
+ * because scrolling up doesn't handle that for us
+ * automatically
+ */
+ moveAndUpdateEscalatorRows(
+ Range.withOnly(escalatorRowCount - 1),
+ 0,
+ getLogicalRowIndex(visualRowOrder.getFirst()) - 1);
+
+ /*
+ * STEP 3:
+ *
+ * update remaining escalator rows
+ */
+ /*-
+ * |1| |1|
+ * |4| ==> |4|
+ * |*| |5| <-- newly rendered
+ *
+ * 5
+ */
+ final int rowsScrolled = (int) (Math
+ .ceil((viewportBottom - (double) contentBottom)
+ / ROW_HEIGHT_PX));
+ final int start = escalatorRowCount
+ - (removedVisualInside.length() - rowsScrolled);
+ final Range visualRefreshRange = Range.between(start,
+ escalatorRowCount);
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst()) + start;
+ // in-place move simply re-renders the rows.
+ moveAndUpdateEscalatorRows(visualRefreshRange, start,
+ logicalTargetIndex);
+ }
+ }
+ }
+
+ /*
+ * this needs to be done after the escalator has been shrunk down,
+ * or it won't work correctly (due to setScrollTop invocation)
+ */
+ scroller.recalculateScrollbarsForVirtualViewport();
+
+ fireRowVisibilityChangeEvent();
+ }
+
+ private void paintRemoveRowsAtMiddle(final Range removedLogicalInside,
+ final Range removedVisualInside, final int logicalOffset) {
+ /*-
+ * : : :
+ * |2| |2| |2|
+ * |3| ==> |*| ==> |4|
+ * |4| |4| |6| <- newly rendered
+ * : : :
+ */
+
+ final int escalatorRowCount = visualRowOrder.size();
+
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getLast())
+ - (removedVisualInside.length() - 1)
+ + logicalOffset;
+ moveAndUpdateEscalatorRows(removedVisualInside, escalatorRowCount,
+ logicalTargetIndex);
+
+ // move the surrounding rows to their correct places.
+ final ListIterator<Element> iterator = visualRowOrder
+ .listIterator(removedVisualInside.getStart());
+ int rowTop = (removedLogicalInside.getStart() + logicalOffset)
+ * ROW_HEIGHT_PX;
+ for (int i = removedVisualInside.getStart(); i < escalatorRowCount
+ - removedVisualInside.length(); i++) {
+ final Element tr = iterator.next();
+ setRowPosition(tr, 0, rowTop);
+ rowTop += ROW_HEIGHT_PX;
+ }
+ }
+
+ private void paintRemoveRowsAtBottom(final Range removedLogicalInside,
+ final Range removedVisualInside) {
+ /*-
+ * :
+ * : : |4| <- newly rendered
+ * |5| |5| |5|
+ * |6| ==> |*| ==> |7|
+ * |7| |7|
+ */
+
+ final int logicalTargetIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst()) - removedVisualInside.length();
+ moveAndUpdateEscalatorRows(removedVisualInside, 0,
+ logicalTargetIndex);
+
+ // move the surrounding rows to their correct places.
+ final ListIterator<Element> iterator = visualRowOrder
+ .listIterator(removedVisualInside.getEnd());
+ int rowTop = removedLogicalInside.getStart() * ROW_HEIGHT_PX;
+ while (iterator.hasNext()) {
+ final Element tr = iterator.next();
+ setRowPosition(tr, 0, rowTop);
+ rowTop += ROW_HEIGHT_PX;
+ }
+ }
+
+ private int getLogicalRowIndex(final Element element) {
+ // TODO [[rowheight]]
+ return getRowTop(element) / ROW_HEIGHT_PX;
+ }
+
+ @Override
+ protected void recalculateSectionHeight() {
+ // disable for body, since it doesn't make any sense.
+ }
+
+ /**
+ * Adjusts the row index and number to be relevant for the current
+ * virtual viewport.
+ * <p>
+ * It converts a logical range of rows index to the matching visual
+ * range, truncating the resulting range with the viewport.
+ * <p>
+ * <ul>
+ * <li>Escalator contains logical rows 0..100
+ * <li>Current viewport showing logical rows 20..29
+ * <li>convertToVisual([20..29]) &rarr; [0..9]
+ * <li>convertToVisual([15..24]) &rarr; [0..4]
+ * <li>convertToVisual([25..29]) &rarr; [5..9]
+ * <li>convertToVisual([26..39]) &rarr; [6..9]
+ * <li>convertToVisual([0..5]) &rarr; [0..-1] <em>(empty)</em>
+ * <li>convertToVisual([35..1]) &rarr; [0..-1] <em>(empty)</em>
+ * <li>convertToVisual([0..100]) &rarr; [0..9]
+ * </ul>
+ *
+ * @return a logical range converted to a visual range, truncated to the
+ * current viewport. The first visual row has the index 0.
+ */
+ private Range convertToVisual(final Range logicalRange) {
+ if (logicalRange.isEmpty()) {
+ return logicalRange;
+ } else if (visualRowOrder.isEmpty()) {
+ // empty range
+ return Range.withLength(0, 0);
+ }
+
+ /*
+ * TODO [[rowheight]]: these assumptions will be totally broken with
+ * variable row heights.
+ */
+ final int topRowHeight = ROW_HEIGHT_PX;
+ final int maxEscalatorRows = (int) Math
+ .ceil((calculateHeight() + topRowHeight) / ROW_HEIGHT_PX);
+ final int currentTopRowIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+
+ final Range[] partitions = logicalRange.partitionWith(Range
+ .withLength(currentTopRowIndex, maxEscalatorRows));
+ final Range insideRange = partitions[1];
+ return insideRange.offsetBy(-currentTopRowIndex);
+ }
+
+ @Override
+ protected String getCellElementTagName() {
+ return "td";
+ }
+
+ private double calculateHeight() {
+ final int tableHeight = tableWrapper.getOffsetHeight();
+ final double footerHeight = footer.height;
+ final double headerHeight = header.height;
+ return tableHeight - footerHeight - headerHeight;
+ }
+
+ @Override
+ public void refreshRows(final int index, final int numberOfRows) {
+ Profiler.enter("Escalator.BodyRowContainer.refreshRows");
+
+ final Range visualRange = convertToVisual(Range.withLength(index,
+ numberOfRows));
+
+ if (!visualRange.isEmpty()) {
+ final int firstLogicalRowIndex = getLogicalRowIndex(visualRowOrder
+ .getFirst());
+ for (int rowNumber = visualRange.getStart(); rowNumber < visualRange
+ .getEnd(); rowNumber++) {
+ refreshRow(visualRowOrder.get(rowNumber),
+ firstLogicalRowIndex + rowNumber);
+ }
+ }
+
+ Profiler.leave("Escalator.BodyRowContainer.refreshRows");
+ }
+
+ @Override
+ protected Element getTrByVisualIndex(final int index)
+ throws IndexOutOfBoundsException {
+ if (index >= 0 && index < visualRowOrder.size()) {
+ return visualRowOrder.get(index);
+ } else {
+ throw new IndexOutOfBoundsException("No such visual index: "
+ + index);
+ }
+ }
+
+ private void setBodyScrollPosition(final int scrollLeft,
+ final int scrollTop) {
+ tBodyScrollLeft = scrollLeft;
+ tBodyScrollTop = scrollTop;
+ position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
+ }
+
+ @Override
+ protected int getTopVisualRowLogicalIndex() {
+ if (!visualRowOrder.isEmpty()) {
+ return getLogicalRowIndex(visualRowOrder.getFirst());
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Make sure that there is a correct amount of escalator rows: Add more
+ * if needed, or remove any superfluous ones.
+ * <p>
+ * This method should be called when e.g. the height of the Escalator
+ * changes.
+ */
+ public void verifyEscalatorCount() {
+ /*
+ * This method indeed has a smell very similar to paintRemoveRows
+ * and paintInsertRows.
+ *
+ * Unfortunately, those the code can't trivially be shared, since
+ * there are some slight differences in the respective
+ * responsibilities. The "paint" methods fake the addition and
+ * removal of rows, and make sure to either push existing data out
+ * of view, or draw new data into view. Only in some special cases
+ * will the DOM element count change.
+ *
+ * This method, however, has the explicit responsibility to verify
+ * that when "something" happens, we still have the correct amount
+ * of escalator rows in the DOM, and if not, we make sure to modify
+ * that count. Only in some special cases do we need to take into
+ * account other things than simply modifying the DOM element count.
+ */
+
+ Profiler.enter("Escalator.BodyRowContainer.verifyEscalatorCount");
+
+ if (!isAttached()) {
+ return;
+ }
+
+ final int maxEscalatorRows = getMaxEscalatorRowCapacity();
+ final int neededEscalatorRows = Math.min(maxEscalatorRows,
+ body.getRowCount());
+ final int neededEscalatorRowsDiff = neededEscalatorRows
+ - visualRowOrder.size();
+
+ if (neededEscalatorRowsDiff > 0) {
+ // needs more
+
+ /*
+ * This is a workaround for the issue where we might be scrolled
+ * to the bottom, and the widget expands beyond the content
+ * range
+ */
+
+ final int index = visualRowOrder.size();
+ final int nextLastLogicalIndex;
+ if (!visualRowOrder.isEmpty()) {
+ nextLastLogicalIndex = getLogicalRowIndex(visualRowOrder
+ .getLast()) + 1;
+ } else {
+ nextLastLogicalIndex = 0;
+ }
+
+ final boolean contentWillFit = nextLastLogicalIndex < getRowCount()
+ - neededEscalatorRowsDiff;
+ if (contentWillFit) {
+ final List<Element> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
+ index, neededEscalatorRowsDiff);
+
+ /*
+ * Since fillAndPopulateEscalatorRowsIfNeeded operates on
+ * the assumption that index == visual index == logical
+ * index, we thank for the added escalator rows, but since
+ * they're painted in the wrong CSS position, we need to
+ * move them to their actual locations.
+ *
+ * Note: this is the second (see body.paintInsertRows)
+ * occasion where fillAndPopulateEscalatorRowsIfNeeded would
+ * behave "more correctly" if it only would add escalator
+ * rows to the DOM and appropriate bookkeping, and not
+ * actually populate them :/
+ */
+ moveAndUpdateEscalatorRows(
+ Range.withLength(index, addedRows.size()), index,
+ nextLastLogicalIndex);
+ } else {
+ /*
+ * TODO [[optimize]]
+ *
+ * We're scrolled so far down that all rows can't be simply
+ * appended at the end, since we might start displaying
+ * escalator rows that don't exist. To avoid the mess that
+ * is body.paintRemoveRows, this is a dirty hack that dumbs
+ * the problem down to a more basic and already-solved
+ * problem:
+ *
+ * 1) scroll all the way up 2) add the missing escalator
+ * rows 3) scroll back to the original position.
+ *
+ * Letting the browser scroll back to our original position
+ * will automatically solve any possible overflow problems,
+ * since the browser will not allow us to scroll beyond the
+ * actual content.
+ */
+
+ final double oldScrollTop = getScrollTop();
+ setScrollTop(0);
+ scroller.onScroll();
+ fillAndPopulateEscalatorRowsIfNeeded(index,
+ neededEscalatorRowsDiff);
+ setScrollTop(oldScrollTop);
+ scroller.onScroll();
+ internalScrollEventCalls++;
+ }
+ }
+
+ else if (neededEscalatorRowsDiff < 0) {
+ // needs less
+
+ final ListIterator<Element> iter = visualRowOrder
+ .listIterator(visualRowOrder.size());
+ for (int i = 0; i < -neededEscalatorRowsDiff; i++) {
+ final Element last = iter.previous();
+ for (int c = 0; c < last.getChildCount(); c++) {
+ detachPossibleWidgetFromCell((Element) last.getChild(c)
+ .cast());
+ }
+ last.removeFromParent();
+ iter.remove();
+ }
+
+ /*
+ * If we were scrolled to the bottom so that we didn't have an
+ * extra escalator row at the bottom, we'll probably end up with
+ * blank space at the bottom of the escalator, and one extra row
+ * above the header.
+ *
+ * Experimentation idea #1: calculate "scrollbottom" vs content
+ * bottom and remove one row from top, rest from bottom. This
+ * FAILED, since setHeight has already happened, thus we never
+ * will detect ourselves having been scrolled all the way to the
+ * bottom.
+ */
+
+ if (!visualRowOrder.isEmpty()) {
+ final int firstRowTop = getRowTop(visualRowOrder.getFirst());
+ final double firstRowMinTop = tBodyScrollTop
+ - ROW_HEIGHT_PX;
+ if (firstRowTop < firstRowMinTop) {
+ final int newLogicalIndex = getLogicalRowIndex(visualRowOrder
+ .getLast()) + 1;
+ moveAndUpdateEscalatorRows(Range.withOnly(0),
+ visualRowOrder.size(), newLogicalIndex);
+ }
+ }
+ }
+
+ if (neededEscalatorRowsDiff != 0) {
+ fireRowVisibilityChangeEvent();
+ }
+
+ Profiler.leave("Escalator.BodyRowContainer.verifyEscalatorCount");
+ }
+ }
+
+ private class ColumnConfigurationImpl implements ColumnConfiguration {
+ public class Column {
+ private static final int DEFAULT_COLUMN_WIDTH_PX = 100;
+
+ private int definedWidth = -1;
+ private int calculatedWidth = DEFAULT_COLUMN_WIDTH_PX;
+
+ public void setWidth(int px) {
+ definedWidth = px;
+ calculatedWidth = (px >= 0) ? px : DEFAULT_COLUMN_WIDTH_PX;
+ }
+
+ public int getDefinedWidth() {
+ return definedWidth;
+ }
+
+ public int getCalculatedWidth() {
+ return calculatedWidth;
+ }
+ }
+
+ private final List<Column> columns = new ArrayList<Column>();
+ private int frozenColumns = 0;
+
+ /**
+ * A cached array of all the calculated column widths.
+ *
+ * @see #getCalculatedColumnWidths()
+ */
+ private int[] widthsArray = null;
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there are no rows in the DOM when
+ * this method is called.
+ *
+ * @see #hasSomethingInDom()
+ */
+ @Override
+ public void removeColumns(final int index, final int numberOfColumns) {
+ assertArgumentsAreValidAndWithinRange(index, numberOfColumns);
+
+ flyweightRow.removeCells(index, numberOfColumns);
+
+ // Cope with removing frozen columns
+ if (index < frozenColumns) {
+ if (index + numberOfColumns < frozenColumns) {
+ /*
+ * Last removed column was frozen, meaning that all removed
+ * columns were frozen. Just decrement the number of frozen
+ * columns accordingly.
+ */
+ frozenColumns -= numberOfColumns;
+ } else {
+ /*
+ * If last removed column was not frozen, we have removed
+ * columns beyond the frozen range, so all remaining frozen
+ * columns are to the left of the removed columns.
+ */
+ frozenColumns = index;
+ }
+ }
+
+ List<Column> removedColumns = new ArrayList<Column>();
+ for (int i = 0; i < numberOfColumns; i++) {
+ removedColumns.add(columns.remove(index));
+ }
+
+ if (hasSomethingInDom()) {
+ for (final AbstractRowContainer rowContainer : rowContainers) {
+ rowContainer.paintRemoveColumns(index, numberOfColumns,
+ removedColumns);
+ }
+ }
+ }
+
+ /**
+ * Calculate the width of a row, as the sum of columns' widths.
+ *
+ * @return the width of a row, in pixels
+ */
+ public int calculateRowWidth() {
+ return getCalculatedColumnsWidth(Range.between(0, getColumnCount()));
+ }
+
+ private void assertArgumentsAreValidAndWithinRange(final int index,
+ final int numberOfColumns) {
+ if (numberOfColumns < 1) {
+ throw new IllegalArgumentException(
+ "Number of columns can't be less than 1 (was "
+ + numberOfColumns + ")");
+ }
+
+ if (index < 0 || index + numberOfColumns > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given "
+ + "column range (" + index + ".."
+ + (index + numberOfColumns)
+ + ") was outside of the current "
+ + "number of columns (" + getColumnCount() + ")");
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Implementation detail:</em> This method does no DOM modifications
+ * (i.e. is very cheap to call) if there is no data for rows when this
+ * method is called.
+ *
+ * @see #hasColumnAndRowData()
+ */
+ @Override
+ public void insertColumns(final int index, final int numberOfColumns) {
+ if (index < 0 || index > getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given index(" + index
+ + ") was outside of the current number of columns (0.."
+ + getColumnCount() + ")");
+ }
+
+ if (numberOfColumns < 1) {
+ throw new IllegalArgumentException(
+ "Number of columns must be 1 or greater (was "
+ + numberOfColumns);
+ }
+
+ flyweightRow.addCells(index, numberOfColumns);
+
+ for (int i = 0; i < numberOfColumns; i++) {
+ columns.add(index, new Column());
+ }
+
+ // Either all or none of the new columns are frozen
+ boolean frozen = index < frozenColumns;
+ if (frozen) {
+ frozenColumns += numberOfColumns;
+ }
+
+ if (hasColumnAndRowData()) {
+ for (final AbstractRowContainer rowContainer : rowContainers) {
+ rowContainer.paintInsertColumns(index, numberOfColumns,
+ frozen);
+ }
+ }
+ }
+
+ @Override
+ public int getColumnCount() {
+ return columns.size();
+ }
+
+ @Override
+ public void setFrozenColumnCount(int count)
+ throws IllegalArgumentException {
+ if (count < 0 || count > getColumnCount()) {
+ throw new IllegalArgumentException(
+ "count must be between 0 and the current number of columns ("
+ + columns + ")");
+ }
+ int oldCount = frozenColumns;
+ if (count == oldCount) {
+ return;
+ }
+
+ frozenColumns = count;
+
+ if (hasSomethingInDom()) {
+ // Are we freezing or unfreezing?
+ boolean frozen = count > oldCount;
+
+ int firstAffectedCol;
+ int firstUnaffectedCol;
+
+ if (frozen) {
+ firstAffectedCol = oldCount;
+ firstUnaffectedCol = count;
+ } else {
+ firstAffectedCol = count;
+ firstUnaffectedCol = oldCount;
+ }
+
+ for (int col = firstAffectedCol; col < firstUnaffectedCol; col++) {
+ header.setColumnFrozen(col, frozen);
+ body.setColumnFrozen(col, frozen);
+ footer.setColumnFrozen(col, frozen);
+ }
+ }
+
+ scroller.recalculateScrollbarsForVirtualViewport();
+ }
+
+ @Override
+ public int getFrozenColumnCount() {
+ return frozenColumns;
+ }
+
+ @Override
+ public void setColumnWidth(int index, int px)
+ throws IllegalArgumentException {
+ checkValidColumnIndex(index);
+
+ columns.get(index).setWidth(px);
+ widthsArray = null;
+
+ /*
+ * TODO [[optimize]]: only modify the elements that are actually
+ * modified.
+ */
+ header.reapplyColumnWidths();
+ body.reapplyColumnWidths();
+ footer.reapplyColumnWidths();
+ recalculateElementSizes();
+ }
+
+ private void checkValidColumnIndex(int index)
+ throws IllegalArgumentException {
+ if (!Range.withLength(0, getColumnCount()).contains(index)) {
+ throw new IllegalArgumentException("The given column index ("
+ + index + ") does not exist");
+ }
+ }
+
+ @Override
+ public int getColumnWidth(int index) throws IllegalArgumentException {
+ checkValidColumnIndex(index);
+ return columns.get(index).getDefinedWidth();
+ }
+
+ @Override
+ public int getColumnWidthActual(int index) {
+ return columns.get(index).getCalculatedWidth();
+ }
+
+ /**
+ * Calculates the width of the columns in a given range.
+ *
+ * @param columns
+ * the columns to calculate
+ * @return the total width of the columns in the given
+ * <code>columns</code>
+ */
+ int getCalculatedColumnsWidth(@SuppressWarnings("hiding")
+ final Range columns) {
+ /*
+ * This is an assert instead of an exception, since this is an
+ * internal method.
+ */
+ assert columns.isSubsetOf(Range.between(0, getColumnCount())) : "Range "
+ + "was outside of current column range (i.e.: "
+ + Range.between(0, getColumnCount())
+ + ", but was given :"
+ + columns;
+
+ int sum = 0;
+ for (int i = columns.getStart(); i < columns.getEnd(); i++) {
+ sum += getColumnWidthActual(i);
+ }
+ return sum;
+ }
+
+ void setCalculatedColumnWidth(int index, int width) {
+ columns.get(index).calculatedWidth = width;
+ widthsArray = null;
+ }
+
+ int[] getCalculatedColumnWidths() {
+ if (widthsArray == null || widthsArray.length != getColumnCount()) {
+ widthsArray = new int[getColumnCount()];
+ for (int i = 0; i < columns.size(); i++) {
+ widthsArray[i] = columns.get(i).getCalculatedWidth();
+ }
+ }
+ return widthsArray;
+ }
+ }
+
+ // abs(atan(y/x))*(180/PI) = n deg, x = 1, solve y
+ /**
+ * The solution to
+ * <code>|tan<sup>-1</sup>(<i>x</i>)|&times;(180/&pi;)&nbsp;=&nbsp;30</code>
+ * .
+ * <p>
+ * This constant is placed in the Escalator class, instead of an inner
+ * class, since even mathematical expressions aren't allowed in non-static
+ * inner classes for constants.
+ */
+ private static final double RATIO_OF_30_DEGREES = 1 / Math.sqrt(3);
+ /**
+ * The solution to
+ * <code>|tan<sup>-1</sup>(<i>x</i>)|&times;(180/&pi;)&nbsp;=&nbsp;40</code>
+ * .
+ * <p>
+ * This constant is placed in the Escalator class, instead of an inner
+ * class, since even mathematical expressions aren't allowed in non-static
+ * inner classes for constants.
+ */
+ private static final double RATIO_OF_40_DEGREES = Math.tan(2 * Math.PI / 9);
+
+ private static final String DEFAULT_WIDTH = "400.0px";
+ private static final String DEFAULT_HEIGHT = "400.0px";
+
+ private FlyweightRow flyweightRow = new FlyweightRow(this);
+
+ /** The {@code <thead/>} tag. */
+ private final Element headElem = DOM.createTHead();
+ /** The {@code <tbody/>} tag. */
+ private final Element bodyElem = DOM.createTBody();
+ /** The {@code <tfoot/>} tag. */
+ private final Element footElem = DOM.createTFoot();
+
+ /**
+ * TODO: investigate whether this field is now unnecessary, as
+ * {@link ScrollbarBundle} now caches its values.
+ *
+ * @deprecated maybe...
+ */
+ @Deprecated
+ private int tBodyScrollTop = 0;
+
+ /**
+ * TODO: investigate whether this field is now unnecessary, as
+ * {@link ScrollbarBundle} now caches its values.
+ *
+ * @deprecated maybe...
+ */
+ @Deprecated
+ private int tBodyScrollLeft = 0;
+
+ private final VerticalScrollbarBundle verticalScrollbar = new VerticalScrollbarBundle();
+ private final HorizontalScrollbarBundle horizontalScrollbar = new HorizontalScrollbarBundle();
+
+ private final HeaderRowContainer header = new HeaderRowContainer(headElem);
+ private final BodyRowContainer body = new BodyRowContainer(bodyElem);
+ private final FooterRowContainer footer = new FooterRowContainer(footElem);
+
+ private final Scroller scroller = new Scroller();
+
+ private final AbstractRowContainer[] rowContainers = new AbstractRowContainer[] {
+ header, body, footer };
+
+ private final ColumnConfigurationImpl columnConfiguration = new ColumnConfigurationImpl();
+ private final Element tableWrapper;
+
+ private PositionFunction position;
+
+ private int internalScrollEventCalls = 0;
+
+ /** The cached width of the escalator, in pixels. */
+ private double width;
+ /** The cached height of the escalator, in pixels. */
+ private double height;
+
+ private static native double getPreciseWidth(Element element)
+ /*-{
+ if (element.getBoundingClientRect) {
+ var rect = element.getBoundingClientRect();
+ return rect.right - rect.left;
+ } else {
+ return element.offsetWidth;
+ }
+ }-*/;
+
+ private static native double getPreciseHeight(Element element)
+ /*-{
+ if (element.getBoundingClientRect) {
+ var rect = element.getBoundingClientRect();
+ return rect.bottom - rect.top;
+ } else {
+ return element.offsetHeight;
+ }
+ }-*/;
+
+ /**
+ * Creates a new Escalator widget instance.
+ */
+ public Escalator() {
+
+ detectAndApplyPositionFunction();
+ getLogger().info(
+ "Using " + position.getClass().getSimpleName()
+ + " for position");
+
+ final Element root = DOM.createDiv();
+ setElement(root);
+
+ root.appendChild(verticalScrollbar.getElement());
+ root.appendChild(horizontalScrollbar.getElement());
+ verticalScrollbar.setScrollbarThickness(Util.getNativeScrollbarSize());
+ horizontalScrollbar
+ .setScrollbarThickness(Util.getNativeScrollbarSize());
+
+ tableWrapper = DOM.createDiv();
+
+ root.appendChild(tableWrapper);
+
+ final Element table = DOM.createTable();
+ tableWrapper.appendChild(table);
+
+ table.appendChild(headElem);
+ table.appendChild(bodyElem);
+ table.appendChild(footElem);
+
+ setStylePrimaryName("v-escalator");
+
+ // init default dimensions
+ setHeight(null);
+ setWidth(null);
+ }
+
+ @Override
+ protected void onLoad() {
+ super.onLoad();
+
+ header.paintInsertRows(0, header.getRowCount());
+ footer.paintInsertRows(0, footer.getRowCount());
+ recalculateElementSizes();
+ /*
+ * Note: There's no need to explicitly insert rows into the body.
+ *
+ * recalculateElementSizes will recalculate the height of the body. This
+ * has the side-effect that as the body's size grows bigger (i.e. from 0
+ * to its actual height), more escalator rows are populated. Those
+ * escalator rows are then immediately rendered. This, in effect, is the
+ * same thing as inserting those rows.
+ *
+ * In fact, having an extra paintInsertRows here would lead to duplicate
+ * rows.
+ */
+
+ scroller.attachScrollListener(verticalScrollbar.getElement());
+ scroller.attachScrollListener(horizontalScrollbar.getElement());
+ scroller.attachMousewheelListener(getElement());
+ scroller.attachTouchListeners(getElement());
+ }
+
+ @Override
+ protected void onUnload() {
+
+ scroller.detachScrollListener(verticalScrollbar.getElement());
+ scroller.detachScrollListener(horizontalScrollbar.getElement());
+ scroller.detachMousewheelListener(getElement());
+ scroller.detachTouchListeners(getElement());
+
+ header.paintRemoveRows(0, header.getRowCount());
+ footer.paintRemoveRows(0, footer.getRowCount());
+ body.paintRemoveRows(0, body.getRowCount());
+
+ super.onUnload();
+ }
+
+ private void detectAndApplyPositionFunction() {
+ /*
+ * firefox has a bug in its translate operation, showing white space
+ * when adjusting the scrollbar in BodyRowContainer.paintInsertRows
+ */
+ if (Window.Navigator.getUserAgent().contains("Firefox")) {
+ position = new AbsolutePosition();
+ return;
+ }
+
+ final Style docStyle = Document.get().getBody().getStyle();
+ if (hasProperty(docStyle, "transform")) {
+ if (hasProperty(docStyle, "transformStyle")) {
+ position = new Translate3DPosition();
+ } else {
+ position = new TranslatePosition();
+ }
+ } else if (hasProperty(docStyle, "webkitTransform")) {
+ position = new WebkitTranslate3DPosition();
+ } else {
+ position = new AbsolutePosition();
+ }
+ }
+
+ private Logger getLogger() {
+ return Logger.getLogger(getClass().getName());
+ }
+
+ private static native boolean hasProperty(Style style, String name)
+ /*-{
+ return style[name] !== undefined;
+ }-*/;
+
+ /**
+ * Check whether there are both columns and any row data (for either
+ * headers, body or footer).
+ *
+ * @return <code>true</code> iff header, body or footer has rows && there
+ * are columns
+ */
+ private boolean hasColumnAndRowData() {
+ return (header.getRowCount() > 0 || body.getRowCount() > 0 || footer
+ .getRowCount() > 0) && columnConfiguration.getColumnCount() > 0;
+ }
+
+ /**
+ * Check whether there are any cells in the DOM.
+ *
+ * @return <code>true</code> iff header, body or footer has any child
+ * elements
+ */
+ private boolean hasSomethingInDom() {
+ return headElem.hasChildNodes() || bodyElem.hasChildNodes()
+ || footElem.hasChildNodes();
+ }
+
+ /**
+ * Returns the representation of this Escalator header.
+ *
+ * @return the header. Never <code>null</code>
+ */
+ public RowContainer getHeader() {
+ return header;
+ }
+
+ /**
+ * Returns the representation of this Escalator body.
+ *
+ * @return the body. Never <code>null</code>
+ */
+ public RowContainer getBody() {
+ return body;
+ }
+
+ /**
+ * Returns the representation of this Escalator footer.
+ *
+ * @return the footer. Never <code>null</code>
+ */
+ public RowContainer getFooter() {
+ return footer;
+ }
+
+ /**
+ * Returns the configuration object for the columns in this Escalator.
+ *
+ * @return the configuration object for the columns in this Escalator. Never
+ * <code>null</code>
+ */
+ public ColumnConfiguration getColumnConfiguration() {
+ return columnConfiguration;
+ }
+
+ /*
+ * TODO remove method once RequiresResize and the Vaadin layoutmanager
+ * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy)
+ */
+ @Override
+ public void setWidth(final String width) {
+ super.setWidth(width != null && !width.isEmpty() ? width
+ : DEFAULT_WIDTH);
+ recalculateElementSizes();
+ }
+
+ /*
+ * TODO remove method once RequiresResize and the Vaadin layoutmanager
+ * listening mechanisms are implemented (https://trello.com/c/r3Kh0Kfy)
+ */
+ @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();
+ }
+ }
+
+ /**
+ * Returns the vertical scroll offset. Note that this is not necessarily the
+ * same as the scroll top in the DOM
+ *
+ * @return the logical vertical scroll offset
+ */
+ public double getScrollTop() {
+ return verticalScrollbar.getScrollPos();
+ }
+
+ /**
+ * Sets the vertical scroll offset. Note that this is not necessarily the
+ * same as the scroll top in the DOM
+ *
+ * @param scrollTop
+ * the number of pixels to scroll vertically
+ */
+ public void setScrollTop(final double scrollTop) {
+ verticalScrollbar.setScrollPos((int) scrollTop);
+ }
+
+ /**
+ * Returns the logical horizontal scroll offset. Note that this is not
+ * necessarily the same as the scroll left in the DOM.
+ *
+ * @return the logical horizontal scroll offset
+ */
+ public int getScrollLeft() {
+ return horizontalScrollbar.getScrollPos();
+ }
+
+ /**
+ * Sets the logical horizontal scroll offset. Note that this is not
+ * necessarily the same as the scroll left in the DOM.
+ *
+ * @param scrollLeft
+ * the number of pixels to scroll horizontally
+ */
+ public void setScrollLeft(final int scrollLeft) {
+ horizontalScrollbar.setScrollPos(scrollLeft);
+ }
+
+ /**
+ * Scrolls the body horizontally so that the column at the given index is
+ * visible and there is at least {@code padding} pixels to the given scroll
+ * destination.
+ *
+ * @param columnIndex
+ * the index of the column to scroll to
+ * @param destination
+ * where the column should be aligned visually after scrolling
+ * @param padding
+ * the number pixels to place between the scrolled-to column and
+ * the viewport edge.
+ * @throws IndexOutOfBoundsException
+ * if {@code columnIndex} is not a valid index for an existing
+ * column
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE}
+ * and padding is nonzero, because having a padding on a
+ * centered column is undefined behavior, or if the column is
+ * frozen
+ */
+ public void scrollToColumn(final int columnIndex,
+ final ScrollDestination destination, final int padding)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ if (destination == ScrollDestination.MIDDLE && padding != 0) {
+ throw new IllegalArgumentException(
+ "You cannot have a padding with a MIDDLE destination");
+ }
+ verifyValidColumnIndex(columnIndex);
+
+ if (columnIndex < columnConfiguration.frozenColumns) {
+ throw new IllegalArgumentException("The given column index "
+ + columnIndex + " is frozen.");
+ }
+
+ scroller.scrollToColumn(columnIndex, destination, padding);
+ }
+
+ private void verifyValidColumnIndex(final int columnIndex)
+ throws IndexOutOfBoundsException {
+ if (columnIndex < 0
+ || columnIndex >= columnConfiguration.getColumnCount()) {
+ throw new IndexOutOfBoundsException("The given column index "
+ + columnIndex + " does not exist.");
+ }
+ }
+
+ /**
+ * Scrolls the body vertically so that the row at the given index is visible
+ * and there is at least {@literal padding} pixels to the given scroll
+ * destination.
+ *
+ * @param rowIndex
+ * the index of the logical row to scroll to
+ * @param destination
+ * where the row should be aligned visually after scrolling
+ * @param padding
+ * the number pixels to place between the scrolled-to row and the
+ * viewport edge.
+ * @throws IndexOutOfBoundsException
+ * if {@code rowIndex} is not a valid index for an existing row
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE}
+ * and padding is nonzero, because having a padding on a
+ * centered row is undefined behavior
+ */
+ public void scrollToRow(final int rowIndex,
+ final ScrollDestination destination, final int padding)
+ throws IndexOutOfBoundsException, IllegalArgumentException {
+ if (destination == ScrollDestination.MIDDLE && padding != 0) {
+ throw new IllegalArgumentException(
+ "You cannot have a padding with a MIDDLE destination");
+ }
+ verifyValidRowIndex(rowIndex);
+
+ scroller.scrollToRow(rowIndex, destination, padding);
+ }
+
+ private void verifyValidRowIndex(final int rowIndex) {
+ if (rowIndex < 0 || rowIndex >= body.getRowCount()) {
+ throw new IndexOutOfBoundsException("The given row index "
+ + rowIndex + " does not exist.");
+ }
+ }
+
+ /**
+ * Recalculates the dimensions for all elements that require manual
+ * calculations. Also updates the dimension caches.
+ * <p>
+ * <em>Note:</em> This method has the <strong>side-effect</strong>
+ * automatically makes sure that an appropriate amount of escalator rows are
+ * present. So, if the body area grows, more <strong>escalator rows might be
+ * inserted</strong>. Conversely, if the body area shrinks,
+ * <strong>escalator rows might be removed</strong>.
+ */
+ private void recalculateElementSizes() {
+ if (!isAttached()) {
+ return;
+ }
+
+ Profiler.enter("Escalator.recalculateElementSizes");
+ width = getPreciseWidth(getElement());
+ height = getPreciseHeight(getElement());
+ for (final AbstractRowContainer rowContainer : rowContainers) {
+ rowContainer.recalculateSectionHeight();
+ }
+
+ scroller.recalculateScrollbarsForVirtualViewport();
+ body.verifyEscalatorCount();
+ Profiler.leave("Escalator.recalculateElementSizes");
+ }
+
+ /**
+ * A routing method for {@link Scroller#onScroll(double, double)}.
+ * <p>
+ * This is a workaround for GWT and JSNI unable to properly handle inner
+ * classes, so instead we call the outer class' method, which calls the
+ * inner class' respective method.
+ * <p>
+ * Ideally, this method would not exist, and
+ * {@link Scroller#onScroll(double, double)} would be called directly.
+ */
+ private void onScroll() {
+ scroller.onScroll();
+ }
+
+ /**
+ * Snap deltas of x and y to the major four axes (up, down, left, right)
+ * with a threshold of a number of degrees from those axes.
+ *
+ * @param deltaX
+ * the delta in the x axis
+ * @param deltaY
+ * the delta in the y axis
+ * @param thresholdRatio
+ * the threshold in ratio (0..1) between x and y for when to snap
+ * @return a two-element array: <code>[snappedX, snappedY]</code>
+ */
+ private static double[] snapDeltas(final double deltaX,
+ final double deltaY, final double thresholdRatio) {
+
+ final double[] array = new double[2];
+ if (deltaX != 0 && deltaY != 0) {
+ final double aDeltaX = Math.abs(deltaX);
+ final double aDeltaY = Math.abs(deltaY);
+ final double yRatio = aDeltaY / aDeltaX;
+ final double xRatio = aDeltaX / aDeltaY;
+
+ array[0] = (xRatio < thresholdRatio) ? 0 : deltaX;
+ array[1] = (yRatio < thresholdRatio) ? 0 : deltaY;
+ } else {
+ array[0] = deltaX;
+ array[1] = deltaY;
+ }
+
+ return array;
+ }
+
+ /**
+ * Adds an event handler that gets notified when the range of visible rows
+ * changes e.g. because of scrolling.
+ *
+ * @param rowVisibilityChangeHandler
+ * the event handler
+ * @return a handler registration for the added handler
+ */
+ public HandlerRegistration addRowVisibilityChangeHandler(
+ RowVisibilityChangeHandler rowVisibilityChangeHandler) {
+ return addHandler(rowVisibilityChangeHandler,
+ RowVisibilityChangeEvent.TYPE);
+ }
+
+ private void fireRowVisibilityChangeEvent() {
+ if (!body.visualRowOrder.isEmpty()) {
+ int visibleRangeStart = body.getLogicalRowIndex(body.visualRowOrder
+ .getFirst());
+ int visibleRangeEnd = body.getLogicalRowIndex(body.visualRowOrder
+ .getLast()) + 1;
+
+ int visibleRowCount = visibleRangeEnd - visibleRangeStart;
+
+ fireEvent(new RowVisibilityChangeEvent(visibleRangeStart,
+ visibleRowCount));
+ } else {
+ fireEvent(new RowVisibilityChangeEvent(0, 0));
+ }
+ }
+
+ /**
+ * Accesses the package private method Widget#setParent()
+ *
+ * @param widget
+ * The widget to access
+ * @param parent
+ * The parent to set
+ */
+ static native final void setParent(Widget widget, Widget parent)
+ /*-{
+ widget.@com.google.gwt.user.client.ui.Widget::setParent(Lcom/google/gwt/user/client/ui/Widget;)(parent);
+ }-*/;
+
+ /**
+ * Returns the widget from a cell node or <code>null</code> if there is no
+ * widget in the cell
+ *
+ * @param cellNode
+ * The cell node
+ */
+ static Widget getWidgetFromCell(Node cellNode) {
+ Node possibleWidgetNode = cellNode.getFirstChild();
+ if (possibleWidgetNode != null
+ && possibleWidgetNode.getNodeType() == Node.ELEMENT_NODE) {
+ @SuppressWarnings("deprecation")
+ com.google.gwt.user.client.Element castElement = (com.google.gwt.user.client.Element) possibleWidgetNode
+ .cast();
+ return Util.findWidget(castElement, null);
+ }
+ return null;
+ }
+
+ /**
+ * Forces the escalator to recalculate the widths of its columns.
+ * <p>
+ * All columns that haven't been assigned an explicit width will be resized
+ * to fit all currently visible contents.
+ *
+ * @see ColumnConfiguration#setColumnWidth(int, int)
+ */
+ public void calculateColumnWidths() {
+ boolean widthsHaveChanged = false;
+ for (int colIndex = 0; colIndex < columnConfiguration.getColumnCount(); colIndex++) {
+ if (columnConfiguration.getColumnWidth(colIndex) >= 0) {
+ continue;
+ }
+
+ final int oldColumnWidth = columnConfiguration
+ .getColumnWidthActual(colIndex);
+
+ int maxColumnWidth = 0;
+ maxColumnWidth = Math.max(maxColumnWidth,
+ header.calculateMaxColWidth(colIndex));
+ maxColumnWidth = Math.max(maxColumnWidth,
+ body.calculateMaxColWidth(colIndex));
+ maxColumnWidth = Math.max(maxColumnWidth,
+ footer.calculateMaxColWidth(colIndex));
+
+ Logger.getLogger("Escalator.calculateColumnWidths").info(
+ "#" + colIndex + ": " + maxColumnWidth + "px");
+
+ if (oldColumnWidth != maxColumnWidth) {
+ columnConfiguration.setCalculatedColumnWidth(colIndex,
+ maxColumnWidth);
+ widthsHaveChanged = true;
+ }
+ }
+
+ if (widthsHaveChanged) {
+ header.reapplyColumnWidths();
+ body.reapplyColumnWidths();
+ footer.reapplyColumnWidths();
+ recalculateElementSizes();
+ }
+ }
+
+ @Override
+ public void setStylePrimaryName(String style) {
+ super.setStylePrimaryName(style);
+
+ verticalScrollbar.setStylePrimaryName(style);
+ horizontalScrollbar.setStylePrimaryName(style);
+
+ UIObject.setStylePrimaryName(tableWrapper, style + "-tablewrapper");
+
+ header.setStylePrimaryName(style);
+ body.setStylePrimaryName(style);
+ footer.setStylePrimaryName(style);
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java b/client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java
new file mode 100644
index 0000000000..283517fcc4
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java
@@ -0,0 +1,66 @@
+/*
+ * 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;
+
+
+/**
+ * A functional interface that allows client code to define how a certain row in
+ * Escalator will be displayed. The contents of an escalator's header, body and
+ * footer are rendered by their respective updaters.
+ * <p>
+ * The updater is responsible for internally handling all remote communication,
+ * should the displayed data need to be fetched remotely.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see RowContainer#setEscalatorUpdater(EscalatorUpdater)
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ */
+public interface EscalatorUpdater {
+ /** An {@link EscalatorUpdater} that doesn't render anything. */
+ public static final EscalatorUpdater NULL = new EscalatorUpdater() {
+ @Override
+ public void updateCells(final Row row,
+ final Iterable<Cell> cellsToUpdate) {
+ // NOOP
+ }
+ };
+
+ /**
+ * Renders a row contained in a row container.
+ * <p>
+ * <em>Note:</em> If rendering of cells is deferred (e.g. because
+ * asynchronous data retrieval), this method is responsible for explicitly
+ * displaying some placeholder data (empty content is valid). Because the
+ * cells (and rows) in an escalator are recycled, failing to reset a cell
+ * will lead to invalid data being displayed in the escalator.
+ * <p>
+ * For performance reasons, the escalator will never autonomously clear any
+ * data in a cell.
+ *
+ * @param row
+ * information about the row to update. <em>Note:</em> You should
+ * not store nor reuse this reference
+ * @param cellsToUpdate
+ * a collection of cells which need to be updated. <em>Note:</em>
+ * You should neither store nor reuse the reference to the list,
+ * nor to the individual cells
+ */
+ public void updateCells(Row row, Iterable<Cell> cellsToUpdate);
+}
diff --git a/client/src/com/vaadin/client/ui/grid/FlyweightCell.java b/client/src/com/vaadin/client/ui/grid/FlyweightCell.java
new file mode 100644
index 0000000000..296a70934b
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/FlyweightCell.java
@@ -0,0 +1,205 @@
+/*
+ * 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 java.util.List;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.ui.grid.FlyweightRow.CellIterator;
+
+/**
+ * An internal implementation of the {@link Cell} interface.
+ * <p>
+ * These instances are populated into a {@link FlyweightRow} instance, and
+ * intended to be reused when rendering cells in an escalator.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see FlyweightRow#getCells()
+ * @see FlyweightRow#addCells(int, int)
+ * @see FlyweightRow#removeCells(int, int)
+ */
+class FlyweightCell implements Cell {
+ static final String COLSPAN_ATTR = "colSpan";
+
+ private final int column;
+ private final FlyweightRow row;
+
+ private CellIterator currentIterator = null;
+
+ private final Escalator escalator;
+
+ public FlyweightCell(final FlyweightRow row, final int column,
+ Escalator escalator) {
+ this.row = row;
+ this.column = column;
+ this.escalator = escalator;
+ }
+
+ @Override
+ public int getRow() {
+ assertSetup();
+ return row.getRow();
+ }
+
+ @Override
+ public int getColumn() {
+ assertSetup();
+ return column;
+ }
+
+ @Override
+ public Element getElement() {
+ return (Element) row.getElement().getChild(column);
+ }
+
+ void setup(final CellIterator cellIterator) {
+ currentIterator = cellIterator;
+
+ final Element e = getElement();
+ e.setPropertyInt(COLSPAN_ATTR, 1);
+ e.getStyle().setWidth(row.getColumnWidth(column), Unit.PX);
+ e.getStyle().clearDisplay();
+ }
+
+ /**
+ * Tear down the state of the Cell.
+ * <p>
+ * This is an internal check method, to prevent retrieving uninitialized
+ * data by calling {@link #getRow()}, {@link #getColumn()} or
+ * {@link #getElement()} at an improper time.
+ * <p>
+ * This should only be used with asserts ("
+ * <code>assert flyweightCell.teardown()</code> ") so that the code is never
+ * run when asserts aren't enabled.
+ *
+ * @return always <code>true</code>
+ * @see FlyweightRow#teardown()
+ */
+ boolean teardown() {
+ currentIterator = null;
+ return true;
+ }
+
+ /**
+ * Asserts that the flyweight cell has properly been set up before trying to
+ * access any of its data.
+ */
+ private void assertSetup() {
+ assert currentIterator != null : "FlyweightCell was not properly "
+ + "initialized. This is either a bug in Grid/Escalator "
+ + "or a Cell reference has been stored and reused "
+ + "inappropriately.";
+ }
+
+ @Override
+ public void setColSpan(final int numberOfCells) {
+ /*-
+ * This will default to 1 if unset, as per DOM specifications:
+ * http://www.w3.org/TR/html5/tabular-data.html#attributes-common-to-td-and-th-elements
+ */
+ final int prevColSpan = getElement().getPropertyInt(COLSPAN_ATTR);
+ if (numberOfCells == 1 && prevColSpan == 1) {
+ return;
+ }
+
+ getElement().setPropertyInt(COLSPAN_ATTR, numberOfCells);
+ adjustCellWidthForSpan(numberOfCells);
+ hideOrRevealAdjacentCellElements(numberOfCells, prevColSpan);
+ currentIterator.setSkipNext(numberOfCells - 1);
+ }
+
+ private void adjustCellWidthForSpan(final int numberOfCells) {
+ final int cellsToTheRight = currentIterator.rawPeekNext(
+ numberOfCells - 1).size();
+
+ final int selfWidth = row.getColumnWidth(column);
+ int widthsOfColumnsToTheRight = 0;
+ for (int i = 0; i < cellsToTheRight; i++) {
+ widthsOfColumnsToTheRight += row.getColumnWidth(column + i + 1);
+ }
+ getElement().getStyle().setWidth(selfWidth + widthsOfColumnsToTheRight,
+ Unit.PX);
+ }
+
+ private void hideOrRevealAdjacentCellElements(final int numberOfCells,
+ final int prevColSpan) {
+ final int affectedCellsNumber = Math.max(prevColSpan, numberOfCells);
+ final List<FlyweightCell> affectedCells = currentIterator
+ .rawPeekNext(affectedCellsNumber - 1);
+ if (prevColSpan < numberOfCells) {
+ for (int i = 0; i < affectedCells.size(); i++) {
+ affectedCells.get(prevColSpan + i - 1).getElement().getStyle()
+ .setDisplay(Display.NONE);
+ }
+ } else if (prevColSpan > numberOfCells) {
+ for (int i = 0; i < affectedCells.size(); i++) {
+ affectedCells.get(numberOfCells + i - 1).getElement()
+ .getStyle().clearDisplay();
+ }
+ }
+ }
+
+ @Override
+ public Widget getWidget() {
+ return Escalator.getWidgetFromCell(getElement());
+ }
+
+ @Override
+ public void setWidget(Widget widget) {
+
+ Widget oldWidget = getWidget();
+
+ // Validate
+ if (oldWidget == widget) {
+ return;
+ }
+
+ // Detach old child.
+ if (oldWidget != null) {
+ // Orphan.
+ Escalator.setParent(oldWidget, null);
+
+ // Physical detach.
+ getElement().removeChild(oldWidget.getElement());
+ }
+
+ // Remove any previous text nodes from previous
+ // setInnerText/setInnerHTML
+ getElement().removeAllChildren();
+
+ // Attach new child.
+ if (widget != null) {
+ // Detach new child from old parent.
+ widget.removeFromParent();
+
+ // Physical attach.
+ getElement().appendChild(widget.getElement());
+
+ Escalator.setParent(widget, escalator);
+ }
+ }
+
+ @Override
+ public void setWidget(IsWidget w) {
+ setWidget(Widget.asWidgetOrNull(w));
+ }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/FlyweightRow.java b/client/src/com/vaadin/client/ui/grid/FlyweightRow.java
new file mode 100644
index 0000000000..6bfd368c6b
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/FlyweightRow.java
@@ -0,0 +1,217 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Node;
+
+/**
+ * An internal implementation of the {@link Row} interface.
+ * <p>
+ * There is only one instance per Escalator. This is designed to be re-used when
+ * rendering rows.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see Escalator.AbstractRowContainer#refreshRow(Node, int)
+ */
+class FlyweightRow implements Row {
+
+ static class CellIterator implements Iterator<Cell> {
+ /** A defensive copy of the cells in the current row. */
+ private final ArrayList<FlyweightCell> cells;
+ private int cursor = 0;
+ private int skipNext = 0;
+
+ public CellIterator(final Collection<FlyweightCell> cells) {
+ this.cells = new ArrayList<FlyweightCell>(cells);
+ }
+
+ @Override
+ public boolean hasNext() {
+ return cursor + skipNext < cells.size();
+ }
+
+ @Override
+ public FlyweightCell next() {
+ // if we needed to skip some cells since the last invocation.
+ for (int i = 0; i < skipNext; i++) {
+ cells.remove(cursor);
+ }
+ skipNext = 0;
+
+ final FlyweightCell cell = cells.get(cursor++);
+ cell.setup(this);
+ return cell;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException(
+ "Cannot remove cells via iterator");
+ }
+
+ /**
+ * Sets the number of cells to skip when {@link #next()} is called the
+ * next time. Cell hiding is also handled eagerly in this method.
+ *
+ * @param colspan
+ * the number of cells to skip on next invocation of
+ * {@link #next()}
+ */
+ public void setSkipNext(final int colspan) {
+ assert colspan > 0 : "Number of cells didn't make sense: "
+ + colspan;
+ skipNext = colspan;
+ }
+
+ /**
+ * Gets the next <code>n</code> cells in the iterator, ignoring any
+ * possibly spanned cells.
+ *
+ * @param n
+ * the number of next cells to retrieve
+ * @return A list of next <code>n</code> cells, or less if there aren't
+ * enough cells to retrieve
+ */
+ public List<FlyweightCell> rawPeekNext(final int n) {
+ final int from = Math.min(cursor, cells.size());
+ final int to = Math.min(cursor + n, cells.size());
+ return cells.subList(from, to);
+ }
+ }
+
+ private static final int BLANK = Integer.MIN_VALUE;
+
+ private int row;
+ private Element element;
+ private int[] columnWidths = null;
+ private final Escalator escalator;
+ private final List<FlyweightCell> cells = new ArrayList<FlyweightCell>();
+
+ public FlyweightRow(final Escalator escalator) {
+ this.escalator = escalator;
+ }
+
+ @Override
+ public Escalator getEscalator() {
+ return escalator;
+ }
+
+ void setup(final Element e, final int row, int[] columnWidths) {
+ element = e;
+ this.row = row;
+ this.columnWidths = columnWidths;
+ }
+
+ /**
+ * Tear down the state of the Row.
+ * <p>
+ * This is an internal check method, to prevent retrieving uninitialized
+ * data by calling {@link #getRow()}, {@link #getElement()} or
+ * {@link #getCells()} at an improper time.
+ * <p>
+ * This should only be used with asserts ("
+ * <code>assert flyweightRow.teardown()</code> ") so that the code is never
+ * run when asserts aren't enabled.
+ *
+ * @return always <code>true</code>
+ */
+ boolean teardown() {
+ element = null;
+ row = BLANK;
+ columnWidths = null;
+ for (final FlyweightCell cell : cells) {
+ assert cell.teardown();
+ }
+ return true;
+ }
+
+ @Override
+ public int getRow() {
+ assertSetup();
+ return row;
+ }
+
+ @Override
+ public Element getElement() {
+ assertSetup();
+ return element;
+ }
+
+ void addCells(final int index, final int numberOfColumns) {
+ for (int i = 0; i < numberOfColumns; i++) {
+ final int col = index + i;
+ cells.add(col, new FlyweightCell(this, col, escalator));
+ }
+ updateRestOfCells(index + numberOfColumns);
+ }
+
+ void removeCells(final int index, final int numberOfColumns) {
+ for (int i = 0; i < numberOfColumns; i++) {
+ cells.remove(index);
+ }
+ updateRestOfCells(index);
+ }
+
+ private void updateRestOfCells(final int startPos) {
+ // update the column number for the cells to the right
+ for (int col = startPos; col < cells.size(); col++) {
+ cells.set(col, new FlyweightCell(this, col, escalator));
+ }
+ }
+
+ /**
+ * Get flyweight cells for the client code to render.
+ *
+ * @return a list of {@link FlyweightCell FlyweightCells}. They are
+ * generified into {@link Cell Cells}, because Java's generics
+ * system isn't expressive enough.
+ * @see #setup(Element, int)
+ * @see #teardown()
+ */
+ Iterable<Cell> getCells() {
+ assertSetup();
+ return new Iterable<Cell>() {
+ @Override
+ public Iterator<Cell> iterator() {
+ return new CellIterator(cells);
+ }
+ };
+ }
+
+ /**
+ * Asserts that the flyweight row has properly been set up before trying to
+ * access any of its data.
+ */
+ private void assertSetup() {
+ assert element != null && row != BLANK && columnWidths != null : "Flyweight row was not "
+ + "properly initialized. Make sure the setup-method is "
+ + "called before retrieving data. This is either a bug "
+ + "in Escalator, or the instance of the flyweight row "
+ + "has been stored and accessed.";
+ }
+
+ int getColumnWidth(int column) {
+ assertSetup();
+ return columnWidths[column];
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java
new file mode 100644
index 0000000000..02aa194655
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Grid.java
@@ -0,0 +1,1318 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+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;
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.ui.grid.renderers.TextRenderer;
+import com.vaadin.shared.ui.grid.GridConstants;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.util.SharedUtil;
+
+/**
+ * A data grid view that supports columns and lazy loading of data rows from a
+ * data source.
+ *
+ * <h3>Columns</h3>
+ * <p>
+ * The {@link GridColumn} class defines the renderer used to render a cell in
+ * the grid. Implement {@link GridColumn#getValue(Object)} to retrieve the cell
+ * value from the row object and return the cell renderer to render that cell.
+ * </p>
+ * <p>
+ * {@link GridColumn}s contain other properties like the width of the column and
+ * the visiblity of the column. If you want to change a column's properties
+ * after it has been added to the grid you can get a column object for a
+ * specific column index using {@link Grid#getColumn(int)}.
+ * </p>
+ * <p>
+ *
+ * TODO Explain about headers/footers once the multiple header/footer api has
+ * been implemented
+ *
+ * <h3>Data sources</h3>
+ * <p>
+ * TODO Explain about what a data source is and how it should be implemented.
+ * </p>
+ *
+ * @param <T>
+ * The row type of the grid. The row type is the POJO type from where
+ * the data is retrieved into the column cells.
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class Grid<T> extends Composite {
+
+ /**
+ * Escalator used internally by grid to render the rows
+ */
+ private Escalator escalator = GWT.create(Escalator.class);
+
+ /**
+ * List of columns in the grid. Order defines the visible order.
+ */
+ 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;
+
+ /**
+ * The column groups rows added to the grid
+ */
+ private final List<ColumnGroupRow<T>> columnGroupRows = new ArrayList<ColumnGroupRow<T>>();
+
+ /**
+ * Are the headers for the columns visible
+ */
+ private boolean columnHeadersVisible = true;
+
+ /**
+ * Are the footers for the columns visible
+ */
+ private boolean columnFootersVisible = false;
+
+ /**
+ * The last column frozen counter from the left
+ */
+ private GridColumn<?, T> lastFrozenColumn;
+
+ /**
+ * Base class for grid columns internally used by the Grid. The user should
+ * use {@link GridColumn} when creating new columns.
+ *
+ * @param <C>
+ * the column type
+ *
+ * @param <T>
+ * the row type
+ */
+ static abstract class AbstractGridColumn<C, T> implements HasVisibility {
+
+ /**
+ * The grid the column is associated with
+ */
+ private Grid<T> grid;
+
+ /**
+ * Should the column be visible in the grid
+ */
+ private boolean visible = true;
+
+ /**
+ * The text displayed in the header of the column
+ */
+ private String header;
+
+ /**
+ * Text displayed in the column footer
+ */
+ private String footer;
+
+ /**
+ * Width of column in pixels
+ */
+ private int width = 100;
+
+ /**
+ * Renderer for rendering a value into the cell
+ */
+ private Renderer<C> bodyRenderer = new Renderer<C>() {
+
+ @Override
+ public void renderCell(Cell cell, C value) {
+ if (value instanceof Widget) {
+ cell.setWidget((Widget) value);
+ } else if (value instanceof String) {
+ cell.getElement().setInnerText(value.toString());
+ } else {
+ throw new IllegalArgumentException(
+ "Cell value cannot be converted into a String. Please use a custom renderer to convert the value.");
+ }
+ }
+ };
+
+ /**
+ * Renderer for rendering the header cell value into the cell
+ */
+ private Renderer<String> headerRenderer = new TextRenderer();
+
+ /**
+ * Renderer for rendering the footer cell value into the cell
+ */
+ private Renderer<String> footerRenderer = new TextRenderer();
+
+ /**
+ * Constructs a new column.
+ */
+ public AbstractGridColumn() {
+
+ }
+
+ /**
+ * Constructs a new column with a custom renderer.
+ *
+ * @param renderer
+ * The renderer to use for rendering the cells
+ */
+ public AbstractGridColumn(Renderer<C> renderer) {
+ if (renderer == null) {
+ throw new IllegalArgumentException("Renderer cannot be null.");
+ }
+ this.bodyRenderer = renderer;
+ }
+
+ /**
+ * Constructs a new column with custom renderers for rows, header and
+ * footer cells.
+ *
+ * @param bodyRenderer
+ * The renderer to use for rendering body cells
+ * @param headerRenderer
+ * The renderer to use for rendering header cells
+ * @param footerRenderer
+ * The renderer to use for rendering footer cells
+ */
+ public AbstractGridColumn(Renderer<C> bodyRenderer,
+ Renderer<String> headerRenderer, Renderer<String> footerRenderer) {
+ this(bodyRenderer);
+ if (headerRenderer == null || footerRenderer == null) {
+ throw new IllegalArgumentException("Renderer cannot be null.");
+ }
+
+ this.headerRenderer = headerRenderer;
+ this.footerRenderer = footerRenderer;
+ }
+
+ /**
+ * Internally used by the grid to set itself
+ *
+ * @param grid
+ */
+ private void setGrid(Grid<T> grid) {
+ if (this.grid != null && grid != null) {
+ // Trying to replace grid
+ throw new IllegalStateException(
+ "Column already is attached to grid. Remove the column first from the grid and then add it.");
+ }
+
+ this.grid = grid;
+
+ setVisible(this.visible);
+ setWidth(this.width);
+ setHeaderCaption(this.header);
+ setFooterCaption(this.footer);
+ }
+
+ /**
+ * Gets text in the header of the column. By default the header caption
+ * is empty.
+ *
+ * @return the text displayed in the column caption
+ */
+ public String getHeaderCaption() {
+ return header;
+ }
+
+ /**
+ * Returns the renderer used for rendering the header cells
+ *
+ * @return a renderer that renders header cells
+ */
+ public Renderer<String> getHeaderRenderer() {
+ return headerRenderer;
+ }
+
+ /**
+ * Sets the renderer that renders header cells. Should not be null.
+ *
+ * @param renderer
+ * The renderer to use for rendering header cells.
+ */
+ public void setHeaderRenderer(Renderer<String> renderer) {
+ if (renderer == null) {
+ throw new IllegalArgumentException("Renderer cannot be null.");
+ }
+ headerRenderer = renderer;
+ if (grid != null) {
+ grid.refreshHeader();
+ }
+ }
+
+ /**
+ * Returns the renderer used for rendering the footer cells
+ *
+ * @return a renderer that renders footer cells
+ */
+ public Renderer<String> getFooterRenderer() {
+ return footerRenderer;
+ }
+
+ /**
+ * Sets the renderer that renders footer cells. Should not be null.
+ *
+ * @param renderer
+ * The renderer to use for rendering footer cells.
+ */
+ public void setFooterRenderer(Renderer<String> renderer) {
+ if (renderer == null) {
+ throw new IllegalArgumentException("Renderer cannot be null.");
+ }
+ footerRenderer = renderer;
+ if (grid != null) {
+ grid.refreshFooter();
+ }
+ }
+
+ /**
+ * Sets the text in the header of the column.
+ *
+ * @param caption
+ * the text displayed in the column header
+ */
+ public void setHeaderCaption(String caption) {
+ if (SharedUtil.equals(caption, header)) {
+ return;
+ }
+
+ header = caption;
+
+ if (grid != null) {
+ grid.refreshHeader();
+ }
+ }
+
+ /**
+ * Gets text in the footer of the column. By default the footer caption
+ * is empty.
+ *
+ * @return The text displayed in the footer of the column
+ */
+ public String getFooterCaption() {
+ return footer;
+ }
+
+ /**
+ * Sets text in the footer of the column.
+ *
+ * @param caption
+ * the text displayed in the footer of the column
+ */
+ public void setFooterCaption(String caption) {
+ if (SharedUtil.equals(caption, footer)) {
+ return;
+ }
+
+ footer = caption;
+
+ if (grid != null) {
+ grid.refreshFooter();
+ }
+ }
+
+ /**
+ * Is the column visible. By default all columns are visible.
+ *
+ * @return <code>true</code> if the column is visible
+ */
+ @Override
+ public boolean isVisible() {
+ return visible;
+ }
+
+ /**
+ * Sets a column as visible in the grid.
+ *
+ * @param visible
+ * <code>true</code> if the column should be displayed in the
+ * grid
+ */
+ @Override
+ public void setVisible(boolean visible) {
+ if (this.visible == visible) {
+ return;
+ }
+
+ this.visible = visible;
+
+ // Remove column
+ if (grid != null) {
+ int index = findIndexOfColumn();
+ ColumnConfiguration conf = grid.escalator
+ .getColumnConfiguration();
+
+ if (visible) {
+ conf.insertColumns(index, 1);
+ } else {
+ conf.removeColumns(index, 1);
+ }
+ }
+
+ }
+
+ /**
+ * Returns the data that should be rendered into the cell. By default
+ * returning Strings and Widgets are supported. If the return type is a
+ * String then it will be treated as preformatted text.
+ * <p>
+ * To support other types you will need to pass a custom renderer to the
+ * column via the column constructor.
+ *
+ * @param row
+ * The row object that provides the cell content.
+ *
+ * @return The cell content
+ */
+ public abstract C getValue(T row);
+
+ /**
+ * The renderer to render the cell width. By default renders the data as
+ * a String or adds the widget into the cell if the column type is of
+ * widget type.
+ *
+ * @return The renderer to render the cell content with
+ */
+ public Renderer<C> getRenderer() {
+ return bodyRenderer;
+ }
+
+ /**
+ * Finds the index of this column instance
+ *
+ */
+ private int findIndexOfColumn() {
+ return grid.findVisibleColumnIndex((GridColumn<?, T>) this);
+ }
+
+ /**
+ * Sets the pixel width of the column. Use a negative value for the grid
+ * to autosize column based on content and available space
+ *
+ * @param pixels
+ * the width in pixels or negative for auto sizing
+ */
+ public void setWidth(int pixels) {
+ this.width = pixels;
+
+ if (grid != null && isVisible()) {
+ int index = findIndexOfColumn();
+ ColumnConfiguration conf = grid.escalator
+ .getColumnConfiguration();
+ conf.setColumnWidth(index, pixels);
+ }
+ }
+
+ /**
+ * Returns the pixel width of the column
+ *
+ * @return pixel width of the column
+ */
+ public int getWidth() {
+ if (grid == null) {
+ return this.width;
+ } else {
+ int index = findIndexOfColumn();
+ ColumnConfiguration conf = grid.escalator
+ .getColumnConfiguration();
+ return conf.getColumnWidth(index);
+ }
+ }
+ }
+
+ /**
+ * Base class for header / footer escalator updater
+ */
+ protected abstract class HeaderFooterEscalatorUpdater implements
+ EscalatorUpdater {
+
+ /**
+ * The row container which contains the header or footer rows
+ */
+ private RowContainer rows;
+
+ /**
+ * Should the index be counted from 0-> or 0<-
+ */
+ private boolean inverted;
+
+ /**
+ * Constructs an updater for updating a header / footer
+ *
+ * @param rows
+ * The row container
+ * @param inverted
+ * Should index counting be inverted
+ */
+ public HeaderFooterEscalatorUpdater(RowContainer rows, boolean inverted) {
+ this.rows = rows;
+ this.inverted = inverted;
+ }
+
+ /**
+ * Gets the header/footer caption value
+ *
+ * @param column
+ * The column to get the value for.
+ *
+ * @return The value that should be rendered for the column caption
+ */
+ public abstract String getColumnValue(GridColumn<?, T> column);
+
+ /**
+ * Gets the group caption value
+ *
+ * @param group
+ * The group for with the caption value should be returned
+ * @return The value that should be rendered for the column caption
+ */
+ public abstract String getGroupValue(ColumnGroup<T> group);
+
+ /**
+ * Is the row visible in the header/footer
+ *
+ * @param row
+ * the row to check
+ *
+ * @return <code>true</code> if the row should be visible
+ */
+ public abstract boolean isRowVisible(ColumnGroupRow<T> row);
+
+ /**
+ * Should the first row be visible
+ *
+ * @return <code>true</code> if the first row should be visible
+ */
+ public abstract boolean firstRowIsVisible();
+
+ /**
+ * The renderer that renders the cell
+ *
+ * @param column
+ * The column for which the cell should be rendered
+ *
+ * @return renderer used for rendering
+ */
+ public abstract Renderer<String> getRenderer(GridColumn<?, T> column);
+
+ /**
+ * The renderer that renders the cell for column groups
+ *
+ * @param group
+ * The group that should be rendered
+ * @return renderer used for rendering
+ */
+ public abstract Renderer<String> getGroupRenderer(ColumnGroup<T> group);
+
+ @Override
+ public void updateCells(Row row, Iterable<Cell> cellsToUpdate) {
+
+ int rowIndex;
+ if (inverted) {
+ rowIndex = rows.getRowCount() - row.getRow() - 1;
+ } else {
+ rowIndex = row.getRow();
+ }
+
+ if (firstRowIsVisible() && rowIndex == 0) {
+ // column headers
+ for (Cell cell : cellsToUpdate) {
+ GridColumn<?, T> column = getColumnFromVisibleIndex(cell
+ .getColumn());
+ if (column != null) {
+ getRenderer(column).renderCell(cell,
+ getColumnValue(column));
+ }
+ }
+
+ } else if (columnGroupRows.size() > 0) {
+ // Adjust for headers
+ if (firstRowIsVisible()) {
+ rowIndex--;
+ }
+
+ // Adjust for previous invisible header rows
+ ColumnGroupRow<T> groupRow = null;
+ for (int i = 0, realIndex = 0; i < columnGroupRows.size(); i++) {
+ groupRow = columnGroupRows.get(i);
+ if (isRowVisible(groupRow)) {
+ if (realIndex == rowIndex) {
+ rowIndex = realIndex;
+ break;
+ }
+ realIndex++;
+ }
+ }
+
+ assert groupRow != null;
+
+ for (Cell cell : cellsToUpdate) {
+ GridColumn<?, T> column = getColumnFromVisibleIndex(cell
+ .getColumn());
+ ColumnGroup<T> group = getGroupForColumn(groupRow, column);
+ Element cellElement = cell.getElement();
+
+ if (group != null) {
+ getGroupRenderer(group).renderCell(cell,
+ getGroupValue(group));
+ cell.setColSpan(group.getColumns().size());
+ } else {
+ // Cells are reused
+ cellElement.setInnerHTML(null);
+ cell.setColSpan(1);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Creates a new instance.
+ */
+ public Grid() {
+ initWidget(escalator);
+
+ setStylePrimaryName("v-grid");
+
+ escalator.getHeader().setEscalatorUpdater(createHeaderUpdater());
+ escalator.getBody().setEscalatorUpdater(createBodyUpdater());
+ escalator.getFooter().setEscalatorUpdater(createFooterUpdater());
+
+ refreshHeader();
+ refreshFooter();
+
+ escalator
+ .addRowVisibilityChangeHandler(new RowVisibilityChangeHandler() {
+ @Override
+ public void onRowVisibilityChange(
+ RowVisibilityChangeEvent event) {
+ if (dataSource != null) {
+ dataSource.ensureAvailability(
+ event.getFirstVisibleRow(),
+ event.getVisibleRowCount());
+ }
+ }
+ });
+
+ }
+
+ @Override
+ public void setStylePrimaryName(String style) {
+ super.setStylePrimaryName(style);
+ escalator.setStylePrimaryName(style);
+
+ }
+
+ /**
+ * Creates the header updater that updates the escalator header rows from
+ * the column and column group rows.
+ *
+ * @return the updater that updates the data in the escalator.
+ */
+ private EscalatorUpdater createHeaderUpdater() {
+ return new HeaderFooterEscalatorUpdater(escalator.getHeader(), true) {
+
+ @Override
+ public boolean isRowVisible(ColumnGroupRow<T> row) {
+ return row.isHeaderVisible();
+ }
+
+ @Override
+ public String getGroupValue(ColumnGroup<T> group) {
+ return group.getHeaderCaption();
+ }
+
+ @Override
+ public String getColumnValue(GridColumn<?, T> column) {
+ return column.getHeaderCaption();
+ }
+
+ @Override
+ public boolean firstRowIsVisible() {
+ return isColumnHeadersVisible();
+ }
+
+ @Override
+ public Renderer<String> getRenderer(GridColumn<?, T> column) {
+ return column.getHeaderRenderer();
+ }
+
+ @Override
+ public Renderer<String> getGroupRenderer(ColumnGroup<T> group) {
+ return group.getHeaderRenderer();
+ }
+ };
+ }
+
+ private EscalatorUpdater createBodyUpdater() {
+ return new EscalatorUpdater() {
+
+ @Override
+ public void updateCells(Row row, Iterable<Cell> cellsToUpdate) {
+ int rowIndex = row.getRow();
+ if (dataSource == null) {
+ setCellsLoading(cellsToUpdate);
+ return;
+ }
+
+ T rowData = dataSource.getRow(rowIndex);
+ if (rowData == null) {
+ setCellsLoading(cellsToUpdate);
+ return;
+ }
+
+ for (Cell cell : cellsToUpdate) {
+ GridColumn column = getColumnFromVisibleIndex(cell
+ .getColumn());
+ if (column != null) {
+ Object value = column.getValue(rowData);
+ column.getRenderer().renderCell(cell, value);
+ }
+ }
+ }
+
+ private void setCellsLoading(Iterable<Cell> cellsToUpdate) {
+ for (Cell cell : cellsToUpdate) {
+ cell.getElement().setInnerText("...");
+ }
+ }
+ };
+ }
+
+ /**
+ * Creates the footer updater that updates the escalator footer rows from
+ * the column and column group rows.
+ *
+ * @return the updater that updates the data in the escalator.
+ */
+ private EscalatorUpdater createFooterUpdater() {
+ return new HeaderFooterEscalatorUpdater(escalator.getFooter(), false) {
+
+ @Override
+ public boolean isRowVisible(ColumnGroupRow<T> row) {
+ return row.isFooterVisible();
+ }
+
+ @Override
+ public String getGroupValue(ColumnGroup<T> group) {
+ return group.getFooterCaption();
+ }
+
+ @Override
+ public String getColumnValue(GridColumn<?, T> column) {
+ return column.getFooterCaption();
+ }
+
+ @Override
+ public boolean firstRowIsVisible() {
+ return isColumnFootersVisible();
+ }
+
+ @Override
+ public Renderer<String> getRenderer(GridColumn<?, T> column) {
+ return column.getFooterRenderer();
+ }
+
+ @Override
+ public Renderer<String> getGroupRenderer(ColumnGroup<T> group) {
+ return group.getFooterRenderer();
+ }
+ };
+ }
+
+ /**
+ * Refreshes header or footer rows on demand
+ *
+ * @param rows
+ * The row container
+ * @param firstRowIsVisible
+ * is the first row visible
+ * @param isHeader
+ * <code>true</code> if we refreshing the header, else assumed
+ * the footer
+ */
+ private void refreshRowContainer(RowContainer rows,
+ boolean firstRowIsVisible, boolean isHeader) {
+
+ // Count needed rows
+ int totalRows = firstRowIsVisible ? 1 : 0;
+ for (ColumnGroupRow<T> 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 header rows
+ */
+ void refreshHeader() {
+ refreshRowContainer(escalator.getHeader(), isColumnHeadersVisible(),
+ true);
+ }
+
+ /**
+ * Refreshes all footer rows
+ */
+ void refreshFooter() {
+ refreshRowContainer(escalator.getFooter(), isColumnFootersVisible(),
+ false);
+ }
+
+ /**
+ * Adds a column as the last column in the grid.
+ *
+ * @param column
+ * the column to add
+ */
+ public void addColumn(GridColumn<?, T> column) {
+ ColumnConfiguration conf = escalator.getColumnConfiguration();
+ addColumn(column, conf.getColumnCount());
+ }
+
+ /**
+ * Inserts a column into a specific position in the grid.
+ *
+ * @param index
+ * the index where the column should be inserted into
+ * @param column
+ * the column to add
+ */
+ public void addColumn(GridColumn<?, T> column, int index) {
+
+ // Register column with grid
+ columns.add(index, column);
+
+ // Insert column into escalator
+ if (column.isVisible()) {
+ int visibleIndex = findVisibleColumnIndex(column);
+ ColumnConfiguration conf = escalator.getColumnConfiguration();
+ conf.insertColumns(visibleIndex, 1);
+ }
+
+ // Register this grid instance with the column
+ ((AbstractGridColumn<?, T>) column).setGrid(this);
+
+ if (lastFrozenColumn != null
+ && ((AbstractGridColumn<?, T>) lastFrozenColumn)
+ .findIndexOfColumn() < index) {
+ refreshFrozenColumns();
+ }
+ }
+
+ private int findVisibleColumnIndex(GridColumn<?, T> column) {
+ int idx = 0;
+ for (GridColumn<?, T> c : columns) {
+ if (c == column) {
+ return idx;
+ } else if (c.isVisible()) {
+ idx++;
+ }
+ }
+ return -1;
+ }
+
+ private GridColumn<?, T> getColumnFromVisibleIndex(int index) {
+ int idx = -1;
+ for (GridColumn<?, T> c : columns) {
+ if (c.isVisible()) {
+ idx++;
+ }
+ if (index == idx) {
+ return c;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Removes a column from the grid.
+ *
+ * @param column
+ * the column to remove
+ */
+ public void removeColumn(GridColumn<?, T> column) {
+
+ int columnIndex = columns.indexOf(column);
+ int visibleIndex = findVisibleColumnIndex(column);
+ columns.remove(columnIndex);
+
+ // de-register column with grid
+ ((AbstractGridColumn<?, T>) column).setGrid(null);
+
+ if (column.isVisible()) {
+ ColumnConfiguration conf = escalator.getColumnConfiguration();
+ conf.removeColumns(visibleIndex, 1);
+ }
+
+ if (column.equals(lastFrozenColumn)) {
+ setLastFrozenColumn(null);
+ } else {
+ refreshFrozenColumns();
+ }
+ }
+
+ /**
+ * Returns the amount of columns in the grid.
+ *
+ * @return The number of columns in the grid
+ */
+ public int getColumnCount() {
+ return columns.size();
+ }
+
+ /**
+ * Returns a list of columns in the grid.
+ *
+ * @return A unmodifiable list of the columns in the grid
+ */
+ public List<GridColumn<?, T>> getColumns() {
+ return Collections.unmodifiableList(new ArrayList<GridColumn<?, T>>(
+ columns));
+ }
+
+ /**
+ * Returns a column by its index in the grid.
+ *
+ * @param index
+ * the index of the column
+ * @return The column in the given index
+ * @throws IllegalArgumentException
+ * if the column index does not exist in the grid
+ */
+ public GridColumn<?, T> getColumn(int index)
+ throws IllegalArgumentException {
+ if (index < 0 || index >= columns.size()) {
+ throw new IllegalStateException("Column not found.");
+ }
+ return columns.get(index);
+ }
+
+ /**
+ * Set the column headers visible.
+ *
+ * <p>
+ * 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.
+ * </p>
+ *
+ * <p>
+ * 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 <code>false</code>.
+ * </p>
+ *
+ * <p>
+ * 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.
+ * </p>
+ *
+ * <p>
+ * The header row is by default visible.
+ * </p>
+ *
+ * @param visible
+ * <code>true</code> if header rows should be visible
+ */
+ public void setColumnHeadersVisible(boolean visible) {
+ if (visible == isColumnHeadersVisible()) {
+ return;
+ }
+ columnHeadersVisible = visible;
+ refreshHeader();
+ }
+
+ /**
+ * Are the column headers visible
+ *
+ * @return <code>true</code> if they are visible
+ */
+ public boolean isColumnHeadersVisible() {
+ return columnHeadersVisible;
+ }
+
+ /**
+ * Set the column footers visible.
+ *
+ * <p>
+ * 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.
+ * </p>
+ *
+ * <p>
+ * 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 <code>false</code>.
+ * </p>
+ *
+ * <p>
+ * 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.
+ * </p>
+ *
+ * <p>
+ * The footer row is by default hidden.
+ * </p>
+ *
+ * @param visible
+ * <code>true</code> if the footer row should be visible
+ */
+ public void setColumnFootersVisible(boolean visible) {
+ if (visible == isColumnFootersVisible()) {
+ return;
+ }
+ this.columnFootersVisible = visible;
+ refreshFooter();
+ }
+
+ /**
+ * Are the column footers visible
+ *
+ * @return <code>true</code> if they are visible
+ *
+ */
+ public boolean isColumnFootersVisible() {
+ return columnFootersVisible;
+ }
+
+ /**
+ * Adds a new column group row to the grid.
+ *
+ * <p>
+ * 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.
+ * </p>
+ *
+ * Example usage:
+ *
+ * <pre>
+ * // Add a new column group row to the grid
+ * ColumnGroupRow row = grid.addColumnGroupRow();
+ *
+ * // Group &quot;Column1&quot; and &quot;Column2&quot; together to form a header in the row
+ * ColumnGroup column12 = row.addGroup(&quot;Column1&quot;, &quot;Column2&quot;);
+ *
+ * // Set a common header for &quot;Column1&quot; and &quot;Column2&quot;
+ * column12.setHeader(&quot;Column 1&amp;2&quot;);
+ *
+ * // Set a common footer for &quot;Column1&quot; and &quot;Column2&quot;
+ * column12.setFooter(&quot;Column 1&amp;2&quot;);
+ * </pre>
+ *
+ * @return a column group row instance you can use to add column groups
+ */
+ public ColumnGroupRow<T> addColumnGroupRow() {
+ ColumnGroupRow<T> row = new ColumnGroupRow<T>(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<T> addColumnGroupRow(int rowIndex) {
+ ColumnGroupRow<T> row = new ColumnGroupRow<T>(this);
+ columnGroupRows.add(rowIndex, row);
+ refreshHeader();
+ refreshFooter();
+ return row;
+ }
+
+ /**
+ * Removes a column group row
+ *
+ * @param row
+ * The row to remove
+ */
+ public void removeColumnGroupRow(ColumnGroupRow<T> row) {
+ columnGroupRows.remove(row);
+ refreshHeader();
+ refreshFooter();
+ }
+
+ /**
+ * Get the column group rows
+ *
+ * @return a unmodifiable list of column group rows
+ *
+ */
+ public List<ColumnGroupRow<T>> getColumnGroupRows() {
+ return Collections.unmodifiableList(new ArrayList<ColumnGroupRow<T>>(
+ columnGroupRows));
+ }
+
+ /**
+ * Returns the column group for a row and column
+ *
+ * @param row
+ * The row of the column
+ * @param column
+ * the column to get the group for
+ * @return A column group for the row and column or <code>null</code> if not
+ * found.
+ */
+ private ColumnGroup<T> getGroupForColumn(ColumnGroupRow<T> row,
+ GridColumn<?, T> column) {
+ for (ColumnGroup<T> group : row.getGroups()) {
+ List<GridColumn<?, T>> columns = group.getColumns();
+ if (columns.contains(column)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void setHeight(String height) {
+ escalator.setHeight(height);
+ }
+
+ @Override
+ public void setWidth(String width) {
+ escalator.setWidth(width);
+ }
+
+ /**
+ * Sets the data source used by this grid.
+ *
+ * @param dataSource
+ * the data source to use, not null
+ * @throws IllegalArgumentException
+ * if <code>dataSource</code> is <code>null</code>
+ */
+ public void setDataSource(DataSource<T> dataSource)
+ throws IllegalArgumentException {
+ if (dataSource == null) {
+ throw new IllegalArgumentException("dataSource can't be null.");
+ }
+
+ if (this.dataSource != null) {
+ this.dataSource.setDataChangeHandler(null);
+ }
+
+ this.dataSource = dataSource;
+ dataSource.setDataChangeHandler(new DataChangeHandler() {
+ @Override
+ public void dataUpdated(int firstIndex, int numberOfItems) {
+ escalator.getBody().refreshRows(firstIndex, numberOfItems);
+ }
+
+ @Override
+ public void dataRemoved(int firstIndex, int numberOfItems) {
+ escalator.getBody().removeRows(firstIndex, numberOfItems);
+ }
+
+ @Override
+ public void dataAdded(int firstIndex, int numberOfItems) {
+ escalator.getBody().insertRows(firstIndex, numberOfItems);
+ }
+ });
+
+ int previousRowCount = escalator.getBody().getRowCount();
+ if (previousRowCount != 0) {
+ escalator.getBody().removeRows(0, previousRowCount);
+ }
+
+ int estimatedSize = dataSource.getEstimatedSize();
+ if (estimatedSize > 0) {
+ escalator.getBody().insertRows(0, estimatedSize);
+ }
+ }
+
+ /**
+ * Sets the rightmost frozen column in the grid.
+ * <p>
+ * 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 <code>null</code> to not
+ * have any columns frozen
+ * @throws IllegalArgumentException
+ * if {@code lastFrozenColumn} is not a column from this grid
+ */
+ public void setLastFrozenColumn(GridColumn<?, T> lastFrozenColumn) {
+ this.lastFrozenColumn = lastFrozenColumn;
+ refreshFrozenColumns();
+ }
+
+ private void refreshFrozenColumns() {
+ final int frozenCount;
+ if (lastFrozenColumn != null) {
+ frozenCount = columns.indexOf(lastFrozenColumn) + 1;
+ if (frozenCount == 0) {
+ throw new IllegalArgumentException(
+ "The given column isn't attached to this grid");
+ }
+ } else {
+ frozenCount = 0;
+ }
+
+ escalator.getColumnConfiguration().setFrozenColumnCount(frozenCount);
+ }
+
+ /**
+ * Gets the rightmost frozen column in the grid.
+ * <p>
+ * <em>Note:</em> Most usually, this method returns the very value set with
+ * {@link #setLastFrozenColumn(GridColumn)}. This value, however, can be
+ * reset to <code>null</code> if the column is removed from this grid.
+ *
+ * @return the rightmost frozen column in the grid, or <code>null</code> if
+ * no columns are frozen.
+ */
+ 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);
+ }
+
+ /**
+ * Scrolls to a certain row, using {@link ScrollDestination#ANY}.
+ *
+ * @param rowIndex
+ * zero-based index of the row to scroll to.
+ * @throws IllegalArgumentException
+ * if rowIndex is below zero, or above the maximum value
+ * supported by the data source.
+ */
+ public void scrollToRow(int rowIndex) throws IllegalArgumentException {
+ scrollToRow(rowIndex, ScrollDestination.ANY,
+ GridConstants.DEFAULT_PADDING);
+ }
+
+ /**
+ * Scrolls to a certain row, using user-specified scroll destination.
+ *
+ * @param rowIndex
+ * zero-based index of the row to scroll to.
+ * @param destination
+ * desired destination placement of scrolled-to-row. See
+ * {@link ScrollDestination} for more information.
+ * @throws IllegalArgumentException
+ * if rowIndex is below zero, or above the maximum value
+ * supported by the data source.
+ */
+ public void scrollToRow(int rowIndex, ScrollDestination destination)
+ throws IllegalArgumentException {
+ scrollToRow(rowIndex, destination,
+ destination == ScrollDestination.MIDDLE ? 0
+ : GridConstants.DEFAULT_PADDING);
+ }
+
+ /**
+ * Scrolls to a certain row using only user-specified parameters.
+ *
+ * @param rowIndex
+ * zero-based index of the row to scroll to.
+ * @param destination
+ * desired destination placement of scrolled-to-row. See
+ * {@link ScrollDestination} for more information.
+ * @param paddingPx
+ * number of pixels to overscroll. Behavior depends on
+ * destination.
+ * @throws IllegalArgumentException
+ * if {@code destination} is {@link ScrollDestination#MIDDLE}
+ * and padding is nonzero, because having a padding on a
+ * centered row is undefined behavior, or if rowIndex is below
+ * zero or above the row count of the data source.
+ */
+ private void scrollToRow(int rowIndex, ScrollDestination destination,
+ int paddingPx) throws IllegalArgumentException {
+ int maxsize = escalator.getBody().getRowCount() - 1;
+
+ if (rowIndex < 0) {
+ throw new IllegalArgumentException("Row index (" + rowIndex
+ + ") is below zero!");
+ }
+
+ if (rowIndex > maxsize) {
+ throw new IllegalArgumentException("Row index (" + rowIndex
+ + ") is above maximum (" + maxsize + ")!");
+ }
+
+ escalator.scrollToRow(rowIndex, destination, paddingPx);
+ }
+
+ /**
+ * Scrolls to the beginning of the very first row.
+ */
+ public void scrollToStart() {
+ scrollToRow(0, ScrollDestination.START);
+ }
+
+ /**
+ * Scrolls to the end of the very last row.
+ */
+ public void scrollToEnd() {
+ scrollToRow(escalator.getBody().getRowCount() - 1,
+ ScrollDestination.END);
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(Grid.class.getName());
+ }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/GridColumn.java b/client/src/com/vaadin/client/ui/grid/GridColumn.java
new file mode 100644
index 0000000000..afd80a5f5a
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/GridColumn.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+/**
+ * Represents a column in the {@link Grid}.
+ *
+ * @param <C>
+ * The column type
+ *
+ * @param <T>
+ * The row type
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public abstract class GridColumn<C, T> extends Grid.AbstractGridColumn<C, T> {
+
+ /*
+ * This class is a convenience class so you do not have to reference
+ * Grid.AbstractGridColumn in your production code. The real implementation
+ * should be in the abstract class.
+ */
+
+ /**
+ * Constructs a new column.
+ */
+ public GridColumn() {
+ super();
+ }
+
+ /**
+ * Constructs a new column with a custom renderer.
+ *
+ * @param renderer
+ * The renderer to use for rendering the cells
+ */
+ public GridColumn(Renderer<C> renderer) {
+ super(renderer);
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java
new file mode 100644
index 0000000000..5e0664667d
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java
@@ -0,0 +1,277 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.ui.AbstractComponentConnector;
+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.GridClientRpc;
+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.ScrollDestination;
+
+/**
+ * Connects the client side {@link Grid} widget with the server side
+ * {@link com.vaadin.ui.components.grid.Grid} component.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.components.grid.Grid.class)
+public class GridConnector extends AbstractComponentConnector {
+
+ /**
+ * Custom implementation of the custom grid column using a String[] to
+ * represent the cell value and String as a column type.
+ */
+ private class CustomGridColumn extends GridColumn<String, String[]> {
+
+ private final int columnIndex;
+
+ public CustomGridColumn(int columnIndex) {
+ this.columnIndex = columnIndex;
+ }
+
+ @Override
+ public String getValue(String[] obj) {
+ return obj[columnIndex];
+ }
+ }
+
+ /**
+ * Maps a generated column id to a grid column instance
+ */
+ private Map<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>();
+
+ @Override
+ protected Grid<String[]> createWidget() {
+ // FIXME Shouldn't be needed after #12873 has been fixed.
+ return new Grid<String[]>();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Grid<String[]> getWidget() {
+ return (Grid<String[]>) super.getWidget();
+ }
+
+ @Override
+ public GridState getState() {
+ 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());
+ }
+ });
+
+ registerRpc(GridClientRpc.class, new GridClientRpc() {
+ @Override
+ public void scrollToStart() {
+ getWidget().scrollToStart();
+ }
+
+ @Override
+ public void scrollToEnd() {
+ getWidget().scrollToEnd();
+ }
+
+ @Override
+ public void scrollToRow(int row, ScrollDestination destination) {
+ getWidget().scrollToRow(row, destination);
+ }
+ });
+ }
+
+ @Override
+ public void onStateChanged(StateChangeEvent stateChangeEvent) {
+ super.onStateChanged(stateChangeEvent);
+
+ // Column updates
+ if (stateChangeEvent.hasPropertyChanged("columns")) {
+
+ int totalColumns = getState().columns.size();
+
+ // Remove old columns
+ purgeRemovedColumns();
+
+ int currentColumns = getWidget().getColumnCount();
+
+ // Add new columns
+ for (int columnIndex = currentColumns; columnIndex < totalColumns; columnIndex++) {
+ addColumnFromStateChangeEvent(columnIndex);
+ }
+
+ // Update old columns
+ for (int columnIndex = 0; columnIndex < currentColumns; columnIndex++) {
+ // FIXME Currently updating all column header / footers when a
+ // change in made in one column. When the framework supports
+ // quering a specific item in a list then it should do so here.
+ updateColumnFromStateChangeEvent(columnIndex);
+ }
+ }
+
+ // Header
+ if (stateChangeEvent.hasPropertyChanged("columnHeadersVisible")) {
+ getWidget()
+ .setColumnHeadersVisible(getState().columnHeadersVisible);
+ }
+
+ // Footer
+ if (stateChangeEvent.hasPropertyChanged("columnFootersVisible")) {
+ getWidget()
+ .setColumnFootersVisible(getState().columnFootersVisible);
+ }
+
+ // Column row groups
+ if (stateChangeEvent.hasPropertyChanged("columnGroupRows")) {
+ updateColumnGroupsFromStateChangeEvent();
+ }
+
+ if (stateChangeEvent.hasPropertyChanged("lastFrozenColumnId")) {
+ String frozenColId = getState().lastFrozenColumnId;
+ if (frozenColId != null) {
+ CustomGridColumn column = columnIdToColumn.get(frozenColId);
+ assert column != null : "Column to be frozen could not be found (id:"
+ + frozenColId + ")";
+ getWidget().setLastFrozenColumn(column);
+ } else {
+ getWidget().setLastFrozenColumn(null);
+ }
+ }
+ }
+
+ /**
+ * Updates a column from a state change event.
+ *
+ * @param columnIndex
+ * The index of the column to update
+ */
+ private void updateColumnFromStateChangeEvent(int columnIndex) {
+ GridColumn<?, String[]> column = getWidget().getColumn(columnIndex);
+ GridColumnState columnState = getState().columns.get(columnIndex);
+ updateColumnFromState(column, columnState);
+ }
+
+ /**
+ * Adds a new column to the grid widget from a state change event
+ *
+ * @param columnIndex
+ * The index of the column, according to how it
+ */
+ private void addColumnFromStateChangeEvent(int columnIndex) {
+ GridColumnState state = getState().columns.get(columnIndex);
+ CustomGridColumn column = new CustomGridColumn(columnIndex);
+ updateColumnFromState(column, state);
+
+ columnIdToColumn.put(state.id, column);
+
+ getWidget().addColumn(column, columnIndex);
+ }
+
+ /**
+ * Updates the column values from a state
+ *
+ * @param column
+ * The column to update
+ * @param state
+ * The state to get the data from
+ */
+ private static void updateColumnFromState(GridColumn<?, String[]> column,
+ GridColumnState state) {
+ column.setVisible(state.visible);
+ column.setHeaderCaption(state.header);
+ column.setFooterCaption(state.footer);
+ column.setWidth(state.width);
+ }
+
+ /**
+ * Removes any orphan columns that has been removed from the state from the
+ * grid
+ */
+ private void purgeRemovedColumns() {
+
+ // Get columns still registered in the state
+ Set<String> columnsInState = new HashSet<String>();
+ for (GridColumnState columnState : getState().columns) {
+ columnsInState.add(columnState.id);
+ }
+
+ // Remove column no longer in state
+ Iterator<String> columnIdIterator = columnIdToColumn.keySet()
+ .iterator();
+ while (columnIdIterator.hasNext()) {
+ String id = columnIdIterator.next();
+ if (!columnsInState.contains(id)) {
+ CustomGridColumn column = columnIdToColumn.get(id);
+ columnIdIterator.remove();
+ getWidget().removeColumn(column);
+ }
+ }
+ }
+
+ /**
+ * Updates the column groups from a state change
+ */
+ private void updateColumnGroupsFromStateChangeEvent() {
+
+ // FIXME When something changes the header/footer rows will be
+ // re-created. At some point we should optimize this so partial updates
+ // can be made on the header/footer.
+ for (ColumnGroupRow<String[]> row : getWidget().getColumnGroupRows()) {
+ getWidget().removeColumnGroupRow(row);
+ }
+
+ for (ColumnGroupRowState rowState : getState().columnGroupRows) {
+ ColumnGroupRow<String[]> row = getWidget().addColumnGroupRow();
+ row.setFooterVisible(rowState.footerVisible);
+ row.setHeaderVisible(rowState.headerVisible);
+
+ for (ColumnGroupState groupState : rowState.groups) {
+ List<GridColumn<String, String[]>> columns = new ArrayList<GridColumn<String, String[]>>();
+ for (String columnId : groupState.columns) {
+ CustomGridColumn column = columnIdToColumn.get(columnId);
+ columns.add(column);
+ }
+ ColumnGroup<String[]> group = row.addGroup(columns
+ .toArray(new GridColumn[columns.size()]));
+ group.setFooterCaption(groupState.footer);
+ group.setHeaderCaption(groupState.header);
+ }
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/PositionFunction.java b/client/src/com/vaadin/client/ui/grid/PositionFunction.java
new file mode 100644
index 0000000000..e41e533996
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/PositionFunction.java
@@ -0,0 +1,118 @@
+/*
+ * 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 com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Unit;
+
+/**
+ * A functional interface that can be used for positioning elements in the DOM.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+interface PositionFunction {
+ /**
+ * A position function using "transform: translate3d(x,y,z)" to position
+ * elements in the DOM.
+ */
+ public static class Translate3DPosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("transform",
+ "translate3d(" + x + "px, " + y + "px, 0)");
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearProperty("transform");
+ }
+ }
+
+ /**
+ * A position function using "transform: translate(x,y)" to position
+ * elements in the DOM.
+ */
+ public static class TranslatePosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("transform",
+ "translate(" + x + "px," + y + "px)");
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearProperty("transform");
+ }
+ }
+
+ /**
+ * A position function using "-webkit-transform: translate3d(x,y,z)" to
+ * position elements in the DOM.
+ */
+ public static class WebkitTranslate3DPosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setProperty("webkitTransform",
+ "translate3d(" + x + "px," + y + "px,0)");
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearProperty("webkitTransform");
+ }
+ }
+
+ /**
+ * A position function using "left: x" and "top: y" to position elements in
+ * the DOM.
+ */
+ public static class AbsolutePosition implements PositionFunction {
+ @Override
+ public void set(Element e, double x, double y) {
+ e.getStyle().setLeft(x, Unit.PX);
+ e.getStyle().setTop(y, Unit.PX);
+ }
+
+ @Override
+ public void reset(Element e) {
+ e.getStyle().clearLeft();
+ e.getStyle().clearTop();
+ }
+ }
+
+ /**
+ * Position an element in an (x,y) coordinate system in the DOM.
+ *
+ * @param e
+ * the element to position. Never <code>null</code>.
+ * @param x
+ * the x coordinate, in pixels
+ * @param y
+ * the y coordinate, in pixels
+ */
+ void set(Element e, double x, double y);
+
+ /**
+ * Resets any previously applied positioning, clearing the used style
+ * attributes.
+ *
+ * @param e
+ * the element for which to reset the positioning
+ */
+ void reset(Element e);
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/Renderer.java b/client/src/com/vaadin/client/ui/grid/Renderer.java
new file mode 100644
index 0000000000..3312ec87e4
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Renderer.java
@@ -0,0 +1,43 @@
+/*
+ * 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;
+
+/**
+ * Renderer for rending a value &lt;T&gt; into cell.
+ * <p>
+ * You can add a renderer to any column by overring the
+ * {@link GridColumn#getRenderer()} method and returning your own renderer. You
+ * can retrieve the cell element using {@link Cell#getElement()}.
+ *
+ * @param <T>
+ * The column type
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface Renderer<T> {
+
+ /**
+ * Called whenever the {@link Grid} updates a cell
+ *
+ * @param cell
+ * The cell that gets updated
+ *
+ * @param data
+ * The column data object
+ */
+ public void renderCell(Cell cell, T data);
+}
diff --git a/client/src/com/vaadin/client/ui/grid/Row.java b/client/src/com/vaadin/client/ui/grid/Row.java
new file mode 100644
index 0000000000..209da58fec
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Row.java
@@ -0,0 +1,55 @@
+/*
+ * 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 com.google.gwt.dom.client.Element;
+
+/**
+ * A representation of a row in an {@link Escalator}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface Row {
+ /**
+ * Gets the escalator containing the row.
+ *
+ * @return the escalator containing the row
+ */
+ public Escalator getEscalator();
+
+ /**
+ * Gets the row index.
+ *
+ * @return the row index
+ */
+ public int getRow();
+
+ /**
+ * Gets the root element for this row.
+ * <p>
+ * The {@link EscalatorUpdater} may update the class names of the element
+ * and add inline styles, but may not modify the contained DOM structure.
+ * <p>
+ * If you wish to modify the cells within this row element, access them via
+ * the <code>List&lt;{@link Cell}&gt;</code> objects passed in to
+ * {@code EscalatorUpdater.updateCells(Row, List)}
+ *
+ * @return the root element of the row
+ */
+ public Element getElement();
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/RowContainer.java b/client/src/com/vaadin/client/ui/grid/RowContainer.java
new file mode 100644
index 0000000000..6ef1c913b3
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/RowContainer.java
@@ -0,0 +1,126 @@
+/*
+ * 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;
+
+/**
+ * A representation of the rows in each of the sections (header, body and
+ * footer) in an {@link Escalator}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ */
+public interface RowContainer {
+ /**
+ * Returns the current {@link EscalatorUpdater} used to render cells.
+ *
+ * @return the current escalator updater
+ */
+ public EscalatorUpdater getEscalatorUpdater();
+
+ /**
+ * Sets the {@link EscalatorUpdater} to use when displaying data in the
+ * escalator.
+ *
+ * @param escalatorUpdater
+ * the escalator updater to use to render cells. May not be
+ * <code>null</code>
+ * @throws IllegalArgumentException
+ * if {@code cellRenderer} is <code>null</code>
+ * @see EscalatorUpdater#NULL
+ */
+ public void setEscalatorUpdater(EscalatorUpdater escalatorUpdater)
+ throws IllegalArgumentException;
+
+ /**
+ * Removes rows at a certain index in the current row container.
+ *
+ * @param index
+ * the index of the first row to be removed
+ * @param numberOfRows
+ * the number of rows to remove, starting from the index
+ * @throws IndexOutOfBoundsException
+ * if any integer number in the range
+ * <code>[index..(index+numberOfRows)]</code> is not an existing
+ * row index
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void removeRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Adds rows at a certain index in this row container.
+ * <p>
+ * The new rows will be inserted between the row at the index, and the row
+ * before (an index of 0 means that the rows are inserted at the beginning).
+ * Therefore, the rows currently at the index and afterwards will be moved
+ * downwards.
+ * <p>
+ * The contents of the inserted rows will subsequently be queried from the
+ * escalator updater.
+ * <p>
+ * <em>Note:</em> Only the contents of the inserted rows will be rendered.
+ * If inserting new rows affects the contents of existing rows,
+ * {@link #refreshRows(int, int)} needs to be called for those rows
+ * separately.
+ *
+ * @param index
+ * the index of the row before which new rows are inserted, or
+ * {@link #getRowCount()} to add rows at the end
+ * @param numberOfRows
+ * the number of rows to insert after the <code>index</code>
+ * @see #setEscalatorUpdater(EscalatorUpdater)
+ * @throws IndexOutOfBoundsException
+ * if <code>index</code> is not an integer in the range
+ * <code>[0..{@link #getRowCount()}]</code>
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void insertRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Refreshes a range of rows in the current row container.
+ * <p>
+ * The data for the refreshed rows are queried from the current cell
+ * renderer.
+ *
+ * @param index
+ * the index of the first row that will be updated
+ * @param numberOfRows
+ * the number of rows to update, starting from the index
+ * @see #setEscalatorUpdater(EscalatorUpdater)
+ * @throws IndexOutOfBoundsException
+ * if any integer number in the range
+ * <code>[index..(index+numberOfColumns)]</code> is not an
+ * existing column index.
+ * @throws IllegalArgumentException
+ * if {@code numberOfRows} is less than 1.
+ */
+ public void refreshRows(int index, int numberOfRows)
+ throws IndexOutOfBoundsException, IllegalArgumentException;
+
+ /**
+ * Gets the number of rows in the current row container.
+ *
+ * @return the number of rows in the current row container
+ */
+ public int getRowCount();
+}
diff --git a/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java
new file mode 100644
index 0000000000..0e9652e215
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java
@@ -0,0 +1,90 @@
+/*
+ * 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 com.google.gwt.event.shared.GwtEvent;
+
+/**
+ * Event fired when the range of visible rows changes e.g. because of scrolling.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class RowVisibilityChangeEvent extends
+ GwtEvent<RowVisibilityChangeHandler> {
+ /**
+ * The type of this event.
+ */
+ public static final Type<RowVisibilityChangeHandler> TYPE = new Type<RowVisibilityChangeHandler>();
+
+ private final int firstVisibleRow;
+ private final int visibleRowCount;
+
+ /**
+ * Creates a new row visibility change event
+ *
+ * @param firstVisibleRow
+ * the index of the first visible row
+ * @param visibleRowCount
+ * the number of visible rows
+ */
+ public RowVisibilityChangeEvent(int firstVisibleRow, int visibleRowCount) {
+ this.firstVisibleRow = firstVisibleRow;
+ this.visibleRowCount = visibleRowCount;
+ }
+
+ /**
+ * Gets the index of the first row that is at least partially visible.
+ *
+ * @return the index of the first visible row
+ */
+ public int getFirstVisibleRow() {
+ return firstVisibleRow;
+ }
+
+ /**
+ * Gets the number of at least partially visible rows.
+ *
+ * @return the number of visible rows
+ */
+ public int getVisibleRowCount() {
+ return visibleRowCount;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.event.shared.GwtEvent#getAssociatedType()
+ */
+ @Override
+ public Type<RowVisibilityChangeHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.shared.GwtEvent#dispatch(com.google.gwt.event.shared
+ * .EventHandler)
+ */
+ @Override
+ protected void dispatch(RowVisibilityChangeHandler handler) {
+ handler.onRowVisibilityChange(this);
+ }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java
new file mode 100644
index 0000000000..dd24521499
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Event handler that gets notified when the range of visible rows changes e.g.
+ * because of scrolling.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface RowVisibilityChangeHandler extends EventHandler {
+
+ /**
+ * Called when the range of visible rows changes e.g. because of scrolling.
+ *
+ * @param event
+ * the row visibility change event describing the change
+ */
+ void onRowVisibilityChange(RowVisibilityChangeEvent event);
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java b/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java
new file mode 100644
index 0000000000..b9267178c1
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java
@@ -0,0 +1,403 @@
+/*
+ * 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 com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.DOM;
+
+/**
+ * An element-like bundle representing a configurable and visual scrollbar in
+ * one axis.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see VerticalScrollbarBundle
+ * @see HorizontalScrollbarBundle
+ */
+abstract class ScrollbarBundle {
+
+ /**
+ * The pixel size for OSX's invisible scrollbars.
+ * <p>
+ * Touch devices don't show a scrollbar at all, so the scrollbar size is
+ * irrelevant in their case. There doesn't seem to be any other popular
+ * platforms that has scrollbars similar to OSX. Thus, this behavior is
+ * tailored for OSX only, until additional platforms start behaving this
+ * way.
+ */
+ private static final int OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX = 13;
+
+ /**
+ * A representation of a single vertical scrollbar.
+ *
+ * @see VerticalScrollbarBundle#getElement()
+ */
+ final static class VerticalScrollbarBundle extends ScrollbarBundle {
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ root.addClassName(primaryStyleName + "-scroller-vertical");
+ }
+
+ @Override
+ protected void internalSetScrollPos(int px) {
+ root.setScrollTop(px);
+ }
+
+ @Override
+ protected int internalGetScrollPos() {
+ return root.getScrollTop();
+ }
+
+ @Override
+ protected void internalSetScrollSize(int px) {
+ scrollSizeElement.getStyle().setHeight(px, Unit.PX);
+ }
+
+ @Override
+ public int getScrollSize() {
+ return scrollSizeElement.getOffsetHeight();
+ }
+
+ @Override
+ protected void internalSetOffsetSize(int px) {
+ root.getStyle().setHeight(px, Unit.PX);
+ }
+
+ @Override
+ public int getOffsetSize() {
+ return root.getOffsetHeight();
+ }
+
+ @Override
+ protected void internalSetScrollbarThickness(int px) {
+ root.getStyle().setWidth(px, Unit.PX);
+ scrollSizeElement.getStyle().setWidth(px, Unit.PX);
+ }
+
+ @Override
+ protected int internalGetScrollbarThickness() {
+ return root.getOffsetWidth();
+ }
+
+ @Override
+ protected void forceScrollbar(boolean enable) {
+ if (enable) {
+ root.getStyle().setOverflowY(Overflow.SCROLL);
+ } else {
+ root.getStyle().clearOverflowY();
+ }
+ }
+ }
+
+ /**
+ * A representation of a single horizontal scrollbar.
+ *
+ * @see HorizontalScrollbarBundle#getElement()
+ */
+ final static class HorizontalScrollbarBundle extends ScrollbarBundle {
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ root.addClassName(primaryStyleName + "-scroller-horizontal");
+ }
+
+ @Override
+ protected void internalSetScrollPos(int px) {
+ root.setScrollLeft(px);
+ }
+
+ @Override
+ protected int internalGetScrollPos() {
+ return root.getScrollLeft();
+ }
+
+ @Override
+ protected void internalSetScrollSize(int px) {
+ scrollSizeElement.getStyle().setWidth(px, Unit.PX);
+ }
+
+ @Override
+ public int getScrollSize() {
+ return scrollSizeElement.getOffsetWidth();
+ }
+
+ @Override
+ protected void internalSetOffsetSize(int px) {
+ root.getStyle().setWidth(px, Unit.PX);
+ }
+
+ @Override
+ public int getOffsetSize() {
+ return root.getOffsetWidth();
+ }
+
+ @Override
+ protected void internalSetScrollbarThickness(int px) {
+ root.getStyle().setHeight(px, Unit.PX);
+ scrollSizeElement.getStyle().setHeight(px, Unit.PX);
+ }
+
+ @Override
+ protected int internalGetScrollbarThickness() {
+ return root.getOffsetHeight();
+ }
+
+ @Override
+ protected void forceScrollbar(boolean enable) {
+ if (enable) {
+ root.getStyle().setOverflowX(Overflow.SCROLL);
+ } else {
+ root.getStyle().clearOverflowX();
+ }
+ }
+ }
+
+ protected final Element root = DOM.createDiv();
+ protected final Element scrollSizeElement = DOM.createDiv();
+ protected boolean isInvisibleScrollbar = false;
+
+ private int scrollPos = 0;
+ private int maxScrollPos = 0;
+
+ private ScrollbarBundle() {
+ root.appendChild(scrollSizeElement);
+ }
+
+ /**
+ * Sets the primary style name
+ *
+ * @param primaryStyleName
+ * The primary style name to use
+ */
+ public void setStylePrimaryName(String primaryStyleName) {
+ root.setClassName(primaryStyleName + "-scroller");
+ }
+
+ /**
+ * Gets the root element of this scrollbar-composition.
+ *
+ * @return the root element
+ */
+ public final Element getElement() {
+ return root;
+ }
+
+ /**
+ * Modifies the scroll position of this scrollbar by a number of pixels
+ *
+ * @param delta
+ * the delta in pixels to change the scroll position by
+ */
+ public final void setScrollPosByDelta(int delta) {
+ if (delta != 0) {
+ setScrollPos(getScrollPos() + delta);
+ }
+ }
+
+ /**
+ * Modifies {@link #root root's} dimensions in the axis the scrollbar is
+ * representing.
+ *
+ * @param px
+ * the new size of {@link #root} in the dimension this scrollbar
+ * is representing
+ */
+ protected abstract void internalSetOffsetSize(int px);
+
+ /**
+ * Sets the length of the scrollbar.
+ *
+ * @param px
+ * the length of the scrollbar in pixels
+ */
+ public final void setOffsetSize(int px) {
+ internalSetOffsetSize(px);
+ forceScrollbar(showsScrollHandle());
+ recalculateMaxScrollPos();
+ }
+
+ /**
+ * Force the scrollbar to be visible with CSS. In practice, this means to
+ * set either <code>overflow-x</code> or <code>overflow-y</code> to "
+ * <code>scroll</code>" in the scrollbar's direction.
+ * <p>
+ * This is an IE8 workaround, since it doesn't always show scrollbars with
+ * <code>overflow: auto</code> enabled.
+ */
+ protected abstract void forceScrollbar(boolean enable);
+
+ /**
+ * Gets the length of the scrollbar
+ *
+ * @return the length of the scrollbar in pixels
+ */
+ public abstract int getOffsetSize();
+
+ /**
+ * Sets the scroll position of the scrollbar in the axis the scrollbar is
+ * representing.
+ *
+ * @param px
+ * the new scroll position in pixels
+ */
+ public final void setScrollPos(int px) {
+ int oldScrollPos = scrollPos;
+ scrollPos = Math.max(0, Math.min(maxScrollPos, px));
+
+ if (oldScrollPos != scrollPos) {
+ internalSetScrollPos(px);
+ }
+ }
+
+ protected abstract void internalSetScrollPos(int px);
+
+ /**
+ * Gets the scroll position of the scrollbar in the axis the scrollbar is
+ * representing.
+ *
+ * @return the new scroll position in pixels
+ */
+ public final int getScrollPos() {
+ assert internalGetScrollPos() == scrollPos : "calculated scroll position ("
+ + scrollPos
+ + ") did not match the DOM element scroll position ("
+ + internalGetScrollPos() + ")";
+ return scrollPos;
+ }
+
+ protected abstract int internalGetScrollPos();
+
+ /**
+ * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in
+ * such a way that the scrollbar is able to scroll a certain number of
+ * pixels in the axis it is representing.
+ *
+ * @param px
+ * the new size of {@link #scrollSizeElement} in the dimension
+ * this scrollbar is representing
+ */
+ protected abstract void internalSetScrollSize(int px);
+
+ /**
+ * Sets the amount of pixels the scrollbar needs to be able to scroll
+ * through.
+ *
+ * @param px
+ * the number of pixels the scrollbar should be able to scroll
+ * through
+ */
+ public final void setScrollSize(int px) {
+ internalSetScrollSize(px);
+ forceScrollbar(showsScrollHandle());
+ recalculateMaxScrollPos();
+ }
+
+ /**
+ * Gets the amount of pixels the scrollbar needs to be able to scroll
+ * through.
+ *
+ * @return the number of pixels the scrollbar should be able to scroll
+ * through
+ */
+ public abstract int getScrollSize();
+
+ /**
+ * Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in the
+ * opposite axis to what the scrollbar is representing.
+ *
+ * @param px
+ * the dimension that {@link #scrollSizeElement} should take in
+ * the opposite axis to what the scrollbar is representing
+ */
+ protected abstract void internalSetScrollbarThickness(int px);
+
+ /**
+ * Sets the scrollbar's thickness.
+ * <p>
+ * If the thickness is set to 0, the scrollbar will be treated as an
+ * "invisible" scrollbar. This means, the DOM structure will be given a
+ * non-zero size, but {@link #getScrollbarThickness()} will still return the
+ * value 0.
+ *
+ * @param px
+ * the scrollbar's thickness in pixels
+ */
+ public final void setScrollbarThickness(int px) {
+ isInvisibleScrollbar = (px == 0);
+ internalSetScrollbarThickness(px != 0 ? px
+ : OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX);
+ }
+
+ /**
+ * Gets the scrollbar's thickness as defined in the DOM.
+ *
+ * @return the scrollbar's thickness as defined in the DOM, in pixels
+ */
+ protected abstract int internalGetScrollbarThickness();
+
+ /**
+ * Gets the scrollbar's thickness.
+ * <p>
+ * This value will differ from the value in the DOM, if the thickness was
+ * set to 0 with {@link #setScrollbarThickness(int)}, as the scrollbar is
+ * then treated as "invisible."
+ *
+ * @return the scrollbar's thickness in pixels
+ */
+ public final int getScrollbarThickness() {
+ if (!isInvisibleScrollbar) {
+ return internalGetScrollbarThickness();
+ } else {
+ return 0;
+ }
+ }
+
+ /**
+ * Checks whether the scrollbar's handle is visible.
+ * <p>
+ * In other words, this method checks whether the contents is larger than
+ * can visually fit in the element.
+ *
+ * @return <code>true</code> iff the scrollbar's handle is visible
+ */
+ public boolean showsScrollHandle() {
+ return getOffsetSize() < getScrollSize();
+ }
+
+ public void recalculateMaxScrollPos() {
+ int scrollSize = getScrollSize();
+ int offsetSize = getOffsetSize();
+ maxScrollPos = Math.max(0, scrollSize - offsetSize);
+
+ // make sure that the correct max scroll position is maintained.
+ setScrollPos(scrollPos);
+ }
+
+ /**
+ * This is a method that JSNI can call to synchronize the object state from
+ * the DOM.
+ */
+ @SuppressWarnings("unused")
+ private final void updateScrollPosFromDom() {
+ scrollPos = internalGetScrollPos();
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java b/client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java
new file mode 100644
index 0000000000..0a3edbd349
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java
@@ -0,0 +1,357 @@
+/*
+ * 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.datasources;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
+
+/**
+ * A simple list based on an in-memory data source for simply adding a list of
+ * row pojos to the grid. Based on a wrapped list instance which supports adding
+ * and removing of items.
+ *
+ * <p>
+ * Usage:
+ *
+ * <pre>
+ * ListDataSource&lt;Integer&gt; ds = new ListDataSource&lt;Integer&gt;(1, 2, 3, 4);
+ *
+ * // Add item to the data source
+ * ds.asList().add(5);
+ *
+ * // Remove item from the data source
+ * ds.asList().remove(3);
+ *
+ * // Add multiple items
+ * ds.asList().addAll(Arrays.asList(5, 6, 7));
+ * </pre>
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class ListDataSource<T> implements DataSource<T> {
+
+ /**
+ * Wraps the datasource list and notifies the change handler of changing to
+ * the list
+ */
+ private class ListWrapper implements List<T> {
+
+ @Override
+ public int size() {
+ return ds.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return ds.isEmpty();
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return contains(o);
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ return new ListWrapperIterator(ds.iterator());
+ }
+
+ @Override
+ public Object[] toArray() {
+ return ds.toArray();
+ }
+
+ @Override
+ public <T> T[] toArray(T[] a) {
+ return toArray(a);
+ }
+
+ @Override
+ public boolean add(T e) {
+ if (ds.add(e)) {
+ if (changeHandler != null) {
+ changeHandler.dataAdded(ds.size() - 1, 1);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ int index = ds.indexOf(o);
+ if (ds.remove(o)) {
+ if (changeHandler != null) {
+ changeHandler.dataRemoved(index, 1);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> c) {
+ return ds.containsAll(c);
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends T> c) {
+ int idx = ds.size();
+ if (ds.addAll(c)) {
+ if (changeHandler != null) {
+ changeHandler.dataAdded(idx, c.size());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean addAll(int index, Collection<? extends T> c) {
+ if (ds.addAll(index, c)) {
+ if (changeHandler != null) {
+ changeHandler.dataAdded(index, c.size());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> c) {
+ if (ds.removeAll(c)) {
+ if (changeHandler != null) {
+ // Have to update the whole list as the removal does not
+ // have to be a continuous range
+ changeHandler.dataUpdated(0, ds.size());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> c) {
+ if (ds.retainAll(c)) {
+ if (changeHandler != null) {
+ // Have to update the whole list as the retain does not
+ // have to be a continuous range
+ changeHandler.dataUpdated(0, ds.size());
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void clear() {
+ int size = ds.size();
+ ds.clear();
+ if (changeHandler != null) {
+ changeHandler.dataRemoved(0, size);
+ }
+ }
+
+ @Override
+ public T get(int index) {
+ return ds.get(index);
+ }
+
+ @Override
+ public T set(int index, T element) {
+ T prev = ds.set(index, element);
+ if (changeHandler != null) {
+ changeHandler.dataUpdated(index, 1);
+ }
+ return prev;
+ }
+
+ @Override
+ public void add(int index, T element) {
+ ds.add(index, element);
+ if (changeHandler != null) {
+ changeHandler.dataAdded(index, 1);
+ }
+ }
+
+ @Override
+ public T remove(int index) {
+ T removed = ds.remove(index);
+ if (changeHandler != null) {
+ changeHandler.dataRemoved(index, 1);
+ }
+ return removed;
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ return ds.indexOf(o);
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ return ds.lastIndexOf(o);
+ }
+
+ @Override
+ public ListIterator<T> listIterator() {
+ // TODO could be implemented by a custom iterator.
+ throw new UnsupportedOperationException(
+ "List iterators not supported at this time.");
+ }
+
+ @Override
+ public ListIterator<T> listIterator(int index) {
+ // TODO could be implemented by a custom iterator.
+ throw new UnsupportedOperationException(
+ "List iterators not supported at this time.");
+ }
+
+ @Override
+ public List<T> subList(int fromIndex, int toIndex) {
+ throw new UnsupportedOperationException("Sub lists not supported.");
+ }
+ }
+
+ /**
+ * Iterator returned by {@link ListWrapper}
+ */
+ private class ListWrapperIterator implements Iterator<T> {
+
+ private final Iterator<T> iterator;
+
+ /**
+ * Constructs a new iterator
+ */
+ public ListWrapperIterator(Iterator<T> iterator) {
+ this.iterator = iterator;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public T next() {
+ return iterator.next();
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException(
+ "Iterator.remove() is not supported by this iterator.");
+ }
+ }
+
+ /**
+ * Datasource for providing row pojo's
+ */
+ private final List<T> ds;
+
+ /**
+ * Wrapper that wraps the data source
+ */
+ private final ListWrapper wrapper;
+
+ /**
+ * Handler for listening to changes in the underlying list.
+ */
+ private DataChangeHandler changeHandler;
+
+ /**
+ * Constructs a new list data source.
+ * <p>
+ * Note: Modifications to the original list will not be reflected in the
+ * data source after the data source has been constructed. To add or remove
+ * items to the data source after it has been constructed use
+ * {@link ListDataSource#asList()}.
+ *
+ *
+ * @param datasource
+ * The list to use for providing the data to the grid
+ */
+ public ListDataSource(List<T> datasource) {
+ if (datasource == null) {
+ throw new IllegalArgumentException("datasource cannot be null");
+ }
+ ds = new ArrayList<T>(datasource);
+ wrapper = new ListWrapper();
+ }
+
+ /**
+ * Constructs a data source with a set of rows. You can dynamically add and
+ * remove rows from the data source via the list you get from
+ * {@link ListDataSource#asList()}
+ *
+ * @param rows
+ * The rows to initially add to the data source
+ */
+ public ListDataSource(T... rows) {
+ if (rows == null) {
+ ds = new ArrayList<T>();
+ } else {
+ ds = new ArrayList<T>(Arrays.asList(rows));
+ }
+ wrapper = new ListWrapper();
+ }
+
+ @Override
+ public void ensureAvailability(int firstRowIndex, int numberOfRows) {
+ if (firstRowIndex >= ds.size()) {
+ throw new IllegalStateException(
+ "Trying to fetch rows outside of array");
+ }
+ }
+
+ @Override
+ public T getRow(int rowIndex) {
+ return ds.get(rowIndex);
+ }
+
+ @Override
+ public int getEstimatedSize() {
+ return ds.size();
+ }
+
+ @Override
+ public void setDataChangeHandler(DataChangeHandler dataChangeHandler) {
+ this.changeHandler = dataChangeHandler;
+ }
+
+ /**
+ * Gets the list that backs this datasource. Any changes made to this list
+ * will be reflected in the datasource.
+ * <p>
+ * Note: The list is not the same list as passed into the data source via
+ * the constructor.
+ *
+ * @return Returns a list implementation that wraps the real list that backs
+ * the data source and provides events for the data source
+ * listeners.
+ */
+ public List<T> asList() {
+ return wrapper;
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java
new file mode 100644
index 0000000000..d1f770f414
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java
@@ -0,0 +1,94 @@
+/*
+ * 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.renderers;
+
+import java.util.Date;
+
+import com.google.gwt.i18n.client.DateTimeFormat;
+import com.google.gwt.i18n.client.TimeZone;
+import com.vaadin.client.ui.grid.Cell;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * A renderer for rendering dates into cells
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class DateRenderer implements Renderer<Date> {
+
+ private DateTimeFormat format = DateTimeFormat.getShortDateTimeFormat();
+
+ private TimeZone timeZone = TimeZone.createTimeZone(new Date()
+ .getTimezoneOffset());
+
+ @Override
+ public void renderCell(Cell cell, Date date) {
+ String dateStr = format.format(date, timeZone);
+ cell.getElement().setInnerText(dateStr);
+ }
+
+ /**
+ * Gets the format of how the date is formatted.
+ *
+ * @return the format
+ * @see <a
+ * href="http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/DateTimeFormat.html">GWT
+ * documentation on DateTimeFormat</a>
+ */
+ public DateTimeFormat getFormat() {
+ return format;
+ }
+
+ /**
+ * Sets the format used for formatting the dates.
+ *
+ * @param format
+ * the format to set
+ * @see <a
+ * href="http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/DateTimeFormat.html">GWT
+ * documentation on DateTimeFormat</a>
+ */
+ public void setFormat(DateTimeFormat format) {
+ if (format == null) {
+ throw new IllegalArgumentException("Format should not be null");
+ }
+ this.format = format;
+ }
+
+ /**
+ * Returns the time zone of the date.
+ *
+ * @return the time zone
+ */
+ public TimeZone getTimeZone() {
+ return timeZone;
+ }
+
+ /**
+ * Sets the time zone of the the date. By default uses the time zone of the
+ * browser.
+ *
+ * @param timeZone
+ * the timeZone to set
+ */
+ public void setTimeZone(TimeZone timeZone) {
+ if (timeZone == null) {
+ throw new IllegalArgumentException("Timezone should not be null");
+ }
+ this.timeZone = timeZone;
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java
new file mode 100644
index 0000000000..0787dc2332
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java
@@ -0,0 +1,42 @@
+/*
+ * 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.renderers;
+
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.vaadin.client.ui.grid.Cell;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * Renders a string as HTML into a cell.
+ * <p>
+ * The html string is rendered as is without any escaping. It is up to the
+ * developer to ensure that the html string honors the {@link SafeHtml}
+ * contract. For more information see
+ * {@link SafeHtmlUtils#fromSafeConstant(String)}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @see SafeHtmlUtils#fromSafeConstant(String)
+ */
+public class HtmlRenderer implements Renderer<String> {
+
+ @Override
+ public void renderCell(Cell cell, String htmlString) {
+ cell.getElement().setInnerSafeHtml(
+ SafeHtmlUtils.fromSafeConstant(htmlString));
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java
new file mode 100644
index 0000000000..f4efea33a5
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java
@@ -0,0 +1,64 @@
+/*
+ * 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.renderers;
+
+import com.google.gwt.i18n.client.NumberFormat;
+import com.vaadin.client.ui.grid.Cell;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * Renders a number into a cell using a specific {@link NumberFormat}. By
+ * default uses the default number format returned by
+ * {@link NumberFormat#getDecimalFormat()}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ * @param <T>
+ * The number type to render.
+ */
+public class NumberRenderer<T extends Number> implements Renderer<T> {
+
+ private NumberFormat format = NumberFormat.getDecimalFormat();
+
+ /**
+ * Gets the number format that the number should be formatted in.
+ *
+ * @return the number format used to render the number
+ */
+ public NumberFormat getFormat() {
+ return format;
+ }
+
+ /**
+ * Sets the number format to use for formatting the number.
+ *
+ * @param format
+ * the format to use
+ * @throws IllegalArgumentException
+ * when the format is null
+ */
+ public void setFormat(NumberFormat format) throws IllegalArgumentException {
+ if (format == null) {
+ throw new IllegalArgumentException("Format cannot be null");
+ }
+ this.format = format;
+ }
+
+ @Override
+ public void renderCell(Cell cell, Number number) {
+ cell.getElement().setInnerText(format.format(number));
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java
new file mode 100644
index 0000000000..1f06a555c3
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java
@@ -0,0 +1,33 @@
+/*
+ * 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.renderers;
+
+import com.vaadin.client.ui.grid.Cell;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * Renderer that renders text into a cell.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class TextRenderer implements Renderer<String> {
+
+ @Override
+ public void renderCell(Cell cell, String text) {
+ cell.getElement().setInnerText(text);
+ }
+}
diff --git a/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java b/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java
new file mode 100644
index 0000000000..5c5e88bf69
--- /dev/null
+++ b/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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 java.util.Arrays;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.ui.grid.datasources.ListDataSource;
+
+/**
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class ListDataSourceTest {
+
+ @Test
+ public void testDataSourceConstruction() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ assertEquals(4, ds.getEstimatedSize());
+ assertEquals(0, (int) ds.getRow(0));
+ assertEquals(1, (int) ds.getRow(1));
+ assertEquals(2, (int) ds.getRow(2));
+ assertEquals(3, (int) ds.getRow(3));
+
+ ds = new ListDataSource<Integer>(Arrays.asList(0, 1, 2, 3));
+
+ assertEquals(4, ds.getEstimatedSize());
+ assertEquals(0, (int) ds.getRow(0));
+ assertEquals(1, (int) ds.getRow(1));
+ assertEquals(2, (int) ds.getRow(2));
+ assertEquals(3, (int) ds.getRow(3));
+ }
+
+ @Test
+ public void testListAddOperation() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ DataChangeHandler handler = EasyMock
+ .createNiceMock(DataChangeHandler.class);
+ ds.setDataChangeHandler(handler);
+
+ handler.dataAdded(4, 1);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(handler);
+
+ ds.asList().add(4);
+
+ assertEquals(5, ds.getEstimatedSize());
+ assertEquals(0, (int) ds.getRow(0));
+ assertEquals(1, (int) ds.getRow(1));
+ assertEquals(2, (int) ds.getRow(2));
+ assertEquals(3, (int) ds.getRow(3));
+ assertEquals(4, (int) ds.getRow(4));
+ }
+
+ @Test
+ public void testListAddAllOperation() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ DataChangeHandler handler = EasyMock
+ .createNiceMock(DataChangeHandler.class);
+ ds.setDataChangeHandler(handler);
+
+ handler.dataAdded(4, 3);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(handler);
+
+ ds.asList().addAll(Arrays.asList(4, 5, 6));
+
+ assertEquals(7, ds.getEstimatedSize());
+ assertEquals(0, (int) ds.getRow(0));
+ assertEquals(1, (int) ds.getRow(1));
+ assertEquals(2, (int) ds.getRow(2));
+ assertEquals(3, (int) ds.getRow(3));
+ assertEquals(4, (int) ds.getRow(4));
+ assertEquals(5, (int) ds.getRow(5));
+ assertEquals(6, (int) ds.getRow(6));
+ }
+
+ @Test
+ public void testListRemoveOperation() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ DataChangeHandler handler = EasyMock
+ .createNiceMock(DataChangeHandler.class);
+ ds.setDataChangeHandler(handler);
+
+ handler.dataRemoved(3, 1);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(handler);
+
+ ds.asList().remove(2);
+
+ assertEquals(3, ds.getEstimatedSize());
+ assertEquals(0, (int) ds.getRow(0));
+ assertEquals(1, (int) ds.getRow(1));
+ assertEquals(3, (int) ds.getRow(2));
+ }
+
+ @Test
+ public void testListRemoveAllOperation() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ DataChangeHandler handler = EasyMock
+ .createNiceMock(DataChangeHandler.class);
+ ds.setDataChangeHandler(handler);
+
+ handler.dataRemoved(0, 3);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(handler);
+
+ ds.asList().removeAll(Arrays.asList(0, 2, 3));
+
+ assertEquals(1, ds.getEstimatedSize());
+ assertEquals(1, (int) ds.getRow(0));
+ }
+
+ @Test
+ public void testListClearOperation() throws Exception {
+
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+
+ DataChangeHandler handler = EasyMock
+ .createNiceMock(DataChangeHandler.class);
+ ds.setDataChangeHandler(handler);
+
+ handler.dataRemoved(0, 4);
+ EasyMock.expectLastCall();
+
+ EasyMock.replay(handler);
+
+ ds.asList().clear();
+
+ assertEquals(0, ds.getEstimatedSize());
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testFetchingNonExistantItem() {
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+ ds.ensureAvailability(5, 1);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testUnsupportedIteratorRemove() {
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
+ ds.asList().iterator().remove();
+ }
+
+}
diff --git a/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
new file mode 100644
index 0000000000..e97bb339e4
--- /dev/null
+++ b/client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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;
+
+import com.vaadin.shared.ui.grid.Range;
+
+@SuppressWarnings("static-method")
+public class PartitioningTest {
+
+ @Test
+ public void selfRangeTest() {
+ final Range range = Range.between(0, 10);
+ final Range[] partitioning = range.partitionWith(range);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertTrue("inside is self", partitioning[1].equals(range));
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void beforeRangeTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(10, 20);
+ final Range[] partitioning = beforeRange.partitionWith(afterRange);
+
+ assertTrue("before is self", partitioning[0].equals(beforeRange));
+ assertTrue("inside is empty", partitioning[1].isEmpty());
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void afterRangeTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(10, 20);
+ final Range[] partitioning = afterRange.partitionWith(beforeRange);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertTrue("inside is empty", partitioning[1].isEmpty());
+ assertTrue("after is self", partitioning[2].equals(afterRange));
+ }
+
+ @Test
+ public void beforeAndInsideRangeTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(5, 15);
+ final Range[] partitioning = beforeRange.partitionWith(afterRange);
+
+ assertEquals("before", Range.between(0, 5), partitioning[0]);
+ assertEquals("inside", Range.between(5, 10), partitioning[1]);
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void insideRangeTest() {
+ final Range fullRange = Range.between(0, 20);
+ final Range insideRange = Range.between(5, 15);
+ final Range[] partitioning = insideRange.partitionWith(fullRange);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertEquals("inside", Range.between(5, 15), partitioning[1]);
+ assertTrue("after is empty", partitioning[2].isEmpty());
+ }
+
+ @Test
+ public void insideAndBelowTest() {
+ final Range beforeRange = Range.between(0, 10);
+ final Range afterRange = Range.between(5, 15);
+ final Range[] partitioning = afterRange.partitionWith(beforeRange);
+
+ assertTrue("before is empty", partitioning[0].isEmpty());
+ assertEquals("inside", Range.between(5, 10), partitioning[1]);
+ assertEquals("after", Range.between(10, 15), partitioning[2]);
+ }
+
+ @Test
+ public void aboveAndBelowTest() {
+ final Range fullRange = Range.between(0, 20);
+ final Range insideRange = Range.between(5, 15);
+ final Range[] partitioning = fullRange.partitionWith(insideRange);
+
+ assertEquals("before", Range.between(0, 5), partitioning[0]);
+ assertEquals("inside", Range.between(5, 15), partitioning[1]);
+ assertEquals("after", Range.between(15, 20), partitioning[2]);
+ }
+}
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java
new file mode 100644
index 0000000000..b22e6a209b
--- /dev/null
+++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java
@@ -0,0 +1,148 @@
+/*
+ * 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.data;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.shared.data.DataProviderRpc;
+import com.vaadin.shared.data.DataProviderState;
+import com.vaadin.shared.data.DataRequestRpc;
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * Provides Vaadin server-side container data source to a
+ * {@link com.vaadin.client.ui.grid.GridConnector}. 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
+ */
+public class RpcDataProviderExtension extends AbstractExtension {
+
+ private final Indexed container;
+
+ /**
+ * Creates a new data provider using the given container.
+ *
+ * @param container
+ * the container to make available
+ */
+ public RpcDataProviderExtension(Indexed container) {
+ this.container = container;
+
+ // TODO support for reacting to events from the container added later
+
+ registerRpc(new DataRequestRpc() {
+ @Override
+ public void requestRows(int firstRow, int numberOfRows) {
+ pushRows(firstRow, numberOfRows);
+ }
+ });
+
+ getState().containerSize = container.size();
+ }
+
+ private void pushRows(int firstRow, int numberOfRows) {
+ List<?> itemIds = container.getItemIds(firstRow, numberOfRows);
+ Collection<?> propertyIds = container.getContainerPropertyIds();
+ List<String[]> rows = new ArrayList<String[]>(itemIds.size());
+ for (Object itemId : itemIds) {
+ 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();
+ }
+
+ /**
+ * Makes the data source available to the given {@link Grid} component.
+ *
+ * @param component
+ * the remote data grid component to extend
+ */
+ public void extend(Grid component) {
+ 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));
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/ColumnGroup.java b/server/src/com/vaadin/ui/components/grid/ColumnGroup.java
new file mode 100644
index 0000000000..6b14ef81d4
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/ColumnGroup.java
@@ -0,0 +1,165 @@
+/*
+ * 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.ui.components.grid;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.shared.ui.grid.ColumnGroupState;
+
+/**
+ * Column groups are used to group columns together for adding common auxiliary
+ * headers and footers. Columns groups are added to {@link ColumnGroupRow}'s.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class ColumnGroup implements Serializable {
+
+ /**
+ * List of property ids belonging to this group
+ */
+ private List<Object> columns;
+
+ /**
+ * The grid the column group is associated with
+ */
+ private final Grid grid;
+
+ /**
+ * The column group row the column group is attached to
+ */
+ private final ColumnGroupRow row;
+
+ /**
+ * The common state between the server and the client
+ */
+ private final ColumnGroupState state;
+
+ /**
+ * Constructs a new column group
+ *
+ * @param grid
+ * the grid the column group is associated with
+ * @param state
+ * the state representing the data of the grid. Sent to the
+ * client
+ * @param propertyIds
+ * the property ids of the columns that belongs to the group
+ * @param groups
+ * the sub groups who should be included in this group
+ *
+ */
+ ColumnGroup(Grid grid, ColumnGroupRow row, ColumnGroupState state,
+ List<Object> propertyIds) {
+ if (propertyIds == null) {
+ throw new IllegalArgumentException(
+ "propertyIds cannot be null. Use empty list instead.");
+ }
+
+ this.state = state;
+ this.row = row;
+ columns = Collections.unmodifiableList(new ArrayList<Object>(
+ propertyIds));
+ this.grid = grid;
+ }
+
+ /**
+ * Sets the text displayed in the header of the column group.
+ *
+ * @param header
+ * the text displayed in the header of the column
+ */
+ public void setHeaderCaption(String header) {
+ checkGroupIsAttached();
+ state.header = header;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Sets the text displayed in the header of the column group.
+ *
+ * @return the text displayed in the header of the column
+ */
+ public String getHeaderCaption() {
+ checkGroupIsAttached();
+ return state.header;
+ }
+
+ /**
+ * Sets the text displayed in the footer of the column group.
+ *
+ * @param footer
+ * the text displayed in the footer of the column
+ */
+ public void setFooterCaption(String footer) {
+ checkGroupIsAttached();
+ state.footer = footer;
+ grid.markAsDirty();
+ }
+
+ /**
+ * The text displayed in the footer of the column group.
+ *
+ * @return the text displayed in the footer of the column
+ */
+ public String getFooterCaption() {
+ checkGroupIsAttached();
+ return state.footer;
+ }
+
+ /**
+ * Is a property id in this group or in some sub group of this group.
+ *
+ * @param propertyId
+ * the property id to check for
+ * @return <code>true</code> if the property id is included in this group.
+ */
+ public boolean isColumnInGroup(Object propertyId) {
+ if (columns.contains(propertyId)) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns a list of property ids where all also the child groups property
+ * ids are included.
+ *
+ * @return a unmodifiable list with all the columns in the group. Includes
+ * any subgroup columns as well.
+ */
+ public List<Object> getColumns() {
+ return columns;
+ }
+
+ /**
+ * Checks if column group is attached to a row and throws an
+ * {@link IllegalStateException} if it is not.
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ protected void checkGroupIsAttached() throws IllegalStateException {
+ if (!row.getState().groups.contains(state)) {
+ throw new IllegalStateException(
+ "Column Group has been removed from the row.");
+ }
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java b/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java
new file mode 100644
index 0000000000..b90b4df2c5
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java
@@ -0,0 +1,303 @@
+/*
+ * 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.ui.components.grid;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import com.vaadin.server.KeyMapper;
+import com.vaadin.shared.ui.grid.ColumnGroupRowState;
+import com.vaadin.shared.ui.grid.ColumnGroupState;
+
+/**
+ * A column group row represents an auxiliary header or footer row added to the
+ * grid. A column group row includes column groups that group columns together.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class ColumnGroupRow implements Serializable {
+
+ /**
+ * The common state shared between the client and server
+ */
+ private final ColumnGroupRowState state;
+
+ /**
+ * The column groups in this row
+ */
+ private List<ColumnGroup> groups = new ArrayList<ColumnGroup>();
+
+ /**
+ * Grid that the group row belongs to
+ */
+ private final Grid grid;
+
+ /**
+ * The column keys used to identify the column on the client side
+ */
+ private final KeyMapper<Object> columnKeys;
+
+ /**
+ * Constructs a new column group
+ *
+ * @param grid
+ * The grid that the column group is associated to
+ * @param state
+ * The shared state which contains the data shared between server
+ * and client
+ * @param columnKeys
+ * The column key mapper for converting property ids to client
+ * side column identifiers
+ */
+ ColumnGroupRow(Grid grid, ColumnGroupRowState state,
+ KeyMapper<Object> columnKeys) {
+ this.grid = grid;
+ this.columnKeys = columnKeys;
+ this.state = state;
+ }
+
+ /**
+ * Gets the shared state for the column group row. Used internally to send
+ * the group row to the client.
+ *
+ * @return The current state of the row
+ */
+ ColumnGroupRowState getState() {
+ return state;
+ }
+
+ /**
+ * Add a new group to the row by using property ids for the columns.
+ *
+ * @param propertyIds
+ * The property ids of the columns that should be included in the
+ * group. A column can only belong in group on a row at a time.
+ * @return a column group representing the collection of columns added to
+ * the group
+ */
+ public ColumnGroup addGroup(Object... propertyIds)
+ throws IllegalArgumentException {
+ assert propertyIds != null : "propertyIds cannot be null.";
+
+ for (Object propertyId : propertyIds) {
+ if (hasColumnBeenGrouped(propertyId)) {
+ throw new IllegalArgumentException("Column "
+ + String.valueOf(propertyId)
+ + " already belongs to another group.");
+ }
+ }
+
+ validateNewGroupProperties(Arrays.asList(propertyIds));
+
+ ColumnGroupState state = new ColumnGroupState();
+ for (Object propertyId : propertyIds) {
+ assert propertyId != null : "null items in columns array not supported.";
+ state.columns.add(columnKeys.key(propertyId));
+ }
+ this.state.groups.add(state);
+
+ ColumnGroup group = new ColumnGroup(grid, this, state,
+ Arrays.asList(propertyIds));
+ groups.add(group);
+
+ grid.markAsDirty();
+ return group;
+ }
+
+ private void validateNewGroupProperties(List<Object> propertyIds)
+ throws IllegalArgumentException {
+
+ /*
+ * Validate parent grouping
+ */
+ int rowIndex = grid.getColumnGroupRows().indexOf(this);
+ int parentRowIndex = rowIndex - 1;
+
+ // Get the parent row of this row.
+ ColumnGroupRow parentRow = null;
+ if (parentRowIndex > -1) {
+ parentRow = grid.getColumnGroupRows().get(parentRowIndex);
+ }
+
+ if (parentRow == null) {
+ // A parentless row is always valid and is usually the first row
+ // added to the grid
+ return;
+ }
+
+ for (Object id : propertyIds) {
+ if (parentRow.hasColumnBeenGrouped(id)) {
+ /*
+ * If a property has been grouped in the parent row then all of
+ * the properties in the parent group also needs to be included
+ * in the child group for the groups to be valid
+ */
+ ColumnGroup parentGroup = parentRow.getGroupForProperty(id);
+ if (!propertyIds.containsAll(parentGroup.getColumns())) {
+ throw new IllegalArgumentException(
+ "Grouped properties overlaps previous grouping bounderies");
+ }
+ }
+ }
+ }
+
+ /**
+ * Add a new group to the row by using column instances.
+ *
+ * @param columns
+ * the columns that should belong to the group
+ * @return a column group representing the collection of columns added to
+ * the group
+ */
+ public ColumnGroup addGroup(GridColumn... columns)
+ throws IllegalArgumentException {
+ assert columns != null : "columns cannot be null";
+
+ List<Object> propertyIds = new ArrayList<Object>();
+ for (GridColumn column : columns) {
+ assert column != null : "null items in columns array not supported.";
+
+ String columnId = column.getState().id;
+ Object propertyId = grid.getPropertyIdByColumnId(columnId);
+ propertyIds.add(propertyId);
+ }
+ return addGroup(propertyIds.toArray());
+ }
+
+ /**
+ * Add a new group to the row by using other already greated groups
+ *
+ * @param groups
+ * the subgroups of the group
+ * @return a column group representing the collection of columns added to
+ * the group
+ *
+ */
+ public ColumnGroup addGroup(ColumnGroup... groups)
+ throws IllegalArgumentException {
+ assert groups != null : "groups cannot be null";
+
+ // Gather all groups columns into one list
+ List<Object> propertyIds = new ArrayList<Object>();
+ for (ColumnGroup group : groups) {
+ propertyIds.addAll(group.getColumns());
+ }
+
+ validateNewGroupProperties(propertyIds);
+
+ ColumnGroupState state = new ColumnGroupState();
+ ColumnGroup group = new ColumnGroup(grid, this, state, propertyIds);
+ this.groups.add(group);
+
+ // Update state
+ for (Object propertyId : group.getColumns()) {
+ state.columns.add(columnKeys.key(propertyId));
+ }
+ this.state.groups.add(state);
+
+ grid.markAsDirty();
+ return group;
+ }
+
+ /**
+ * Removes a group from the row. Does not remove the group from subgroups,
+ * to remove it from the subgroup invoke removeGroup on the subgroup.
+ *
+ * @param group
+ * the group to remove
+ */
+ public void removeGroup(ColumnGroup group) {
+ int index = groups.indexOf(group);
+ groups.remove(index);
+ state.groups.remove(index);
+ grid.markAsDirty();
+ }
+
+ /**
+ * Get the groups in the row.
+ *
+ * @return unmodifiable list of groups in this row
+ */
+ public List<ColumnGroup> getGroups() {
+ return Collections.unmodifiableList(groups);
+ }
+
+ /**
+ * Checks if a property id has been added to a group in this row.
+ *
+ * @param propertyId
+ * the property id to check for
+ * @return <code>true</code> if the column is included in a group
+ */
+ private boolean hasColumnBeenGrouped(Object propertyId) {
+ return getGroupForProperty(propertyId) != null;
+ }
+
+ private ColumnGroup getGroupForProperty(Object propertyId) {
+ for (ColumnGroup group : groups) {
+ if (group.isColumnInGroup(propertyId)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Is the header visible for the row.
+ *
+ * @return <code>true</code> if header is visible
+ */
+ public boolean isHeaderVisible() {
+ return state.headerVisible;
+ }
+
+ /**
+ * Sets the header visible for the row.
+ *
+ * @param visible
+ * should the header be shown
+ */
+ public void setHeaderVisible(boolean visible) {
+ state.headerVisible = visible;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Is the footer visible for the row.
+ *
+ * @return <code>true</code> if footer is visible
+ */
+ public boolean isFooterVisible() {
+ return state.footerVisible;
+ }
+
+ /**
+ * Sets the footer visible for the row.
+ *
+ * @param visible
+ * should the footer be shown
+ */
+ public void setFooterVisible(boolean visible) {
+ state.footerVisible = visible;
+ grid.markAsDirty();
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java
new file mode 100644
index 0000000000..4126ec6d93
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/Grid.java
@@ -0,0 +1,861 @@
+/*
+ * 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.ui.components.grid;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+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.GridClientRpc;
+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.shared.ui.grid.ScrollDestination;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.Component;
+
+/**
+ * Data grid component
+ *
+ * <h3>Lazy loading</h3> TODO To be revised when the data data source
+ * implementation has been don.
+ *
+ * <h3>Columns</h3> The grid columns are based on the property ids of the
+ * underlying data source. Each property id represents one column in the grid.
+ * To retrive a column in the grid you can use {@link Grid#getColumn(Object)}
+ * with the property id of the column. A grid column contains properties like
+ * the width, the footer and header captions of the column.
+ *
+ * <h3>Auxiliary headers and footers</h3> TODO To be revised when column
+ * grouping is implemented.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+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
+ */
+ private Container.Indexed datasource;
+
+ /**
+ * Property id to column instance mapping
+ */
+ private final Map<Object, GridColumn> columns = new HashMap<Object, GridColumn>();
+
+ /**
+ * Key generator for column server-to-client communication
+ */
+ private final KeyMapper<Object> columnKeys = new KeyMapper<Object>();
+
+ /**
+ * The column groups added to the grid
+ */
+ private final List<ColumnGroupRow> columnGroupRows = new ArrayList<ColumnGroupRow>();
+
+ /**
+ * Property listener for listening to changes in data source properties.
+ */
+ private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() {
+
+ @Override
+ public void containerPropertySetChange(PropertySetChangeEvent event) {
+ Collection<?> properties = new HashSet<Object>(event.getContainer()
+ .getContainerPropertyIds());
+
+ // Cleanup columns that are no longer in grid
+ List<Object> removedColumns = new LinkedList<Object>();
+ for (Object columnId : columns.keySet()) {
+ if (!properties.contains(columnId)) {
+ removedColumns.add(columnId);
+ }
+ }
+ for (Object columnId : removedColumns) {
+ GridColumn column = columns.remove(columnId);
+ 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);
+ if (!columns.containsKey(frozenPropertyId)) {
+ setLastFrozenPropertyId(null);
+ }
+ }
+ };
+
+ 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.
+ *
+ * @param datasource
+ * the data source for the grid
+ */
+ public Grid(Container.Indexed datasource) {
+ setContainerDatasource(datasource);
+
+ registerRpc(new GridServerRpc() {
+ @Override
+ public void setVisibleRows(int firstVisibleRow, int visibleRowCount) {
+ activeRowHandler
+ .setActiveRows(firstVisibleRow, visibleRowCount);
+ }
+ });
+ }
+
+ /**
+ * Sets the grid data source.
+ *
+ * @param container
+ * The container data source. Cannot be null.
+ * @throws IllegalArgumentException
+ * if the data source is null
+ */
+ public void setContainerDatasource(Container.Indexed container) {
+ if (container == null) {
+ throw new IllegalArgumentException(
+ "Cannot set the datasource to null");
+ }
+ if (datasource == container) {
+ return;
+ }
+
+ // 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);
+ }
+
+ datasource = container;
+ datasourceExtension = new RpcDataProviderExtension(container);
+ datasourceExtension.extend(this);
+
+ // Listen to changes in properties and remove columns if needed
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((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);
+
+ // Add columns
+ for (Object propertyId : datasource.getContainerPropertyIds()) {
+ if (!columns.containsKey(propertyId)) {
+ GridColumn column = appendColumn(propertyId);
+
+ // Add by default property id as column header
+ column.setHeaderCaption(String.valueOf(propertyId));
+ }
+ }
+
+ }
+
+ /**
+ * Returns the grid data source.
+ *
+ * @return the container data source of the grid
+ */
+ public Container.Indexed getContainerDatasource() {
+ return datasource;
+ }
+
+ /**
+ * Returns a column based on the property id
+ *
+ * @param propertyId
+ * the property id of the column
+ * @return the column or <code>null</code> if not found
+ */
+ public GridColumn getColumn(Object propertyId) {
+ return columns.get(propertyId);
+ }
+
+ /**
+ * Sets the header rows visible.
+ *
+ * @param visible
+ * <code>true</code> if the header rows should be visible
+ */
+ public void setColumnHeadersVisible(boolean visible) {
+ getState().columnHeadersVisible = visible;
+ }
+
+ /**
+ * Are the header rows visible?
+ *
+ * @return <code>true</code> if the headers of the columns are visible
+ */
+ public boolean isColumnHeadersVisible() {
+ return getState(false).columnHeadersVisible;
+ }
+
+ /**
+ * Sets the footer rows visible.
+ *
+ * @param visible
+ * <code>true</code> if the footer rows should be visible
+ */
+ public void setColumnFootersVisible(boolean visible) {
+ getState().columnFootersVisible = visible;
+ }
+
+ /**
+ * Are the footer rows visible.
+ *
+ * @return <code>true</code> if the footer rows should be visible
+ */
+ public boolean isColumnFootersVisible() {
+ return getState(false).columnFootersVisible;
+ }
+
+ /**
+ * <p>
+ * Adds a new column group to the grid.
+ *
+ * <p>
+ * 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.
+ * </p>
+ * </p>
+ *
+ * <p>
+ * Example usage:
+ *
+ * <pre>
+ * // Add a new column group row to the grid
+ * ColumnGroupRow row = grid.addColumnGroupRow();
+ *
+ * // Group &quot;Column1&quot; and &quot;Column2&quot; together to form a header in the row
+ * ColumnGroup column12 = row.addGroup(&quot;Column1&quot;, &quot;Column2&quot;);
+ *
+ * // Set a common header for &quot;Column1&quot; and &quot;Column2&quot;
+ * column12.setHeader(&quot;Column 1&amp;2&quot;);
+ * </pre>
+ *
+ * </p>
+ *
+ * @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<ColumnGroupRow> getColumnGroupRows() {
+ return Collections.unmodifiableList(new ArrayList<ColumnGroupRow>(
+ columnGroupRows));
+ }
+
+ /**
+ * Used internally by the {@link Grid} to get a {@link GridColumn} by
+ * referencing its generated state id. Also used by {@link GridColumn} to
+ * verify if it has been detached from the {@link Grid}.
+ *
+ * @param columnId
+ * the client id generated for the column when the column is
+ * added to the grid
+ * @return the column with the id or <code>null</code> if not found
+ */
+ GridColumn getColumnByColumnId(String 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();
+ }
+
+ @Override
+ protected GridState getState(boolean markAsDirty) {
+ return (GridState) super.getState(markAsDirty);
+ }
+
+ /**
+ * Creates a new column based on a property id and appends it as the last
+ * column.
+ *
+ * @param datasourcePropertyId
+ * The property id of a property in the datasource
+ */
+ private GridColumn appendColumn(Object datasourcePropertyId) {
+ if (datasourcePropertyId == null) {
+ throw new IllegalArgumentException("Property id cannot be null");
+ }
+ assert datasource.getContainerPropertyIds().contains(
+ datasourcePropertyId) : "Datasource should contain the property id";
+
+ GridColumnState columnState = new GridColumnState();
+ columnState.id = columnKeys.key(datasourcePropertyId);
+ getState().columns.add(columnState);
+
+ GridColumn column = new GridColumn(this, columnState);
+ columns.put(datasourcePropertyId, column);
+
+ return column;
+ }
+
+ /**
+ * Sets (or unsets) the rightmost frozen column in the grid.
+ * <p>
+ * 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 <code>null</code> 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.
+ * <p>
+ * All columns up to and including the indicated property will be frozen in
+ * place when the grid is scrolled sideways.
+ * <p>
+ * <em>Note:</em> If the container used by this grid supports a propertyId
+ * <code>null</code>, it can never be defined as the last frozen column, as
+ * a <code>null</code> 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 <code>null</code> 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.
+ * <p>
+ * <em>Note:</em> Most often, this method returns the very value set with
+ * {@link #setLastFrozenPropertyId(Object)}. This value, however, can be
+ * reset to <code>null</code> if the column is detached from this grid.
+ *
+ * @return the rightmost frozen column in the grid, or <code>null</code> if
+ * no columns are frozen.
+ */
+ public Object getLastFrozenPropertyId() {
+ return columnKeys.get(getState().lastFrozenColumnId);
+ }
+
+ /**
+ * Scrolls to a certain item, using {@link ScrollDestination#ANY}.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollToItem(Object itemId) throws IllegalArgumentException {
+ scrollToItem(itemId, ScrollDestination.ANY);
+ }
+
+ /**
+ * Scrolls to a certain item, using user-specified scroll destination.
+ *
+ * @param itemId
+ * id of item to scroll to.
+ * @param destination
+ * value specifying desired position of scrolled-to row.
+ * @throws IllegalArgumentException
+ * if the provided id is not recognized by the data source.
+ */
+ public void scrollToItem(Object itemId, ScrollDestination destination)
+ throws IllegalArgumentException {
+
+ int row = datasource.indexOfId(itemId);
+
+ if (row == -1) {
+ throw new IllegalArgumentException(
+ "Item with specified ID does not exist in data source");
+ }
+
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToRow(row, destination);
+ }
+
+ /**
+ * Scrolls to the beginning of the first data row.
+ */
+ public void scrollToStart() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToStart();
+ }
+
+ /**
+ * Scrolls to the end of the last data row.
+ */
+ public void scrollToEnd() {
+ GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
+ clientRPC.scrollToEnd();
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/GridColumn.java b/server/src/com/vaadin/ui/components/grid/GridColumn.java
new file mode 100644
index 0000000000..852db21275
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java
@@ -0,0 +1,216 @@
+/*
+ * 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.ui.components.grid;
+
+import java.io.Serializable;
+
+import com.vaadin.shared.ui.grid.GridColumnState;
+
+/**
+ * A column in the grid. Can be obtained by calling
+ * {@link Grid#getColumn(Object propertyId)}.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class GridColumn implements Serializable {
+
+ /**
+ * The state of the column shared to the client
+ */
+ private final GridColumnState state;
+
+ /**
+ * The grid this column is associated with
+ */
+ private final Grid grid;
+
+ /**
+ * Internally used constructor.
+ *
+ * @param grid
+ * The grid this column belongs to. Should not be null.
+ * @param state
+ * the shared state of this column
+ */
+ GridColumn(Grid grid, GridColumnState state) {
+ this.grid = grid;
+ this.state = state;
+ }
+
+ /**
+ * Returns the serializable state of this column that is sent to the client
+ * side connector.
+ *
+ * @return the internal state of the column
+ */
+ GridColumnState getState() {
+ return state;
+ }
+
+ /**
+ * Returns the caption of the header. By default the header caption is the
+ * property id of the column.
+ *
+ * @return the text in the header
+ *
+ * @throws IllegalStateException
+ * if the column no longer is attached to the grid
+ */
+ public String getHeaderCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.header;
+ }
+
+ /**
+ * Sets the caption of the header.
+ *
+ * @param caption
+ * the text to show in the caption
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public void setHeaderCaption(String caption) throws IllegalStateException {
+ checkColumnIsAttached();
+ state.header = caption;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Returns the caption of the footer. By default the captions are
+ * <code>null</code>.
+ *
+ * @return the text in the footer
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public String getFooterCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.footer;
+ }
+
+ /**
+ * Sets the caption of the footer.
+ *
+ * @param caption
+ * the text to show in the caption
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public void setFooterCaption(String caption) throws IllegalStateException {
+ checkColumnIsAttached();
+ state.footer = caption;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Returns the width (in pixels). By default a column is 100px wide.
+ *
+ * @return the width in pixels of the column
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public int getWidth() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.width;
+ }
+
+ /**
+ * Sets the width (in pixels).
+ *
+ * @param pixelWidth
+ * 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,
+ IllegalArgumentException {
+ checkColumnIsAttached();
+ if (pixelWidth < 0) {
+ throw new IllegalArgumentException(
+ "Pixel width should be greated than 0");
+ }
+ state.width = pixelWidth;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Marks the column width as undefined meaning that the grid is free to
+ * resize the column based on the cell contents and available space in the
+ * grid.
+ */
+ public void setWidthUndefined() {
+ checkColumnIsAttached();
+ state.width = -1;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Is this column visible in the grid. By default all columns are visible.
+ *
+ * @return <code>true</code> if the column is visible
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public boolean isVisible() throws IllegalStateException {
+ checkColumnIsAttached();
+ return state.visible;
+ }
+
+ /**
+ * Set the visibility of this column
+ *
+ * @param visible
+ * is the column visible
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ public void setVisible(boolean visible) throws IllegalStateException {
+ checkColumnIsAttached();
+ state.visible = visible;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Checks if column is attached and throws an {@link IllegalStateException}
+ * if it is not
+ *
+ * @throws IllegalStateException
+ * if the column is no longer attached to any grid
+ */
+ protected void checkColumnIsAttached() throws IllegalStateException {
+ if (grid.getColumnByColumnId(state.id) == null) {
+ 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/GridColumnGroups.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java
new file mode 100644
index 0000000000..4350bf1a7b
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java
@@ -0,0 +1,265 @@
+/*
+ * 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.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.KeyMapper;
+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;
+
+/**
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridColumnGroups {
+
+ private Grid grid;
+
+ private GridState state;
+
+ private Method getStateMethod;
+
+ private Field columnIdGeneratorField;
+
+ private KeyMapper<Object> columnIdMapper;
+
+ @Before
+ public void setup() throws Exception {
+ IndexedContainer ds = new IndexedContainer();
+ for (int c = 0; c < 10; c++) {
+ ds.addContainerProperty("column" + c, String.class, "");
+ }
+ grid = new Grid(ds);
+
+ getStateMethod = Grid.class.getDeclaredMethod("getState");
+ getStateMethod.setAccessible(true);
+
+ state = (GridState) getStateMethod.invoke(grid);
+
+ columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys");
+ columnIdGeneratorField.setAccessible(true);
+
+ columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid);
+ }
+
+ @Test
+ public void testColumnGroupRows() throws Exception {
+
+ // No column group rows by default
+ List<ColumnGroupRow> rows = grid.getColumnGroupRows();
+ assertEquals(0, rows.size());
+
+ // Add some rows
+ ColumnGroupRow row1 = grid.addColumnGroupRow();
+ ColumnGroupRow row3 = grid.addColumnGroupRow();
+ ColumnGroupRow row2 = grid.addColumnGroupRow(1);
+
+ rows = grid.getColumnGroupRows();
+ assertEquals(3, rows.size());
+ assertEquals(row1, rows.get(0));
+ assertEquals(row2, rows.get(1));
+ assertEquals(row3, rows.get(2));
+
+ // Header should be visible by default, footer should not
+ assertTrue(row1.isHeaderVisible());
+ assertFalse(row1.isFooterVisible());
+
+ row1.setHeaderVisible(false);
+ assertFalse(row1.isHeaderVisible());
+ row1.setHeaderVisible(true);
+ assertTrue(row1.isHeaderVisible());
+
+ row1.setFooterVisible(true);
+ assertTrue(row1.isFooterVisible());
+ row1.setFooterVisible(false);
+ assertFalse(row1.isFooterVisible());
+
+ row1.setHeaderVisible(true);
+ row1.setFooterVisible(true);
+ assertTrue(row1.isHeaderVisible());
+ assertTrue(row1.isFooterVisible());
+
+ row1.setHeaderVisible(false);
+ row1.setFooterVisible(false);
+ assertFalse(row1.isHeaderVisible());
+ assertFalse(row1.isFooterVisible());
+ }
+
+ @Test
+ public void testColumnGroupsInState() 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);
+ }
+
+ @Test
+ public void testAddingColumnGroups() throws Exception {
+
+ ColumnGroupRow row = grid.addColumnGroupRow();
+
+ // By property id
+ ColumnGroup columns01 = row.addGroup("column0", "column1");
+ assertEquals(2, columns01.getColumns().size());
+ assertEquals("column0", columns01.getColumns().get(0));
+ assertTrue(columns01.isColumnInGroup("column0"));
+ assertEquals("column1", columns01.getColumns().get(1));
+ assertTrue(columns01.isColumnInGroup("column1"));
+
+ // By grid column
+ ColumnGroup columns23 = row.addGroup(grid.getColumn("column2"),
+ grid.getColumn("column3"));
+ assertEquals(2, columns23.getColumns().size());
+ assertEquals("column2", columns23.getColumns().get(0));
+ assertTrue(columns23.isColumnInGroup("column2"));
+ assertEquals("column3", columns23.getColumns().get(1));
+ assertTrue(columns23.isColumnInGroup("column3"));
+
+ // Combine groups
+ ColumnGroupRow row2 = grid.addColumnGroupRow();
+ ColumnGroup columns0123 = row2.addGroup(columns01, columns23);
+ assertEquals(4, columns0123.getColumns().size());
+ assertEquals("column0", columns0123.getColumns().get(0));
+ assertTrue(columns0123.isColumnInGroup("column0"));
+ assertEquals("column1", columns0123.getColumns().get(1));
+ assertTrue(columns0123.isColumnInGroup("column1"));
+ assertEquals("column2", columns0123.getColumns().get(2));
+ assertTrue(columns0123.isColumnInGroup("column2"));
+ assertEquals("column3", columns0123.getColumns().get(3));
+ assertTrue(columns0123.isColumnInGroup("column3"));
+ }
+
+ @Test
+ public void testColumnGroupHeadersAndFooters() throws Exception {
+
+ ColumnGroupRow row = grid.addColumnGroupRow();
+ ColumnGroup group = row.addGroup("column1", "column2");
+
+ // Header
+ assertNull(group.getHeaderCaption());
+ group.setHeaderCaption("My header");
+ assertEquals("My header", group.getHeaderCaption());
+ group.setHeaderCaption(null);
+ assertNull(group.getHeaderCaption());
+
+ // Footer
+ assertNull(group.getFooterCaption());
+ group.setFooterCaption("My footer");
+ assertEquals("My footer", group.getFooterCaption());
+ group.setFooterCaption(null);
+ assertNull(group.getFooterCaption());
+ }
+
+ @Test
+ public void testColumnGroupDetachment() throws Exception {
+
+ ColumnGroupRow row = grid.addColumnGroupRow();
+ ColumnGroup group = row.addGroup("column1", "column2");
+
+ // Remove group
+ row.removeGroup(group);
+
+ try {
+ group.setHeaderCaption("Header");
+ fail("Should throw exception for setting header caption on detached group");
+ } catch (IllegalStateException ise) {
+
+ }
+
+ try {
+ group.setFooterCaption("Footer");
+ fail("Should throw exception for setting footer caption on detached group");
+ } catch (IllegalStateException ise) {
+
+ }
+ }
+
+ @Test
+ public void testColumnGroupLimits() throws Exception {
+
+ ColumnGroupRow row = grid.addColumnGroupRow();
+ row.addGroup("column1", "column2");
+ row.addGroup("column3", "column4");
+
+ try {
+ row.addGroup("column2", "column3");
+ fail("Adding a group with already grouped properties should throw exception");
+ } catch (IllegalArgumentException iae) {
+
+ }
+
+ ColumnGroupRow row2 = grid.addColumnGroupRow();
+
+ try {
+ row2.addGroup("column2", "column3");
+ fail("Adding a group that breaks previous grouping boundaries should throw exception");
+ } catch (IllegalArgumentException iae) {
+
+ }
+
+ // This however should not throw an exception as it spans completely
+ // over the parent rows groups
+ row2.addGroup("column1", "column2", "column3", "column4");
+
+ }
+}
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
new file mode 100644
index 0000000000..da07611b48
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
@@ -0,0 +1,228 @@
+/*
+ * 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.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+import org.junit.Before;
+import org.junit.Test;
+
+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.Grid;
+import com.vaadin.ui.components.grid.GridColumn;
+
+public class GridColumns {
+
+ private Grid grid;
+
+ private GridState state;
+
+ private Method getStateMethod;
+
+ private Field columnIdGeneratorField;
+
+ private KeyMapper<Object> columnIdMapper;
+
+ @Before
+ public void setup() throws Exception {
+ IndexedContainer ds = new IndexedContainer();
+ for (int c = 0; c < 10; c++) {
+ ds.addContainerProperty("column" + c, String.class, "");
+ }
+ grid = new Grid(ds);
+
+ getStateMethod = Grid.class.getDeclaredMethod("getState");
+ getStateMethod.setAccessible(true);
+
+ state = (GridState) getStateMethod.invoke(grid);
+
+ columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys");
+ columnIdGeneratorField.setAccessible(true);
+
+ columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid);
+ }
+
+ @Test
+ public void testColumnGeneration() throws Exception {
+
+ for (Object propertyId : grid.getContainerDatasource()
+ .getContainerPropertyIds()) {
+
+ // All property ids should get a column
+ GridColumn column = grid.getColumn(propertyId);
+ assertNotNull(column);
+
+ // Property id should be the column header by default
+ assertEquals(propertyId.toString(), column.getHeaderCaption());
+ }
+ }
+
+ @Test
+ public void testModifyingColumnProperties() throws Exception {
+
+ // Modify first column
+ GridColumn column = grid.getColumn("column1");
+ assertNotNull(column);
+
+ column.setFooterCaption("CustomFooter");
+ assertEquals("CustomFooter", column.getFooterCaption());
+ assertEquals(column.getFooterCaption(),
+ getColumnState("column1").footer);
+
+ column.setHeaderCaption("CustomHeader");
+ assertEquals("CustomHeader", column.getHeaderCaption());
+ assertEquals(column.getHeaderCaption(),
+ getColumnState("column1").header);
+
+ column.setVisible(false);
+ assertFalse(column.isVisible());
+ assertFalse(getColumnState("column1").visible);
+
+ column.setVisible(true);
+ assertTrue(column.isVisible());
+ assertTrue(getColumnState("column1").visible);
+
+ column.setWidth(100);
+ assertEquals(100, column.getWidth());
+ assertEquals(column.getWidth(), 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
+ public void testRemovingColumn() throws Exception {
+
+ GridColumn column = grid.getColumn("column1");
+ assertNotNull(column);
+
+ // Remove column
+ grid.getContainerDatasource().removeContainerProperty("column1");
+
+ try {
+ column.setHeaderCaption("asd");
+
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ try {
+ column.setFooterCaption("asd");
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ try {
+ column.setVisible(false);
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ try {
+ column.setWidth(123);
+ fail("Succeeded in modifying a detached column");
+ } catch (IllegalStateException ise) {
+ // Detached state should throw exception
+ }
+
+ assertNull(grid.getColumn("column1"));
+ assertNull(getColumnState("column1"));
+ }
+
+ @Test
+ public void testAddingColumn() throws Exception {
+ grid.getContainerDatasource().addContainerProperty("columnX",
+ String.class, "");
+ GridColumn column = grid.getColumn("columnX");
+ assertNotNull(column);
+ }
+
+ @Test
+ public void testHeaderVisiblility() throws Exception {
+
+ assertTrue(grid.isColumnHeadersVisible());
+ assertTrue(state.columnHeadersVisible);
+
+ grid.setColumnHeadersVisible(false);
+ assertFalse(grid.isColumnHeadersVisible());
+ assertFalse(state.columnHeadersVisible);
+
+ grid.setColumnHeadersVisible(true);
+ assertTrue(grid.isColumnHeadersVisible());
+ assertTrue(state.columnHeadersVisible);
+ }
+
+ @Test
+ public void testFooterVisibility() throws Exception {
+
+ assertFalse(grid.isColumnFootersVisible());
+ assertFalse(state.columnFootersVisible);
+
+ grid.setColumnFootersVisible(false);
+ assertFalse(grid.isColumnFootersVisible());
+ assertFalse(state.columnFootersVisible);
+
+ grid.setColumnFootersVisible(true);
+ assertTrue(grid.isColumnFootersVisible());
+ assertTrue(state.columnFootersVisible);
+ }
+
+ @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) {
+ if (columnState.id.equals(columnId)) {
+ return columnState;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java
new file mode 100644
index 0000000000..79e3f17f8d
--- /dev/null
+++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java
@@ -0,0 +1,61 @@
+/*
+ * 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.data;
+
+import java.util.List;
+
+import com.vaadin.shared.communication.ClientRpc;
+
+/**
+ * RPC interface used for pushing container data to the client.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface DataProviderRpc extends ClientRpc {
+
+ /**
+ * Sends updated row data to a client.
+ *
+ * @param firstRowIndex
+ * the index of the first updated row
+ * @param rowData
+ * 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/data/DataProviderState.java b/shared/src/com/vaadin/shared/data/DataProviderState.java
new file mode 100644
index 0000000000..2eabe0b0e1
--- /dev/null
+++ b/shared/src/com/vaadin/shared/data/DataProviderState.java
@@ -0,0 +1,32 @@
+/*
+ * 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.data;
+
+import com.vaadin.shared.communication.SharedState;
+
+/**
+ * Shared state used by client-side data sources.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class DataProviderState extends SharedState {
+ /**
+ * The size of the container.
+ */
+ public int containerSize;
+}
diff --git a/shared/src/com/vaadin/shared/data/DataRequestRpc.java b/shared/src/com/vaadin/shared/data/DataRequestRpc.java
new file mode 100644
index 0000000000..eaf17df8f6
--- /dev/null
+++ b/shared/src/com/vaadin/shared/data/DataRequestRpc.java
@@ -0,0 +1,38 @@
+/*
+ * 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.data;
+
+import com.vaadin.shared.communication.ServerRpc;
+
+/**
+ * RPC interface used for requesting container data to the client.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface DataRequestRpc extends ServerRpc {
+
+ /**
+ * Request rows from the server.
+ *
+ * @param firstRowIndex
+ * the index of the first requested row
+ * @param numberOfRows
+ * the number of requested rows
+ */
+ public void requestRows(int firstRowIndex, int numberOfRows);
+}
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 List<ColumnGroupState> groups = new ArrayList<ColumnGroupState>();
+
+ /**
+ * Is the header shown
+ */
+ public boolean headerVisible = true;
+
+ /**
+ * Is the footer shown
+ */
+ public boolean footerVisible = false;
+
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java
new file mode 100644
index 0000000000..3992b6611f
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java
@@ -0,0 +1,45 @@
+/*
+ * 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 data shared between the server and the client
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class ColumnGroupState implements Serializable {
+
+ /**
+ * The columns that is included in the group
+ */
+ public List<String> columns = new ArrayList<String>();
+
+ /**
+ * The header text of the group
+ */
+ public String header;
+
+ /**
+ * The footer text of the group
+ */
+ public String footer;
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java
new file mode 100644
index 0000000000..00cc93d371
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java
@@ -0,0 +1,53 @@
+/*
+ * 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.ClientRpc;
+
+/**
+ * Server-to-client RPC interface for the Grid component.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public interface GridClientRpc extends ClientRpc {
+
+ /**
+ * Command client Grid to scroll to a specific data row.
+ *
+ * @param row
+ * zero-based row index. If the row index is below zero or above
+ * the row count of the client-side data source, a client-side
+ * exception will be triggered. Since this exception has no
+ * handling by default, an out-of-bounds value will cause a
+ * client-side crash.
+ * @param destination
+ * desired placement of scrolled-to row. See the documentation
+ * for {@link ScrollDestination} for more information.
+ */
+ public void scrollToRow(int row, ScrollDestination destination);
+
+ /**
+ * Command client Grid to scroll to the first row.
+ */
+ public void scrollToStart();
+
+ /**
+ * Command client Grid to scroll to the last row.
+ */
+ public void scrollToEnd();
+
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java
new file mode 100644
index 0000000000..0301c5ead2
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+/**
+ * Column state DTO for transferring column properties from the server to the
+ * client
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class GridColumnState implements Serializable {
+
+ /**
+ * Id used by grid connector to map server side column with client side
+ * column
+ */
+ public String id;
+
+ /**
+ * Header caption for the column
+ */
+ public String header;
+
+ /**
+ * Footer caption for the column
+ */
+ public String footer;
+
+ /**
+ * Has the column been hidden. By default the column is visible.
+ */
+ public boolean visible = true;
+
+ /**
+ * Column width in pixels. Default column width is 100px.
+ */
+ public int width = 100;
+
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridConstants.java b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java
new file mode 100644
index 0000000000..5b88fad5a8
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java
@@ -0,0 +1,33 @@
+/*
+ * 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;
+
+/**
+ * Container class for common constants and default values used by the Grid
+ * component.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public final class GridConstants {
+
+ /**
+ * Default padding in pixels when scrolling programmatically, without an
+ * explicitly defined padding value.
+ */
+ public static final int DEFAULT_PADDING = 0;
+
+}
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
+ * <code>firstVisibleRow</code>
+ */
+ void setVisibleRows(int firstVisibleRow, int visibleRowCount);
+
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java
new file mode 100644
index 0000000000..93e602a539
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java
@@ -0,0 +1,64 @@
+/*
+ * 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.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.shared.AbstractComponentState;
+
+/**
+ * The shared state for the {@link com.vaadin.ui.components.grid.Grid} component
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class GridState extends AbstractComponentState {
+ {
+ // FIXME Grid currently does not support undefined size
+ width = "400px";
+ height = "400px";
+ }
+
+ /**
+ * Columns in grid. Column order implicitly deferred from list order.
+ */
+ public List<GridColumnState> columns = new ArrayList<GridColumnState>();
+
+ /**
+ * Is the column header row visible
+ */
+ public boolean columnHeadersVisible = true;
+
+ /**
+ * Is the column footer row visible
+ */
+ public boolean columnFootersVisible = false;
+
+ /**
+ * The column groups added to the grid
+ */
+ public List<ColumnGroupRowState> columnGroupRows = new ArrayList<ColumnGroupRowState>();
+
+ /**
+ * The id for the last frozen column.
+ *
+ * @see GridColumnState#id
+ */
+ public String lastFrozenColumnId = null;
+
+}
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.
+ * <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/src/com/vaadin/shared/ui/grid/ScrollDestination.java b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java
new file mode 100644
index 0000000000..decc2fab5f
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java
@@ -0,0 +1,55 @@
+/*
+ * 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;
+
+/**
+ * Enumeration, specifying the destinations that are supported when scrolling
+ * rows or columns into view.
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public enum ScrollDestination {
+
+ /**
+ * Scroll as little as possible to show the target element. If the element
+ * fits into view, this works as START or END depending on the current
+ * scroll position. If the element does not fit into view, this works as
+ * START.
+ */
+ ANY,
+
+ /**
+ * Scrolls so that the element is shown at the start of the viewport. The
+ * viewport will, however, not scroll beyond its contents.
+ */
+ START,
+
+ /**
+ * Scrolls so that the element is shown in the middle of the viewport. The
+ * viewport will, however, not scroll beyond its contents, given more
+ * elements than what the viewport is able to show at once. Under no
+ * circumstances will the viewport scroll before its first element.
+ */
+ MIDDLE,
+
+ /**
+ * Scrolls so that the element is shown at the end of the viewport. The
+ * viewport will, however, not scroll before its first element.
+ */
+ END
+
+}
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/BasicEscalator.html b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.html
new file mode 100644
index 0000000000..70aa0fe195
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.html
@@ -0,0 +1,176 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+<head profile="http://selenium-ide.openqa.org/profiles/test-case">
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<link rel="selenium.base" href="http://localhost:8888/" />
+<title>BasicEscalator</title>
+</head>
+<body>
+<table cellpadding="1" cellspacing="1" border="1">
+<thead>
+<tr><td rowspan="1" colspan="3">BasicEscalator</td></tr>
+</thead><tbody>
+<tr>
+ <td>open</td>
+ <td>/run/com.vaadin.tests.components.grid.BasicEscalator?restartApplication</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]</td>
+ <td>Row 0: 0,0 (0)</td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[17]/domChild[9]</td>
+ <td>Cell: 9,17 (179)</td>
+</tr>
+<tr>
+ <td>verifyTextNotPresent</td>
+ <td>Cell: 0,100</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyTextNotPresent</td>
+ <td>Cell: 0,101</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
+ <td>0</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
+ <td>1</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[18]/domChild[0]</td>
+ <td>Row 0: 0,100 (190)</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
+ <td>11</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[17]/domChild[0]</td>
+ <td>Row 11: 0,101 (200)</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
+ <td>0</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
+ <td>100</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[16]/domChild[0]</td>
+ <td>Row 0: 0,102 (210)</td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[1]/domChild[0]</td>
+ <td>Row 16: 0,118 (370)</td>
+</tr>
+<tr>
+ <td>scroll</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[0]</td>
+ <td>1109</td>
+</tr>
+<tr>
+ <td>verifyTextPresent</td>
+ <td>Row 56: 0,158</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyTextPresent</td>
+ <td>Row 72: 0,174</td>
+ <td></td>
+</tr>
+<tr>
+ <td>scroll</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[0]</td>
+ <td>3690</td>
+</tr>
+<tr>
+ <td>verifyTextPresent</td>
+ <td>Row 201: 0,99</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
+ <td>201</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
+ <td>1</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[1]/domChild[0]</td>
+ <td>Row 200: 0,98 (960)</td>
+</tr>
+<tr>
+ <td>verifyTextNotPresent</td>
+ <td>Row 201:</td>
+ <td></td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[3]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
+ <td>0</td>
+</tr>
+<tr>
+ <td>type</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[3]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
+ <td>2</td>
+</tr>
+<tr>
+ <td>click</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[3]/VHorizontalLayout[0]/Slot[2]/VButton[0]</td>
+ <td></td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[16]/domChild[0]</td>
+ <td>Row 184: 10,82 (974)</td>
+</tr>
+<tr>
+ <td>verifyText</td>
+ <td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[1]/domChild[0]</td>
+ <td>Row 200: 10,98 (1006)</td>
+</tr>
+</tbody></table>
+</body>
+</html>
diff --git a/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java
new file mode 100644
index 0000000000..fc9c217328
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java
@@ -0,0 +1,304 @@
+/*
+ * 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.tests.components.grid;
+
+import java.util.Random;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.server.grid.TestGrid;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.Layout;
+import com.vaadin.ui.NativeSelect;
+import com.vaadin.ui.TextField;
+
+/**
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+@Widgetset(TestingWidgetSet.NAME)
+public class BasicEscalator extends AbstractTestUI {
+ public static final String ESCALATOR = "escalator";
+ public static final String INSERT_ROWS_OFFSET = "iro";
+ public static final String INSERT_ROWS_AMOUNT = "ira";
+ public static final String INSERT_ROWS_BUTTON = "irb";
+
+ private final Random random = new Random();
+
+ @Override
+ protected void setup(final VaadinRequest request) {
+ final TestGrid grid = new TestGrid();
+ grid.setId(ESCALATOR);
+ addComponent(grid);
+
+ final Layout insertRowsLayout = new HorizontalLayout();
+ final TextField insertRowsOffset = new TextField();
+ insertRowsOffset.setId(INSERT_ROWS_OFFSET);
+ insertRowsLayout.addComponent(insertRowsOffset);
+ final TextField insertRowsAmount = new TextField();
+ insertRowsAmount.setId(INSERT_ROWS_AMOUNT);
+ insertRowsLayout.addComponent(insertRowsAmount);
+ insertRowsLayout.addComponent(new Button("insert rows",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(final ClickEvent event) {
+ final int offset = Integer.parseInt(insertRowsOffset
+ .getValue());
+ final int amount = Integer.parseInt(insertRowsAmount
+ .getValue());
+ grid.insertRows(offset, amount);
+ }
+ }) {
+ {
+ setId(INSERT_ROWS_BUTTON);
+ }
+ });
+ addComponent(insertRowsLayout);
+
+ final Layout removeRowsLayout = new HorizontalLayout();
+ final TextField removeRowsOffset = new TextField();
+ removeRowsLayout.addComponent(removeRowsOffset);
+ final TextField removeRowsAmount = new TextField();
+ removeRowsLayout.addComponent(removeRowsAmount);
+ removeRowsLayout.addComponent(new Button("remove rows",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(final ClickEvent event) {
+ final int offset = Integer.parseInt(removeRowsOffset
+ .getValue());
+ final int amount = Integer.parseInt(removeRowsAmount
+ .getValue());
+ grid.removeRows(offset, amount);
+ }
+ }));
+ addComponent(removeRowsLayout);
+
+ final Layout insertColumnsLayout = new HorizontalLayout();
+ final TextField insertColumnsOffset = new TextField();
+ insertColumnsLayout.addComponent(insertColumnsOffset);
+ final TextField insertColumnsAmount = new TextField();
+ insertColumnsLayout.addComponent(insertColumnsAmount);
+ insertColumnsLayout.addComponent(new Button("insert columns",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(final ClickEvent event) {
+ final int offset = Integer.parseInt(insertColumnsOffset
+ .getValue());
+ final int amount = Integer.parseInt(insertColumnsAmount
+ .getValue());
+ grid.insertColumns(offset, amount);
+ }
+ }));
+ addComponent(insertColumnsLayout);
+
+ final Layout removeColumnsLayout = new HorizontalLayout();
+ final TextField removeColumnsOffset = new TextField();
+ removeColumnsLayout.addComponent(removeColumnsOffset);
+ final TextField removeColumnsAmount = new TextField();
+ removeColumnsLayout.addComponent(removeColumnsAmount);
+ removeColumnsLayout.addComponent(new Button("remove columns",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(final ClickEvent event) {
+ final int offset = Integer.parseInt(removeColumnsOffset
+ .getValue());
+ final int amount = Integer.parseInt(removeColumnsAmount
+ .getValue());
+ grid.removeColumns(offset, amount);
+ }
+ }));
+ addComponent(removeColumnsLayout);
+
+ final HorizontalLayout rowScroll = new HorizontalLayout();
+ final NativeSelect destination = new NativeSelect();
+ destination.setNullSelectionAllowed(false);
+ destination.addItem("any");
+ destination.setValue("any");
+ destination.addItem("start");
+ destination.addItem("end");
+ destination.addItem("middle");
+ rowScroll.addComponent(destination);
+ final TextField rowIndex = new TextField();
+ rowScroll.addComponent(rowIndex);
+ final TextField rowPadding = new TextField();
+ rowScroll.addComponent(rowPadding);
+ rowScroll.addComponent(new Button("scroll to row",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(final ClickEvent event) {
+ int index;
+ try {
+ index = Integer.parseInt(rowIndex.getValue());
+ } catch (NumberFormatException e) {
+ index = 0;
+ }
+
+ int padding;
+ try {
+ padding = Integer.parseInt(rowPadding.getValue());
+ } catch (NumberFormatException e) {
+ padding = 0;
+ }
+
+ grid.scrollToRow(index,
+ (String) destination.getValue(), padding);
+ }
+ }));
+ addComponent(rowScroll);
+
+ final HorizontalLayout colScroll = new HorizontalLayout();
+ final NativeSelect colDestination = new NativeSelect();
+ colDestination.setNullSelectionAllowed(false);
+ colDestination.addItem("any");
+ colDestination.setValue("any");
+ colDestination.addItem("start");
+ colDestination.addItem("end");
+ colDestination.addItem("middle");
+ colScroll.addComponent(colDestination);
+ final TextField colIndex = new TextField();
+ colScroll.addComponent(colIndex);
+ final TextField colPadding = new TextField();
+ colScroll.addComponent(colPadding);
+ colScroll.addComponent(new Button("scroll to column",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(final ClickEvent event) {
+ int index;
+ try {
+ index = Integer.parseInt(colIndex.getValue());
+ } catch (NumberFormatException e) {
+ index = 0;
+ }
+
+ int padding;
+ try {
+ padding = Integer.parseInt(colPadding.getValue());
+ } catch (NumberFormatException e) {
+ padding = 0;
+ }
+
+ grid.scrollToColumn(index,
+ (String) colDestination.getValue(), padding);
+ }
+ }));
+ addComponent(colScroll);
+
+ final TextField freezeCount = new TextField();
+ freezeCount.setConverter(Integer.class);
+ freezeCount.setNullRepresentation("");
+ addComponent(new HorizontalLayout(freezeCount, new Button(
+ "set frozen columns", new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.setFrozenColumns(((Integer) freezeCount
+ .getConvertedValue()).intValue());
+ freezeCount.setValue(null);
+ }
+ })));
+
+ addComponent(new Button("Resize randomly", new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ int width = random.nextInt(300) + 500;
+ int height = random.nextInt(300) + 200;
+ grid.setWidth(width + "px");
+ grid.setHeight(height + "px");
+ }
+ }));
+
+ addComponent(new Button("Random headers count",
+ new Button.ClickListener() {
+ private int headers = 1;
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ int diff = 0;
+ while (diff == 0) {
+ final int nextHeaders = random.nextInt(4);
+ diff = nextHeaders - headers;
+ headers = nextHeaders;
+ }
+ if (diff > 0) {
+ grid.insertHeaders(0, diff);
+ } else if (diff < 0) {
+ grid.removeHeaders(0, -diff);
+ }
+ }
+ }));
+
+ addComponent(new Button("Random footers count",
+ new Button.ClickListener() {
+ private int footers = 1;
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ int diff = 0;
+ while (diff == 0) {
+ final int nextFooters = random.nextInt(4);
+ diff = nextFooters - footers;
+ footers = nextFooters;
+ }
+ if (diff > 0) {
+ grid.insertFooters(0, diff);
+ } else if (diff < 0) {
+ grid.removeFooters(0, -diff);
+ }
+ }
+ }));
+
+ final Layout resizeColumnsLayout = new HorizontalLayout();
+ final TextField resizeColumnIndex = new TextField();
+ resizeColumnsLayout.addComponent(resizeColumnIndex);
+ final TextField resizeColumnPx = new TextField();
+ resizeColumnsLayout.addComponent(resizeColumnPx);
+ resizeColumnsLayout.addComponent(new Button("resize column",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(final ClickEvent event) {
+ final int index = Integer.parseInt(resizeColumnIndex
+ .getValue());
+ final int px = Integer.parseInt(resizeColumnPx
+ .getValue());
+ grid.setColumnWidth(index, px);
+ }
+ }));
+ addComponent(resizeColumnsLayout);
+
+ addComponent(new Button("Autoresize columns",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.calculateColumnWidths();
+ }
+ }));
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return null;
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return null;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
new file mode 100644
index 0000000000..c28feb8d10
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
@@ -0,0 +1,343 @@
+/*
+ * 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.tests.components.grid;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.tests.components.AbstractComponentTest;
+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;
+
+/**
+ * Tests the basic features like columns, footers and headers
+ *
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class GridBasicFeatures extends AbstractComponentTest<Grid> {
+
+ private final int COLUMNS = 10;
+
+ private int columnGroupRows = 0;
+
+ private final int ROWS = 1000;
+
+ private IndexedContainer ds;
+
+ @Override
+ protected Grid constructComponent() {
+
+ // Build data source
+ ds = new IndexedContainer();
+
+ for (int col = 0; col < COLUMNS; col++) {
+ 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(getColumnProperty(col)).setValue(
+ "(" + row + ", " + col + ")");
+ }
+ }
+
+ // Create grid
+ Grid grid = new Grid(ds);
+
+ // Add footer values (header values are automatically created)
+ for (int col = 0; col < COLUMNS; col++) {
+ grid.getColumn(getColumnProperty(col)).setFooterCaption(
+ "Footer " + col);
+ }
+
+ // Set varying column widths
+ for (int col = 0; col < COLUMNS; col++) {
+ grid.getColumn("Column" + col).setWidth(100 + col * 50);
+ }
+
+ createGridActions();
+
+ createColumnActions();
+
+ createHeaderActions();
+
+ createFooterActions();
+
+ createColumnGroupActions();
+
+ createRowActions();
+
+ return grid;
+ }
+
+ protected void createGridActions() {
+ LinkedHashMap<String, String> primaryStyleNames = new LinkedHashMap<String, String>();
+ primaryStyleNames.put("v-grid", "v-grid");
+ primaryStyleNames.put("v-escalator", "v-escalator");
+ primaryStyleNames.put("my-grid", "my-grid");
+
+ createMultiClickAction("Primary style name", "State",
+ primaryStyleNames, new Command<Grid, String>() {
+
+ @Override
+ public void execute(Grid grid, String value, Object data) {
+ grid.setPrimaryStyleName(value);
+
+ }
+ }, primaryStyleNames.get("v-grid"));
+ }
+
+ protected void createHeaderActions() {
+ createCategory("Headers", null);
+
+ createBooleanAction("Visible", "Headers", true,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid grid, Boolean value, Object data) {
+ grid.setColumnHeadersVisible(value);
+ }
+ });
+ }
+
+ protected void createFooterActions() {
+ createCategory("Footers", null);
+
+ createBooleanAction("Visible", "Footers", false,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid grid, Boolean value, Object data) {
+ grid.setColumnFootersVisible(value);
+ }
+ });
+ }
+
+ protected void createColumnActions() {
+ createCategory("Columns", null);
+
+ for (int c = 0; c < COLUMNS; c++) {
+ createCategory(getColumnProperty(c), "Columns");
+
+ createBooleanAction("Visible", getColumnProperty(c), true,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid grid, Boolean value,
+ Object columnIndex) {
+ Object propertyId = (new ArrayList(grid
+ .getContainerDatasource()
+ .getContainerPropertyIds())
+ .get((Integer) columnIndex));
+ GridColumn column = grid.getColumn(propertyId);
+ column.setVisible(!column.isVisible());
+ }
+ }, c);
+
+ createClickAction("Remove", getColumnProperty(c),
+ new Command<Grid, String>() {
+
+ @Override
+ public void execute(Grid grid, String value, Object data) {
+ grid.getContainerDatasource()
+ .removeContainerProperty("Column" + data);
+ }
+ }, null, c);
+
+ createClickAction("Freeze", getColumnProperty(c),
+ new Command<Grid, String>() {
+
+ @Override
+ public void execute(Grid grid, String value, Object data) {
+ grid.setLastFrozenPropertyId("Column" + data);
+ }
+ }, null, c);
+
+ createCategory("Column" + c + " Width", getColumnProperty(c));
+
+ createClickAction("Auto", "Column" + c + " Width",
+ new Command<Grid, Integer>() {
+
+ @Override
+ public void execute(Grid grid, Integer value,
+ Object columnIndex) {
+ Object propertyId = (new ArrayList(grid
+ .getContainerDatasource()
+ .getContainerPropertyIds())
+ .get((Integer) columnIndex));
+ GridColumn column = grid.getColumn(propertyId);
+ column.setWidthUndefined();
+ }
+ }, -1, c);
+
+ for (int w = 50; w < 300; w += 50) {
+ createClickAction(w + "px", "Column" + c + " Width",
+ new Command<Grid, Integer>() {
+
+ @Override
+ public void execute(Grid grid, Integer value,
+ Object columnIndex) {
+ Object propertyId = (new ArrayList(grid
+ .getContainerDatasource()
+ .getContainerPropertyIds())
+ .get((Integer) columnIndex));
+ GridColumn column = grid.getColumn(propertyId);
+ column.setWidth(value);
+ }
+ }, w, c);
+ }
+ }
+ }
+
+ private static String getColumnProperty(int c) {
+ return "Column" + c;
+ }
+
+ protected void createColumnGroupActions() {
+ createCategory("Column groups", null);
+
+ createClickAction("Add group row", "Column groups",
+ new Command<Grid, String>() {
+
+ @Override
+ public void execute(Grid grid, String value, Object data) {
+ final ColumnGroupRow row = grid.addColumnGroupRow();
+ columnGroupRows++;
+ createCategory("Column group row " + columnGroupRows,
+ "Column groups");
+
+ createBooleanAction("Header Visible",
+ "Column group row " + columnGroupRows, true,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid grid,
+ Boolean value, Object columnIndex) {
+ row.setHeaderVisible(value);
+ }
+ }, row);
+
+ createBooleanAction("Footer Visible",
+ "Column group row " + columnGroupRows, false,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid grid,
+ Boolean value, Object columnIndex) {
+ row.setFooterVisible(value);
+ }
+ }, row);
+
+ for (int i = 0; i < COLUMNS; i++) {
+ final int columnIndex = i;
+ createClickAction("Group Column " + columnIndex
+ + " & " + (columnIndex + 1),
+ "Column group row " + columnGroupRows,
+ new Command<Grid, Integer>() {
+
+ @Override
+ public void execute(Grid c,
+ Integer value, Object data) {
+ final ColumnGroup group = row
+ .addGroup(
+ "Column" + value,
+ "Column"
+ + (value + 1));
+
+ group.setHeaderCaption("Column "
+ + value + " & "
+ + (value + 1));
+
+ group.setFooterCaption("Column "
+ + value + " & "
+ + (value + 1));
+ }
+ }, i, row);
+ }
+ }
+ }, null, null);
+
+ }
+
+ 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;
+ }
+
+ @Override
+ protected Class<Grid> getTestClass() {
+ return Grid.class;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java
new file mode 100644
index 0000000000..bc43f2be98
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java
@@ -0,0 +1,373 @@
+/*
+ * 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.tests.components.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+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;
+
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+/**
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridBasicFeaturesTest extends MultiBrowserTest {
+
+ @Test
+ public void testColumnHeaderCaptions() throws Exception {
+ openTestURL();
+
+ // Column headers should be visible
+ List<WebElement> cells = getGridHeaderRowCells();
+ assertEquals(10, cells.size());
+ assertEquals("Column0", cells.get(0).getText());
+ assertEquals("Column1", cells.get(1).getText());
+ assertEquals("Column2", cells.get(2).getText());
+ }
+
+ @Test
+ public void testColumnFooterCaptions() throws Exception {
+ openTestURL();
+
+ // footer row should by default be hidden
+ List<WebElement> cells = getGridFooterRowCells();
+ assertEquals(0, cells.size());
+
+ // Open footer row
+ selectMenuPath("Component", "Footers", "Visible");
+
+ // Footers should now be visible
+ cells = getGridFooterRowCells();
+ assertEquals(10, cells.size());
+ assertEquals("Footer 0", cells.get(0).getText());
+ assertEquals("Footer 1", cells.get(1).getText());
+ assertEquals("Footer 2", cells.get(2).getText());
+ }
+
+ @Test
+ public void testColumnGroupHeaders() throws Exception {
+ openTestURL();
+
+ // Hide column headers for this test
+ selectMenuPath("Component", "Headers", "Visible");
+
+ List<WebElement> cells = getGridHeaderRowCells();
+
+ // header row should be empty
+ assertEquals(0, cells.size());
+
+ // add a group row
+ selectMenuPath("Component", "Column groups", "Add group row");
+
+ // Empty group row cells should be present
+ cells = getGridHeaderRowCells();
+ assertEquals(10, cells.size());
+
+ // Group columns 0 & 1
+ selectMenuPath("Component", "Column groups", "Column group row 1",
+ "Group Column 0 & 1");
+
+ cells = getGridHeaderRowCells();
+ assertEquals("Column 0 & 1", cells.get(0).getText());
+ }
+
+ @Test
+ public void testColumnGroupFooters() throws Exception {
+ openTestURL();
+
+ // add a group row
+ selectMenuPath("Component", "Column groups", "Add group row");
+
+ // Set footer visible
+ selectMenuPath("Component", "Column groups", "Column group row 1",
+ "Footer Visible");
+
+ // Group columns 0 & 1
+ selectMenuPath("Component", "Column groups", "Column group row 1",
+ "Group Column 0 & 1");
+
+ List<WebElement> cells = getGridFooterRowCells();
+ assertEquals("Column 0 & 1", cells.get(0).getText());
+ }
+
+ @Test
+ public void testGroupingSameColumnsOnRowThrowsException() throws Exception {
+ openTestURL();
+
+ // add a group row
+ selectMenuPath("Component", "Column groups", "Add group row");
+
+ // Group columns 0 & 1
+ selectMenuPath("Component", "Column groups", "Column group row 1",
+ "Group Column 0 & 1");
+
+ // Group columns 1 & 2 shoud fail
+ selectMenuPath("Component", "Column groups", "Column group row 1",
+ "Group Column 1 & 2");
+
+ assertTrue(getLogRow(0)
+ .contains(
+ "Exception occured, java.lang.IllegalArgumentExceptionColumn Column1 already belongs to another group."));
+ }
+
+ @Test
+ public void testHidingColumn() throws Exception {
+ openTestURL();
+
+ // Column 0 should be visible
+ List<WebElement> cells = getGridHeaderRowCells();
+ assertEquals("Column0", cells.get(0).getText());
+
+ // Hide column 0
+ selectMenuPath("Component", "Columns", "Column0", "Visible");
+
+ // Column 1 should now be the first cell
+ cells = getGridHeaderRowCells();
+ assertEquals("Column1", cells.get(0).getText());
+ }
+
+ @Test
+ public void testRemovingColumn() throws Exception {
+ openTestURL();
+
+ // Column 0 should be visible
+ List<WebElement> cells = getGridHeaderRowCells();
+ assertEquals("Column0", cells.get(0).getText());
+
+ // Hide column 0
+ selectMenuPath("Component", "Columns", "Column0", "Remove");
+
+ // Column 1 should now be the first cell
+ cells = getGridHeaderRowCells();
+ assertEquals("Column1", cells.get(0).getText());
+ }
+
+ @Test
+ public void testFreezingColumn() throws Exception {
+ openTestURL();
+
+ // Freeze column 2
+ selectMenuPath("Component", "Columns", "Column2", "Freeze");
+
+ WebElement cell = getBodyCellByRowAndColumn(1, 1);
+ assertTrue(cell.getAttribute("class").contains("frozen"));
+
+ cell = getBodyCellByRowAndColumn(1, 2);
+ assertTrue(cell.getAttribute("class").contains("frozen"));
+ }
+
+ @Test
+ public void testInitialColumnWidths() throws Exception {
+ openTestURL();
+
+ // Default borders and margins implemented by escalator
+ int cellBorder = 1 + 1;
+ int cellMargin = 2 + 2;
+
+ WebElement cell = getBodyCellByRowAndColumn(1, 1);
+ assertEquals((100 - cellBorder - cellMargin) + "px",
+ cell.getCssValue("width"));
+
+ cell = getBodyCellByRowAndColumn(1, 2);
+ assertEquals((150 - cellBorder - cellMargin) + "px",
+ cell.getCssValue("width"));
+
+ cell = getBodyCellByRowAndColumn(1, 3);
+ assertEquals((200 - cellBorder - cellMargin) + "px",
+ cell.getCssValue("width"));
+ }
+
+ @Test
+ public void testColumnWidths() throws Exception {
+ openTestURL();
+
+ // Default borders and margins implemented by escalator
+ int cellBorder = 1 + 1;
+ int cellMargin = 2 + 2;
+
+ // Default column width is 100px
+ WebElement cell = getBodyCellByRowAndColumn(1, 1);
+ assertEquals((100 - cellBorder - cellMargin) + "px",
+ cell.getCssValue("width"));
+
+ // Set first column to be 200px wide
+ selectMenuPath("Component", "Columns", "Column0", "Column0 Width",
+ "200px");
+
+ cell = getBodyCellByRowAndColumn(1, 1);
+ assertEquals((200 - cellBorder - cellMargin) + "px",
+ cell.getCssValue("width"));
+
+ // Set second column to be 150px wide
+ selectMenuPath("Component", "Columns", "Column1", "Column1 Width",
+ "150px");
+ cell = getBodyCellByRowAndColumn(1, 2);
+ assertEquals((150 - cellBorder - cellMargin) + "px",
+ cell.getCssValue("width"));
+
+ // Set first column to be auto sized (defaults to 100px currently)
+ selectMenuPath("Component", "Columns", "Column0", "Column0 Width",
+ "Auto");
+
+ cell = getBodyCellByRowAndColumn(1, 1);
+ assertEquals((100 - cellBorder - cellMargin) + "px",
+ cell.getCssValue("width"));
+ }
+
+ @Test
+ public void testPrimaryStyleNames() throws Exception {
+ openTestURL();
+
+ // v-grid is default primary style namea
+ assertPrimaryStylename("v-grid");
+
+ selectMenuPath("Component", "State", "Primary style name",
+ "v-escalator");
+ assertPrimaryStylename("v-escalator");
+
+ selectMenuPath("Component", "State", "Primary style name", "my-grid");
+ assertPrimaryStylename("my-grid");
+
+ selectMenuPath("Component", "State", "Primary style name", "v-grid");
+ 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));
+
+ String tableWrapperStyleName = getTableWrapper().getAttribute("class");
+ assertTrue(tableWrapperStyleName.contains(stylename + "-tablewrapper"));
+
+ String hscrollStyleName = getHorizontalScroller().getAttribute("class");
+ assertTrue(hscrollStyleName.contains(stylename + "-scroller"));
+ assertTrue(hscrollStyleName
+ .contains(stylename + "-scroller-horizontal"));
+
+ String vscrollStyleName = getVerticalScroller().getAttribute("class");
+ assertTrue(vscrollStyleName.contains(stylename + "-scroller"));
+ assertTrue(vscrollStyleName.contains(stylename + "-scroller-vertical"));
+ }
+
+ private WebElement getBodyCellByRowAndColumn(int row, int column) {
+ return getDriver().findElement(
+ By.xpath("//div[@id='testComponent']//tbody/tr[" + row
+ + "]/td[" + column + "]"));
+ }
+
+ private void selectSubMenu(String menuCaption) {
+ selectMenu(menuCaption);
+ new Actions(getDriver()).moveByOffset(100, 0).build().perform();
+ }
+
+ private void selectMenu(String menuCaption) {
+ getDriver().findElement(
+ By.xpath("//span[text() = '" + menuCaption + "']")).click();
+ }
+
+ private void selectMenuPath(String... menuCaptions) {
+ selectMenu(menuCaptions[0]);
+ for (int i = 1; i < menuCaptions.length; i++) {
+ selectSubMenu(menuCaptions[i]);
+ }
+ }
+
+ private WebElement getVerticalScroller() {
+ return getDriver().findElement(
+ By.xpath("//div[@id='testComponent']/div[1]"));
+ }
+
+ private WebElement getHorizontalScroller() {
+ return getDriver().findElement(
+ By.xpath("//div[@id='testComponent']/div[2]"));
+ }
+
+ private WebElement getTableWrapper() {
+ return getDriver().findElement(
+ By.xpath("//div[@id='testComponent']/div[3]"));
+ }
+
+ private WebElement getGridElement() {
+ return getDriver().findElement(By.id("testComponent"));
+ }
+
+ private List<WebElement> getGridHeaderRowCells() {
+ return getDriver().findElements(
+ By.xpath("//div[@id='testComponent']//thead//th"));
+ }
+
+ private List<WebElement> getGridFooterRowCells() {
+ return getDriver().findElements(
+ By.xpath("//div[@id='testComponent']//tfoot//td"));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java b/uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java
new file mode 100644
index 0000000000..66e7651f76
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java
@@ -0,0 +1,111 @@
+/*
+ * 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.tests.components.grid;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+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;
+
+/**
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridColumnGroups extends AbstractTestUI {
+
+ private final int COLUMNS = 4;
+
+ @Override
+ protected void setup(VaadinRequest request) {
+
+ // Setup grid
+ IndexedContainer ds = new IndexedContainer();
+ for (int col = 0; col < COLUMNS; col++) {
+ ds.addContainerProperty("Column" + col, String.class, "");
+ }
+ Grid grid = new Grid(ds);
+ addComponent(grid);
+
+ /*-
+ * ---------------------------------------------
+ * | Header 1 | <- Auxiliary row 2
+ * |-------------------------------------------|
+ * | Header 2 | Header 3 | <- Auxiliary row 1
+ * |-------------------------------------------|
+ * | Column 1 | Column 2 | Column 3 | Column 4 | <- Column headers
+ * --------------------------------------------|
+ * | ... | ... | ... | ... |
+ * | ... | ... | ... | ... |
+ * --------------------------------------------|
+ * | Column 1 | Column 2 | Column 3 | Column 4 | <- Column footers
+ * --------------------------------------------|
+ * | Footer 2 | Footer 3 | <- Auxiliary row 1
+ * --------------------------------------------|
+ * | Footer 1 | <- Auxiliary row 2
+ * ---------------------------------------------
+ -*/
+
+ // Set column footers (headers are generated automatically)
+ grid.setColumnFootersVisible(true);
+ for (Object propertyId : ds.getContainerPropertyIds()) {
+ GridColumn column = grid.getColumn(propertyId);
+ column.setFooterCaption(String.valueOf(propertyId));
+ }
+
+ // First auxiliary row
+ ColumnGroupRow auxRow1 = grid.addColumnGroupRow();
+
+ // Using property id to create a column group
+ ColumnGroup columns12 = auxRow1.addGroup("Column0", "Column1");
+ columns12.setHeaderCaption("Header 2");
+ columns12.setFooterCaption("Footer 2");
+
+ // Using grid columns to create a column group
+ GridColumn column3 = grid.getColumn("Column2");
+ GridColumn column4 = grid.getColumn("Column3");
+ ColumnGroup columns34 = auxRow1.addGroup(column3, column4);
+ columns34.setHeaderCaption("Header 3");
+ columns34.setFooterCaption("Footer 3");
+
+ // Second auxiliary row
+ ColumnGroupRow auxRow2 = grid.addColumnGroupRow();
+
+ // Using previous groups to create a column group
+ ColumnGroup columns1234 = auxRow2.addGroup(columns12, columns34);
+ columns1234.setHeaderCaption("Header 1");
+ columns1234.setFooterCaption("Footer 1");
+
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Grid should support headers and footer groups";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return 12894;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java
new file mode 100644
index 0000000000..d514fbd0c5
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java
@@ -0,0 +1,115 @@
+/*
+ * 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.tests.components.grid;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.HorizontalLayout;
+import com.vaadin.ui.VerticalLayout;
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ *
+ */
+@SuppressWarnings("serial")
+public class GridScrolling extends AbstractTestUI {
+
+ private Grid grid;
+
+ private IndexedContainer ds;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void setup(VaadinRequest request) {
+ // Build data source
+ ds = new IndexedContainer();
+
+ for (int col = 0; col < 5; col++) {
+ ds.addContainerProperty("col" + col, String.class, "");
+ }
+
+ for (int row = 0; row < 65536; row++) {
+ Item item = ds.addItem(Integer.valueOf(row));
+ for (int col = 0; col < 5; col++) {
+ item.getItemProperty("col" + col).setValue(
+ "(" + row + ", " + col + ")");
+ }
+ }
+
+ grid = new Grid(ds);
+
+ HorizontalLayout hl = new HorizontalLayout();
+ hl.addComponent(grid);
+ hl.setMargin(true);
+ hl.setSpacing(true);
+
+ VerticalLayout vl = new VerticalLayout();
+ vl.setSpacing(true);
+
+ // Add scroll buttons
+ Button scrollUpButton = new Button("Top", new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.scrollToStart();
+ }
+ });
+ scrollUpButton.setSizeFull();
+ vl.addComponent(scrollUpButton);
+
+ for (int i = 1; i < 7; ++i) {
+ final int row = (ds.size() / 7) * i;
+ Button scrollButton = new Button("Scroll to row " + row,
+ new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.scrollToItem(Integer.valueOf(row),
+ ScrollDestination.MIDDLE);
+ }
+ });
+ scrollButton.setSizeFull();
+ vl.addComponent(scrollButton);
+ }
+
+ Button scrollDownButton = new Button("Bottom", new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.scrollToEnd();
+ }
+ });
+ scrollDownButton.setSizeFull();
+ vl.addComponent(scrollDownButton);
+
+ hl.addComponent(vl);
+ addComponent(hl);
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Test Grid programmatic scrolling features";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return 13327;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java
new file mode 100644
index 0000000000..df2b8eb075
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.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.tests.widgetset.client.grid;
+
+import com.vaadin.shared.communication.ClientRpc;
+
+public interface TestGridClientRpc extends ClientRpc {
+ void insertRows(int offset, int amount);
+
+ void removeRows(int offset, int amount);
+
+ void insertColumns(int offset, int amount);
+
+ void removeColumns(int offset, int amount);
+
+ void scrollToRow(int index, String destination, int padding);
+
+ void scrollToColumn(int index, String destination, int padding);
+
+ void setFrozenColumns(int frozenColumns);
+
+ void insertHeaders(int index, int amount);
+
+ void removeHeaders(int index, int amount);
+
+ void insertFooters(int index, int amount);
+
+ void removeFooters(int index, int amount);
+
+ void setColumnWidth(int index, int px);
+
+ void calculateColumnWidths();
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java
new file mode 100644
index 0000000000..b8ea380301
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java
@@ -0,0 +1,127 @@
+/*
+ * 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.tests.widgetset.client.grid;
+
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.tests.widgetset.server.grid.TestGrid;
+
+/**
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+@Connect(TestGrid.class)
+public class TestGridConnector extends AbstractComponentConnector {
+ @Override
+ protected void init() {
+ super.init();
+ registerRpc(TestGridClientRpc.class, new TestGridClientRpc() {
+ @Override
+ public void insertRows(int offset, int amount) {
+ getWidget().insertRows(offset, amount);
+ }
+
+ @Override
+ public void removeRows(int offset, int amount) {
+ getWidget().removeRows(offset, amount);
+ }
+
+ @Override
+ public void removeColumns(int offset, int amount) {
+ getWidget().removeColumns(offset, amount);
+ }
+
+ @Override
+ public void insertColumns(int offset, int amount) {
+ getWidget().insertColumns(offset, amount);
+ }
+
+ @Override
+ public void scrollToRow(int index, String destination, int padding) {
+ getWidget().scrollToRow(index, getDestination(destination),
+ padding);
+ }
+
+ @Override
+ public void scrollToColumn(int index, String destination,
+ int padding) {
+ getWidget().scrollToColumn(index, getDestination(destination),
+ padding);
+ }
+
+ private ScrollDestination getDestination(String destination) {
+ final ScrollDestination d;
+ if (destination.equals("start")) {
+ d = ScrollDestination.START;
+ } else if (destination.equals("middle")) {
+ d = ScrollDestination.MIDDLE;
+ } else if (destination.equals("end")) {
+ d = ScrollDestination.END;
+ } else {
+ d = ScrollDestination.ANY;
+ }
+ return d;
+ }
+
+ @Override
+ public void setFrozenColumns(int frozenColumns) {
+ getWidget().getColumnConfiguration().setFrozenColumnCount(
+ frozenColumns);
+ }
+
+ @Override
+ public void insertHeaders(int index, int amount) {
+ getWidget().getHeader().insertRows(index, amount);
+ }
+
+ @Override
+ public void removeHeaders(int index, int amount) {
+ getWidget().getHeader().removeRows(index, amount);
+ }
+
+ @Override
+ public void insertFooters(int index, int amount) {
+ getWidget().getFooter().insertRows(index, amount);
+ }
+
+ @Override
+ public void removeFooters(int index, int amount) {
+ getWidget().getFooter().removeRows(index, amount);
+ }
+
+ @Override
+ public void setColumnWidth(int index, int px) {
+ getWidget().getColumnConfiguration().setColumnWidth(index, px);
+ }
+
+ @Override
+ public void calculateColumnWidths() {
+ getWidget().calculateColumnWidths();
+ }
+ });
+ }
+
+ @Override
+ public VTestGrid getWidget() {
+ return (VTestGrid) super.getWidget();
+ }
+
+ @Override
+ public TestGridState getState() {
+ return (TestGridState) super.getState();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java
new file mode 100644
index 0000000000..73d6ba311c
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java
@@ -0,0 +1,29 @@
+/*
+ * 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.tests.widgetset.client.grid;
+
+import com.vaadin.shared.AbstractComponentState;
+
+/**
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class TestGridState extends AbstractComponentState {
+ public static final String DEFAULT_HEIGHT = "400.0px";
+
+ /* TODO: this should be "100%" before setting final. */
+ public static final String DEFAULT_WIDTH = "800.0px";
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java
new file mode 100644
index 0000000000..0230367b85
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java
@@ -0,0 +1,216 @@
+package com.vaadin.tests.widgetset.client.grid;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.vaadin.client.ui.grid.Cell;
+import com.vaadin.client.ui.grid.ColumnConfiguration;
+import com.vaadin.client.ui.grid.Escalator;
+import com.vaadin.client.ui.grid.EscalatorUpdater;
+import com.vaadin.client.ui.grid.Row;
+import com.vaadin.client.ui.grid.RowContainer;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+
+public class VTestGrid extends Composite {
+
+ private static class Data {
+ private int columnCounter = 0;
+ private int rowCounter = 0;
+ private final List<Integer> columns = new ArrayList<Integer>();
+ private final List<Integer> rows = new ArrayList<Integer>();
+
+ @SuppressWarnings("boxing")
+ public void insertRows(final int offset, final int amount) {
+ final List<Integer> newRows = new ArrayList<Integer>();
+ for (int i = 0; i < amount; i++) {
+ newRows.add(rowCounter++);
+ }
+ rows.addAll(offset, newRows);
+ }
+
+ @SuppressWarnings("boxing")
+ public void insertColumns(final int offset, final int amount) {
+ final List<Integer> newColumns = new ArrayList<Integer>();
+ for (int i = 0; i < amount; i++) {
+ newColumns.add(columnCounter++);
+ }
+ columns.addAll(offset, newColumns);
+ }
+
+ public EscalatorUpdater createHeaderUpdater() {
+ return new EscalatorUpdater() {
+ @Override
+ public void updateCells(final Row row,
+ final Iterable<Cell> cellsToUpdate) {
+ for (final Cell cell : cellsToUpdate) {
+ if (cell.getColumn() % 3 == 0) {
+ cell.setColSpan(2);
+ }
+
+ final Integer columnName = columns
+ .get(cell.getColumn());
+ cell.getElement().setInnerText("Header " + columnName);
+ }
+ }
+ };
+ }
+
+ public EscalatorUpdater createFooterUpdater() {
+ return new EscalatorUpdater() {
+ @Override
+ public void updateCells(final Row row,
+ final Iterable<Cell> cellsToUpdate) {
+ for (final Cell cell : cellsToUpdate) {
+ if (cell.getColumn() % 3 == 1) {
+ cell.setColSpan(2);
+ }
+
+ final Integer columnName = columns
+ .get(cell.getColumn());
+ cell.getElement().setInnerText("Footer " + columnName);
+ }
+ }
+ };
+ }
+
+ public EscalatorUpdater createBodyUpdater() {
+ return new EscalatorUpdater() {
+ private int i = 0;
+
+ public void renderCell(final Cell cell) {
+ final Integer columnName = columns.get(cell.getColumn());
+ final Integer rowName = rows.get(cell.getRow());
+ final String cellInfo = columnName + "," + rowName + " ("
+ + i + ")";
+
+ if (cell.getColumn() > 0) {
+ cell.getElement().setInnerText("Cell: " + cellInfo);
+ } else {
+ cell.getElement().setInnerText(
+ "Row " + cell.getRow() + ": " + cellInfo);
+ }
+
+ if (cell.getColumn() % 3 == cell.getRow() % 3) {
+ cell.setColSpan(3);
+ }
+
+ final double c = i * .1;
+ final int r = (int) ((Math.cos(c) + 1) * 128);
+ final int g = (int) ((Math.cos(c / Math.PI) + 1) * 128);
+ final int b = (int) ((Math.cos(c / (Math.PI * 2)) + 1) * 128);
+ cell.getElement()
+ .getStyle()
+ .setBackgroundColor(
+ "rgb(" + r + "," + g + "," + b + ")");
+ if ((r * .8 + g * 1.3 + b * .9) / 3 < 127) {
+ cell.getElement().getStyle().setColor("white");
+ } else {
+ cell.getElement().getStyle().clearColor();
+ }
+
+ i++;
+ }
+
+ @Override
+ public void updateCells(final Row row,
+ final Iterable<Cell> cellsToUpdate) {
+ for (final Cell cell : cellsToUpdate) {
+ renderCell(cell);
+ }
+ }
+ };
+ }
+
+ public void removeRows(final int offset, final int amount) {
+ for (int i = 0; i < amount; i++) {
+ rows.remove(offset);
+ }
+ }
+
+ public void removeColumns(final int offset, final int amount) {
+ for (int i = 0; i < amount; i++) {
+ columns.remove(offset);
+ }
+ }
+ }
+
+ private final Escalator escalator = new Escalator();
+ private final Data data = new Data();
+
+ public VTestGrid() {
+ initWidget(escalator);
+ final RowContainer header = escalator.getHeader();
+ header.setEscalatorUpdater(data.createHeaderUpdater());
+ header.insertRows(0, 1);
+
+ final RowContainer footer = escalator.getFooter();
+ footer.setEscalatorUpdater(data.createFooterUpdater());
+ footer.insertRows(0, 1);
+
+ escalator.getBody().setEscalatorUpdater(data.createBodyUpdater());
+
+ insertRows(0, 100);
+ insertColumns(0, 10);
+
+ setWidth(TestGridState.DEFAULT_WIDTH);
+ setHeight(TestGridState.DEFAULT_HEIGHT);
+
+ }
+
+ public void insertRows(final int offset, final int number) {
+ data.insertRows(offset, number);
+ escalator.getBody().insertRows(offset, number);
+ }
+
+ public void insertColumns(final int offset, final int number) {
+ data.insertColumns(offset, number);
+ escalator.getColumnConfiguration().insertColumns(offset, number);
+ }
+
+ public ColumnConfiguration getColumnConfiguration() {
+ return escalator.getColumnConfiguration();
+ }
+
+ public void scrollToRow(final int index,
+ final ScrollDestination destination, final int padding) {
+ escalator.scrollToRow(index, destination, padding);
+ }
+
+ public void scrollToColumn(final int index,
+ final ScrollDestination destination, final int padding) {
+ escalator.scrollToColumn(index, destination, padding);
+ }
+
+ public void removeRows(final int offset, final int amount) {
+ data.removeRows(offset, amount);
+ escalator.getBody().removeRows(offset, amount);
+ }
+
+ public void removeColumns(final int offset, final int amount) {
+ data.removeColumns(offset, amount);
+ escalator.getColumnConfiguration().removeColumns(offset, amount);
+ }
+
+ @Override
+ public void setWidth(String width) {
+ escalator.setWidth(width);
+ }
+
+ @Override
+ public void setHeight(String height) {
+ escalator.setHeight(height);
+ }
+
+ public RowContainer getHeader() {
+ return escalator.getHeader();
+ }
+
+ public RowContainer getFooter() {
+ return escalator.getFooter();
+ }
+
+ public void calculateColumnWidths() {
+ escalator.calculateColumnWidths();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java b/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java
new file mode 100644
index 0000000000..bdc5d46c1d
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java
@@ -0,0 +1,92 @@
+/*
+ * 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.tests.widgetset.server.grid;
+
+import com.vaadin.tests.widgetset.client.grid.TestGridClientRpc;
+import com.vaadin.tests.widgetset.client.grid.TestGridState;
+import com.vaadin.ui.AbstractComponent;
+
+/**
+ * @since 7.2
+ * @author Vaadin Ltd
+ */
+public class TestGrid extends AbstractComponent {
+ public TestGrid() {
+ setWidth(TestGridState.DEFAULT_WIDTH);
+ setHeight(TestGridState.DEFAULT_HEIGHT);
+ }
+
+ @Override
+ protected TestGridState getState() {
+ return (TestGridState) super.getState();
+ }
+
+ public void insertRows(int offset, int amount) {
+ rpc().insertRows(offset, amount);
+ }
+
+ public void removeRows(int offset, int amount) {
+ rpc().removeRows(offset, amount);
+ }
+
+ public void insertColumns(int offset, int amount) {
+ rpc().insertColumns(offset, amount);
+ }
+
+ public void removeColumns(int offset, int amount) {
+ rpc().removeColumns(offset, amount);
+ }
+
+ private TestGridClientRpc rpc() {
+ return getRpcProxy(TestGridClientRpc.class);
+ }
+
+ public void scrollToRow(int index, String destination, int padding) {
+ rpc().scrollToRow(index, destination, padding);
+ }
+
+ public void scrollToColumn(int index, String destination, int padding) {
+ rpc().scrollToColumn(index, destination, padding);
+ }
+
+ public void setFrozenColumns(int frozenColumns) {
+ rpc().setFrozenColumns(frozenColumns);
+ }
+
+ public void insertHeaders(int index, int amount) {
+ rpc().insertHeaders(index, amount);
+ }
+
+ public void removeHeaders(int index, int amount) {
+ rpc().removeHeaders(index, amount);
+ }
+
+ public void insertFooters(int index, int amount) {
+ rpc().insertFooters(index, amount);
+ }
+
+ public void removeFooters(int index, int amount) {
+ rpc().removeFooters(index, amount);
+ }
+
+ public void setColumnWidth(int index, int px) {
+ rpc().setColumnWidth(index, px);
+ }
+
+ public void calculateColumnWidths() {
+ rpc().calculateColumnWidths();
+ }
+}