aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--WebContent/VAADIN/themes/base/base.scss4
-rw-r--r--WebContent/VAADIN/themes/base/escalator/escalator.scss120
-rw-r--r--WebContent/VAADIN/themes/base/grid/grid.scss38
-rw-r--r--WebContent/VAADIN/themes/reindeer-tests/styles.css4
-rw-r--r--WebContent/release-notes.html6
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java22
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java26
-rw-r--r--client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java99
-rw-r--r--client/ivy.xml3
-rw-r--r--client/src/com/vaadin/client/data/AbstractRemoteDataSource.java570
-rw-r--r--client/src/com/vaadin/client/data/CacheStrategy.java183
-rw-r--r--client/src/com/vaadin/client/data/DataChangeHandler.java70
-rw-r--r--client/src/com/vaadin/client/data/DataSource.java188
-rw-r--r--client/src/com/vaadin/client/data/RpcDataSourceConnector.java153
-rw-r--r--client/src/com/vaadin/client/metadata/TypeDataStore.java11
-rw-r--r--client/src/com/vaadin/client/ui/grid/Cell.java85
-rw-r--r--client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java145
-rw-r--r--client/src/com/vaadin/client/ui/grid/DataAvailableEvent.java55
-rw-r--r--client/src/com/vaadin/client/ui/grid/DataAvailableHandler.java37
-rw-r--r--client/src/com/vaadin/client/ui/grid/Escalator.java4626
-rw-r--r--client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java155
-rw-r--r--client/src/com/vaadin/client/ui/grid/FlyweightCell.java194
-rw-r--r--client/src/com/vaadin/client/ui/grid/FlyweightRow.java293
-rw-r--r--client/src/com/vaadin/client/ui/grid/Grid.java2562
-rw-r--r--client/src/com/vaadin/client/ui/grid/GridColumn.java47
-rw-r--r--client/src/com/vaadin/client/ui/grid/GridConnector.java610
-rw-r--r--client/src/com/vaadin/client/ui/grid/GridFooter.java74
-rw-r--r--client/src/com/vaadin/client/ui/grid/GridHeader.java150
-rw-r--r--client/src/com/vaadin/client/ui/grid/GridStaticSection.java532
-rw-r--r--client/src/com/vaadin/client/ui/grid/PositionFunction.java118
-rw-r--r--client/src/com/vaadin/client/ui/grid/Renderer.java45
-rw-r--r--client/src/com/vaadin/client/ui/grid/Row.java48
-rw-r--r--client/src/com/vaadin/client/ui/grid/RowContainer.java194
-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.java639
-rw-r--r--client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java438
-rw-r--r--client/src/com/vaadin/client/ui/grid/datasources/ListSorter.java177
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/AbstractGridKeyEventHandler.java43
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyDownHandler.java28
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyPressHandler.java28
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyUpHandler.java28
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyDownHandler.java28
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyPressHandler.java28
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyUpHandler.java28
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/GridKeyDownEvent.java51
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/GridKeyPressEvent.java51
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/GridKeyUpEvent.java51
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyDownHandler.java28
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyPressHandler.java28
-rw-r--r--client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyUpHandler.java28
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/AbstractRendererConnector.java156
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/ComplexRenderer.java153
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java98
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/DateRendererConnector.java34
-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/NumberRendererConnector.java35
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java33
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/TextRendererConnector.java33
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/UnsafeHtmlRendererConnector.java43
-rw-r--r--client/src/com/vaadin/client/ui/grid/renderers/WidgetRenderer.java67
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/AbstractRowHandleSelectionModel.java65
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/HasSelectionChangeHandlers.java43
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java764
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/SelectionChangeEvent.java170
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/SelectionChangeHandler.java39
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java237
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java258
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java73
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java139
-rw-r--r--client/src/com/vaadin/client/ui/grid/sort/Sort.java155
-rw-r--r--client/src/com/vaadin/client/ui/grid/sort/SortEvent.java128
-rw-r--r--client/src/com/vaadin/client/ui/grid/sort/SortEventHandler.java38
-rw-r--r--client/src/com/vaadin/client/ui/grid/sort/SortOrder.java72
-rw-r--r--client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java192
-rw-r--r--client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java104
-rw-r--r--server/src/com/vaadin/data/Container.java54
-rw-r--r--server/src/com/vaadin/data/RpcDataProviderExtension.java936
-rw-r--r--server/src/com/vaadin/data/util/AbstractBeanContainer.java20
-rw-r--r--server/src/com/vaadin/data/util/AbstractInMemoryContainer.java151
-rw-r--r--server/src/com/vaadin/data/util/IndexedContainer.java7
-rw-r--r--server/src/com/vaadin/ui/components/grid/AbstractRenderer.java88
-rw-r--r--server/src/com/vaadin/ui/components/grid/Grid.java1307
-rw-r--r--server/src/com/vaadin/ui/components/grid/GridColumn.java427
-rw-r--r--server/src/com/vaadin/ui/components/grid/GridFooter.java66
-rw-r--r--server/src/com/vaadin/ui/components/grid/GridHeader.java124
-rw-r--r--server/src/com/vaadin/ui/components/grid/GridStaticSection.java425
-rw-r--r--server/src/com/vaadin/ui/components/grid/Renderer.java71
-rw-r--r--server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java76
-rw-r--r--server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java34
-rw-r--r--server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java152
-rw-r--r--server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java38
-rw-r--r--server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java159
-rw-r--r--server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java39
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java71
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java138
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java54
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java73
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java35
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java43
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java234
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java81
-rw-r--r--server/src/com/vaadin/ui/components/grid/sort/Sort.java153
-rw-r--r--server/src/com/vaadin/ui/components/grid/sort/SortOrder.java106
-rw-r--r--server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java185
-rw-r--r--server/tests/src/com/vaadin/data/util/TestIndexedContainer.java113
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java88
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java224
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java306
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSection.java105
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/RendererTest.java198
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java198
-rw-r--r--shared/src/com/vaadin/shared/data/DataProviderRpc.java76
-rw-r--r--shared/src/com/vaadin/shared/data/DataProviderState.java32
-rw-r--r--shared/src/com/vaadin/shared/data/DataRequestRpc.java55
-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.java65
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridConstants.java44
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java33
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridState.java130
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java39
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java53
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/HeightMode.java42
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/Range.java431
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java55
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/SortDirection.java37
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/SortEventOriginator.java41
-rw-r--r--shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java406
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java319
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/BasicEscalatorTest.java295
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java76
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java62
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java260
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridColspans.java81
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java73
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridElement.java279
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridScrolling.java112
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridSingleColumn.java59
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java45
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java36
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java41
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java41
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java63
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java701
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java110
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java59
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java108
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java37
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java207
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java359
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java90
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java119
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java225
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java179
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java197
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java55
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java229
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml2
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesConnector.java37
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java755
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java378
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java48
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java46
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java308
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java76
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java48
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java138
-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.java249
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java153
-rw-r--r--uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java96
173 files changed, 31603 insertions, 25 deletions
diff --git a/WebContent/VAADIN/themes/base/base.scss b/WebContent/VAADIN/themes/base/base.scss
index 3570c5efec..6263646ce1 100644
--- a/WebContent/VAADIN/themes/base/base.scss
+++ b/WebContent/VAADIN/themes/base/base.scss
@@ -16,8 +16,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..0246224fd3
--- /dev/null
+++ b/WebContent/VAADIN/themes/base/escalator/escalator.scss
@@ -0,0 +1,120 @@
+@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 &, .v-ie9 & {
+ /*
+ * Neither IE8 nor IE9 let table rows be longer than tbody, with only
+ * "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;
+
+ /*
+ * Because Vaadin changes the font size after the initial render, we
+ * need to mention the font size here explicitly, otherwise automatic
+ * row height detection gets broken.
+ */
+ font-size: $font-size;
+}
+
+.#{$primaryStyleName}-cell.frozen {
+ position: relative;
+ z-index: 1;
+}
+
+}
diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss
new file mode 100644
index 0000000000..88c7754a10
--- /dev/null
+++ b/WebContent/VAADIN/themes/base/grid/grid.scss
@@ -0,0 +1,38 @@
+@mixin base-grid($primaryStyleName : v-grid) {
+ @include base-escalator($primaryStyleName);
+
+ .#{$primaryStyleName} {
+
+ th {
+ position: relative;
+ }
+
+ th.sort-asc:after {
+ content: "\25B2" attr(sort-order);
+ position: absolute;
+ right: 5px;
+ }
+
+ th.sort-desc:after {
+ content: "\25BC" attr(sort-order);
+ position: absolute;
+ right: 5px;
+ }
+
+ .#{$primaryStyleName}-cell-active {
+ border-color: blue;
+ }
+
+ .#{$primaryStyleName}-header-active {
+ background: lightgray;
+ }
+
+ .#{$primaryStyleName}-row-active > td {
+ background: rgb(244,244,244);
+ }
+ }
+
+ .#{$primaryStyleName}-row-selected > td {
+ background: lightblue;
+ }
+}
diff --git a/WebContent/VAADIN/themes/reindeer-tests/styles.css b/WebContent/VAADIN/themes/reindeer-tests/styles.css
index 679de01b9c..9dd88707d1 100644
--- a/WebContent/VAADIN/themes/reindeer-tests/styles.css
+++ b/WebContent/VAADIN/themes/reindeer-tests/styles.css
@@ -32,3 +32,7 @@
.popup-style .v-datefield-calendarpanel-body {
background: yellow;
}
+
+#escalator .v-escalator-body .v-escalator-cell {
+ height: 50px;
+} \ No newline at end of file
diff --git a/WebContent/release-notes.html b/WebContent/release-notes.html
index 0ede61d729..3fa008f3f3 100644
--- a/WebContent/release-notes.html
+++ b/WebContent/release-notes.html
@@ -97,11 +97,7 @@
enhancements. Below is a list of the most notable changes:</p>
<ul>
- <li>Valo is a brand new built-in theme for Vaadin. It leverages
- the Sass CSS preprocessor heavily,
- providing a variety of ways to customize the look and feel of your theme.
- See <a href="https://vaadin.com/wiki/-/wiki/Main/Valo+theme+-+Getting+started">the Valo theme tutorial</a> or <a href="https://vaadin.com/book/-/page/themes.valo.html">the Valo theme section</a> in Book of Vaadin for information on how to get started.</li>
- <li>Support for changing theme on the fly</li>
+ <li>Grid</li>
</ul>
<p>
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java b/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java
index a6ca690a8a..7c3bb1eb77 100644
--- a/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/ConnectorBundleLoaderFactory.java
@@ -61,6 +61,7 @@ import com.vaadin.server.widgetsetutils.metadata.ConnectorInitVisitor;
import com.vaadin.server.widgetsetutils.metadata.GeneratedSerializer;
import com.vaadin.server.widgetsetutils.metadata.OnStateChangeVisitor;
import com.vaadin.server.widgetsetutils.metadata.Property;
+import com.vaadin.server.widgetsetutils.metadata.RendererVisitor;
import com.vaadin.server.widgetsetutils.metadata.ServerRpcVisitor;
import com.vaadin.server.widgetsetutils.metadata.StateInitVisitor;
import com.vaadin.server.widgetsetutils.metadata.TypeVisitor;
@@ -503,6 +504,7 @@ public class ConnectorBundleLoaderFactory extends Generator {
// this after the JS property data has been initialized
writePropertyTypes(logger, w, bundle);
writeSerializers(logger, w, bundle);
+ writePresentationTypes(w, bundle);
writeDelegateToWidget(logger, w, bundle);
writeOnStateChangeHandlers(logger, w, bundle);
}
@@ -684,6 +686,21 @@ public class ConnectorBundleLoaderFactory extends Generator {
}
}
+ private void writePresentationTypes(SplittingSourceWriter w,
+ ConnectorBundle bundle) {
+ Map<JClassType, JType> presentationTypes = bundle
+ .getPresentationTypes();
+ for (Entry<JClassType, JType> entry : presentationTypes.entrySet()) {
+
+ w.print("store.setPresentationType(");
+ writeClassLiteral(w, entry.getKey());
+ w.print(", ");
+ writeClassLiteral(w, entry.getValue());
+ w.println(");");
+ w.splitIfNeeded();
+ }
+ }
+
private void writePropertyTypes(TreeLogger logger, SplittingSourceWriter w,
ConnectorBundle bundle) {
Set<Property> properties = bundle.getNeedsProperty();
@@ -1240,8 +1257,9 @@ public class ConnectorBundleLoaderFactory extends Generator {
throws NotFoundException {
List<TypeVisitor> visitors = Arrays.<TypeVisitor> asList(
new ConnectorInitVisitor(), new StateInitVisitor(),
- new WidgetInitVisitor(), new ClientRpcVisitor(),
- new ServerRpcVisitor(), new OnStateChangeVisitor());
+ new WidgetInitVisitor(), new RendererVisitor(),
+ new ClientRpcVisitor(), new ServerRpcVisitor(),
+ new OnStateChangeVisitor());
for (TypeVisitor typeVisitor : visitors) {
typeVisitor.init(oracle);
}
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java
index e8a384298f..4a079c38b0 100644
--- a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/ConnectorBundle.java
@@ -44,6 +44,7 @@ import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.communication.JSONSerializer;
import com.vaadin.client.ui.UnknownComponentConnector;
+import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector;
import com.vaadin.shared.communication.ClientRpc;
import com.vaadin.shared.communication.ServerRpc;
import com.vaadin.shared.ui.Connect;
@@ -59,6 +60,7 @@ public class ConnectorBundle {
private final Set<JType> hasSerializeSupport = new HashSet<JType>();
private final Set<JType> needsSerializeSupport = new HashSet<JType>();
private final Map<JType, GeneratedSerializer> serializers = new HashMap<JType, GeneratedSerializer>();
+ private final Map<JClassType, JType> presentationTypes = new HashMap<JClassType, JType>();
private final Set<JClassType> needsSuperClass = new HashSet<JClassType>();
private final Set<JClassType> needsGwtConstructor = new HashSet<JClassType>();
@@ -306,6 +308,25 @@ public class ConnectorBundle {
return Collections.unmodifiableMap(serializers);
}
+ public void setPresentationType(JClassType type, JType presentationType) {
+ if (!hasPresentationType(type)) {
+ presentationTypes.put(type, presentationType);
+ }
+ }
+
+ private boolean hasPresentationType(JClassType type) {
+ if (presentationTypes.containsKey(type)) {
+ return true;
+ } else {
+ return previousBundle != null
+ && previousBundle.hasPresentationType(type);
+ }
+ }
+
+ public Map<JClassType, JType> getPresentationTypes() {
+ return Collections.unmodifiableMap(presentationTypes);
+ }
+
private void setNeedsSuperclass(JClassType typeAsClass) {
if (!isNeedsSuperClass(typeAsClass)) {
needsSuperClass.add(typeAsClass);
@@ -415,6 +436,11 @@ public class ConnectorBundle {
return isConnected(type) && isType(type, ComponentConnector.class);
}
+ public static boolean isConnectedRendererConnector(JClassType type) {
+ return isConnected(type)
+ && isType(type, AbstractRendererConnector.class);
+ }
+
private static boolean isInterfaceType(JClassType type, Class<?> class1) {
return type.isInterface() != null && isType(type, class1);
}
diff --git a/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java
new file mode 100644
index 0000000000..6c6d6d116c
--- /dev/null
+++ b/client-compiler/src/com/vaadin/server/widgetsetutils/metadata/RendererVisitor.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2000-2014 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.server.widgetsetutils.metadata;
+
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.TreeLogger.Type;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.JMethod;
+import com.google.gwt.core.ext.typeinfo.JType;
+import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector;
+
+/**
+ * Generates type data for renderer connectors.
+ * <ul>
+ * <li>Stores the return type of the overridden
+ * {@link AbstractRendererConnector#getRenderer() getRenderer} method to enable
+ * automatic creation of an instance of the proper renderer type.
+ * <li>Stores the presentation type of the connector to enable the
+ * {@link AbstractRendererConnector#decode(com.google.gwt.json.client.JSONValue)
+ * decode} method to work without having to implement a "getPresentationType"
+ * method.
+ * </ul>
+ *
+ * @see WidgetInitVisitor
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class RendererVisitor extends TypeVisitor {
+
+ @Override
+ public void visitConnector(TreeLogger logger, JClassType type,
+ ConnectorBundle bundle) {
+ if (ConnectorBundle.isConnectedRendererConnector(type)) {
+ doRendererType(logger, type, bundle);
+ doPresentationType(logger, type, bundle);
+ }
+ }
+
+ private static void doRendererType(TreeLogger logger, JClassType type,
+ ConnectorBundle bundle) {
+ // The class in which createRenderer is implemented
+ JClassType createRendererClass = ConnectorBundle.findInheritedMethod(
+ type, "createRenderer").getEnclosingType();
+
+ // Needs GWT constructor if createRenderer is not overridden
+ if (createRendererClass.getQualifiedSourceName().equals(
+ AbstractRendererConnector.class.getCanonicalName())) {
+
+ JMethod getRenderer = ConnectorBundle.findInheritedMethod(type,
+ "getRenderer");
+ JClassType rendererType = getRenderer.getReturnType().isClass();
+
+ bundle.setNeedsGwtConstructor(rendererType);
+
+ // Also needs renderer type to find the right GWT constructor
+ bundle.setNeedsReturnType(type, getRenderer);
+
+ logger.log(Type.DEBUG, "Renderer type of " + type + " is "
+ + rendererType);
+ }
+ }
+
+ private void doPresentationType(TreeLogger logger, JClassType type,
+ ConnectorBundle bundle) {
+ JType presentationType = getPresentationType(type);
+ bundle.setPresentationType(type, presentationType);
+
+ logger.log(Type.DEBUG, "Presentation type of " + type + " is "
+ + presentationType);
+ }
+
+ private static JType getPresentationType(JClassType type) {
+ JClassType originalType = type;
+ while (type != null) {
+ if (type.getQualifiedBinaryName().equals(
+ AbstractRendererConnector.class.getName())) {
+ return type.isParameterized().getTypeArgs()[0];
+ }
+ type = type.getSuperclass();
+ }
+ throw new IllegalArgumentException("The type "
+ + originalType.getQualifiedSourceName() + " does not extend "
+ + AbstractRendererConnector.class.getName());
+ }
+}
diff --git a/client/ivy.xml b/client/ivy.xml
index 3abdcf9ba5..6b941af818 100644
--- a/client/ivy.xml
+++ b/client/ivy.xml
@@ -42,6 +42,9 @@
<dependency org="javax.validation" name="validation-api"
rev="1.0.0.GA" conf="build->default,sources" />
+ <!-- Testing dependencies -->
+ <dependency org="org.easymock" name="easymock" rev="3.0"
+ conf="test,ide-> default" />
</dependencies>
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..ca2d2bc83e
--- /dev/null
+++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
@@ -0,0 +1,570 @@
+/*
+ * Copyright 2000-2014 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.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.core.client.Duration;
+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
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type
+ */
+public abstract class AbstractRemoteDataSource<T> implements DataSource<T> {
+
+ protected class RowHandleImpl extends RowHandle<T> {
+ private T row;
+ private final Object key;
+
+ public RowHandleImpl(final T row, final Object key) {
+ this.row = row;
+ this.key = key;
+ }
+
+ /**
+ * A method for the data source to update the row data.
+ *
+ * @param row
+ * the updated row object
+ */
+ public void setRow(final T row) {
+ this.row = row;
+ assert getRowKey(row).equals(key) : "The old key does not "
+ + "equal the new key for the given row (old: " + key
+ + ", new :" + getRowKey(row) + ")";
+ }
+
+ @Override
+ public T getRow() throws IllegalStateException {
+ if (isPinned()) {
+ return row;
+ } else {
+ throw new IllegalStateException("The row handle for key " + key
+ + " was not pinned");
+ }
+ }
+
+ private boolean isPinned() {
+ return pinnedRows.containsKey(key);
+ }
+
+ @Override
+ public void pin() {
+ Integer count = pinnedCounts.get(key);
+ if (count == null) {
+ count = Integer.valueOf(0);
+ pinnedRows.put(key, this);
+ }
+ pinnedCounts.put(key, Integer.valueOf(count.intValue() + 1));
+ }
+
+ @Override
+ public void unpin() throws IllegalStateException {
+ final Integer count = pinnedCounts.get(key);
+ if (count == null) {
+ throw new IllegalStateException("Row " + row + " with key "
+ + key + " was not pinned to begin with");
+ } else if (count.equals(Integer.valueOf(1))) {
+ pinnedRows.remove(key);
+ pinnedCounts.remove(key);
+ } else {
+ pinnedCounts.put(key, Integer.valueOf(count.intValue() - 1));
+ }
+ }
+
+ @Override
+ protected boolean equalsExplicit(final Object obj) {
+ if (obj instanceof AbstractRemoteDataSource.RowHandleImpl) {
+ /*
+ * Java prefers AbstractRemoteDataSource<?>.RowHandleImpl. I
+ * like the @SuppressWarnings more (keeps the line length in
+ * check.)
+ */
+ @SuppressWarnings("unchecked")
+ final RowHandleImpl rhi = (RowHandleImpl) obj;
+ return key.equals(rhi.key);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected int hashCodeExplicit() {
+ return key.hashCode();
+ }
+ }
+
+ /**
+ * Records the start of the previously requested range. This is used when
+ * tracking request timings to distinguish between explicit responses and
+ * arbitrary updates pushed from the server.
+ */
+ private int lastRequestStart = -1;
+ private double pendingRequestTime;
+
+ 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 Range estimatedAvailableRange = Range.between(0, 0);
+
+ private CacheStrategy cacheStrategy = new CacheStrategy.DefaultCacheStrategy();
+
+ private final ScheduledCommand coverageChecker = new ScheduledCommand() {
+ @Override
+ public void execute() {
+ coverageCheckPending = false;
+ checkCacheCoverage();
+ }
+ };
+
+ private Map<Object, Integer> pinnedCounts = new HashMap<Object, Integer>();
+ private Map<Object, RowHandleImpl> pinnedRows = new HashMap<Object, RowHandleImpl>();
+ protected Collection<T> temporarilyPinnedRows = Collections.emptySet();
+
+ /**
+ * 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
+ estimatedAvailableRange = Range.withLength(0, 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 (lastRequestStart != -1) {
+ // Anyone clearing lastRequestStart should run this method again
+ return;
+ }
+
+ Profiler.enter("AbstractRemoteDataSource.checkCacheCoverage");
+
+ Range minCacheRange = getMinCacheRange();
+
+ if (!minCacheRange.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(getMaxCacheRange());
+ } else {
+ discardStaleCacheEntries();
+
+ // Might need more rows -> request them
+ if (!minCacheRange.isSubsetOf(cached)) {
+ Range[] missingCachePartition = getMaxCacheRange()
+ .partitionWith(cached);
+ handleMissingRows(missingCachePartition[0]);
+ handleMissingRows(missingCachePartition[2]);
+ }
+ }
+
+ Profiler.leave("AbstractRemoteDataSource.checkCacheCoverage");
+ }
+
+ private void discardStaleCacheEntries() {
+ Range[] cacheParition = cached.partitionWith(getMaxCacheRange());
+ 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;
+ }
+ lastRequestStart = range.getStart();
+ pendingRequestTime = Duration.currentTimeMillis();
+ 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 estimatedAvailableRange.length();
+ }
+
+ @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());
+ dataChangeHandler.dataAvailable(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) {
+
+ Profiler.enter("AbstractRemoteDataSource.setRowData");
+
+ Range received = Range.withLength(firstRowIndex, rowData.size());
+
+ if (firstRowIndex == lastRequestStart) {
+ // Provide timing information if we know when we asked for this data
+ cacheStrategy.onDataArrive(Duration.currentTimeMillis()
+ - pendingRequestTime, received.length());
+ }
+ lastRequestStart = -1;
+
+ Range maxCacheRange = getMaxCacheRange();
+
+ Range[] partition = received.partitionWith(maxCacheRange);
+
+ 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;
+ }
+ }
+ dataChangeHandler.dataAvailable(cached.getStart(), cached.length());
+
+ updatePinnedRows(rowData);
+ }
+
+ 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");
+ }
+
+ private void updatePinnedRows(final List<T> rowData) {
+ for (final T row : rowData) {
+ final Object key = getRowKey(row);
+ final RowHandleImpl handle = pinnedRows.get(key);
+ if (handle != null) {
+ handle.setRow(row);
+ }
+ }
+ }
+
+ /**
+ * 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 (cached.isSubsetOf(removedRange)) {
+ cached = Range.withLength(0, 0);
+ } else if (removedRange.intersects(cached)) {
+ Range[] partitions = cached.partitionWith(removedRange);
+ Range remainsBefore = partitions[0];
+ Range transposedRemainsAfter = partitions[2].offsetBy(-removedRange
+ .length());
+ cached = remainsBefore.combineWith(transposedRemainsAfter);
+ }
+ setEstimatedSize(getEstimatedSize() - 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));
+ }
+ }
+
+ setEstimatedSize(getEstimatedSize() + count);
+ dataChangeHandler.dataAdded(firstRowIndex, count);
+ checkCacheCoverage();
+
+ Profiler.leave("AbstractRemoteDataSource.insertRowData");
+ }
+
+ /**
+ * Gets the current range of cached rows
+ *
+ * @return the range of currently cached rows
+ */
+ public Range getCachedRange() {
+ return cached;
+ }
+
+ /**
+ * Sets the cache strategy that is used to determine how much data is
+ * fetched and cached.
+ * <p>
+ * The new strategy is immediately used to evaluate whether currently cached
+ * rows should be discarded or new rows should be fetched.
+ *
+ * @param cacheStrategy
+ * a cache strategy implementation, not <code>null</code>
+ */
+ public void setCacheStrategy(CacheStrategy cacheStrategy) {
+ if (cacheStrategy == null) {
+ throw new IllegalArgumentException();
+ }
+
+ if (this.cacheStrategy != cacheStrategy) {
+ this.cacheStrategy = cacheStrategy;
+
+ checkCacheCoverage();
+ }
+ }
+
+ private Range getMinCacheRange() {
+ Range minCacheRange = cacheStrategy.getMinCacheRange(
+ requestedAvailability, cached, estimatedAvailableRange);
+
+ assert minCacheRange.isSubsetOf(estimatedAvailableRange);
+
+ return minCacheRange;
+ }
+
+ private Range getMaxCacheRange() {
+ Range maxCacheRange = cacheStrategy.getMaxCacheRange(
+ requestedAvailability, cached, estimatedAvailableRange);
+
+ assert maxCacheRange.isSubsetOf(estimatedAvailableRange);
+
+ return maxCacheRange;
+ }
+
+ @Override
+ public RowHandle<T> getHandle(T row) throws IllegalStateException {
+ Object key = getRowKey(row);
+
+ if (key == null) {
+ throw new NullPointerException("key may not be null (row: " + row
+ + ")");
+ }
+
+ if (pinnedRows.containsKey(key)) {
+ return pinnedRows.get(key);
+ } else if (rowCache.containsValue(row)) {
+ return new RowHandleImpl(row, key);
+ } else {
+ throw new IllegalStateException("The cache of this DataSource "
+ + "does not currently contain the row " + row);
+ }
+ }
+
+ /**
+ * Gets a stable key for the row object.
+ * <p>
+ * This method is a workaround for the fact that there is no means to force
+ * proper implementations for {@link #hashCode()} and
+ * {@link #equals(Object)} methods.
+ * <p>
+ * Since the same row object will be created several times for the same
+ * logical data, the DataSource needs a mechanism to be able to compare two
+ * objects, and figure out whether or not they represent the same data. Even
+ * if all the fields of an entity would be changed, it still could represent
+ * the very same thing (say, a person changes all of her names.)
+ * <p>
+ * A very usual and simple example what this could be, is an unique ID for
+ * this object that would also be stored in a database.
+ *
+ * @param row
+ * the row object for which to get the key
+ * @return a non-null object that uniquely and consistently represents the
+ * row object
+ */
+ abstract public Object getRowKey(T row);
+
+ /**
+ * Marks rows as pinned when fetching new rows.
+ * <p>
+ * This collection of rows are intended to remain pinned if new rows are
+ * fetched from the data source, even if some of the pinned rows would fall
+ * off the cache and become inactive.
+ * <p>
+ * This method does nothing by itself, other than it stores the rows into a
+ * field. The implementation needs to make all the adjustments for itself.
+ * Check {@link RpcDataSourceConnector.RpcDataSource#requestRows(int, int)}
+ * for an implementation example.
+ *
+ * @param keys
+ * a collection of rows to keep pinned
+ *
+ * @see #temporarilyPinnedRows
+ * @see RpcDataSourceConnector.RpcDataSource#requestRows(int, int)
+ * @deprecated You probably don't want to call this method unless you're
+ * writing a Renderer for a selection model. Even if you are, be
+ * very aware what this method does and how it behaves.
+ */
+ @Deprecated
+ public void transactionPin(Collection<T> rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("argument may not be null");
+ }
+ temporarilyPinnedRows = rows;
+ }
+}
diff --git a/client/src/com/vaadin/client/data/CacheStrategy.java b/client/src/com/vaadin/client/data/CacheStrategy.java
new file mode 100644
index 0000000000..3448659e61
--- /dev/null
+++ b/client/src/com/vaadin/client/data/CacheStrategy.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2000-2014 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 com.vaadin.shared.ui.grid.Range;
+
+/**
+ * Determines what data an {@link AbstractRemoteDataSource} should fetch and
+ * keep cached.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface CacheStrategy {
+ /**
+ * A helper class for creating a simple symmetric cache strategy that uses
+ * the same logic for both rows before and after the currently cached range.
+ * <p>
+ * This simple approach rules out more advanced heuristics that would take
+ * the current scrolling direction or past scrolling behavior into account.
+ */
+ public static abstract class AbstractBasicSymmetricalCacheStrategy
+ implements CacheStrategy {
+
+ @Override
+ public void onDataArrive(double roundTripTime, int rowCount) {
+ // NOP
+ }
+
+ @Override
+ public Range getMinCacheRange(Range displayedRange, Range cachedRange,
+ Range estimatedAvailableRange) {
+ int cacheSize = getMinimumCacheSize(displayedRange.length());
+
+ return displayedRange.expand(cacheSize, cacheSize).restrictTo(
+ estimatedAvailableRange);
+ }
+
+ @Override
+ public Range getMaxCacheRange(Range displayedRange, Range cachedRange,
+ Range estimatedAvailableRange) {
+ int cacheSize = getMaximumCacheSize(displayedRange.length());
+
+ return displayedRange.expand(cacheSize, cacheSize).restrictTo(
+ estimatedAvailableRange);
+ }
+
+ /**
+ * Gets the maximum number of extra items to cache in one direction.
+ *
+ * @param pageSize
+ * the current number of items used at once
+ * @return maximum of items to cache
+ */
+ public abstract int getMaximumCacheSize(int pageSize);
+
+ /**
+ * Gets the the minimum number of extra items to cache in one direction.
+ *
+ * @param pageSize
+ * the current number of items used at once
+ * @return minimum number of items to cache
+ */
+ public abstract int getMinimumCacheSize(int pageSize);
+ }
+
+ /**
+ * The default cache strategy used by {@link AbstractRemoteDataSource},
+ * using multiples of the page size for determining the minimum and maximum
+ * number of items to keep in the cache. By default, at least three times
+ * the page size both before and after the currently used range are kept in
+ * the cache and items are discarded if there's yet another page size worth
+ * of items cached in either direction.
+ */
+ public static class DefaultCacheStrategy extends
+ AbstractBasicSymmetricalCacheStrategy {
+ private final int minimumRatio;
+ private final int maximumRatio;
+
+ /**
+ * Creates a DefaultCacheStrategy keeping between 3 and 4 pages worth of
+ * data cached both before and after the active range.
+ */
+ public DefaultCacheStrategy() {
+ this(3, 4);
+ }
+
+ /**
+ * Creates a DefaultCacheStrategy with custom ratios for how much data
+ * to cache. The ratios denote how many multiples of the currently used
+ * page size are kept in the cache in each direction.
+ *
+ * @param minimumRatio
+ * the minimum number of pages to keep in the cache in each
+ * direction
+ * @param maximumRatio
+ * the maximum number of pages to keep in the cache in each
+ * direction
+ */
+ public DefaultCacheStrategy(int minimumRatio, int maximumRatio) {
+ this.minimumRatio = minimumRatio;
+ this.maximumRatio = maximumRatio;
+ }
+
+ @Override
+ public int getMinimumCacheSize(int pageSize) {
+ return pageSize * minimumRatio;
+ }
+
+ @Override
+ public int getMaximumCacheSize(int pageSize) {
+ return pageSize * maximumRatio;
+ }
+ }
+
+ /**
+ * Called whenever data requested by the data source has arrived. This
+ * information can e.g. be used for measuring how long it takes to fetch
+ * different number of rows from the server.
+ * <p>
+ * A cache strategy implementation cannot use this information to keep track
+ * of which items are in the cache since the data source might discard items
+ * without notifying the cache strategy.
+ *
+ * @param roundTripTime
+ * the total number of milliseconds elapsed from requesting the
+ * data until the response was passed to the data source
+ * @param rowCount
+ * the number of received rows
+ */
+ public void onDataArrive(double roundTripTime, int rowCount);
+
+ /**
+ * Gets the minimum row range that should be cached. The data source will
+ * fetch new data if the currently cached range does not fill the entire
+ * minimum cache range.
+ *
+ * @param displayedRange
+ * the range of currently displayed rows
+ * @param cachedRange
+ * the range of currently cached rows
+ * @param estimatedAvailableRange
+ * the estimated range of rows available for the data source
+ *
+ * @return the minimum range of rows that should be cached, should at least
+ * include the displayed range and should not exceed the total
+ * estimated available range
+ */
+ public Range getMinCacheRange(Range displayedRange, Range cachedRange,
+ Range estimatedAvailableRange);
+
+ /**
+ * Gets the maximum row range that should be cached. The data source will
+ * discard cached rows that are outside the maximum range.
+ *
+ * @param displayedRange
+ * the range of currently displayed rows
+ * @param cachedRange
+ * the range of currently cached rows
+ * @param estimatedAvailableRange
+ * the estimated range of rows available for the data source
+ *
+ * @return the maximum range of rows that should be cached, should at least
+ * include the displayed range and should not exceed the total
+ * estimated available range
+ */
+ public Range getMaxCacheRange(Range displayedRange, Range cachedRange,
+ Range estimatedAvailableRange);
+}
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..fe72fe673a
--- /dev/null
+++ b/client/src/com/vaadin/client/data/DataChangeHandler.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2000-2014 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
+ * @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);
+
+ /**
+ * Called when rows requested with
+ * {@link DataSource#ensureAvailability(int, int)} rows are available.
+ *
+ * @param firstRowIndex
+ * the index of the first available row
+ * @param numberOfRows
+ * the number of available rows
+ */
+ public void dataAvailable(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..24cef548c5
--- /dev/null
+++ b/client/src/com/vaadin/client/data/DataSource.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2000-2014 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
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row type
+ */
+public interface DataSource<T> {
+
+ /**
+ * A handle that contains information on whether a row should be
+ * {@link #pin() pinned} or {@link #unpin() unpinned}, and also always the
+ * most recent representation for that particular row.
+ *
+ * @param <T>
+ * the row type
+ */
+ public abstract class RowHandle<T> {
+ /**
+ * Gets the most recent representation for the row this handle
+ * represents.
+ *
+ * @return the most recent representation for the row this handle
+ * represents
+ * @throws IllegalStateException
+ * if this row handle isn't currently pinned
+ * @see #pin()
+ */
+ public abstract T getRow() throws IllegalStateException;
+
+ /**
+ * Marks this row as pinned.
+ * <p>
+ * <em>Note:</em> Pinning a row multiple times requires an equal amount
+ * of unpins to free the row from the "pinned" status.
+ * <p>
+ * <em>Technical Note:</em> Pinning a row makes sure that the row object
+ * for a particular set of data is always kept as up to date as the data
+ * source is able to. Since the DataSource might create a new instance
+ * of an object, object references aren't necessarily kept up-to-date.
+ * This is a technical work-around for that.
+ *
+ * @see #unpin()
+ */
+ public abstract void pin();
+
+ /**
+ * Marks this row as unpinned.
+ * <p>
+ * <em>Note:</em> Pinning a row multiple times requires an equal amount
+ * of unpins to free the row from the "pinned" status.
+ * <p>
+ * <em>Technical Note:</em> Pinning a row makes sure that the row object
+ * for a particular set of data is always kept as up to date as the data
+ * source is able to. Since the DataSource might create a new instance
+ * of an object, object references aren't necessarily kept up-to-date.
+ * This is a technical work-around for that.
+ *
+ * @throws IllegalStateException
+ * if this row handle has not been pinned before
+ * @see #pin()
+ */
+ public abstract void unpin() throws IllegalStateException;
+
+ /**
+ * An explicit override for {@link Object#equals(Object)}. This method
+ * should be functionally equivalent to a properly implemented equals
+ * method.
+ * <p>
+ * Having a properly implemented equals method is imperative for
+ * RowHandle to function. Because Java has no mechanism to force an
+ * override of an existing method, we're defining a new method for that
+ * instead.
+ *
+ * @param rowHandle
+ * the reference object with which to compare
+ * @return {@code true} if this object is the same as the obj argument;
+ * {@code false} otherwise.
+ */
+ protected abstract boolean equalsExplicit(Object obj);
+
+ /**
+ * An explicit override for {@link Object#hashCode()}. This method
+ * should be functionally equivalent to a properly implemented hashCode
+ * method.
+ * <p>
+ * Having a properly implemented hashCode method is imperative for
+ * RowHandle to function. Because Java has no mechanism to force an
+ * override of an existing method, we're defining a new method for that
+ * instead.
+ *
+ * @return a hash code value for this object
+ */
+ protected abstract int hashCodeExplicit();
+
+ @Override
+ public int hashCode() {
+ return hashCodeExplicit();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return equalsExplicit(obj);
+ }
+ }
+
+ /**
+ * 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.
+ * <p>
+ * After any possible lazy loading and updates are done, the change handler
+ * is informed that new data is available.
+ *
+ * @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);
+
+ /**
+ * Gets a {@link RowHandle} of a row object in the cache.
+ *
+ * @param row
+ * the row object for which to retrieve a row handle
+ * @return a non-<code>null</code> row handle of the given row object
+ * @throw IllegalStateException if this data source cannot be sure whether
+ * or not the given row exists. <em>In practice</em> this usually
+ * means that the row is not currently in this data source's cache.
+ */
+ public RowHandle<T> getHandle(T row);
+}
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..2819837504
--- /dev/null
+++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2000-2014 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.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONString;
+import com.google.gwt.json.client.JSONValue;
+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;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.Range;
+
+/**
+ * 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
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.data.RpcDataProviderExtension.class)
+public class RpcDataSourceConnector extends AbstractExtensionConnector {
+
+ public class RpcDataSource extends AbstractRemoteDataSource<JSONObject> {
+
+ private Collection<JSONObject> prevRows = Collections.emptySet();
+
+ @Override
+ protected void requestRows(int firstRowIndex, int numberOfRows) {
+ Range cached = getCachedRange();
+
+ Collection<JSONObject> newRows = new ArrayList<JSONObject>(
+ temporarilyPinnedRows);
+ newRows.removeAll(prevRows);
+
+ List<String> temporarilyPinnedKeys = new ArrayList<String>(
+ newRows.size());
+ for (JSONObject row : newRows) {
+ temporarilyPinnedKeys.add((String) getRowKey(row));
+ }
+
+ getRpcProxy(DataRequestRpc.class).requestRows(firstRowIndex,
+ numberOfRows, cached.getStart(), cached.length(),
+ temporarilyPinnedKeys);
+
+ prevRows = temporarilyPinnedRows;
+ }
+
+ @Override
+ public Object getRowKey(JSONObject row) {
+ JSONString string = row.get(GridState.JSONKEY_ROWKEY).isString();
+ if (string != null) {
+ return string.stringValue();
+ } else {
+ return null;
+ }
+ }
+
+ public RowHandle<JSONObject> getHandleByKey(Object key) {
+ JSONObject row = new JSONObject();
+ row.put(GridState.JSONKEY_ROWKEY, new JSONString((String) key));
+ return new RowHandleImpl(row, key);
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public void transactionPin(Collection<JSONObject> keys) {
+ super.transactionPin(keys);
+ if (keys.isEmpty() && !prevRows.isEmpty()) {
+ prevRows = Collections.emptySet();
+ getRpcProxy(DataRequestRpc.class)
+ .releaseTemporarilyPinnedKeys();
+ }
+ }
+ }
+
+ private final RpcDataSource dataSource = new RpcDataSource();
+
+ @Override
+ protected void extend(ServerConnector target) {
+ dataSource.setEstimatedSize(getState().containerSize);
+ ((GridConnector) target).setDataSource(dataSource);
+
+ registerRpc(DataProviderRpc.class, new DataProviderRpc() {
+ @Override
+ public void setRowData(int firstRow, String rowsJson) {
+ JSONValue parsedJson = JSONParser.parseStrict(rowsJson);
+ JSONArray rowArray = parsedJson.isArray();
+ assert rowArray != null : "Was unable to parse JSON into an array: "
+ + parsedJson;
+
+ ArrayList<JSONObject> rows = new ArrayList<JSONObject>(rowArray
+ .size());
+ for (int i = 0; i < rowArray.size(); i++) {
+ JSONValue rowValue = rowArray.get(i);
+ JSONObject rowObject = rowValue.isObject();
+ assert rowObject != null : "Was unable to parse JSON into an object: "
+ + rowValue;
+ rows.add(rowObject);
+ }
+
+ 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/metadata/TypeDataStore.java b/client/src/com/vaadin/client/metadata/TypeDataStore.java
index 7aa952d0f2..9b1fd7d45c 100644
--- a/client/src/com/vaadin/client/metadata/TypeDataStore.java
+++ b/client/src/com/vaadin/client/metadata/TypeDataStore.java
@@ -37,6 +37,8 @@ public class TypeDataStore {
.create();
private final FastStringMap<JsArrayString> delegateToWidgetProperties = FastStringMap
.create();
+ private final FastStringMap<Type> presentationTypes = FastStringMap
+ .create();
/**
* Maps connector class -> state property name -> hander method data
@@ -135,6 +137,10 @@ public class TypeDataStore {
return get().delegateToWidgetProperties.get(type.getBaseTypeName());
}
+ public static Type getPresentationType(Class<?> type) {
+ return get().presentationTypes.get(getType(type).getBaseTypeName());
+ }
+
public void setDelegateToWidget(Class<?> clazz, String propertyName,
String delegateValue) {
Type type = getType(clazz);
@@ -150,6 +156,11 @@ public class TypeDataStore {
typeProperties.push(propertyName);
}
+ public void setPresentationType(Class<?> type, Class<?> presentationType) {
+ presentationTypes.put(getType(type).getBaseTypeName(),
+ getType(presentationType));
+ }
+
public void setReturnType(Class<?> type, String methodName, Type returnType) {
returnTypes.put(new Method(getType(type), methodName).getLookupKey(),
returnType);
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..ede8bb22d0
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Cell.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2000-2014 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.TableCellElement;
+
+/**
+ * Describes a cell
+ * <p>
+ * It's a representation of the element in a grid cell, and its row and column
+ * indices.
+ * <p>
+ * Unlike the {@link FlyweightRow}, an instance of {@link Cell} can be stored in
+ * a field.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class Cell {
+
+ private final int row;
+
+ private final int column;
+
+ private final TableCellElement element;
+
+ /**
+ * Constructs a new {@link Cell}.
+ *
+ * @param row
+ * The index of the row
+ * @param column
+ * The index of the column
+ * @param element
+ * The cell element
+ */
+ public Cell(int row, int column, TableCellElement element) {
+ super();
+ this.row = row;
+ this.column = column;
+ this.element = element;
+ }
+
+ /**
+ * Returns the index of the row the cell resides in.
+ *
+ * @return the row index
+ *
+ */
+ public int getRow() {
+ return row;
+ }
+
+ /**
+ * Returns the index of the column the cell resides in.
+ *
+ * @return the column index
+ */
+ public int getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns the element of the cell.
+ *
+ * @return the cell element
+ */
+ public TableCellElement getElement() {
+ return element;
+ }
+
+}
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..f523fdbbd4
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2000-2014 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
+ * @author Vaadin Ltd
+ * @see Escalator#getColumnConfiguration()
+ */
+public interface ColumnConfiguration {
+
+ /**
+ * Removes columns at certain indices.
+ * <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 {@code index}
+ * @throws IndexOutOfBoundsException
+ * if the entire range of removed columns is not currently
+ * present in the escalator
+ * @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/DataAvailableEvent.java b/client/src/com/vaadin/client/ui/grid/DataAvailableEvent.java
new file mode 100644
index 0000000000..62b188c0ea
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/DataAvailableEvent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2014 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;
+import com.vaadin.shared.ui.grid.Range;
+
+/**
+ * Event object describing a change of row availability in DataSource of a Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class DataAvailableEvent extends GwtEvent<DataAvailableHandler> {
+
+ private Range rowsAvailable;
+ public static final Type<DataAvailableHandler> TYPE = new Type<DataAvailableHandler>();
+
+ public DataAvailableEvent(Range rowsAvailable) {
+ this.rowsAvailable = rowsAvailable;
+ }
+
+ /**
+ * Returns the range of available rows in {@link DataSource} for this event.
+ *
+ * @return range of available rows
+ */
+ public Range getAvailableRows() {
+ return rowsAvailable;
+ }
+
+ @Override
+ public Type<DataAvailableHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(DataAvailableHandler handler) {
+ handler.onDataAvailable(this);
+ }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/DataAvailableHandler.java b/client/src/com/vaadin/client/ui/grid/DataAvailableHandler.java
new file mode 100644
index 0000000000..06ea08a17e
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/DataAvailableHandler.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2014 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;
+
+/**
+ * Handler for {@link DataAvailableEvent}s.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface DataAvailableHandler extends EventHandler {
+
+ /**
+ * Called when DataSource has data available. Supplied with row range.
+ *
+ * @param availableRows
+ * Range of rows available in the DataSource
+ * @return true if the command was successfully completed, false to call
+ * again the next time new data is available
+ */
+ public void onDataAvailable(DataAvailableEvent event);
+}
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..bf62805034
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Escalator.java
@@ -0,0 +1,4626 @@
+/*
+ * Copyright 2000-2014 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.Level;
+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.core.client.Scheduler;
+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.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableRowElement;
+import com.google.gwt.dom.client.TableSectionElement;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.logging.client.LogConfiguration;
+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.GridState;
+import com.vaadin.shared.ui.grid.HeightMode;
+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.
+ (note for editors: line width is 80 chars, including the
+ one-space indentation)
+
+
+ == 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.
+
+ Currently, the physical and visual indices are kept in sync
+ _most of the time_ by a deferred rearrangement of rows.
+ They become desynced when scrolling. This is to help screen
+ readers to read the contents from the DOM in a natural
+ order. See BodyRowContainer.DeferredDomSorter for more
+ about that.
+
+ */
+
+/**
+ * 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 extend a non-inner-class (or
+ * implement such an interface), makes it possible for JSNI to indirectly refer
+ * to the inner class, by invoking methods and fields in the non-inner-class
+ * API.
+ *
+ * @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()
+ * @see Escalator.Scroller#onScroll()
+ */
+ 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()
+ * @see Escalator.Scroller#onScroll()
+ */
+ 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 TouchHandlerBundle touchHandlerBundle;
+
+ protected JsniWorkaround(final Escalator escalator) {
+ scrollListenerFunction = createScrollListenerFunction(escalator);
+ mousewheelListenerFunction = createMousewheelListenerFunction(escalator);
+
+ touchHandlerBundle = new TouchHandlerBundle(escalator);
+ touchStartFunction = touchHandlerBundle.getTouchStartHandler();
+ touchMoveFunction = touchHandlerBundle.getTouchMoveHandler();
+ touchEndFunction = touchHandlerBundle.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()
+ */
+ 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()
+ */
+ protected abstract JavaScriptObject createMousewheelListenerFunction(
+ Escalator esc);
+}
+
+/**
+ * A low-level table-like widget that features a scrolling virtual viewport and
+ * lazily generated rows.
+ *
+ * @since
+ * @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.
+ */
+ /*
+ * [[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 doNotUseThisTimestamp) {
+ /*
+ * We can't use the timestamp parameter here, since it is
+ * not in any predetermined format; TouchEnd does not
+ * provide a compatible timestamp, and we need to be able to
+ * get a comparable timestamp to determine whether to
+ * trigger a flick scroll or not.
+ */
+
+ if (touches != 1) {
+ return;
+ }
+
+ final int x = latestTouchMoveEvent.getPageX();
+ final int y = latestTouchMoveEvent.getPageY();
+ deltaX = x - lastX;
+ deltaY = y - lastY;
+ lastX = x;
+ lastY = y;
+
+ /*
+ * Instead of using the provided arbitrary timestamp, let's
+ * use a known-format and reproducible timestamp.
+ */
+ lastTime = Duration.currentTimeMillis();
+
+ // 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 = event.getNativeEvent().getTouches().length();
+ 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();
+
+ /*
+ * this initializes a correct timestamp, and also renders the
+ * first frame for added responsiveness.
+ */
+ mover.execute(Duration.currentTimeMillis());
+ }
+
+ public void touchEnd(final CustomTouchEvent event) {
+ touches = event.getNativeEvent().getTouches().length();
+
+ if (touches == 0) {
+ escalator.scroller.handleFlickScroll(deltaX, deltaY,
+ lastTime);
+ escalator.body.domSorter.reschedule();
+ }
+ }
+ }
+
+ public static void moveScrollFromEvent(final Escalator escalator,
+ final double deltaX, final double deltaY,
+ final NativeEvent event) {
+
+ if (!Double.isNaN(deltaX)) {
+ escalator.horizontalScrollbar.setScrollPosByDelta(deltaX);
+ }
+
+ if (!Double.isNaN(deltaY)) {
+ escalator.verticalScrollbar.setScrollPosByDelta(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;
+ private double lastLeft;
+ private double lastTop;
+
+ /**
+ * 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);
+
+ lastLeft = horizontalScrollbar.getScrollPos();
+ lastTop = verticalScrollbar.getScrollPos();
+
+ /*
+ * 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 doNotUseThisTimestamp) {
+ /*
+ * We cannot use the timestamp provided to this method since it is
+ * of a format that cannot be determined at will. Therefore, we need
+ * a timestamp format that we can handle, so our calculations are
+ * correct.
+ */
+
+ if (millisLeft <= 0 || cancelled) {
+ scroller.currentFlickScroller = null;
+ return;
+ }
+
+ final double timestamp = Duration.currentTimeMillis();
+ if (prevTime == 0) {
+ prevTime = timestamp;
+ AnimationScheduler.get().requestAnimationFrame(this);
+ return;
+ }
+
+ double currentLeft = horizontalScrollbar.getScrollPos();
+ double currentTop = verticalScrollbar.getScrollPos();
+
+ final double timeDiff = timestamp - prevTime;
+ double left = currentLeft - velX * timeDiff;
+ setScrollLeft(left);
+ velX -= xFric * timeDiff;
+
+ double top = currentTop - velY * timeDiff;
+ setScrollTop(top);
+ velY -= yFric * timeDiff;
+
+ cancelBecauseOfEdgeOrCornerMaybe();
+
+ prevTime = timestamp;
+ millisLeft -= timeDiff;
+ lastLeft = currentLeft;
+ lastTop = currentTop;
+ AnimationScheduler.get().requestAnimationFrame(this);
+ }
+
+ private void cancelBecauseOfEdgeOrCornerMaybe() {
+ if (lastLeft == horizontalScrollbar.getScrollPos()
+ && lastTop == verticalScrollbar.getScrollPos()) {
+ cancel();
+ }
+ }
+
+ public void cancel() {
+ cancelled = true;
+ }
+ }
+
+ /**
+ * 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 = body.calculateEstimatedTotalRowHeight();
+ int scrollContentWidth = columnConfiguration.calculateRowWidth();
+
+ double tableWrapperHeight = heightOfEscalator;
+ double tableWrapperWidth = widthOfEscalator;
+
+ boolean verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
+ - header.heightOfSection - footer.heightOfSection;
+ boolean horizontalScrollNeeded = scrollContentWidth > tableWrapperWidth;
+
+ // One dimension got scrollbars, but not the other. Recheck time!
+ if (verticalScrollNeeded != horizontalScrollNeeded) {
+ if (!verticalScrollNeeded && horizontalScrollNeeded) {
+ verticalScrollNeeded = scrollContentHeight > tableWrapperHeight
+ - header.heightOfSection
+ - footer.heightOfSection
+ - 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(tableWrapperHeight
+ - footer.heightOfSection - header.heightOfSection);
+ 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.
+ */
+ double prevScrollPos = horizontalScrollbar.getScrollPos();
+
+ int unfrozenPixels = columnConfiguration
+ .getCalculatedColumnsWidth(Range.between(
+ columnConfiguration.getFrozenColumnCount(),
+ columnConfiguration.getColumnCount()));
+ int frozenPixels = scrollContentWidth - unfrozenPixels;
+ double hScrollOffsetWidth = tableWrapperWidth - frozenPixels;
+ horizontalScrollbar.setOffsetSize(hScrollOffsetWidth);
+ horizontalScrollbar.setScrollSize(unfrozenPixels);
+ horizontalScrollbar.getElement().getStyle()
+ .setLeft(frozenPixels, Unit.PX);
+ horizontalScrollbar.setScrollPos(prevScrollPos);
+ }
+
+ /**
+ * Logical scrolling event handler for the entire widget.
+ */
+ public void onScroll() {
+ if (internalScrollEventCalls > 0) {
+ internalScrollEventCalls--;
+ return;
+ }
+
+ final double scrollTop = verticalScrollbar.getScrollPos();
+ final double scrollLeft = horizontalScrollbar.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 double viewportStartPx = getScrollLeft();
+ double 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(scrollLeft);
+ }
+
+ public void scrollToRow(final int rowIndex,
+ final ScrollDestination destination, final int padding) {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ final int targetStartPx = body.getDefaultRowHeight() * rowIndex;
+ final int targetEndPx = targetStartPx + body.getDefaultRowHeight();
+
+ 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 TableSectionElement root;
+
+ /** The height of the combined rows in the DOM. */
+ protected double heightOfSection = -1;
+
+ /**
+ * The primary style name of the escalator. Most commonly provided by
+ * Escalator as "v-escalator".
+ */
+ private String primaryStyleName = null;
+
+ /**
+ * A map containing cached values of an element's current top position.
+ * <p>
+ * 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)},
+ * {@link #getRowTop(Element)} and
+ * {@link #removeRowPosition(Element)} instead.
+ */
+ @Deprecated
+ private final Map<TableRowElement, Integer> rowTopPositionMap = new HashMap<TableRowElement, Integer>();
+
+ private boolean defaultRowHeightShouldBeAutodetected = true;
+
+ private int defaultRowHeight = INITIAL_DEFAULT_ROW_HEIGHT;
+
+ public AbstractRowContainer(
+ final TableSectionElement rowContainerElement) {
+ root = rowContainerElement;
+ }
+
+ @Override
+ public Element getElement() {
+ return root;
+ }
+
+ /**
+ * 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(int, int)} instead.
+ *
+ * @return the tag name for the element to represent cells as
+ * @see #createCellElement(int, int)
+ */
+ 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);
+ }
+ }
+
+ /**
+ * Removes those row elements from the DOM that correspond to the given
+ * range of logical indices. This may be fewer than {@code numberOfRows}
+ * , even zero, if not all the removed rows are actually visible.
+ * <p>
+ * The implementation must call {@link #paintRemoveRow(Element, int)}
+ * for each row that is removed from the DOM.
+ *
+ * @param index
+ * the logical index of the first removed row
+ * @param numberOfRows
+ * number of logical rows to remove
+ */
+ protected abstract void paintRemoveRows(final int index,
+ final int numberOfRows);
+
+ /**
+ * Removes a row element from the DOM, invoking
+ * {@link #getEscalatorUpdater()}
+ * {@link EscalatorUpdater#preDetach(Row, Iterable) preDetach} and
+ * {@link EscalatorUpdater#postDetach(Row, Iterable) postDetach} before
+ * and after removing the row, respectively.
+ * <p>
+ * This method must be called for each removed DOM row by any
+ * {@link #paintRemoveRows(int, int)} implementation.
+ *
+ * @param tr
+ * the row element to remove.
+ */
+ protected void paintRemoveRow(final TableRowElement tr,
+ final int logicalRowIndex) {
+
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ getEscalatorUpdater().preDetach(flyweightRow,
+ flyweightRow.getCells());
+
+ tr.removeFromParent();
+
+ getEscalatorUpdater().postDetach(flyweightRow,
+ flyweightRow.getCells());
+
+ /*
+ * the "assert" guarantees that this code is run only during
+ * development/debugging.
+ */
+ assert flyweightRow.teardown();
+
+ }
+
+ 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<TableRowElement> paintInsertRows(final int visualIndex,
+ final int numberOfRows) {
+ assert isAttached() : "Can't paint rows if Escalator is not attached";
+
+ final List<TableRowElement> addedRows = new ArrayList<TableRowElement>();
+
+ 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 = getDefaultRowHeight();
+ final TableRowElement tr = TableRowElement.as(DOM.createTR());
+ addedRows.add(tr);
+ tr.addClassName(getStylePrimaryName() + "-row");
+
+ for (int col = 0; col < columnConfiguration.getColumnCount(); col++) {
+ final int colWidth = columnConfiguration
+ .getColumnWidthActual(col);
+ final TableCellElement 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);
+ }
+ }
+
+ referenceRow = paintInsertRow(referenceRow, tr, row);
+ }
+ reapplyRowWidths();
+
+ recalculateSectionHeight();
+
+ return addedRows;
+ }
+
+ /**
+ * Inserts a single row into the DOM, invoking
+ * {@link #getEscalatorUpdater()}
+ * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
+ * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
+ * and after inserting the row, respectively. The row should have its
+ * cells already inserted.
+ *
+ * @param referenceRow
+ * the row after which to insert or null if insert as first
+ * @param tr
+ * the row to be inserted
+ * @param logicalRowIndex
+ * the logical index of the inserted row
+ * @return the inserted row to be used as the new reference
+ */
+ protected Node paintInsertRow(Node referenceRow,
+ final TableRowElement tr, int logicalRowIndex) {
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ getEscalatorUpdater().preAttach(flyweightRow,
+ flyweightRow.getCells());
+
+ referenceRow = insertAfterReferenceAndUpdateIt(root, tr,
+ referenceRow);
+
+ getEscalatorUpdater().postAttach(flyweightRow,
+ flyweightRow.getCells());
+ updater.update(flyweightRow, flyweightRow.getCells());
+
+ /*
+ * the "assert" guarantees that this code is run only during
+ * development/debugging.
+ */
+ assert flyweightRow.teardown();
+ return referenceRow;
+ }
+
+ 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;
+ }
+
+ abstract protected void recalculateSectionHeight();
+
+ /**
+ * Returns the estimated height of all rows in the row container.
+ * <p>
+ * The estimate is promised to be correct as long as there are no rows
+ * with calculated heights.
+ */
+ protected int calculateEstimatedTotalRowHeight() {
+ return getDefaultRowHeight() * getRowCount();
+ }
+
+ /**
+ * {@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 TableRowElement tr = getTrByVisualIndex(row);
+ refreshRow(tr, row);
+ }
+ }
+
+ Profiler.leave("Escalator.AbstractRowContainer.refreshRows");
+ }
+
+ void refreshRow(final TableRowElement tr, final int logicalRowIndex) {
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+ updater.update(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 TableCellElement createCellElement(final int height,
+ final int width) {
+ final TableCellElement cellElem = TableCellElement.as(DOM
+ .createElement(getCellElementTagName()));
+ cellElem.getStyle().setHeight(height, Unit.PX);
+ cellElem.getStyle().setWidth(width, Unit.PX);
+ cellElem.addClassName(getStylePrimaryName() + "-cell");
+ return cellElem;
+ }
+
+ @Override
+ public TableRowElement getRowElement(int index) {
+ return getTrByVisualIndex(index);
+ }
+
+ /**
+ * 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}
+ */
+ protected abstract TableRowElement getTrByVisualIndex(int index)
+ throws IndexOutOfBoundsException;
+
+ 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 TableRowElement tr = getTrByVisualIndex(visualRowIndex);
+
+ flyweightRow.setup(tr, visualRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ Iterable<FlyweightCell> cells = flyweightRow.getCells(offset,
+ numberOfColumns);
+
+ getEscalatorUpdater().preDetach(flyweightRow, cells);
+
+ for (FlyweightCell cell : cells) {
+ Element cellElement = cell.getElement();
+ cellElement.removeFromParent();
+ }
+
+ /**
+ * We need a new iterable that does not try to reset the cell
+ * elements from the tr as they're not attached anymore. Instead
+ * the cells simply retain the now-unattached elements that were
+ * assigned on the above iteration.
+ *
+ * TODO a cleaner solution, eg. an iterable that only associates
+ * the elements once
+ */
+ cells = flyweightRow
+ .getUnattachedCells(offset, numberOfColumns);
+ getEscalatorUpdater().postDetach(flyweightRow, cells);
+
+ assert flyweightRow.teardown();
+ }
+ 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());
+ }
+ }
+
+ 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 TableRowElement tr = getTrByVisualIndex(row);
+ paintInsertCells(tr, row, offset, numberOfColumns);
+ }
+ 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(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());
+ }
+ }
+
+ /**
+ * Inserts new cell elements into a single row element, invoking
+ * {@link #getEscalatorUpdater()}
+ * {@link EscalatorUpdater#preAttach(Row, Iterable) preAttach} and
+ * {@link EscalatorUpdater#postAttach(Row, Iterable) postAttach} before
+ * and after inserting the cells, respectively.
+ * <p>
+ * Precondition: The row must be already attached to the DOM and the
+ * FlyweightCell instances corresponding to the new columns added to
+ * {@code flyweightRow}.
+ *
+ * @param tr
+ * the row in which to insert the cells
+ * @param logicalRowIndex
+ * the index of the row
+ * @param offset
+ * the index of the first cell
+ * @param numberOfCells
+ * the number of cells to insert
+ */
+ private void paintInsertCells(final TableRowElement tr,
+ int logicalRowIndex, final int offset, final int numberOfCells) {
+
+ assert Document.get().isOrHasChild(tr) : "The row must be attached to the document";
+
+ flyweightRow.setup(tr, logicalRowIndex,
+ columnConfiguration.getCalculatedColumnWidths());
+
+ Iterable<FlyweightCell> cells = flyweightRow.getUnattachedCells(
+ offset, numberOfCells);
+
+ final int rowHeight = getDefaultRowHeight();
+ for (FlyweightCell cell : cells) {
+ final int colWidth = columnConfiguration
+ .getColumnWidthActual(cell.getColumn());
+ final TableCellElement cellElem = createCellElement(rowHeight,
+ colWidth);
+ cell.setElement(cellElem);
+ }
+
+ getEscalatorUpdater().preAttach(flyweightRow, cells);
+
+ Node referenceCell;
+ if (offset != 0) {
+ referenceCell = tr.getChild(offset - 1);
+ } else {
+ referenceCell = null;
+ }
+ for (FlyweightCell cell : cells) {
+ referenceCell = insertAfterReferenceAndUpdateIt(tr,
+ cell.getElement(), referenceCell);
+ }
+
+ getEscalatorUpdater().postAttach(flyweightRow, cells);
+
+ assert flyweightRow.teardown();
+ }
+
+ public void setColumnFrozen(int column, boolean frozen) {
+ final NodeList<TableRowElement> childRows = root.getRows();
+
+ for (int row = 0; row < childRows.getLength(); row++) {
+ final TableRowElement tr = childRows.getItem(row);
+
+ TableCellElement cell = tr.getCells().getItem(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<TableRowElement> childRows = root.getRows();
+
+ for (int row = 0; row < childRows.getLength(); row++) {
+ final TableRowElement tr = childRows.getItem(row);
+
+ TableCellElement cell = tr.getCells().getItem(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) {
+ TableRowElement row = TableRowElement.as(root
+ .getFirstChildElement());
+ int maxWidth = 0;
+ while (row != null) {
+ final TableCellElement cell = row.getCells().getItem(index);
+ final boolean isVisible = !cell.getStyle().getDisplay()
+ .equals(Display.NONE.getCssName());
+ if (isVisible) {
+ maxWidth = Math.max(maxWidth, cell.getScrollWidth());
+ }
+ row = TableRowElement.as(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
+ TableRowElement row = root.getRows().getItem(0);
+ while (row != null) {
+ UIObject.setStylePrimaryName(row, primaryStyleName + "-row");
+ TableCellElement cell = row.getCells().getItem(0);
+ while (cell != null) {
+ UIObject.setStylePrimaryName(cell, primaryStyleName
+ + "-cell");
+ cell = TableCellElement.as(cell.getNextSiblingElement());
+ }
+ row = TableRowElement.as(row.getNextSiblingElement());
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ @Override
+ public void setDefaultRowHeight(int px) throws IllegalArgumentException {
+ if (px < 1) {
+ throw new IllegalArgumentException("Height must be positive. "
+ + px + " was given.");
+ }
+
+ defaultRowHeightShouldBeAutodetected = false;
+ defaultRowHeight = px;
+ reapplyDefaultRowHeights();
+ }
+
+ @Override
+ public int getDefaultRowHeight() {
+ return defaultRowHeight;
+ }
+
+ /**
+ * The default height of rows has (most probably) changed.
+ * <p>
+ * Make sure that the displayed rows with a default height are updated
+ * in height and top position.
+ * <p>
+ * <em>Note:</em>This implementation should not call
+ * {@link Escalator#recalculateElementSizes()} - it is done by the
+ * discretion of the caller of this method.
+ */
+ protected abstract void reapplyDefaultRowHeights();
+
+ protected void reapplyRowHeight(final TableRowElement tr,
+ final int heightPx) {
+ Element cellElem = tr.getFirstChildElement();
+ while (cellElem != null) {
+ cellElem.getStyle().setHeight(heightPx, Unit.PX);
+ cellElem = cellElem.getNextSiblingElement();
+ }
+
+ /*
+ * no need to apply height to tr-element, it'll be resized
+ * implicitly.
+ */
+ }
+
+ @SuppressWarnings("boxing")
+ protected void setRowPosition(final TableRowElement tr, final int x,
+ final int y) {
+ position.set(tr, x, y);
+ rowTopPositionMap.put(tr, y);
+ }
+
+ @SuppressWarnings("boxing")
+ protected int getRowTop(final TableRowElement tr) {
+ return rowTopPositionMap.get(tr);
+ }
+
+ protected void removeRowPosition(TableRowElement tr) {
+ rowTopPositionMap.remove(tr);
+ }
+
+ public void autodetectRowHeight() {
+ Scheduler.get().scheduleDeferred(new Scheduler.ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (defaultRowHeightShouldBeAutodetected && isAttached()) {
+ final Element detectionTr = DOM.createTR();
+ detectionTr
+ .setClassName(getStylePrimaryName() + "-row");
+
+ final Element cellElem = DOM
+ .createElement(getCellElementTagName());
+ cellElem.setClassName(getStylePrimaryName() + "-cell");
+ cellElem.setInnerHTML("foo");
+
+ detectionTr.appendChild(cellElem);
+ root.appendChild(detectionTr);
+ defaultRowHeight = Math.max(1,
+ cellElem.getOffsetHeight());
+ root.removeChild(detectionTr);
+
+ if (root.hasChildNodes()) {
+ reapplyDefaultRowHeights();
+ }
+
+ defaultRowHeightShouldBeAutodetected = false;
+ }
+ }
+ });
+ }
+
+ @Override
+ public Cell getCell(final Element element) {
+ if (element == null) {
+ throw new IllegalArgumentException("Element cannot be null");
+ }
+
+ /*
+ * Ensure that element is not root nor the direct descendant of root
+ * (a row) and ensure the element is inside the dom hierarchy of the
+ * root element. If not, return.
+ */
+ if (root == element || element.getParentElement() == root
+ || !root.isOrHasChild(element)) {
+ return null;
+ }
+
+ /*
+ * Ensure element is the cell element by iterating up the DOM
+ * hierarchy until reaching cell element.
+ */
+ Element cellElementCandidate = element;
+ while (cellElementCandidate.getParentElement().getParentElement() != root) {
+ cellElementCandidate = cellElementCandidate.getParentElement();
+ }
+ final TableCellElement cellElement = TableCellElement
+ .as(cellElementCandidate);
+
+ // Find dom column
+ int domColumnIndex = -1;
+ for (Element e = cellElement; e != null; e = e
+ .getPreviousSiblingElement()) {
+ domColumnIndex++;
+ }
+
+ // Find dom row
+ int domRowIndex = -1;
+ for (Element e = cellElement.getParentElement(); e != null; e = e
+ .getPreviousSiblingElement()) {
+ domRowIndex++;
+ }
+
+ return new Cell(domRowIndex, domColumnIndex, cellElement);
+ }
+ }
+
+ private abstract class AbstractStaticRowContainer extends
+ AbstractRowContainer {
+ public AbstractStaticRowContainer(final TableSectionElement headElement) {
+ super(headElement);
+ }
+
+ @Override
+ protected void paintRemoveRows(final int index, final int numberOfRows) {
+ for (int i = index; i < index + numberOfRows; i++) {
+ final TableRowElement tr = root.getRows().getItem(index);
+ paintRemoveRow(tr, index);
+ }
+ recalculateSectionHeight();
+ }
+
+ @Override
+ protected TableRowElement getTrByVisualIndex(final int index)
+ throws IndexOutOfBoundsException {
+ if (index >= 0 && index < root.getChildCount()) {
+ return root.getRows().getItem(index);
+ } else {
+ throw new IndexOutOfBoundsException("No such visual index: "
+ + index);
+ }
+ }
+
+ @Override
+ public void insertRows(int index, int numberOfRows) {
+ super.insertRows(index, numberOfRows);
+ recalculateElementSizes();
+ applyHeightByRows();
+ }
+
+ @Override
+ public void removeRows(int index, int numberOfRows) {
+ super.removeRows(index, numberOfRows);
+ recalculateElementSizes();
+ applyHeightByRows();
+ }
+
+ @Override
+ protected void reapplyDefaultRowHeights() {
+ if (root.getChildCount() == 0) {
+ return;
+ }
+
+ Profiler.enter("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
+
+ TableRowElement tr = root.getRows().getItem(0);
+ while (tr != null) {
+ reapplyRowHeight(tr, getDefaultRowHeight());
+ tr = TableRowElement.as(tr.getNextSiblingElement());
+ }
+
+ /*
+ * Because all rows are immediately displayed in the static row
+ * containers, the section's overall height has most probably
+ * changed.
+ */
+ recalculateSectionHeight();
+
+ Profiler.leave("Escalator.AbstractStaticRowContainer.reapplyDefaultRowHeights");
+ }
+
+ @Override
+ protected void recalculateSectionHeight() {
+ Profiler.enter("Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
+
+ int newHeight = calculateEstimatedTotalRowHeight();
+ if (newHeight != heightOfSection) {
+ heightOfSection = newHeight;
+ sectionHeightCalculated();
+ body.verifyEscalatorCount();
+ }
+
+ Profiler.leave("Escalator.AbstractStaticRowContainer.recalculateSectionHeight");
+ }
+
+ /**
+ * 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 abstract void sectionHeightCalculated();
+ }
+
+ private class HeaderRowContainer extends AbstractStaticRowContainer {
+ public HeaderRowContainer(final TableSectionElement headElement) {
+ super(headElement);
+ }
+
+ @Override
+ protected void sectionHeightCalculated() {
+ bodyElem.getStyle().setMarginTop(heightOfSection, Unit.PX);
+ verticalScrollbar.getElement().getStyle()
+ .setTop(heightOfSection, 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 TableSectionElement footElement) {
+ super(footElement);
+ }
+
+ @Override
+ public void setStylePrimaryName(String primaryStyleName) {
+ super.setStylePrimaryName(primaryStyleName);
+ UIObject.setStylePrimaryName(root, primaryStyleName + "-footer");
+ }
+
+ @Override
+ protected String getCellElementTagName() {
+ return "td";
+ }
+
+ @Override
+ protected void sectionHeightCalculated() {
+ int vscrollHeight = (int) Math.floor(heightOfEscalator
+ - header.heightOfSection - footer.heightOfSection);
+
+ final boolean horizontalScrollbarNeeded = columnConfiguration
+ .calculateRowWidth() > widthOfEscalator;
+ if (horizontalScrollbarNeeded) {
+ vscrollHeight -= horizontalScrollbar.getScrollbarThickness();
+ }
+
+ verticalScrollbar.setOffsetSize(vscrollHeight);
+ }
+ }
+
+ 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.
+ *
+ * @see #sortDomElements()
+ */
+ private final LinkedList<TableRowElement> visualRowOrder = new LinkedList<TableRowElement>();
+
+ /**
+ * The logical index of the topmost row.
+ *
+ * @deprecated Use the accessors {@link #setTopRowLogicalIndex(int)},
+ * {@link #updateTopRowLogicalIndex(int)} and
+ * {@link #getTopRowLogicalIndex()} instead
+ */
+ @Deprecated
+ private int topRowLogicalIndex = 0;
+
+ private void setTopRowLogicalIndex(int topRowLogicalIndex) {
+ if (LogConfiguration.loggingIsEnabled(Level.INFO)) {
+ Logger.getLogger("Escalator.BodyRowContainer").fine(
+ "topRowLogicalIndex: " + this.topRowLogicalIndex
+ + " -> " + topRowLogicalIndex);
+ }
+ assert topRowLogicalIndex >= 0 : "topRowLogicalIndex became negative";
+ /*
+ * if there's a smart way of evaluating and asserting the max index,
+ * this would be a nice place to put it. I haven't found out an
+ * effective and generic solution.
+ */
+
+ this.topRowLogicalIndex = topRowLogicalIndex;
+ }
+
+ private int getTopRowLogicalIndex() {
+ return topRowLogicalIndex;
+ }
+
+ private void updateTopRowLogicalIndex(int diff) {
+ setTopRowLogicalIndex(topRowLogicalIndex + diff);
+ }
+
+ private class DeferredDomSorter {
+ private static final int SORT_DELAY_MILLIS = 50;
+
+ // as it happens, 3 frames = 50ms @ 60fps.
+ private static final int REQUIRED_FRAMES_PASSED = 3;
+
+ private final AnimationCallback frameCounter = new AnimationCallback() {
+ @Override
+ public void execute(double timestamp) {
+ framesPassed++;
+ boolean domWasSorted = sortIfConditionsMet();
+ if (!domWasSorted) {
+ animationHandle = AnimationScheduler.get()
+ .requestAnimationFrame(this);
+ }
+ }
+ };
+
+ private int framesPassed;
+ private double startTime;
+ private AnimationHandle animationHandle;
+
+ public void reschedule() {
+ resetConditions();
+ animationHandle = AnimationScheduler.get()
+ .requestAnimationFrame(frameCounter);
+ }
+
+ private boolean sortIfConditionsMet() {
+ boolean enoughFramesHavePassed = framesPassed >= REQUIRED_FRAMES_PASSED;
+ boolean enoughTimeHasPassed = (Duration.currentTimeMillis() - startTime) >= SORT_DELAY_MILLIS;
+ boolean conditionsMet = enoughFramesHavePassed
+ && enoughTimeHasPassed;
+
+ if (conditionsMet) {
+ resetConditions();
+ sortDomElements();
+ }
+
+ return conditionsMet;
+ }
+
+ private void resetConditions() {
+ if (animationHandle != null) {
+ animationHandle.cancel();
+ animationHandle = null;
+ }
+ startTime = Duration.currentTimeMillis();
+ framesPassed = 0;
+ }
+ }
+
+ private DeferredDomSorter domSorter = new DeferredDomSorter();
+
+ public BodyRowContainer(final TableSectionElement 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 double topRowPos = getRowTop(visualRowOrder.getFirst());
+ // TODO [[mpixscroll]]
+ final double scrollTop = tBodyScrollTop;
+ final double 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
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ int originalRowsToMove = (int) Math.ceil(viewportOffset
+ / getDefaultRowHeight());
+ int rowsToMove = Math.min(originalRowsToMove,
+ root.getChildCount());
+
+ final int end = root.getChildCount();
+ final int start = end - rowsToMove;
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ final int logicalRowIndex = (int) (scrollTop / getDefaultRowHeight());
+ moveAndUpdateEscalatorRows(Range.between(start, end), 0,
+ logicalRowIndex);
+
+ updateTopRowLogicalIndex(-originalRowsToMove);
+
+ rowsWereMoved = true;
+ }
+
+ else if (viewportOffset + getDefaultRowHeight() <= 0) {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+
+ /*
+ * the viewport has been scrolled more than the topmost visual
+ * row.
+ */
+
+ int originalRowsToMove = (int) Math.abs(viewportOffset
+ / getDefaultRowHeight());
+ int rowsToMove = Math.min(originalRowsToMove,
+ 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 {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ /*
+ * Since we're moving all escalator rows, we need to
+ * calculate the first logical row index from the scroll
+ * position.
+ */
+ logicalRowIndex = (int) (scrollTop / getDefaultRowHeight());
+ }
+
+ /*
+ * 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);
+
+ /*
+ * We cannot trust getLogicalRowIndex, because it hasn't yet
+ * been updated. But since we're leaving rows behind, it
+ * means we've scrolled to the bottom. So, instead, we
+ * simply count backwards from the end.
+ */
+ final int topLogicalIndex = getRowCount()
+ - visualRowOrder.size();
+ moveAndUpdateEscalatorRows(strayRow, 0, topLogicalIndex);
+ }
+
+ final int naiveNewLogicalIndex = getTopRowLogicalIndex()
+ + originalRowsToMove;
+ final int maxLogicalIndex = getRowCount()
+ - visualRowOrder.size();
+ setTopRowLogicalIndex(Math.min(naiveNewLogicalIndex,
+ maxLogicalIndex));
+
+ rowsWereMoved = true;
+ }
+
+ if (rowsWereMoved) {
+ fireRowVisibilityChangeEvent();
+
+ if (scroller.touchHandlerBundle.touches == 0) {
+ /*
+ * this will never be called on touch scrolling. That is
+ * handled separately and explicitly by
+ * TouchHandlerBundle.touchEnd();
+ */
+ domSorter.reschedule();
+ }
+ }
+ }
+
+ @Override
+ protected List<TableRowElement> 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<TableRowElement> addedRows = fillAndPopulateEscalatorRowsIfNeeded(
+ index, numberOfRows);
+
+ /*
+ * insertRows will always change the number of rows - update the
+ * scrollbar sizes.
+ */
+ scroller.recalculateScrollbarsForVirtualViewport();
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ final boolean addedRowsAboveCurrentViewport = index
+ * getDefaultRowHeight() < getScrollTop();
+ final boolean addedRowsBelowCurrentViewport = index
+ * getDefaultRowHeight() > 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.
+ */
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ final int yDelta = numberOfRows * getDefaultRowHeight();
+ adjustScrollPosIgnoreEvents(yDelta);
+ updateTopRowLogicalIndex(numberOfRows);
+ }
+
+ 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);
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ // move the surrounding rows to their correct places.
+ int rowTop = (unupdatedLogicalStart + (end - start))
+ * getDefaultRowHeight();
+ final ListIterator<TableRowElement> i = visualRowOrder
+ .listIterator(visualTargetIndex + (end - start));
+ while (i.hasNext()) {
+ final TableRowElement tr = i.next();
+ setRowPosition(tr, 0, rowTop);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ rowTop += getDefaultRowHeight();
+ }
+
+ fireRowVisibilityChangeEvent();
+ sortDomElements();
+ }
+ 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<TableRowElement> removedRows = new ArrayList<TableRowElement>(
+ visualSourceRange.length());
+ for (int i = 0; i < visualSourceRange.length(); i++) {
+ final TableRowElement tr = visualRowOrder
+ .remove(visualSourceRange.getStart());
+ removedRows.add(tr);
+ }
+ visualRowOrder.addAll(adjustedVisualTargetIndex, removedRows);
+ }
+
+ { // Refresh the contents of the affected rows
+ final ListIterator<TableRowElement> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ for (int logicalIndex = logicalTargetIndex; logicalIndex < logicalTargetIndex
+ + visualSourceRange.length(); logicalIndex++) {
+ final TableRowElement tr = iter.next();
+ refreshRow(tr, logicalIndex);
+ }
+ }
+
+ { // Reposition the rows that were moved
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ int newRowTop = logicalTargetIndex * getDefaultRowHeight();
+
+ final ListIterator<TableRowElement> iter = visualRowOrder
+ .listIterator(adjustedVisualTargetIndex);
+ for (int i = 0; i < visualSourceRange.length(); i++) {
+ final TableRowElement tr = iter.next();
+ setRowPosition(tr, 0, newRowTop);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ newRowTop += getDefaultRowHeight();
+ }
+ }
+ }
+
+ /**
+ * Adjust the scroll position without having the scroll handler have any
+ * side-effects.
+ * <p>
+ * <em>Note:</em> {@link Scroller#onScroll()} <em>will</em> be
+ * triggered, but it will 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 double yDelta) {
+ if (yDelta == 0) {
+ return;
+ }
+
+ internalScrollEventCalls++;
+ verticalScrollbar.setScrollPosByDelta(yDelta);
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ final int rowTopPos = (int) yDelta
+ - ((int) yDelta % getDefaultRowHeight());
+ for (final TableRowElement tr : visualRowOrder) {
+ setRowPosition(tr, 0, getRowTop(tr) + rowTopPos);
+ }
+ setBodyScrollPosition(tBodyScrollLeft, tBodyScrollTop + yDelta);
+ }
+
+ /**
+ * 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<TableRowElement> 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<TableRowElement> 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++) {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ setRowPosition(addedRows.get(i), 0, (index + i)
+ * getDefaultRowHeight());
+ }
+
+ /* Move the other rows away from above the added escalator rows */
+ for (int i = index + addedRows.size(); i < visualRowOrder
+ .size(); i++) {
+ final TableRowElement tr = visualRowOrder.get(i);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ setRowPosition(tr, 0, i * getDefaultRowHeight());
+ }
+
+ return addedRows;
+ } else {
+ return new ArrayList<TableRowElement>();
+ }
+ }
+
+ private int getMaxEscalatorRowCapacity() {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ final int maxEscalatorRowCapacity = (int) Math
+ .ceil(calculateHeight() / getDefaultRowHeight()) + 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) {
+ if (numberOfRows == 0) {
+ return;
+ }
+
+ 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) {
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ final int yDelta = removedAbove.length()
+ * getDefaultRowHeight();
+ final int firstLogicalRowHeight = getDefaultRowHeight();
+ 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 TableRowElement tr = visualRowOrder
+ .remove(removedVisualInside.getStart());
+
+ paintRemoveRow(tr, index);
+ removeRowPosition(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 TableRowElement tr = visualRowOrder.get(i);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default
+ * row heights - will not work with variable row heights
+ */
+ setRowPosition(tr, 0, i * getDefaultRowHeight());
+ }
+
+ /*
+ * 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 TableRowElement 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.
+ */
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ final int contentBottom = getRowCount()
+ * getDefaultRowHeight();
+ 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 * getDefaultRowHeight())
+ - viewportBottom < getDefaultRowHeight()) {
+ /*
+ * FIXME [[rowheight]]: above coded to work only with
+ * default row heights - will not work with variable row
+ * heights
+ */
+
+ /*
+ * We're at the end of the row container, everything is
+ * added to the top.
+ */
+ paintRemoveRowsAtBottom(removedLogicalInside,
+ removedVisualInside);
+ updateTopRowLogicalIndex(-removedLogicalInside.length());
+ }
+
+ 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
+ */
+ double newTop = getRowTop(visualRowOrder
+ .get(removedVisualInside.getStart()));
+ for (int i = 0; i < removedVisualInside.length(); i++) {
+ final TableRowElement tr = visualRowOrder
+ .remove(removedVisualInside.getStart());
+ visualRowOrder.addLast(tr);
+ }
+
+ for (int i = removedVisualInside.getStart(); i < escalatorRowCount; i++) {
+ final TableRowElement tr = visualRowOrder.get(i);
+ setRowPosition(tr, 0, (int) newTop);
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with
+ * default row heights - will not work with variable
+ * row heights
+ */
+ newTop += getDefaultRowHeight();
+ }
+
+ /*
+ * 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);
+ updateTopRowLogicalIndex(-1);
+
+ /*
+ * STEP 3:
+ *
+ * update remaining escalator rows
+ */
+ /*-
+ * |1| |1|
+ * |4| ==> |4|
+ * |*| |5| <-- newly rendered
+ *
+ * 5
+ */
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default
+ * row heights - will not work with variable row heights
+ */
+ final int rowsScrolled = (int) (Math
+ .ceil((viewportBottom - (double) contentBottom)
+ / getDefaultRowHeight()));
+ 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);
+ }
+ }
+
+ fireRowVisibilityChangeEvent();
+ sortDomElements();
+ }
+
+ updateTopRowLogicalIndex(-removedAbove.length());
+
+ /*
+ * this needs to be done after the escalator has been shrunk down,
+ * or it won't work correctly (due to setScrollTop invocation)
+ */
+ scroller.recalculateScrollbarsForVirtualViewport();
+ }
+
+ 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<TableRowElement> iterator = visualRowOrder
+ .listIterator(removedVisualInside.getStart());
+
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ int rowTop = (removedLogicalInside.getStart() + logicalOffset)
+ * getDefaultRowHeight();
+ for (int i = removedVisualInside.getStart(); i < escalatorRowCount
+ - removedVisualInside.length(); i++) {
+ final TableRowElement tr = iterator.next();
+ setRowPosition(tr, 0, rowTop);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ rowTop += getDefaultRowHeight();
+ }
+ }
+
+ 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<TableRowElement> iterator = visualRowOrder
+ .listIterator(removedVisualInside.getEnd());
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row heights
+ * - will not work with variable row heights
+ */
+ int rowTop = removedLogicalInside.getStart()
+ * getDefaultRowHeight();
+ while (iterator.hasNext()) {
+ final TableRowElement tr = iterator.next();
+ setRowPosition(tr, 0, rowTop);
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ rowTop += getDefaultRowHeight();
+ }
+ }
+
+ private int getLogicalRowIndex(final Element tr) {
+ assert tr.getParentNode() == root : "The given element isn't a row element in the body";
+ int internalIndex = visualRowOrder.indexOf(tr);
+ return getTopRowLogicalIndex() + internalIndex;
+ }
+
+ @Override
+ protected void recalculateSectionHeight() {
+ // NOOP 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 maxEscalatorRows = getMaxEscalatorRowCapacity();
+ 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";
+ }
+
+ /**
+ * Calculates the height of the {@code <tbody>} as it should be rendered
+ * in the DOM.
+ */
+ private double calculateHeight() {
+ final int tableHeight = tableWrapper.getOffsetHeight();
+ final double footerHeight = footer.heightOfSection;
+ final double headerHeight = header.heightOfSection;
+ 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 TableRowElement 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);
+ }
+ }
+
+ @Override
+ public TableRowElement getRowElement(int index) {
+ if (index < 0 || index >= getRowCount()) {
+ throw new IndexOutOfBoundsException("No such logical index: "
+ + index);
+ }
+ int visualIndex = index
+ - getLogicalRowIndex(visualRowOrder.getFirst());
+ if (visualIndex >= 0 && visualIndex < visualRowOrder.size()) {
+ return super.getRowElement(visualIndex);
+ } else {
+ throw new IllegalStateException("Row with logical index "
+ + index + " is currently not available in the DOM");
+ }
+ }
+
+ private void setBodyScrollPosition(final double scrollLeft,
+ final double scrollTop) {
+ tBodyScrollLeft = scrollLeft;
+ tBodyScrollTop = scrollTop;
+ position.set(bodyElem, -tBodyScrollLeft, -tBodyScrollTop);
+ }
+
+ /**
+ * 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.
+ * <p>
+ * <em>Note:</em> This method will make sure that the escalator rows are
+ * placed in the proper places. By default new rows are added below, but
+ * if the content is scrolled down, the rows are populated on top
+ * instead.
+ */
+ 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<TableRowElement> 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<TableRowElement> iter = visualRowOrder
+ .listIterator(visualRowOrder.size());
+ for (int i = 0; i < -neededEscalatorRowsDiff; i++) {
+ final Element last = iter.previous();
+ 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());
+ /*
+ * FIXME [[rowheight]]: coded to work only with default row
+ * heights - will not work with variable row heights
+ */
+ final double firstRowMinTop = tBodyScrollTop
+ - getDefaultRowHeight();
+ 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");
+ }
+
+ @Override
+ protected void reapplyDefaultRowHeights() {
+ if (visualRowOrder.isEmpty()) {
+ return;
+ }
+
+ /*
+ * As an intermediate step between hard-coded row heights to crazily
+ * varying row heights, Escalator will support the modification of
+ * the default row height (which is applied to all rows).
+ *
+ * This allows us to do some assumptions and simplifications for
+ * now. This code is intended to be quite short-lived, but gives
+ * insight into what needs to be done when row heights change in the
+ * body, in a general sense.
+ *
+ * TODO [[rowheight]] remove this comment once row heights may
+ * genuinely vary.
+ */
+
+ Profiler.enter("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
+
+ /* step 1: resize and reposition rows */
+ for (int i = 0; i < visualRowOrder.size(); i++) {
+ TableRowElement tr = visualRowOrder.get(i);
+ reapplyRowHeight(tr, getDefaultRowHeight());
+
+ final int logicalIndex = getTopRowLogicalIndex() + i;
+ setRowPosition(tr, 0, logicalIndex * getDefaultRowHeight());
+ }
+
+ /*
+ * step 2: move scrollbar so that it corresponds to its previous
+ * place
+ */
+
+ /*
+ * This ratio needs to be calculated with the scrollsize (not max
+ * scroll position) in order to align the top row with the new
+ * scroll position.
+ */
+ double scrollRatio = verticalScrollbar.getScrollPos()
+ / verticalScrollbar.getScrollSize();
+ scroller.recalculateScrollbarsForVirtualViewport();
+ internalScrollEventCalls++;
+ verticalScrollbar.setScrollPos((int) (getDefaultRowHeight()
+ * getRowCount() * scrollRatio));
+ setBodyScrollPosition(horizontalScrollbar.getScrollPos(),
+ verticalScrollbar.getScrollPos());
+ scroller.onScroll();
+
+ /* step 3: make sure we have the correct amount of escalator rows. */
+ verifyEscalatorCount();
+
+ /*
+ * TODO [[rowheight]] This simply doesn't work with variable rows
+ * heights.
+ */
+ setTopRowLogicalIndex(getRowTop(visualRowOrder.getFirst())
+ / getDefaultRowHeight());
+
+ Profiler.leave("Escalator.BodyRowContainer.reapplyDefaultRowHeights");
+ }
+
+ /**
+ * Sorts the rows in the DOM to correspond to the visual order.
+ *
+ * @see #visualRowOrder
+ */
+ private void sortDomElements() {
+ final String profilingName = "Escalator.BodyRowContainer.sortDomElements";
+ Profiler.enter(profilingName);
+
+ /*
+ * Focus is lost from an element if that DOM element is (or any of
+ * its parents are) removed from the document. Therefore, we sort
+ * everything around that row instead.
+ */
+ final TableRowElement activeRow = getEscalatorRowWithFocus();
+
+ if (activeRow != null) {
+ assert activeRow.getParentElement() == root : "Trying to sort around a row that doesn't exist in body";
+ assert visualRowOrder.contains(activeRow) : "Trying to sort around a row that doesn't exist in visualRowOrder.";
+ }
+
+ /*
+ * Two cases handled simultaneously:
+ *
+ * 1) No focus on rows. We iterate visualRowOrder backwards, and
+ * take the respective element in the DOM, and place it as the first
+ * child in the body element. Then we take the next-to-last from
+ * visualRowOrder, and put that first, pushing the previous row as
+ * the second child. And so on...
+ *
+ * 2) Focus on some row within Escalator body. Again, we iterate
+ * visualRowOrder backwards. This time, we use the focused row as a
+ * pivot: Instead of placing rows from the bottom of visualRowOrder
+ * and placing it first, we place it underneath the focused row.
+ * Once we hit the focused row, we don't move it (to not reset
+ * focus) but change sorting mode. After that, we place all rows as
+ * the first child.
+ */
+
+ /*
+ * If we have a focused row, start in the mode where we put
+ * everything underneath that row. Otherwise, all rows are placed as
+ * first child.
+ */
+ boolean insertFirst = (activeRow == null);
+
+ final ListIterator<TableRowElement> i = visualRowOrder
+ .listIterator(visualRowOrder.size());
+ while (i.hasPrevious()) {
+ TableRowElement tr = i.previous();
+
+ if (tr == activeRow) {
+ insertFirst = true;
+ } else if (insertFirst) {
+ root.insertFirst(tr);
+ } else {
+ root.insertAfter(tr, activeRow);
+ }
+ }
+
+ Profiler.leave(profilingName);
+ }
+
+ /**
+ * Get the escalator row that has focus.
+ *
+ * @return The escalator row that contains a focused DOM element, or
+ * <code>null</code> if focus is outside of a body row.
+ */
+ private TableRowElement getEscalatorRowWithFocus() {
+ TableRowElement activeRow = null;
+
+ final Element activeElement = Util.getFocusedElement();
+
+ if (root.isOrHasChild(activeElement)) {
+ Element e = activeElement;
+
+ while (e != null && e != root) {
+ /*
+ * You never know if there's several tables embedded in a
+ * cell... We'll take the deepest one.
+ */
+ if (TableRowElement.is(e)) {
+ activeRow = TableRowElement.as(e);
+ }
+ e = e.getParentElement();
+ }
+ }
+
+ return activeRow;
+ }
+
+ @Override
+ public Cell getCell(Element element) {
+ Cell cell = super.getCell(element);
+ if (cell == null) {
+ return null;
+ }
+
+ // Convert DOM coordinates to logical coordinates for rows
+ Element rowElement = cell.getElement().getParentElement();
+ return new Cell(getLogicalRowIndex(rowElement), cell.getColumn(),
+ cell.getElement());
+ }
+ }
+
+ 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 = "500.0px";
+ private static final String DEFAULT_HEIGHT = "400.0px";
+
+ private FlyweightRow flyweightRow = new FlyweightRow();
+
+ /** The {@code <thead/>} tag. */
+ private final TableSectionElement headElem = TableSectionElement.as(DOM
+ .createTHead());
+ /** The {@code <tbody/>} tag. */
+ private final TableSectionElement bodyElem = TableSectionElement.as(DOM
+ .createTBody());
+ /** The {@code <tfoot/>} tag. */
+ private final TableSectionElement footElem = TableSectionElement.as(DOM
+ .createTFoot());
+
+ /**
+ * TODO: investigate whether this field is now unnecessary, as
+ * {@link ScrollbarBundle} now caches its values.
+ *
+ * @deprecated maybe...
+ */
+ @Deprecated
+ private double tBodyScrollTop = 0;
+
+ /**
+ * TODO: investigate whether this field is now unnecessary, as
+ * {@link ScrollbarBundle} now caches its values.
+ *
+ * @deprecated maybe...
+ */
+ @Deprecated
+ private double 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 widthOfEscalator;
+ /** The cached height of the escalator, in pixels. */
+ private double heightOfEscalator;
+
+ /** The height of Escalator in terms of body rows. */
+ private double heightByRows = GridState.DEFAULT_HEIGHT_BY_ROWS;
+
+ /** The height of Escalator, as defined by {@link #setHeight(String)} */
+ private String heightByCss = "";
+
+ private HeightMode heightMode = HeightMode.CSS;
+
+ 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());
+ verticalScrollbar.getElement().setTabIndex(-1);
+ verticalScrollbar.setScrollbarThickness(Util.getNativeScrollbarSize());
+
+ root.appendChild(horizontalScrollbar.getElement());
+ horizontalScrollbar.getElement().setTabIndex(-1);
+ horizontalScrollbar
+ .setScrollbarThickness(Util.getNativeScrollbarSize());
+ horizontalScrollbar
+ .addVisibilityHandler(new ScrollbarBundle.VisibilityHandler() {
+ @Override
+ public void visibilityChanged(
+ ScrollbarBundle.VisibilityChangeEvent event) {
+ /*
+ * We either lost or gained a scrollbar. In any case, we
+ * need to change the height, if it's defined by rows.
+ */
+ applyHeightByRows();
+ }
+ });
+
+ 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.autodetectRowHeight();
+ body.autodetectRowHeight();
+ footer.autodetectRowHeight();
+
+ 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 row container for the header in this Escalator.
+ *
+ * @return the header. Never <code>null</code>
+ */
+ public RowContainer getHeader() {
+ return header;
+ }
+
+ /**
+ * Returns the row container for the body in this Escalator.
+ *
+ * @return the body. Never <code>null</code>
+ */
+ public RowContainer getBody() {
+ return body;
+ }
+
+ /**
+ * Returns the row container for the footer in this Escalator.
+ *
+ * @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;
+ }
+
+ @Override
+ public void setWidth(final String width) {
+ super.setWidth(width != null && !width.isEmpty() ? width
+ : DEFAULT_WIDTH);
+ recalculateElementSizes();
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * If Escalator is currently not in {@link HeightMode#CSS}, the given value
+ * is remembered, and applied once the mode is applied.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(String height) {
+ /*
+ * TODO remove method once RequiresResize and the Vaadin layoutmanager
+ * listening mechanisms are implemented
+ */
+
+ heightByCss = height;
+ if (getHeightMode() == HeightMode.CSS) {
+ setHeightInternal(height);
+ }
+ }
+
+ private void setHeightInternal(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 {@code scrollTop} attribute in the DOM.
+ *
+ * @return the logical vertical scroll offset
+ */
+ public double getScrollTop() {
+ return verticalScrollbar.getScrollPos();
+ }
+
+ /**
+ * Sets the vertical scroll offset. Note that this will not necessarily
+ * become the same as the {@code scrollTop} attribute in the DOM.
+ *
+ * @param scrollTop
+ * the number of pixels to scroll vertically
+ */
+ public void setScrollTop(final double scrollTop) {
+ verticalScrollbar.setScrollPos(scrollTop);
+ }
+
+ /**
+ * Returns the logical horizontal scroll offset. Note that this is not
+ * necessarily the same as the {@code scrollLeft} attribute in the DOM.
+ *
+ * @return the logical horizontal scroll offset
+ */
+ public double getScrollLeft() {
+ return horizontalScrollbar.getScrollPos();
+ }
+
+ /**
+ * Sets the logical horizontal scroll offset. Note that will not necessarily
+ * become the same as the {@code scrollLeft} attribute in the DOM.
+ *
+ * @param scrollLeft
+ * the number of pixels to scroll horizontally
+ */
+ public void setScrollLeft(final double 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 in the direction of
+ * 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, or if the indicated 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
+ */
+ 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");
+ widthOfEscalator = getPreciseWidth(getElement());
+ heightOfEscalator = getPreciseHeight(getElement());
+ for (final AbstractRowContainer rowContainer : rowContainers) {
+ rowContainer.recalculateSectionHeight();
+ }
+
+ scroller.recalculateScrollbarsForVirtualViewport();
+ body.verifyEscalatorCount();
+ Profiler.leave("Escalator.recalculateElementSizes");
+ }
+
+ /**
+ * A routing method for {@link Scroller#onScroll()}.
+ * <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()}
+ * 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 or row resizing.
+ *
+ * @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));
+ }
+ }
+
+ /**
+ * Gets the range of currently visible rows.
+ *
+ * @return range of visible rows
+ */
+ public Range getVisibleRowRange() {
+ return Range.withLength(
+ body.getLogicalRowIndex(body.visualRowOrder.getFirst()),
+ body.visualRowOrder.size());
+ }
+
+ /**
+ * 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();
+ Widget w = Util.findWidget(castElement, null);
+
+ // Ensure findWidget did not traverse past the cell element in the
+ // DOM hierarchy
+ if (cellNode.isOrHasChild(w.getElement())) {
+ return w;
+ }
+ }
+ 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);
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Escalator's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Escalator is currently not in {@link HeightMode#ROW}, the given value
+ * is remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * the number of rows that should be visible in Escalator's body
+ * @throws IllegalArgumentException
+ * if {@code rows} is &leq; 0,
+ * {@link Double#isInifinite(double) infinite} or
+ * {@link Double#isNaN(double) NaN}.
+ * @see #setHeightMode(HeightMode)
+ */
+ public void setHeightByRows(double rows) throws IllegalArgumentException {
+ if (rows <= 0) {
+ throw new IllegalArgumentException(
+ "The number of rows must be a positive number.");
+ } else if (Double.isInfinite(rows)) {
+ throw new IllegalArgumentException(
+ "The number of rows must be finite.");
+ } else if (Double.isNaN(rows)) {
+ throw new IllegalArgumentException("The number must not be NaN.");
+ }
+
+ heightByRows = rows;
+ applyHeightByRows();
+ }
+
+ /**
+ * Gets the amount of rows in Escalator's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * By default, it is {@value GridState#DEFAULT_HEIGHT_BY_ROWS}.
+ *
+ * @return the amount of rows that are being shown in Escalator's body
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return heightByRows;
+ }
+
+ /**
+ * Reapplies the row-based height of the Grid, if Grid currently should
+ * define its height that way.
+ */
+ private void applyHeightByRows() {
+ if (heightMode != HeightMode.ROW) {
+ return;
+ }
+
+ double headerHeight = header.heightOfSection;
+ double footerHeight = footer.heightOfSection;
+ double bodyHeight = body.getDefaultRowHeight() * heightByRows;
+ double scrollbar = horizontalScrollbar.showsScrollHandle() ? horizontalScrollbar
+ .getScrollbarThickness() : 0;
+
+ double totalHeight = headerHeight + bodyHeight + scrollbar
+ + footerHeight;
+ setHeightInternal(totalHeight + "px");
+ }
+
+ /**
+ * Defines the mode in which the Escalator widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Escalator will respect the values
+ * given via {@link #setHeight(String)}, and behave as a traditional Widget.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Escalator will make sure that the
+ * {@link #getBody() body} will display as many rows as
+ * {@link #getHeightByRows()} defines. <em>Note:</em> If headers/footers are
+ * inserted or removed, the widget will resize itself to still display the
+ * required amount of rows in its body. It also takes the horizontal
+ * scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Escalator should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight an setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ if (heightMode != this.heightMode) {
+ this.heightMode = heightMode;
+
+ switch (this.heightMode) {
+ case CSS:
+ setHeight(heightByCss);
+ break;
+ case ROW:
+ setHeightByRows(heightByRows);
+ break;
+ default:
+ throw new IllegalStateException("Unimplemented feature "
+ + "- unknown HeightMode: " + this.heightMode);
+ }
+ }
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Escalator is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return heightMode;
+ }
+
+ /**
+ * Returns the {@link RowContainer} which contains the element.
+ *
+ * @param element
+ * the element to check for
+ * @return the container the element is in or <code>null</code> if element
+ * is not present in any container.
+ */
+ public RowContainer findRowContainer(Element element) {
+ if (getHeader().getElement().isOrHasChild(element)) {
+ return getHeader();
+ } else if (getBody().getElement().isOrHasChild(element)) {
+ return getBody();
+ } else if (getFooter().getElement().isOrHasChild(element)) {
+ return getFooter();
+ }
+ return null;
+ }
+}
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..aae6b63d20
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.vaadin.client.ui.grid;
+
+/**
+ * An 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.
+ * <p>
+ * This has a similar function to {@link Grid Grid's} {@link Renderer Renderers}
+ * , although they operate on different abstraction levels.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @see RowContainer#setEscalatorUpdater(EscalatorUpdater)
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ * @see Renderer
+ */
+public interface EscalatorUpdater {
+
+ /**
+ * An {@link EscalatorUpdater} that doesn't render anything.
+ */
+ public static final EscalatorUpdater NULL = new EscalatorUpdater() {
+ @Override
+ public void update(final Row row,
+ final Iterable<FlyweightCell> cellsToUpdate) {
+ // NOOP
+ }
+
+ @Override
+ public void preAttach(final Row row,
+ final Iterable<FlyweightCell> cellsToAttach) {
+ // NOOP
+
+ }
+
+ @Override
+ public void postAttach(final Row row,
+ final Iterable<FlyweightCell> attachedCells) {
+ // NOOP
+ }
+
+ @Override
+ public void preDetach(final Row row,
+ final Iterable<FlyweightCell> cellsToDetach) {
+ // NOOP
+ }
+
+ @Override
+ public void postDetach(final Row row,
+ final Iterable<FlyweightCell> detachedCells) {
+ // 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's
+ * presentation will lead to wrong 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 that is being updated.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param cellsToUpdate
+ * A collection of cells that need to be updated. <em>Note:</em>
+ * You should neither store nor reuse the reference to the
+ * iterable, nor to the individual cells.
+ */
+ public void update(Row row, Iterable<FlyweightCell> cellsToUpdate);
+
+ /**
+ * Called before attaching new cells to the escalator.
+ *
+ * @param row
+ * Information about the row to which the cells will be added.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param cellsToAttach
+ * A collection of cells that are about to be attached.
+ * <em>Note:</em> You should neither store nor reuse the
+ * reference to the iterable, nor to the individual cells.
+ *
+ */
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach);
+
+ /**
+ * Called after attaching new cells to the escalator.
+ *
+ * @param row
+ * Information about the row to which the cells were added.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param attachedCells
+ * A collection of cells that were attached. <em>Note:</em> You
+ * should neither store nor reuse the reference to the iterable,
+ * nor to the individual cells.
+ *
+ */
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells);
+
+ /**
+ * Called before detaching cells from the escalator.
+ *
+ * @param row
+ * Information about the row from which the cells will be
+ * removed. <em>Note:</em> You should not store nor reuse this
+ * reference.
+ * @param cellsToAttach
+ * A collection of cells that are about to be detached.
+ * <em>Note:</em> You should neither store nor reuse the
+ * reference to the iterable, nor to the individual cells.
+ *
+ */
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach);
+
+ /**
+ * Called after detaching cells from the escalator.
+ *
+ * @param row
+ * Information about the row from which the cells were removed.
+ * <em>Note:</em> You should not store nor reuse this reference.
+ * @param attachedCells
+ * A collection of cells that were detached. <em>Note:</em> You
+ * should neither store nor reuse the reference to the iterable,
+ * nor to the individual cells.
+ *
+ */
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells);
+
+}
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..30cc4fc79e
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/FlyweightCell.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2000-2014 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.Style.Display;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.TableCellElement;
+import com.vaadin.client.ui.grid.FlyweightRow.CellIterator;
+
+/**
+ * A {@link FlyweightCell} represents a cell in the {@link Grid} or
+ * {@link Escalator} at a certain point in time.
+ *
+ * <p>
+ * Since the {@link FlyweightCell} follows the <code>Flyweight</code>-pattern
+ * any instance of this object is subject to change without the user knowing it
+ * and so should not be stored anywhere outside of the method providing these
+ * instances.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class FlyweightCell {
+ static final String COLSPAN_ATTR = "colSpan";
+
+ private final int column;
+ private final FlyweightRow row;
+
+ private TableCellElement element = null;
+ private CellIterator currentIterator = null;
+
+ public FlyweightCell(final FlyweightRow row, final int column) {
+ this.row = row;
+ this.column = column;
+ }
+
+ /**
+ * Returns the row index of the cell
+ */
+ public int getRow() {
+ assertSetup();
+ return row.getRow();
+ }
+
+ /**
+ * Returns the column index of the cell
+ */
+ public int getColumn() {
+ assertSetup();
+ return column;
+ }
+
+ /**
+ * Returns the element of the cell. Can be either a <code>TD</code> element
+ * or a <code>TH</code> element.
+ */
+ public TableCellElement getElement() {
+ assertSetup();
+ return element;
+ }
+
+ /**
+ * Return the colspan attribute of the element of the cell.
+ */
+ public int getColSpan() {
+ assertSetup();
+ return element.getPropertyInt(COLSPAN_ATTR);
+ }
+
+ /**
+ * Sets the DOM element for this FlyweightCell, either a <code>TD</code> or
+ * a <code>TH</code>. It is the caller's responsibility to actually insert
+ * the given element to the document when needed.
+ *
+ * @param element
+ * the element corresponding to this cell, cannot be null
+ */
+ void setElement(TableCellElement element) {
+ assert element != null;
+ assertSetup();
+ this.element = element;
+ }
+
+ void setup(final CellIterator iterator) {
+ currentIterator = iterator;
+
+ if (iterator.areCellsAttached()) {
+ final TableCellElement e = row.getElement().getCells()
+ .getItem(column);
+ e.setPropertyInt(COLSPAN_ATTR, 1);
+ e.getStyle().setWidth(row.getColumnWidth(column), Unit.PX);
+ e.getStyle().clearDisplay();
+ setElement(e);
+ }
+ }
+
+ /**
+ * 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;
+ element = 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.";
+ }
+
+ public void setColSpan(final int numberOfCells) {
+ if (numberOfCells < 1) {
+ throw new IllegalArgumentException(
+ "Number of cells should be more than 0");
+ }
+
+ /*-
+ * 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();
+ }
+ }
+ }
+}
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..08f4f1d33c
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/FlyweightRow.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2000-2014 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.TableRowElement;
+
+/**
+ * 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
+ * @author Vaadin Ltd
+ * @see Escalator.AbstractRowContainer#refreshRow(Node, int)
+ */
+class FlyweightRow implements Row {
+
+ static class CellIterator implements Iterator<FlyweightCell> {
+ /** A defensive copy of the cells in the current row. */
+ private final ArrayList<FlyweightCell> cells;
+ private final boolean cellsAttached;
+ private int cursor = 0;
+ private int skipNext = 0;
+
+ /**
+ * Creates a new iterator of attached flyweight cells. A cell is
+ * attached if it has a corresponding {@link FlyweightCell#getElement()
+ * DOM element} attached to the row element.
+ *
+ * @param cells
+ * the collection of cells to iterate
+ */
+ public static CellIterator attached(
+ final Collection<FlyweightCell> cells) {
+ return new CellIterator(cells, true);
+ }
+
+ /**
+ * Creates a new iterator of unattached flyweight cells. A cell is
+ * unattached if it does not have a corresponding
+ * {@link FlyweightCell#getElement() DOM element} attached to the row
+ * element.
+ *
+ * @param cells
+ * the collection of cells to iterate
+ */
+ public static CellIterator unattached(
+ final Collection<FlyweightCell> cells) {
+ return new CellIterator(cells, false);
+ }
+
+ private CellIterator(final Collection<FlyweightCell> cells,
+ final boolean attached) {
+ this.cells = new ArrayList<FlyweightCell>(cells);
+ cellsAttached = attached;
+ }
+
+ @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());
+ List<FlyweightCell> nextCells = cells.subList(from, to);
+ for (FlyweightCell cell : nextCells) {
+ cell.setup(this);
+ }
+ return nextCells;
+ }
+
+ public boolean areCellsAttached() {
+ return cellsAttached;
+ }
+ }
+
+ private static final int BLANK = Integer.MIN_VALUE;
+
+ private int row;
+ private TableRowElement element;
+ private int[] columnWidths = null;
+ private final List<FlyweightCell> cells = new ArrayList<FlyweightCell>();
+
+ void setup(final TableRowElement 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 TableRowElement 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));
+ }
+ 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));
+ }
+ }
+
+ /**
+ * Returns flyweight cells for the client code to render. The cells get
+ * their associated {@link FlyweightCell#getElement() elements} from the row
+ * element.
+ * <p>
+ * Precondition: each cell has a corresponding element in the row
+ *
+ * @return an iterable of flyweight cells
+ *
+ * @see #setup(Element, int, int[])
+ * @see #teardown()
+ */
+ Iterable<FlyweightCell> getCells() {
+ return getCells(0, cells.size());
+ }
+
+ /**
+ * Returns a subrange of flyweight cells for the client code to render. The
+ * cells get their associated {@link FlyweightCell#getElement() elements}
+ * from the row element.
+ * <p>
+ * Precondition: each cell has a corresponding element in the row
+ *
+ * @param offset
+ * the index of the first cell to return
+ * @param numberOfCells
+ * the number of cells to return
+ * @return an iterable of flyweight cells
+ */
+ Iterable<FlyweightCell> getCells(final int offset, final int numberOfCells) {
+ assertSetup();
+ return new Iterable<FlyweightCell>() {
+ @Override
+ public Iterator<FlyweightCell> iterator() {
+ return CellIterator.attached(cells.subList(offset, offset
+ + numberOfCells));
+ }
+ };
+ }
+
+ /**
+ * Returns a subrange of unattached flyweight cells. Unattached cells do not
+ * have {@link FlyweightCell#getElement() elements} associated. Note that
+ * FlyweightRow does not keep track of whether cells in actuality have
+ * corresponding DOM elements or not; it is the caller's responsibility to
+ * invoke this method with correct parameters.
+ * <p>
+ * Precondition: the range [offset, offset + numberOfCells) must be valid
+ *
+ * @param offset
+ * the index of the first cell to return
+ * @param numberOfCells
+ * the number of cells to return
+ * @return an iterable of flyweight cells
+ */
+ Iterable<FlyweightCell> getUnattachedCells(final int offset,
+ final int numberOfCells) {
+ assertSetup();
+ assert offset >= 0 && offset + numberOfCells <= cells.size() : "Invalid range of cells";
+ return new Iterable<FlyweightCell>() {
+ @Override
+ public Iterator<FlyweightCell> iterator() {
+ return CellIterator.unattached(cells.subList(offset, offset
+ + numberOfCells));
+ }
+ };
+ }
+
+ /**
+ * 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..70d8286c90
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Grid.java
@@ -0,0 +1,2562 @@
+/*
+ * Copyright 2000-2014 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;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.core.shared.GWT;
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableRowElement;
+import com.google.gwt.dom.client.Touch;
+import com.google.gwt.event.dom.client.KeyCodeEvent;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.touch.client.Point;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Timer;
+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.Util;
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.ui.SubPartAware;
+import com.vaadin.client.ui.grid.GridFooter.FooterRow;
+import com.vaadin.client.ui.grid.GridHeader.HeaderRow;
+import com.vaadin.client.ui.grid.GridStaticSection.StaticCell;
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler;
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyDownHandler;
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyPressHandler;
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyUpHandler;
+import com.vaadin.client.ui.grid.keyevents.BodyKeyDownHandler;
+import com.vaadin.client.ui.grid.keyevents.BodyKeyPressHandler;
+import com.vaadin.client.ui.grid.keyevents.BodyKeyUpHandler;
+import com.vaadin.client.ui.grid.keyevents.FooterKeyDownHandler;
+import com.vaadin.client.ui.grid.keyevents.FooterKeyPressHandler;
+import com.vaadin.client.ui.grid.keyevents.FooterKeyUpHandler;
+import com.vaadin.client.ui.grid.keyevents.GridKeyDownEvent;
+import com.vaadin.client.ui.grid.keyevents.GridKeyPressEvent;
+import com.vaadin.client.ui.grid.keyevents.GridKeyUpEvent;
+import com.vaadin.client.ui.grid.keyevents.HeaderKeyDownHandler;
+import com.vaadin.client.ui.grid.keyevents.HeaderKeyPressHandler;
+import com.vaadin.client.ui.grid.keyevents.HeaderKeyUpHandler;
+import com.vaadin.client.ui.grid.renderers.ComplexRenderer;
+import com.vaadin.client.ui.grid.renderers.WidgetRenderer;
+import com.vaadin.client.ui.grid.selection.HasSelectionChangeHandlers;
+import com.vaadin.client.ui.grid.selection.SelectionChangeEvent;
+import com.vaadin.client.ui.grid.selection.SelectionChangeHandler;
+import com.vaadin.client.ui.grid.selection.SelectionModel;
+import com.vaadin.client.ui.grid.selection.SelectionModelMulti;
+import com.vaadin.client.ui.grid.selection.SelectionModelNone;
+import com.vaadin.client.ui.grid.selection.SelectionModelSingle;
+import com.vaadin.client.ui.grid.sort.Sort;
+import com.vaadin.client.ui.grid.sort.SortEvent;
+import com.vaadin.client.ui.grid.sort.SortEventHandler;
+import com.vaadin.client.ui.grid.sort.SortOrder;
+import com.vaadin.shared.ui.grid.GridConstants;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.ui.grid.SortDirection;
+import com.vaadin.shared.ui.grid.SortEventOriginator;
+
+/**
+ * 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
+ * @author Vaadin Ltd
+ */
+public class Grid<T> extends Composite implements
+ HasSelectionChangeHandlers<T>, SubPartAware {
+
+ public static abstract class AbstractGridKeyEvent<T, HANDLER extends AbstractGridKeyEventHandler>
+ extends KeyCodeEvent<HANDLER> {
+
+ /**
+ * Enum describing different section of Grid.
+ */
+ public enum GridSection {
+ HEADER, BODY, FOOTER
+ }
+
+ private Grid<T> grid;
+ protected Cell activeCell;
+ protected GridSection activeSection;
+ private final Type<HANDLER> associatedType = new Type<HANDLER>(
+ getBrowserEventType(), this);
+
+ public AbstractGridKeyEvent(Grid<T> grid) {
+ this.grid = grid;
+ }
+
+ protected abstract String getBrowserEventType();
+
+ /**
+ * Gets the Grid instance for this event.
+ *
+ * @return grid
+ */
+ public Grid<T> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Gets the active cell for this event.
+ *
+ * @return active cell
+ */
+ public Cell getActiveCell() {
+ return activeCell;
+ }
+
+ @Override
+ protected void dispatch(HANDLER handler) {
+ activeCell = grid.activeCellHandler.getActiveCell();
+ activeSection = GridSection.FOOTER;
+ final RowContainer container = grid.activeCellHandler.container;
+ if (container == grid.escalator.getHeader()) {
+ activeSection = GridSection.HEADER;
+ } else if (container == grid.escalator.getBody()) {
+ activeSection = GridSection.BODY;
+ }
+ }
+
+ @Override
+ public Type<HANDLER> getAssociatedType() {
+ return associatedType;
+ }
+ }
+
+ private GridKeyDownEvent<T> keyDown = new GridKeyDownEvent<T>(this);
+ private GridKeyUpEvent<T> keyUp = new GridKeyUpEvent<T>(this);
+ private GridKeyPressEvent<T> keyPress = new GridKeyPressEvent<T>(this);
+
+ private class ActiveCellHandler {
+
+ private RowContainer container = escalator.getBody();
+ private int activeRow = 0;
+ private int activeColumn = 0;
+ private int lastActiveBodyRow = 0;
+ private int lastActiveHeaderRow = 0;
+ private int lastActiveFooterRow = 0;
+ private TableCellElement cellWithActiveStyle = null;
+ private TableRowElement rowWithActiveStyle = null;
+
+ public ActiveCellHandler() {
+ sinkEvents(getNavigationEvents());
+ }
+
+ private Cell getActiveCell() {
+ return new Cell(activeRow, activeColumn, cellWithActiveStyle);
+ }
+
+ /**
+ * Sets style names for given cell when needed.
+ */
+ public void updateActiveCellStyle(FlyweightCell cell,
+ RowContainer cellContainer) {
+ int cellRow = cell.getRow();
+ int cellColumn = cell.getColumn();
+ int colSpan = cell.getColSpan();
+ boolean columnActive = Range.withLength(cellColumn, colSpan)
+ .contains(activeColumn);
+
+ if (cellContainer == container) {
+ // Cell is in the current container
+ if (cellRow == activeRow && columnActive) {
+ if (cellWithActiveStyle != cell.getElement()) {
+ // Cell is correct but it does not have active style
+ if (cellWithActiveStyle != null) {
+ // Remove old active style
+ setStyleName(cellWithActiveStyle,
+ cellActiveStyleName, false);
+ }
+ cellWithActiveStyle = cell.getElement();
+ // Add active style to correct cell.
+ setStyleName(cellWithActiveStyle, cellActiveStyleName,
+ true);
+ }
+ } else if (cellWithActiveStyle == cell.getElement()) {
+ // Due to escalator reusing cells, a new cell has the same
+ // element but is not the active cell.
+ setStyleName(cellWithActiveStyle, cellActiveStyleName,
+ false);
+ cellWithActiveStyle = null;
+ }
+ }
+
+ if (cellContainer == escalator.getHeader()
+ || cellContainer == escalator.getFooter()) {
+ // Correct header and footer column also needs highlighting
+ setStyleName(cell.getElement(), headerFooterActiveStyleName,
+ columnActive);
+ }
+ }
+
+ /**
+ * Sets active row style name for given row if needed.
+ *
+ * @param row
+ * a row object
+ */
+ public void updateActiveRowStyle(Row row) {
+ if (activeRow == row.getRow() && container == escalator.getBody()) {
+ if (row.getElement() != rowWithActiveStyle) {
+ // Row should have active style but does not have it.
+ if (rowWithActiveStyle != null) {
+ setStyleName(rowWithActiveStyle, rowActiveStyleName,
+ false);
+ }
+ rowWithActiveStyle = row.getElement();
+ setStyleName(rowWithActiveStyle, rowActiveStyleName, true);
+ }
+ } else if (rowWithActiveStyle == row.getElement()
+ || (container != escalator.getBody() && rowWithActiveStyle != null)) {
+ // Remove active style.
+ setStyleName(rowWithActiveStyle, rowActiveStyleName, false);
+ rowWithActiveStyle = null;
+ }
+ }
+
+ /**
+ * Sets currently active cell to a cell in given container with given
+ * indices.
+ *
+ * @param row
+ * new active row
+ * @param column
+ * new active column
+ * @param container
+ * new container
+ */
+ private void setActiveCell(int row, int column, RowContainer container) {
+ if (row == activeRow && column == activeColumn
+ && container == this.container) {
+ return;
+ }
+
+ int oldRow = activeRow;
+ int oldColumn = activeColumn;
+ activeRow = row;
+ activeColumn = column;
+
+ if (container == escalator.getBody()) {
+ scrollToRow(activeRow);
+ }
+ escalator.scrollToColumn(activeColumn, ScrollDestination.ANY, 10);
+
+ if (this.container == container) {
+ if (container != escalator.getBody()) {
+ if (oldColumn == activeColumn && oldRow != activeRow) {
+ refreshRow(oldRow);
+ } else if (oldColumn != activeColumn) {
+ refreshHeader();
+ refreshFooter();
+ }
+ } else {
+ if (oldRow != activeRow) {
+ refreshRow(oldRow);
+ }
+
+ if (oldColumn != activeColumn) {
+ refreshHeader();
+ refreshFooter();
+ }
+ }
+ } else {
+ RowContainer oldContainer = this.container;
+ this.container = container;
+
+ if (oldContainer == escalator.getBody()) {
+ lastActiveBodyRow = oldRow;
+ } else if (oldContainer == escalator.getHeader()) {
+ lastActiveHeaderRow = oldRow;
+ } else {
+ lastActiveFooterRow = oldRow;
+ }
+
+ if (oldColumn != activeColumn) {
+ refreshHeader();
+ refreshFooter();
+ if (oldContainer == escalator.getBody()) {
+ oldContainer.refreshRows(oldRow, 1);
+ }
+ } else {
+ oldContainer.refreshRows(oldRow, 1);
+ }
+ }
+ refreshRow(activeRow);
+ }
+
+ /**
+ * Sets currently active cell used for keyboard navigation. Note that
+ * active cell is not JavaScript {@code document.activeElement}.
+ *
+ * @param cell
+ * a cell object
+ */
+ public void setActiveCell(Cell cell) {
+ setActiveCell(cell.getRow(), cell.getColumn(),
+ escalator.findRowContainer(cell.getElement()));
+ }
+
+ /**
+ * Gets list of events that can be used for active cell navigation.
+ *
+ * @return list of navigation related event types
+ */
+ public Collection<String> getNavigationEvents() {
+ return Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.CLICK);
+ }
+
+ /**
+ * Handle events that can change the currently active cell.
+ */
+ public void handleNavigationEvent(Event event, Cell cell) {
+ if (event.getType().equals(BrowserEvents.CLICK) && cell != null) {
+ setActiveCell(cell);
+ getElement().focus();
+ } else if (event.getType().equals(BrowserEvents.KEYDOWN)) {
+ int newRow = activeRow;
+ int newColumn = activeColumn;
+ RowContainer newContainer = container;
+
+ switch (event.getKeyCode()) {
+ case KeyCodes.KEY_DOWN:
+ newRow += 1;
+ break;
+ case KeyCodes.KEY_UP:
+ newRow -= 1;
+ break;
+ case KeyCodes.KEY_RIGHT:
+ newColumn += 1;
+ break;
+ case KeyCodes.KEY_LEFT:
+ newColumn -= 1;
+ break;
+ case KeyCodes.KEY_TAB:
+ if (event.getShiftKey()) {
+ newContainer = getPreviousContainer(container);
+ } else {
+ newContainer = getNextContainer(container);
+ }
+
+ if (newContainer == container) {
+ return;
+ }
+ break;
+ default:
+ return;
+ }
+
+ if (newContainer != container) {
+ if (newContainer == escalator.getBody()) {
+ newRow = lastActiveBodyRow;
+ } else if (newContainer == escalator.getHeader()) {
+ newRow = lastActiveHeaderRow;
+ } else {
+ newRow = lastActiveFooterRow;
+ }
+ } else if (newRow < 0) {
+ newContainer = getPreviousContainer(newContainer);
+
+ if (newContainer == container) {
+ newRow = 0;
+ } else if (newContainer == escalator.getBody()) {
+ newRow = getLastVisibleRowIndex();
+ } else {
+ newRow = newContainer.getRowCount() - 1;
+ }
+ } else if (newRow >= container.getRowCount()) {
+ newContainer = getNextContainer(newContainer);
+
+ if (newContainer == container) {
+ newRow = container.getRowCount() - 1;
+ } else if (newContainer == escalator.getBody()) {
+ newRow = getFirstVisibleRowIndex();
+ } else {
+ newRow = 0;
+ }
+ }
+
+ if (newContainer.getRowCount() == 0) {
+ // There are no rows in the container. Can't change the
+ // active cell.
+ return;
+ }
+
+ if (newColumn < 0) {
+ newColumn = 0;
+ } else if (newColumn >= getColumnCount()) {
+ newColumn = getColumnCount() - 1;
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ setActiveCell(newRow, newColumn, newContainer);
+ }
+
+ }
+
+ private RowContainer getPreviousContainer(RowContainer current) {
+ if (current == escalator.getFooter()) {
+ current = escalator.getBody();
+ } else if (current == escalator.getBody()) {
+ current = escalator.getHeader();
+ } else {
+ return current;
+ }
+
+ if (current.getRowCount() == 0) {
+ return getPreviousContainer(current);
+ }
+ return current;
+ }
+
+ private RowContainer getNextContainer(RowContainer current) {
+ if (current == escalator.getHeader()) {
+ current = escalator.getBody();
+ } else if (current == escalator.getBody()) {
+ current = escalator.getFooter();
+ } else {
+ return current;
+ }
+
+ if (current.getRowCount() == 0) {
+ return getNextContainer(current);
+ }
+ return current;
+ }
+
+ private void refreshRow(int row) {
+ container.refreshRows(row, 1);
+ }
+ }
+
+ private class SelectionColumn extends GridColumn<Boolean, T> {
+ private boolean initDone = false;
+
+ public SelectionColumn(final Renderer<Boolean> selectColumnRenderer) {
+ super(selectColumnRenderer);
+ }
+
+ public void initDone() {
+ initDone = true;
+ }
+
+ @Override
+ public void setVisible(boolean visible) {
+ if (!visible && initDone) {
+ throw new UnsupportedOperationException("The selection "
+ + "column cannot be modified after init");
+ } else {
+ super.setVisible(visible);
+ }
+ }
+
+ @Override
+ public void setWidth(int pixels) {
+ if (pixels != getWidth() && initDone) {
+ throw new UnsupportedOperationException("The selection "
+ + "column cannot be modified after init");
+ } else {
+ super.setWidth(pixels);
+ }
+ }
+
+ @Override
+ public Boolean getValue(T row) {
+ return Boolean.valueOf(isSelected(row));
+ }
+ }
+
+ /**
+ * Class for sorting at a later time
+ */
+ private class LazySorter extends Timer {
+
+ private Cell cell;
+
+ private boolean multisort;
+
+ @Override
+ public void run() {
+ SortOrder sortingOrder = getSortOrder(getColumnFromVisibleIndex(cell
+ .getColumn()));
+ if (sortingOrder == null) {
+ /*
+ * No previous sorting, sort Ascending
+ */
+ sort(cell, SortDirection.ASCENDING, multisort);
+
+ } else {
+ // Toggle sorting
+ SortDirection direction = sortingOrder.getDirection();
+ if (direction == SortDirection.ASCENDING) {
+ sort(cell, SortDirection.DESCENDING, multisort);
+ } else {
+ sort(cell, SortDirection.ASCENDING, multisort);
+ }
+ }
+ }
+
+ /**
+ * Set the cell reference to the primary cell that sorting should be
+ * done for.
+ *
+ * @param cell
+ *
+ */
+ public void setCellReference(Cell cell) {
+ this.cell = cell;
+ }
+
+ /**
+ * Is multiple column sorting is enabled/disabled
+ *
+ * @param multisort
+ * true if multiple column sorting is enabled
+ */
+ public void setMultisort(boolean multisort) {
+ this.multisort = multisort;
+ }
+
+ /**
+ * Sorts the column in a direction
+ */
+ private void sort(Cell cell, SortDirection direction, boolean multisort) {
+ TableCellElement th = TableCellElement.as(cell.getElement());
+
+ // Apply primary sorting on clicked column
+ GridColumn<?, ?> columnInstance = getColumnFromVisibleIndex(cell
+ .getColumn());
+ Sort sorting = Sort.by(columnInstance, direction);
+
+ // Re-apply old sorting to the sort order
+ if (multisort) {
+ for (SortOrder order : getSortOrder()) {
+ if (order.getColumn() != columnInstance) {
+ sorting = sorting.then(order.getColumn(),
+ order.getDirection());
+ }
+ }
+ }
+
+ // Perform sorting; indicate originator as user
+ Grid.this.setSortOrder(sorting.build(), SortEventOriginator.USER);
+ }
+ }
+
+ /**
+ * Escalator used internally by grid to render the rows
+ */
+ private Escalator escalator = GWT.create(Escalator.class);
+
+ private final GridHeader header = GWT.create(GridHeader.class);
+
+ private final GridFooter footer = GWT.create(GridFooter.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;
+
+ /**
+ * Currently available row range in DataSource.
+ */
+ private Range currentDataAvailable = Range.withLength(0, 0);
+
+ /**
+ * The last column frozen counter from the left
+ */
+ private GridColumn<?, T> lastFrozenColumn;
+
+ /**
+ * Current sort order. The (private) sort() method reads this list to
+ * determine the order in which to present rows.
+ */
+ private List<SortOrder> sortOrder = new ArrayList<SortOrder>();
+
+ private Renderer<Boolean> selectColumnRenderer = null;
+
+ private SelectionColumn selectionColumn;
+
+ private String rowHasDataStyleName;
+ private String rowSelectedStyleName;
+ private String cellActiveStyleName;
+ private String rowActiveStyleName;
+ private String headerFooterActiveStyleName;
+
+ /**
+ * Current selection model.
+ */
+ private SelectionModel<T> selectionModel;
+
+ protected final ActiveCellHandler activeCellHandler;
+
+ private final LazySorter lazySorter = new LazySorter();
+
+ /**
+ * Enumeration for easy setting of selection mode.
+ */
+ public enum SelectionMode {
+
+ /**
+ * Shortcut for {@link SelectionModelSingle}.
+ */
+ SINGLE {
+
+ @Override
+ protected <T> SelectionModel<T> createModel() {
+ return new SelectionModelSingle<T>();
+ }
+ },
+
+ /**
+ * Shortcut for {@link SelectionModelMulti}.
+ */
+ MULTI {
+
+ @Override
+ protected <T> SelectionModel<T> createModel() {
+ return new SelectionModelMulti<T>();
+ }
+ },
+
+ /**
+ * Shortcut for {@link SelectionModelNone}.
+ */
+ NONE {
+
+ @Override
+ protected <T> SelectionModel<T> createModel() {
+ return new SelectionModelNone<T>();
+ }
+ };
+
+ protected abstract <T> SelectionModel<T> createModel();
+ }
+
+ /**
+ * 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 column is associated with
+ */
+ private Grid<T> grid;
+
+ /**
+ * Should the column be visible in the grid
+ */
+ private boolean visible = true;
+
+ /**
+ * Width of column in pixels
+ */
+ private int width = 100;
+
+ /**
+ * Renderer for rendering a value into the cell
+ */
+ private Renderer<? super C> bodyRenderer;
+
+ private boolean sortable = false;
+
+ /**
+ * Constructs a new column with a custom renderer.
+ *
+ * @param renderer
+ * The renderer to use for rendering the cells
+ */
+ public AbstractGridColumn(Renderer<? super C> renderer) {
+ if (renderer == null) {
+ throw new IllegalArgumentException("Renderer cannot be null.");
+ }
+ bodyRenderer = renderer;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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;
+
+ if (grid != null) {
+ int index = findIndexOfColumn();
+ ColumnConfiguration conf = grid.escalator
+ .getColumnConfiguration();
+
+ if (visible) {
+ conf.insertColumns(index, 1);
+ } else {
+ conf.removeColumns(index, 1);
+ }
+
+ for (HeaderRow row : grid.getHeader().getRows()) {
+ row.calculateColspans();
+ }
+
+ for (FooterRow row : grid.getFooter().getRows()) {
+ row.calculateColspans();
+ }
+ }
+ }
+
+ /**
+ * 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<? super 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) {
+ 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() {
+ return width;
+ }
+
+ /**
+ * Enables sort indicators for the grid.
+ * <p>
+ * <b>Note:</b>The API can still sort the column even if this is set to
+ * <code>false</code>.
+ *
+ * @param sortable
+ * <code>true</code> when column sort indicators are visible.
+ */
+ public void setSortable(boolean sortable) {
+ if (this.sortable != sortable) {
+ this.sortable = sortable;
+ grid.refreshHeader();
+ }
+ }
+
+ /**
+ * Are sort indicators shown for the column.
+ *
+ * @return <code>true</code> if the column is sortable
+ */
+ public boolean isSortable() {
+ return sortable;
+ }
+ }
+
+ protected class BodyUpdater implements EscalatorUpdater {
+
+ @Override
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) {
+ for (FlyweightCell cell : cellsToAttach) {
+ Renderer<?> renderer = findRenderer(cell);
+ if (renderer instanceof ComplexRenderer) {
+ ((ComplexRenderer<?>) renderer).init(cell);
+ }
+ }
+ }
+
+ @Override
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) {
+ for (FlyweightCell cell : attachedCells) {
+ Renderer<?> renderer = findRenderer(cell);
+ if (renderer instanceof WidgetRenderer) {
+ WidgetRenderer<?, ?> widgetRenderer = (WidgetRenderer<?, ?>) renderer;
+
+ Widget widget = widgetRenderer.createWidget();
+ assert widget != null : "WidgetRenderer.createWidget() returned null. It should return a widget.";
+ assert widget.getParent() == null : "WidgetRenderer.createWidget() returned a widget which already is attached.";
+ assert cell.getElement().getChildCount() == 0 : "Cell content should be empty when adding Widget";
+
+ // Physical attach
+ cell.getElement().appendChild(widget.getElement());
+
+ // Logical attach
+ setParent(widget, Grid.this);
+ }
+ }
+ }
+
+ @Override
+ public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) {
+ int rowIndex = row.getRow();
+ Element rowElement = row.getElement();
+ T rowData = dataSource.getRow(rowIndex);
+
+ boolean hasData = rowData != null;
+
+ // Assign stylename for rows with data
+ boolean usedToHaveData = rowElement
+ .hasClassName(rowHasDataStyleName);
+
+ if (usedToHaveData != hasData) {
+ setStyleName(rowElement, rowHasDataStyleName, hasData);
+ }
+
+ // Assign stylename for selected rows
+ if (hasData) {
+ setStyleName(rowElement, rowSelectedStyleName,
+ isSelected(rowData));
+ } else if (usedToHaveData) {
+ setStyleName(rowElement, rowSelectedStyleName, false);
+ }
+
+ activeCellHandler.updateActiveRowStyle(row);
+
+ for (FlyweightCell cell : cellsToUpdate) {
+ GridColumn<?, T> column = getColumnFromVisibleIndex(cell
+ .getColumn());
+
+ assert column != null : "Column was not found from cell ("
+ + cell.getColumn() + "," + cell.getRow() + ")";
+
+ activeCellHandler.updateActiveCellStyle(cell,
+ escalator.getBody());
+
+ Renderer renderer = column.getRenderer();
+
+ // Hide cell content if needed
+ if (renderer instanceof ComplexRenderer) {
+ ComplexRenderer clxRenderer = (ComplexRenderer) renderer;
+ if (hasData) {
+ if (!usedToHaveData) {
+ // Prepare cell for rendering
+ clxRenderer.setContentVisible(cell, true);
+ }
+
+ Object value = column.getValue(rowData);
+ clxRenderer.render(cell, value);
+
+ } else {
+ // Prepare cell for no data
+ clxRenderer.setContentVisible(cell, false);
+ }
+
+ } else if (hasData) {
+ // Simple renderers just render
+ Object value = column.getValue(rowData);
+ renderer.render(cell, value);
+
+ } else {
+ // Clear cell if there is no data
+ cell.getElement().removeAllChildren();
+ }
+ }
+ }
+
+ @Override
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) {
+ for (FlyweightCell cell : cellsToDetach) {
+ Renderer renderer = findRenderer(cell);
+ if (renderer instanceof WidgetRenderer) {
+ Widget w = Util.findWidget(cell.getElement()
+ .getFirstChildElement(), Widget.class);
+ if (w != null) {
+
+ // Logical detach
+ setParent(w, null);
+
+ // Physical detach
+ cell.getElement().removeChild(w.getElement());
+ }
+ }
+ }
+ }
+
+ @Override
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) {
+ for (FlyweightCell cell : detachedCells) {
+ Renderer renderer = findRenderer(cell);
+ if (renderer instanceof ComplexRenderer) {
+ ((ComplexRenderer) renderer).destroy(cell);
+ }
+ }
+ }
+ }
+
+ protected class StaticSectionUpdater implements EscalatorUpdater {
+
+ private GridStaticSection<?> section;
+ private RowContainer container;
+
+ public StaticSectionUpdater(GridStaticSection<?> section,
+ RowContainer container) {
+ super();
+ this.section = section;
+ this.container = container;
+ }
+
+ @Override
+ public void update(Row row, Iterable<FlyweightCell> cellsToUpdate) {
+ GridStaticSection.StaticRow<?> staticRow = section.getRow(row
+ .getRow());
+
+ final List<Integer> columnIndices = getVisibleColumnIndices();
+
+ for (FlyweightCell cell : cellsToUpdate) {
+
+ int index = columnIndices.get(cell.getColumn());
+ final StaticCell metadata = staticRow.getCell(index);
+
+ // Decorate default row with sorting indicators
+ if (staticRow instanceof HeaderRow) {
+ addSortingIndicatorsToHeaderRow((HeaderRow) staticRow, cell);
+ }
+
+ // Assign colspan to cell before rendering
+ cell.setColSpan(metadata.getColspan());
+
+ switch (metadata.getType()) {
+ case TEXT:
+ cell.getElement().setInnerText(metadata.getText());
+ break;
+ case HTML:
+ cell.getElement().setInnerHTML(metadata.getHtml());
+ break;
+ case WIDGET:
+ preDetach(row, Arrays.asList(cell));
+ cell.getElement().setInnerHTML("");
+ postAttach(row, Arrays.asList(cell));
+ break;
+ }
+
+ activeCellHandler.updateActiveCellStyle(cell, container);
+ }
+ }
+
+ private void addSortingIndicatorsToHeaderRow(HeaderRow headerRow,
+ FlyweightCell cell) {
+
+ cleanup(cell);
+
+ GridColumn<?, ?> column = getColumnFromVisibleIndex(cell
+ .getColumn());
+ SortOrder sortingOrder = getSortOrder(column);
+ if (!headerRow.isDefault() || !column.isSortable()
+ || sortingOrder == null) {
+ // Only apply sorting indicators to sortable header columns in
+ // the default header row
+ return;
+ }
+
+ Element cellElement = cell.getElement();
+
+ if (SortDirection.ASCENDING == sortingOrder.getDirection()) {
+ cellElement.addClassName("sort-asc");
+ } else {
+ cellElement.addClassName("sort-desc");
+ }
+
+ int sortIndex = Grid.this.getSortOrder().indexOf(sortingOrder);
+ if (sortIndex > -1 && Grid.this.getSortOrder().size() > 1) {
+ // Show sort order indicator if column is
+ // sorted and other sorted columns also exists.
+ cellElement.setAttribute("sort-order",
+ String.valueOf(sortIndex + 1));
+ }
+ }
+
+ /**
+ * Finds the sort order for this column
+ */
+ private SortOrder getSortOrder(GridColumn<?, ?> column) {
+ for (SortOrder order : Grid.this.getSortOrder()) {
+ if (order.getColumn() == column) {
+ return order;
+ }
+ }
+ return null;
+ }
+
+ private void cleanup(FlyweightCell cell) {
+ Element cellElement = cell.getElement();
+ cellElement.removeAttribute("sort-order");
+ cellElement.removeClassName("sort-desc");
+ cellElement.removeClassName("sort-asc");
+ }
+
+ @Override
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) {
+ }
+
+ @Override
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) {
+ GridStaticSection.StaticRow<?> gridRow = section.getRow(row
+ .getRow());
+ List<Integer> columnIndices = getVisibleColumnIndices();
+
+ for (FlyweightCell cell : attachedCells) {
+ int index = columnIndices.get(cell.getColumn());
+ StaticCell metadata = gridRow.getCell(index);
+ /*
+ * If the cell contains widgets that are not currently attach
+ * then attach them now.
+ */
+ if (GridStaticCellType.WIDGET.equals(metadata.getType())) {
+ final Widget widget = metadata.getWidget();
+ final Element cellElement = cell.getElement();
+
+ if (!widget.isAttached()) {
+
+ // Physical attach
+ cellElement.appendChild(widget.getElement());
+
+ // Logical attach
+ setParent(widget, Grid.this);
+
+ getLogger().info("Attached widget " + widget);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) {
+ if (section.getRowCount() > row.getRow()) {
+ GridStaticSection.StaticRow<?> gridRow = section.getRow(row
+ .getRow());
+ List<Integer> columnIndices = getVisibleColumnIndices();
+ for (FlyweightCell cell : cellsToDetach) {
+ int index = columnIndices.get(cell.getColumn());
+ StaticCell metadata = gridRow.getCell(index);
+
+ if (GridStaticCellType.WIDGET.equals(metadata.getType())
+ && metadata.getWidget().isAttached()) {
+
+ Widget widget = metadata.getWidget();
+
+ // Logical detach
+ setParent(widget, null);
+
+ // Physical detach
+ widget.getElement().removeFromParent();
+
+ getLogger().info("Detached widget " + widget);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) {
+ }
+
+ private List<Integer> getVisibleColumnIndices() {
+ List<Integer> indices = new ArrayList<Integer>(getColumnCount());
+ for (int i = 0; i < getColumnCount(); i++) {
+ if (getColumn(i).isVisible()) {
+ indices.add(i);
+ }
+ }
+ return indices;
+ }
+ };
+
+ /**
+ * Creates a new instance.
+ */
+ public Grid() {
+ initWidget(escalator);
+ getElement().setTabIndex(0);
+ activeCellHandler = new ActiveCellHandler();
+
+ setStylePrimaryName("v-grid");
+
+ escalator.getHeader().setEscalatorUpdater(createHeaderUpdater());
+ escalator.getBody().setEscalatorUpdater(createBodyUpdater());
+ escalator.getFooter().setEscalatorUpdater(createFooterUpdater());
+
+ header.setGrid(this);
+ HeaderRow defaultRow = header.appendRow();
+ header.setDefaultRow(defaultRow);
+
+ footer.setGrid(this);
+
+ setSelectionMode(SelectionMode.SINGLE);
+
+ escalator
+ .addRowVisibilityChangeHandler(new RowVisibilityChangeHandler() {
+ @Override
+ public void onRowVisibilityChange(
+ RowVisibilityChangeEvent event) {
+ if (dataSource != null) {
+ dataSource.ensureAvailability(
+ event.getFirstVisibleRow(),
+ event.getVisibleRowCount());
+ }
+ }
+ });
+
+ // Default action on SelectionChangeEvents. Refresh the body so changed
+ // become visible.
+ addSelectionChangeHandler(new SelectionChangeHandler<T>() {
+
+ @Override
+ public void onSelectionChange(SelectionChangeEvent<T> event) {
+ refreshBody();
+ }
+ });
+
+ // Sink header events and key events
+ sinkEvents(getHeader().getConsumedEvents());
+ sinkEvents(Arrays.asList(BrowserEvents.KEYDOWN, BrowserEvents.KEYUP,
+ BrowserEvents.KEYPRESS));
+ }
+
+ @Override
+ public void setStylePrimaryName(String style) {
+ super.setStylePrimaryName(style);
+ escalator.setStylePrimaryName(style);
+ rowHasDataStyleName = getStylePrimaryName() + "-row-has-data";
+ rowSelectedStyleName = getStylePrimaryName() + "-row-selected";
+ cellActiveStyleName = getStylePrimaryName() + "-cell-active";
+ headerFooterActiveStyleName = getStylePrimaryName() + "-header-active";
+ rowActiveStyleName = getStylePrimaryName() + "-row-active";
+
+ if (isAttached()) {
+ refreshHeader();
+ refreshBody();
+ refreshFooter();
+ }
+ }
+
+ /**
+ * Creates the escalator updater used to update the header rows in this
+ * grid. The updater is invoked when header rows or columns are added or
+ * removed, or the content of existing header cells is changed.
+ *
+ * @return the new header updater instance
+ *
+ * @see GridHeader
+ * @see Grid#getHeader()
+ */
+ protected EscalatorUpdater createHeaderUpdater() {
+ return new StaticSectionUpdater(header, escalator.getHeader());
+ }
+
+ /**
+ * Creates the escalator updater used to update the body rows in this grid.
+ * The updater is invoked when body rows or columns are added or removed,
+ * the content of body cells is changed, or the body is scrolled to expose
+ * previously hidden content.
+ *
+ * @return the new body updater instance
+ */
+ protected EscalatorUpdater createBodyUpdater() {
+ return new BodyUpdater();
+ }
+
+ /**
+ * Creates the escalator updater used to update the footer rows in this
+ * grid. The updater is invoked when header rows or columns are added or
+ * removed, or the content of existing header cells is changed.
+ *
+ * @return the new footer updater instance
+ *
+ * @see GridFooter
+ * @see #getFooter()
+ */
+ protected EscalatorUpdater createFooterUpdater() {
+ return new StaticSectionUpdater(footer, escalator.getFooter());
+ }
+
+ /**
+ * 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,
+ GridStaticSection<?> section) {
+
+ // Add or Remove rows on demand
+ int rowDiff = section.getVisibleRowCount() - 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(), header);
+ }
+
+ /**
+ * Refreshes all body rows
+ */
+ private void refreshBody() {
+ escalator.getBody().refreshRows(0, escalator.getBody().getRowCount());
+ }
+
+ /**
+ * Refreshes all footer rows
+ */
+ void refreshFooter() {
+ refreshRowContainer(escalator.getFooter(), footer);
+ }
+
+ /**
+ * Adds a column as the last column in the grid.
+ *
+ * @param column
+ * the column to add
+ */
+ public void addColumn(GridColumn<?, T> column) {
+ addColumn(column, 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
+ * @throws IllegalStateException
+ * if Grid's current selection model renders a selection column,
+ * and {@code index} is 0.
+ */
+ public void addColumn(GridColumn<?, T> column, int index) {
+ if (column == selectionColumn) {
+ throw new IllegalArgumentException("The selection column many "
+ + "not be added manually");
+ } else if (selectionColumn != null && index == 0) {
+ throw new IllegalStateException("A column cannot be inserted "
+ + "before the selection column");
+ }
+
+ addColumnSkipSelectionColumnCheck(column, index);
+ }
+
+ private void addColumnSkipSelectionColumnCheck(GridColumn<?, T> column,
+ int index) {
+ // Register column with grid
+ columns.add(index, column);
+
+ header.addColumn(column, index);
+ footer.addColumn(column, index);
+
+ // Register this grid instance with the column
+ ((AbstractGridColumn<?, T>) column).setGrid(this);
+
+ // Insert column into escalator
+ if (column.isVisible()) {
+ int visibleIndex = findVisibleColumnIndex(column);
+ ColumnConfiguration conf = escalator.getColumnConfiguration();
+
+ // Insert column
+ conf.insertColumns(visibleIndex, 1);
+
+ // Transfer column width from column object to escalator
+ conf.setColumnWidth(visibleIndex, column.getWidth());
+ }
+
+ if (lastFrozenColumn != null
+ && ((AbstractGridColumn<?, T>) lastFrozenColumn)
+ .findIndexOfColumn() < index) {
+ refreshFrozenColumns();
+ }
+
+ // Sink all renderer events
+ Set<String> events = new HashSet<String>();
+ events.addAll(getConsumedEventsForRenderer(column.getRenderer()));
+
+ sinkEvents(events);
+ }
+
+ private void sinkEvents(Collection<String> events) {
+ assert events != null;
+
+ int eventsToSink = 0;
+ for (String typeName : events) {
+ int typeInt = Event.getTypeInt(typeName);
+ if (typeInt < 0) {
+ // Type not recognized by typeInt
+ sinkBitlessEvent(typeName);
+ } else {
+ eventsToSink |= typeInt;
+ }
+ }
+
+ if (eventsToSink > 0) {
+ sinkEvents(eventsToSink);
+ }
+ }
+
+ 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;
+ }
+
+ private Renderer<?> findRenderer(FlyweightCell cell) {
+ GridColumn<?, T> column = getColumnFromVisibleIndex(cell.getColumn());
+ assert column != null : "Could not find column at index:"
+ + cell.getColumn();
+ return column.getRenderer();
+ }
+
+ /**
+ * Removes a column from the grid.
+ *
+ * @param column
+ * the column to remove
+ */
+ public void removeColumn(GridColumn<?, T> column) {
+ if (column != null && column.equals(selectionColumn)) {
+ throw new IllegalArgumentException(
+ "The selection column may not be removed manually.");
+ }
+
+ removeColumnSkipSelectionColumnCheck(column);
+ }
+
+ private void removeColumnSkipSelectionColumnCheck(GridColumn<?, T> column) {
+ int columnIndex = columns.indexOf(column);
+ int visibleIndex = findVisibleColumnIndex(column);
+ columns.remove(columnIndex);
+
+ header.removeColumn(columnIndex);
+ footer.removeColumn(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);
+ }
+
+ /**
+ * Returns the header section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the header
+ */
+ public GridHeader getHeader() {
+ return header;
+ }
+
+ /**
+ * Returns the footer section of this grid. The default footer is empty.
+ *
+ * @return the footer
+ */
+ public GridFooter getFooter() {
+ return footer;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Note:</em> This method will change the widget's size in the browser
+ * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @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.");
+ }
+
+ selectionModel.reset();
+
+ 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);
+ }
+
+ @Override
+ public void dataAvailable(int firstIndex, int numberOfItems) {
+ currentDataAvailable = Range.withLength(firstIndex,
+ numberOfItems);
+ fireEvent(new DataAvailableEvent(currentDataAvailable));
+ }
+ });
+
+ 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);
+ }
+
+ }
+
+ /**
+ * Gets the {@Link DataSource} for this Grid.
+ *
+ * @return the data source used by this grid
+ */
+ public DataSource<T> getDataSource() {
+ return dataSource;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Sets the vertical scroll offset.
+ *
+ * @param px
+ * the number of pixels this grid should be scrolled down
+ */
+ public void setScrollTop(double px) {
+ escalator.setScrollTop(px);
+ }
+
+ /**
+ * Gets the vertical scroll offset
+ *
+ * @return the number of pixels this grid is scrolled down
+ */
+ public double getScrollTop() {
+ return escalator.getScrollTop();
+ }
+
+ private static final Logger getLogger() {
+ return Logger.getLogger(Grid.class.getName());
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Grid's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Grid is currently not in {@link HeightMode#ROW}, the given value is
+ * remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * The height in terms of number of rows displayed in Grid's
+ * body. If Grid doesn't contain enough rows, white space is
+ * displayed instead.
+ * @throws IllegalArgumentException
+ * if {@code rows} is zero or less
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isInifinite(double)
+ * infinite}
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isNaN(double) NaN}
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ public void setHeightByRows(double rows) throws IllegalArgumentException {
+ escalator.setHeightByRows(rows);
+ }
+
+ /**
+ * Gets the amount of rows in Grid's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * By default, it is {@value Escalator#DEFAULT_HEIGHT_BY_ROWS}.
+ *
+ * @return the amount of rows that should be shown in Grid's body, while in
+ * {@link HeightMode#ROW}.
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return escalator.getHeightByRows();
+ }
+
+ /**
+ * Defines the mode in which the Grid widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Grid will respect the values given
+ * via {@link #setHeight(String)}, and behave as a traditional Widget.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Grid will make sure that the body
+ * will display as many rows as {@link #getHeightByRows()} defines.
+ * <em>Note:</em> If headers/footers are inserted or removed, the widget
+ * will resize itself to still display the required amount of rows in its
+ * body. It also takes the horizontal scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Grid should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight an setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ escalator.setHeightMode(heightMode);
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Grid is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return escalator.getHeightMode();
+ }
+
+ private Set<String> getConsumedEventsForRenderer(Renderer<?> renderer) {
+ Set<String> events = new HashSet<String>();
+ if (renderer instanceof ComplexRenderer) {
+ Collection<String> consumedEvents = ((ComplexRenderer<?>) renderer)
+ .getConsumedEvents();
+ if (consumedEvents != null) {
+ events.addAll(consumedEvents);
+ }
+ }
+ return events;
+ }
+
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ EventTarget target = event.getEventTarget();
+ if (Element.is(target)) {
+ Element e = Element.as(target);
+ RowContainer container = escalator.findRowContainer(e);
+ Cell cell = null;
+ if (container != null) {
+ cell = container.getCell(e);
+ if (cell != null) {
+ // FIXME getFromVisibleIndex???
+ GridColumn<?, T> gridColumn = columns.get(cell.getColumn());
+
+ if (container == escalator.getHeader()) {
+ if (getHeader().getRow(cell.getRow()).isDefault()) {
+ handleDefaultRowEvent(cell, event);
+ }
+ } else if (container == escalator.getFooter()) {
+ // NOP
+ } else if (gridColumn.getRenderer() instanceof ComplexRenderer) {
+ ComplexRenderer<?> cplxRenderer = (ComplexRenderer<?>) gridColumn
+ .getRenderer();
+ if (cplxRenderer.getConsumedEvents().contains(
+ event.getType())) {
+ if (cplxRenderer.onBrowserEvent(cell, event)) {
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ Collection<String> navigation = activeCellHandler
+ .getNavigationEvents();
+ if (navigation.contains(event.getType())
+ && (Util.getFocusedElement() == getElement() || cell != null)) {
+ activeCellHandler.handleNavigationEvent(event, cell);
+ }
+
+ handleGridNavigation(event, cell);
+ }
+ }
+
+ private void handleGridNavigation(Event event, Cell cell) {
+ if (!event.getType().equals(BrowserEvents.KEYDOWN)) {
+ // Only handle key downs
+ return;
+ }
+
+ int newRow = -1;
+ RowContainer container = escalator.getBody();
+ switch (event.getKeyCode()) {
+ case KeyCodes.KEY_HOME:
+ if (container.getRowCount() > 0) {
+ newRow = 0;
+ }
+ break;
+ case KeyCodes.KEY_END:
+ if (container.getRowCount() > 0) {
+ newRow = container.getRowCount() - 1;
+ }
+ break;
+ case KeyCodes.KEY_PAGEUP: {
+ Range range = escalator.getVisibleRowRange();
+ if (!range.isEmpty()) {
+ int firstIndex = getFirstVisibleRowIndex();
+ newRow = firstIndex - range.length();
+ if (newRow < 0) {
+ newRow = 0;
+ }
+ }
+ break;
+ }
+ case KeyCodes.KEY_PAGEDOWN: {
+ Range range = escalator.getVisibleRowRange();
+ if (!range.isEmpty()) {
+ int lastIndex = getLastVisibleRowIndex();
+ newRow = lastIndex + range.length();
+ if (newRow >= container.getRowCount()) {
+ newRow = container.getRowCount() - 1;
+ }
+ }
+ break;
+ }
+ default:
+ return;
+ }
+
+ scrollToRow(newRow);
+ }
+
+ private Point rowEventTouchStartingPoint;
+
+ private boolean handleDefaultRowEvent(final Cell cell, NativeEvent event) {
+ if (!getColumn(cell.getColumn()).isSortable()) {
+ // Only handle sorting events if the column is sortable
+ return false;
+ }
+
+ if (BrowserEvents.TOUCHSTART.equals(event.getType())) {
+ if (event.getTouches().length() > 1) {
+ return false;
+ }
+
+ event.preventDefault();
+
+ Touch touch = event.getChangedTouches().get(0);
+ rowEventTouchStartingPoint = new Point(touch.getClientX(),
+ touch.getClientY());
+
+ lazySorter.setCellReference(cell);
+ lazySorter.setMultisort(true);
+ lazySorter.schedule(GridConstants.LONG_TAP_DELAY);
+
+ } else if (BrowserEvents.TOUCHMOVE.equals(event.getType())) {
+ if (event.getTouches().length() > 1) {
+ return false;
+ }
+
+ event.preventDefault();
+
+ Touch touch = event.getChangedTouches().get(0);
+ double diffX = Math.abs(touch.getClientX()
+ - rowEventTouchStartingPoint.getX());
+ double diffY = Math.abs(touch.getClientY()
+ - rowEventTouchStartingPoint.getY());
+
+ // Cancel long tap if finger strays too far from
+ // starting point
+ if (diffX > GridConstants.LONG_TAP_THRESHOLD
+ || diffY > GridConstants.LONG_TAP_THRESHOLD) {
+ lazySorter.cancel();
+ }
+
+ } else if (BrowserEvents.TOUCHEND.equals(event.getType())) {
+ if (event.getTouches().length() > 0) {
+ return false;
+ }
+
+ if (lazySorter.isRunning()) {
+ // Not a long tap yet, perform single sort
+ lazySorter.cancel();
+ lazySorter.setMultisort(false);
+ lazySorter.run();
+ }
+
+ } else if (BrowserEvents.TOUCHCANCEL.equals(event.getType())) {
+ if (event.getChangedTouches().length() > 1) {
+ return false;
+ }
+
+ lazySorter.cancel();
+
+ } else if (BrowserEvents.CLICK.equals(event.getType())) {
+ lazySorter.setCellReference(cell);
+ lazySorter.setMultisort(event.getShiftKey());
+ lazySorter.run();
+
+ // Active cell handling is also monitoring the click
+ // event so we allow event to propagate for it
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public com.google.gwt.user.client.Element getSubPartElement(String subPart) {
+ // Parse SubPart string to type and indices
+ String[] splitArgs = subPart.split("\\[");
+
+ String type = splitArgs[0];
+ int[] indices = new int[splitArgs.length - 1];
+ for (int i = 0; i < indices.length; ++i) {
+ String tmp = splitArgs[i + 1];
+ indices[i] = Integer.parseInt(tmp.substring(0, tmp.length() - 1));
+ }
+
+ // Get correct RowContainer for type from Escalator
+ RowContainer container = null;
+ if (type.equalsIgnoreCase("header")) {
+ container = escalator.getHeader();
+ } else if (type.equalsIgnoreCase("cell")) {
+ // If wanted row is not visible, we need to scroll there.
+ Range visibleRowRange = escalator.getVisibleRowRange();
+ if (indices.length > 0 && !visibleRowRange.contains(indices[0])) {
+ try {
+ scrollToRow(indices[0]);
+ } catch (IllegalArgumentException e) {
+ getLogger().log(Level.SEVERE, e.getMessage());
+ }
+ // Scrolling causes a lazy loading event. No element can
+ // currently be retrieved.
+ return null;
+ }
+ container = escalator.getBody();
+ } else if (type.equalsIgnoreCase("footer")) {
+ container = escalator.getFooter();
+ }
+
+ if (null != container) {
+ if (indices.length == 0) {
+ // No indexing. Just return the wanted container element
+ return DOM.asOld(container.getElement());
+ } else {
+ try {
+ return DOM.asOld(getSubPart(container, indices));
+ } catch (Exception e) {
+ getLogger().log(Level.SEVERE, e.getMessage());
+ }
+ }
+ }
+ return null;
+ }
+
+ private Element getSubPart(RowContainer container, int[] indices) {
+ // Scroll wanted column to view if able
+ if (indices.length > 1
+ && escalator.getColumnConfiguration().getFrozenColumnCount() <= indices[1]) {
+ escalator.scrollToColumn(indices[1], ScrollDestination.ANY, 0);
+ }
+
+ Element targetElement = container.getRowElement(indices[0]);
+ for (int i = 1; i < indices.length && targetElement != null; ++i) {
+ targetElement = (Element) targetElement.getChild(indices[i]);
+ }
+ return targetElement;
+ }
+
+ @Override
+ public String getSubPartName(com.google.gwt.user.client.Element subElement) {
+ // Containers and matching SubPart types
+ List<RowContainer> containers = Arrays.asList(escalator.getHeader(),
+ escalator.getBody(), escalator.getFooter());
+ List<String> containerType = Arrays.asList("header", "cell", "footer");
+
+ for (int i = 0; i < containers.size(); ++i) {
+ RowContainer container = containers.get(i);
+ boolean containerRow = (subElement.getTagName().equalsIgnoreCase(
+ "tr") && subElement.getParentElement() == container
+ .getElement());
+ if (containerRow) {
+ // Wanted SubPart is row that is a child of containers root
+ // To get indices, we use a cell that is a child of this row
+ subElement = DOM.asOld(subElement.getFirstChildElement());
+ }
+
+ Cell cell = container.getCell(subElement);
+ if (cell != null) {
+ // Skip the column index if subElement was a child of root
+ return containerType.get(i) + "[" + cell.getRow()
+ + (containerRow ? "]" : "][" + cell.getColumn() + "]");
+ }
+ }
+ return null;
+ }
+
+ private void setSelectColumnRenderer(
+ final Renderer<Boolean> selectColumnRenderer) {
+ if (this.selectColumnRenderer == selectColumnRenderer) {
+ return;
+ }
+
+ if (this.selectColumnRenderer != null) {
+ removeColumnSkipSelectionColumnCheck(selectionColumn);
+ --activeCellHandler.activeColumn;
+ }
+
+ this.selectColumnRenderer = selectColumnRenderer;
+
+ if (selectColumnRenderer != null) {
+ ++activeCellHandler.activeColumn;
+ selectionColumn = new SelectionColumn(selectColumnRenderer);
+
+ // FIXME: this needs to be done elsewhere, requires design...
+ selectionColumn.setWidth(25);
+ addColumnSkipSelectionColumnCheck(selectionColumn, 0);
+ selectionColumn.initDone();
+ } else {
+ selectionColumn = null;
+ refreshBody();
+ }
+ }
+
+ /**
+ * Accesses the package private method Widget#setParent()
+ *
+ * @param widget
+ * The widget to access
+ * @param parent
+ * The parent to set
+ */
+ private 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);
+ }-*/;
+
+ /**
+ * Sets the current selection model.
+ * <p>
+ * This function will call {@link SelectionModel#setGrid(Grid)}.
+ *
+ * @param selectionModel
+ * a selection model implementation.
+ * @throws IllegalArgumentException
+ * if selection model argument is null
+ */
+ public void setSelectionModel(SelectionModel<T> selectionModel) {
+
+ if (selectionModel == null) {
+ throw new IllegalArgumentException("Selection model can't be null");
+ }
+
+ if (selectColumnRenderer != null
+ && selectColumnRenderer instanceof ComplexRenderer) {
+ ((ComplexRenderer<?>) selectColumnRenderer).destroy();
+ }
+
+ this.selectionModel = selectionModel;
+ selectionModel.setGrid(this);
+ setSelectColumnRenderer(this.selectionModel
+ .getSelectionColumnRenderer());
+ }
+
+ /**
+ * Gets a reference to the current selection model.
+ *
+ * @return the currently used SelectionModel instance.
+ */
+ public SelectionModel<T> getSelectionModel() {
+ return selectionModel;
+ }
+
+ /**
+ * Sets current selection mode.
+ * <p>
+ * This is a shorthand method for {@link Grid#setSelectionModel}.
+ *
+ * @param mode
+ * a selection mode value
+ * @see {@link SelectionMode}.
+ */
+ public void setSelectionMode(SelectionMode mode) {
+ SelectionModel<T> model = mode.createModel();
+ setSelectionModel(model);
+ }
+
+ /**
+ * Test if a row is selected.
+ *
+ * @param row
+ * a row object
+ * @return true, if the current selection model considers the provided row
+ * object selected.
+ */
+ public boolean isSelected(T row) {
+ return selectionModel.isSelected(row);
+ }
+
+ /**
+ * Select a row using the current selection model.
+ * <p>
+ * Only selection models implementing {@link SelectionModel.Single} and
+ * {@link SelectionModel.Multi} are supported; for anything else, an
+ * exception will be thrown.
+ *
+ * @param row
+ * a row object
+ * @return <code>true</code> iff the current selection changed
+ * @throws IllegalStateException
+ * if the current selection model is not an instance of
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ @SuppressWarnings("unchecked")
+ public boolean select(T row) {
+ if (selectionModel instanceof SelectionModel.Single<?>) {
+ return ((SelectionModel.Single<T>) selectionModel).select(row);
+ } else if (selectionModel instanceof SelectionModel.Multi<?>) {
+ return ((SelectionModel.Multi<T>) selectionModel).select(row);
+ } else {
+ throw new IllegalStateException("Unsupported selection model");
+ }
+ }
+
+ /**
+ * Deselect a row using the current selection model.
+ * <p>
+ * Only selection models implementing {@link SelectionModel.Single} and
+ * {@link SelectionModel.Multi} are supported; for anything else, an
+ * exception will be thrown.
+ *
+ * @param row
+ * a row object
+ * @return <code>true</code> iff the current selection changed
+ * @throws IllegalStateException
+ * if the current selection model is not an instance of
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ @SuppressWarnings("unchecked")
+ public boolean deselect(T row) {
+ if (selectionModel instanceof SelectionModel.Single<?>) {
+ return ((SelectionModel.Single<T>) selectionModel).deselect(row);
+ } else if (selectionModel instanceof SelectionModel.Multi<?>) {
+ return ((SelectionModel.Multi<T>) selectionModel).deselect(row);
+ } else {
+ throw new IllegalStateException("Unsupported selection model");
+ }
+ }
+
+ /**
+ * Gets last selected row from the current SelectionModel.
+ * <p>
+ * Only selection models implementing {@link SelectionModel.Single} are
+ * valid for this method; for anything else, use the
+ * {@link Grid#getSelectedRows()} method.
+ *
+ * @return a selected row reference, or null, if no row is selected
+ * @throws IllegalStateException
+ * if the current selection model is not an instance of
+ * {@link SelectionModel.Single}
+ */
+ public T getSelectedRow() {
+ if (selectionModel instanceof SelectionModel.Single<?>) {
+ return ((SelectionModel.Single<T>) selectionModel).getSelectedRow();
+ } else {
+ throw new IllegalStateException(
+ "Unsupported selection model; can not get single selected row");
+ }
+ }
+
+ /**
+ * Gets currently selected rows from the current selection model.
+ *
+ * @return a non-null collection containing all currently selected rows.
+ */
+ public Collection<T> getSelectedRows() {
+ return selectionModel.getSelectedRows();
+ }
+
+ @Override
+ public HandlerRegistration addSelectionChangeHandler(
+ final SelectionChangeHandler<T> handler) {
+ return addHandler(handler, SelectionChangeEvent.getType());
+ }
+
+ /**
+ * Sets the current sort order using the fluid Sort API. Read the
+ * documentation for {@link Sort} for more information.
+ *
+ * @param s
+ * a sort instance
+ */
+ public void sort(Sort s) {
+ setSortOrder(s.build());
+ }
+
+ /**
+ * Sorts the Grid data in ascending order along one column.
+ *
+ * @param column
+ * a grid column reference
+ */
+ public <C> void sort(GridColumn<C, T> column) {
+ sort(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Sorts the Grid data along one column.
+ *
+ * @param column
+ * a grid column reference
+ * @param direction
+ * a sort direction value
+ */
+ public <C> void sort(GridColumn<C, T> column, SortDirection direction) {
+ sort(Sort.by(column, direction));
+ }
+
+ /**
+ * Sets the sort order to use. Setting this causes the Grid to re-sort
+ * itself.
+ *
+ * @param order
+ * a sort order list. If set to null, the sort order is cleared.
+ */
+ public void setSortOrder(List<SortOrder> order) {
+ setSortOrder(order, SortEventOriginator.API);
+ }
+
+ private void setSortOrder(List<SortOrder> order,
+ SortEventOriginator originator) {
+ sortOrder.clear();
+ if (order != null) {
+ sortOrder.addAll(order);
+ }
+ sort(originator);
+ }
+
+ /**
+ * Get a copy of the current sort order array.
+ *
+ * @return a copy of the current sort order array
+ */
+ public List<SortOrder> getSortOrder() {
+ return Collections.unmodifiableList(sortOrder);
+ }
+
+ /**
+ * Finds the sorting order for this column
+ */
+ private SortOrder getSortOrder(GridColumn<?, ?> column) {
+ for (SortOrder order : getSortOrder()) {
+ if (order.getColumn() == column) {
+ return order;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Register a GWT event handler for a sorting event. This handler gets
+ * called whenever this Grid needs its data source to provide data sorted in
+ * a specific order.
+ *
+ * @param handler
+ * a sort event handler
+ * @return the registration for the event
+ */
+ public HandlerRegistration addSortHandler(SortEventHandler<T> handler) {
+ return addHandler(handler, SortEvent.getType());
+ }
+
+ /**
+ * Register a GWT event handler for a data available event. This handler
+ * gets called whenever the {@link DataSource} for this Grid has new data
+ * available.
+ * <p>
+ * This handle will be fired with the current available data after
+ * registration is done.
+ *
+ * @param handler
+ * a data available event handler
+ * @return the registartion for the event
+ */
+ public HandlerRegistration addDataAvailableHandler(
+ final DataAvailableHandler handler) {
+ // Deferred call to handler with current row range
+ Scheduler.get().scheduleFinally(new ScheduledCommand() {
+ @Override
+ public void execute() {
+ handler.onDataAvailable(new DataAvailableEvent(
+ currentDataAvailable));
+ }
+ });
+ return addHandler(handler, DataAvailableEvent.TYPE);
+ }
+
+ /**
+ * Register a KeyDown handler to this Grid. If the handler is a
+ * HeaderKeyDownHandler, it will be fired only when a header cell is active.
+ * The same goes for body and footer with their respective handlers.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public <HANDLER extends GridKeyDownHandler<T>> HandlerRegistration addKeyDownHandler(
+ HANDLER handler) {
+ if (handler instanceof BodyKeyDownHandler
+ || handler instanceof HeaderKeyDownHandler
+ || handler instanceof FooterKeyDownHandler) {
+ return addHandler(handler, keyDown.getAssociatedType());
+ }
+ throw new IllegalArgumentException(
+ "Handler not a valid extension of GridKeyDownHandler");
+ }
+
+ /**
+ * Register a KeyUp handler to this Grid. If the handler is a
+ * HeaderKeyUpHandler, it will be fired only when a header cell is active.
+ * The same goes for body and footer with their respective handlers.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public <HANDLER extends GridKeyUpHandler<T>> HandlerRegistration addKeyUpHandler(
+ HANDLER handler) {
+ if (handler instanceof BodyKeyUpHandler
+ || handler instanceof HeaderKeyUpHandler
+ || handler instanceof FooterKeyUpHandler) {
+ return addHandler(handler, keyUp.getAssociatedType());
+ }
+ throw new IllegalArgumentException(
+ "Handler not a valid extension of GridKeyUpHandler");
+ }
+
+ /**
+ * Register a KeyPress handler to this Grid. If the handler is a
+ * HeaderKeyPressHandler, it will be fired only when a header cell is
+ * active. The same goes for body and footer with their respective handlers.
+ *
+ * @param handler
+ * the key handler to register
+ * @return the registration for the event
+ */
+ public <HANDLER extends GridKeyPressHandler<T>> HandlerRegistration addKeyPressHandler(
+ HANDLER handler) {
+ if (handler instanceof BodyKeyPressHandler
+ || handler instanceof HeaderKeyPressHandler
+ || handler instanceof FooterKeyPressHandler) {
+ return addHandler(handler, keyPress.getAssociatedType());
+ }
+ throw new IllegalArgumentException(
+ "Handler not a valid extension of GridKeyPressHandler");
+ }
+
+ /**
+ * Apply sorting to data source.
+ */
+ private void sort(SortEventOriginator originator) {
+ refreshHeader();
+ fireEvent(new SortEvent<T>(this,
+ Collections.unmodifiableList(sortOrder), originator));
+ }
+
+ private int getLastVisibleRowIndex() {
+ int lastRowIndex = escalator.getVisibleRowRange().getEnd();
+ int footerTop = escalator.getFooter().getElement().getAbsoluteTop();
+ Element lastRow;
+
+ do {
+ lastRow = escalator.getBody().getRowElement(--lastRowIndex);
+ } while (lastRow.getAbsoluteBottom() > footerTop);
+
+ return lastRowIndex;
+ }
+
+ private int getFirstVisibleRowIndex() {
+ int firstRowIndex = escalator.getVisibleRowRange().getStart();
+ int headerBottom = escalator.getHeader().getElement()
+ .getAbsoluteBottom();
+ Element firstRow = escalator.getBody().getRowElement(firstRowIndex);
+
+ while (firstRow.getAbsoluteTop() < headerBottom) {
+ firstRow = escalator.getBody().getRowElement(++firstRowIndex);
+ }
+
+ return firstRowIndex;
+ }
+}
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..69be2d5532
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/GridColumn.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2000-2014 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
+ * @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 with a custom renderer.
+ *
+ * @param renderer
+ * The renderer to use for rendering the cells
+ */
+ public GridColumn(Renderer<? super 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..73440232b1
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright 2000-2014 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.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import com.google.gwt.json.client.JSONArray;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONValue;
+import com.vaadin.client.ComponentConnector;
+import com.vaadin.client.ConnectorHierarchyChangeEvent;
+import com.vaadin.client.annotations.OnStateChange;
+import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.client.data.RpcDataSourceConnector.RpcDataSource;
+import com.vaadin.client.ui.AbstractHasComponentsConnector;
+import com.vaadin.client.ui.grid.GridHeader.HeaderRow;
+import com.vaadin.client.ui.grid.GridStaticSection.StaticCell;
+import com.vaadin.client.ui.grid.GridStaticSection.StaticRow;
+import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector;
+import com.vaadin.client.ui.grid.selection.AbstractRowHandleSelectionModel;
+import com.vaadin.client.ui.grid.selection.SelectionChangeEvent;
+import com.vaadin.client.ui.grid.selection.SelectionChangeHandler;
+import com.vaadin.client.ui.grid.selection.SelectionModel;
+import com.vaadin.client.ui.grid.selection.SelectionModelMulti;
+import com.vaadin.client.ui.grid.selection.SelectionModelNone;
+import com.vaadin.client.ui.grid.selection.SelectionModelSingle;
+import com.vaadin.client.ui.grid.sort.SortEvent;
+import com.vaadin.client.ui.grid.sort.SortEventHandler;
+import com.vaadin.client.ui.grid.sort.SortOrder;
+import com.vaadin.shared.ui.Connect;
+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.GridState.SharedSelectionMode;
+import com.vaadin.shared.ui.grid.GridStaticSectionState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.ui.grid.SortDirection;
+
+/**
+ * Connects the client side {@link Grid} widget with the server side
+ * {@link com.vaadin.ui.components.grid.Grid} component.
+ * <p>
+ * The Grid is typed to JSONObject. The structure of the JSONObject is described
+ * at {@link com.vaadin.shared.data.DataProviderRpc#setRowData(int, List)
+ * DataProviderRpc.setRowData(int, List)}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.components.grid.Grid.class)
+public class GridConnector extends AbstractHasComponentsConnector {
+
+ /**
+ * Custom implementation of the custom grid column using a JSONObject to
+ * represent the cell value and String as a column type.
+ */
+ private class CustomGridColumn extends GridColumn<Object, JSONObject> {
+
+ private final String id;
+
+ private AbstractRendererConnector<Object> rendererConnector;
+
+ public CustomGridColumn(String id,
+ AbstractRendererConnector<Object> rendererConnector) {
+ super(rendererConnector.getRenderer());
+ this.rendererConnector = rendererConnector;
+ this.id = id;
+ }
+
+ @Override
+ public Object getValue(final JSONObject obj) {
+ final JSONValue rowData = obj.get(GridState.JSONKEY_DATA);
+ final JSONArray rowDataArray = rowData.isArray();
+ assert rowDataArray != null : "Was unable to parse JSON into an array: "
+ + rowData;
+
+ final int columnIndex = resolveCurrentIndexFromState();
+ final JSONValue columnValue = rowDataArray.get(columnIndex);
+ return rendererConnector.decode(columnValue);
+ }
+
+ /*
+ * Only used to check that the renderer connector will not change during
+ * the column lifetime.
+ *
+ * TODO remove once support for changing renderers is implemented
+ */
+ private AbstractRendererConnector<Object> getRendererConnector() {
+ return rendererConnector;
+ }
+
+ private int resolveCurrentIndexFromState() {
+ List<GridColumnState> columns = getState().columns;
+ int numColumns = columns.size();
+ for (int index = 0; index < numColumns; index++) {
+ if (columns.get(index).id.equals(id)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+ }
+
+ /**
+ * Maps a generated column id to a grid column instance
+ */
+ private Map<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>();
+
+ private AbstractRowHandleSelectionModel<JSONObject> selectionModel = createSelectionModel(SharedSelectionMode.NONE);
+ private Set<String> selectedKeys = new LinkedHashSet<String>();
+
+ /**
+ * updateFromState is set to true when {@link #updateSelectionFromState()}
+ * makes changes to selection. This flag tells the
+ * {@code internalSelectionChangeHandler} to not send same data straight
+ * back to server. Said listener sets it back to false when handling that
+ * event.
+ */
+ private boolean updatedFromState = false;
+
+ private RpcDataSource dataSource;
+
+ private SelectionChangeHandler<JSONObject> internalSelectionChangeHandler = new SelectionChangeHandler<JSONObject>() {
+ @Override
+ public void onSelectionChange(SelectionChangeEvent<JSONObject> event) {
+ if (event.isBatchedSelection()) {
+ return;
+ }
+ if (!updatedFromState) {
+ for (JSONObject row : event.getRemoved()) {
+ selectedKeys.remove(dataSource.getRowKey(row));
+ }
+
+ for (JSONObject row : event.getAdded()) {
+ selectedKeys.add((String) dataSource.getRowKey(row));
+ }
+
+ getRpcProxy(GridServerRpc.class).selectionChange(
+ new ArrayList<String>(selectedKeys));
+ } else {
+ updatedFromState = false;
+ }
+ }
+ };
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Grid<JSONObject> getWidget() {
+ return (Grid<JSONObject>) super.getWidget();
+ }
+
+ @Override
+ public GridState getState() {
+ return (GridState) super.getState();
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+
+ 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);
+ }
+ });
+
+ getWidget().setSelectionModel(selectionModel);
+
+ getWidget().addSelectionChangeHandler(internalSelectionChangeHandler);
+
+ getWidget().addSortHandler(new SortEventHandler<JSONObject>() {
+ @Override
+ public void sort(SortEvent<JSONObject> event) {
+ List<SortOrder> order = event.getOrder();
+ String[] columnIds = new String[order.size()];
+ SortDirection[] directions = new SortDirection[order.size()];
+ for (int i = 0; i < order.size(); i++) {
+ SortOrder sortOrder = order.get(i);
+ CustomGridColumn column = (CustomGridColumn) sortOrder
+ .getColumn();
+ columnIds[i] = column.id;
+
+ directions[i] = sortOrder.getDirection();
+ }
+
+ if (!Arrays.equals(columnIds, getState().sortColumns)
+ || !Arrays.equals(directions, getState().sortDirs)) {
+ // Report back to server if changed
+ getRpcProxy(GridServerRpc.class).sort(columnIds,
+ directions, event.getOriginator());
+ }
+ }
+ });
+ }
+
+ @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();
+ if (getWidget().getSelectionModel().getSelectionColumnRenderer() != null) {
+ currentColumns--;
+ }
+
+ // 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);
+ }
+ }
+
+ if (stateChangeEvent.hasPropertyChanged("header")) {
+ updateSectionFromState(getWidget().getHeader(), getState().header);
+ }
+
+ if (stateChangeEvent.hasPropertyChanged("footer")) {
+ updateSectionFromState(getWidget().getFooter(), getState().footer);
+ }
+
+ 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);
+ }
+ }
+ }
+
+ private void updateSectionFromState(GridStaticSection<?> section,
+ GridStaticSectionState state) {
+
+ while (section.getRowCount() != 0) {
+ section.removeRow(0);
+ }
+
+ for (RowState rowState : state.rows) {
+ StaticRow<?> row = section.appendRow();
+
+ int selectionOffset = 1;
+ if (getWidget().getSelectionModel() instanceof SelectionModel.None) {
+ selectionOffset = 0;
+ }
+
+ assert rowState.cells.size() == getWidget().getColumnCount() - selectionOffset;
+
+ int i = 0 + selectionOffset;
+ for (CellState cellState : rowState.cells) {
+ StaticCell cell = row.getCell(i++);
+ switch (cellState.type) {
+ case TEXT:
+ cell.setText(cellState.text);
+ break;
+ case HTML:
+ cell.setHtml(cellState.text);
+ break;
+ case WIDGET:
+ ComponentConnector connector = (ComponentConnector) cellState.connector;
+ cell.setWidget(connector.getWidget());
+ break;
+ default:
+ throw new IllegalStateException("unexpected cell type: "
+ + cellState.type);
+ }
+ }
+
+ for (List<Integer> group : rowState.cellGroups) {
+ GridColumn<?, ?>[] columns = new GridColumn<?, ?>[group.size()];
+ i = 0;
+ for (Integer colIndex : group) {
+ columns[i++] = getWidget().getColumn(selectionOffset + colIndex);
+ }
+ row.join(columns);
+ }
+
+ if (section instanceof GridHeader && rowState.defaultRow) {
+ ((GridHeader) section).setDefaultRow((HeaderRow) row);
+ }
+ }
+
+ section.setVisible(state.visible);
+
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Updates a column from a state change event.
+ *
+ * @param columnIndex
+ * The index of the column to update
+ */
+ private void updateColumnFromStateChangeEvent(final int columnIndex) {
+ /*
+ * We use the widget column index here instead of the given column
+ * index. SharedState contains information only about the explicitly
+ * defined columns, while the widget counts the selection column as an
+ * explicit one.
+ */
+ GridColumn<?, JSONObject> column = getWidget().getColumn(
+ getWidgetColumnIndex(columnIndex));
+
+ GridColumnState columnState = getState().columns.get(columnIndex);
+ updateColumnFromState(column, columnState);
+
+ assert column instanceof CustomGridColumn : "column at index "
+ + columnIndex + " is not a "
+ + CustomGridColumn.class.getSimpleName() + ", but a "
+ + column.getClass().getSimpleName();
+
+ if (columnState.rendererConnector != ((CustomGridColumn) column)
+ .getRendererConnector()) {
+ throw new UnsupportedOperationException(
+ "Changing column renderer after initialization is currently unsupported");
+ }
+ }
+
+ /**
+ * 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);
+ @SuppressWarnings("unchecked")
+ CustomGridColumn column = new CustomGridColumn(state.id,
+ ((AbstractRendererConnector<Object>) state.rendererConnector));
+ columnIdToColumn.put(state.id, column);
+
+ /*
+ * Adds a column to grid, and registers Grid with the column.
+ *
+ * We use the widget column index here instead of the given column
+ * index. SharedState contains information only about the explicitly
+ * defined columns, while the widget counts the selection column as an
+ * explicit one.
+ */
+ getWidget().addColumn(column, getWidgetColumnIndex(columnIndex));
+
+ /*
+ * Have to update state _after_ the column has been added to the grid as
+ * then, and only then, the column will call the grid which in turn will
+ * call the escalator's refreshRow methods on header/footer/body and
+ * visually refresh the row. If this is done in the reverse order the
+ * first column state update will be lost as no grid instance is
+ * present.
+ */
+ updateColumnFromState(column, state);
+ }
+
+ /**
+ * If we have a selection column renderer, we need to offset the index by
+ * one when referring to the column index in the widget.
+ */
+ private int getWidgetColumnIndex(final int columnIndex) {
+ Renderer<Boolean> selectionColumnRenderer = getWidget()
+ .getSelectionModel().getSelectionColumnRenderer();
+ int widgetColumnIndex = columnIndex;
+ if (selectionColumnRenderer != null) {
+ widgetColumnIndex++;
+ }
+ return widgetColumnIndex;
+ }
+
+ /**
+ * 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<?, JSONObject> column,
+ GridColumnState state) {
+ column.setVisible(state.visible);
+ column.setWidth(state.width);
+ column.setSortable(state.sortable);
+ }
+
+ /**
+ * 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);
+ }
+ }
+ }
+
+ public void setDataSource(RpcDataSource dataSource) {
+ this.dataSource = dataSource;
+ getWidget().setDataSource(this.dataSource);
+ }
+
+ @OnStateChange("selectionMode")
+ private void onSelectionModeChange() {
+ SharedSelectionMode mode = getState().selectionMode;
+ if (mode == null) {
+ getLogger().fine("ignored mode change");
+ return;
+ }
+
+ AbstractRowHandleSelectionModel<JSONObject> model = createSelectionModel(mode);
+ if (!model.getClass().equals(selectionModel.getClass())) {
+ selectionModel = model;
+ getWidget().setSelectionModel(model);
+ selectedKeys.clear();
+ }
+ }
+
+ @OnStateChange("selectedKeys")
+ private void updateSelectionFromState() {
+ boolean changed = false;
+
+ List<String> stateKeys = getState().selectedKeys;
+
+ // find new deselections
+ for (String key : selectedKeys) {
+ if (!stateKeys.contains(key)) {
+ changed = true;
+ deselectByHandle(dataSource.getHandleByKey(key));
+ }
+ }
+
+ // find new selections
+ for (String key : stateKeys) {
+ if (!selectedKeys.contains(key)) {
+ changed = true;
+ selectByHandle(dataSource.getHandleByKey(key));
+ }
+ }
+
+ /*
+ * A defensive copy in case the collection in the state is mutated
+ * instead of re-assigned.
+ */
+ selectedKeys = new LinkedHashSet<String>(stateKeys);
+
+ /*
+ * We need to fire this event so that Grid is able to re-render the
+ * selection changes (if applicable).
+ */
+ if (changed) {
+ // At least for now there's no way to send the selected and/or
+ // deselected row data. Some data is only stored as keys
+ updatedFromState = true;
+ getWidget().fireEvent(
+ new SelectionChangeEvent<JSONObject>(getWidget(),
+ (List<JSONObject>) null, null, false));
+ }
+ }
+
+ @OnStateChange({ "sortColumns", "sortDirs" })
+ private void onSortStateChange() {
+ List<SortOrder> sortOrder = new ArrayList<SortOrder>();
+
+ String[] sortColumns = getState().sortColumns;
+ SortDirection[] sortDirs = getState().sortDirs;
+
+ for (int i = 0; i < sortColumns.length; i++) {
+ sortOrder.add(new SortOrder(columnIdToColumn.get(sortColumns[i]),
+ sortDirs[i]));
+ }
+
+ getWidget().setSortOrder(sortOrder);
+ }
+
+ private Logger getLogger() {
+ return Logger.getLogger(getClass().getName());
+ }
+
+ @SuppressWarnings("static-method")
+ private AbstractRowHandleSelectionModel<JSONObject> createSelectionModel(
+ SharedSelectionMode mode) {
+ switch (mode) {
+ case SINGLE:
+ return new SelectionModelSingle<JSONObject>();
+ case MULTI:
+ return new SelectionModelMulti<JSONObject>();
+ case NONE:
+ return new SelectionModelNone<JSONObject>();
+ default:
+ throw new IllegalStateException("unexpected mode value: " + mode);
+ }
+ }
+
+ /**
+ * A workaround method for accessing the protected method
+ * {@code AbstractRowHandleSelectionModel.selectByHandle}
+ */
+ private native void selectByHandle(RowHandle<JSONObject> handle)
+ /*-{
+ var model = this.@com.vaadin.client.ui.grid.GridConnector::selectionModel;
+ model.@com.vaadin.client.ui.grid.selection.AbstractRowHandleSelectionModel::selectByHandle(*)(handle);
+ }-*/;
+
+ /**
+ * A workaround method for accessing the protected method
+ * {@code AbstractRowHandleSelectionModel.deselectByHandle}
+ */
+ private native void deselectByHandle(RowHandle<JSONObject> handle)
+ /*-{
+ var model = this.@com.vaadin.client.ui.grid.GridConnector::selectionModel;
+ model.@com.vaadin.client.ui.grid.selection.AbstractRowHandleSelectionModel::deselectByHandle(*)(handle);
+ }-*/;
+
+ /**
+ * Gets the row key for a row by index.
+ *
+ * @param index
+ * the index of the row for which to get the key
+ * @return the key for the row at {@code index}
+ */
+ public String getRowKey(int index) {
+ final JSONObject row = dataSource.getRow(index);
+ final Object key = dataSource.getRowKey(row);
+ assert key instanceof String : "Internal key was not a String but a "
+ + key.getClass().getSimpleName() + " (" + key + ")";
+ return (String) key;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.client.HasComponentsConnector#updateCaption(com.vaadin.client
+ * .ComponentConnector)
+ */
+ @Override
+ public void updateCaption(ComponentConnector connector) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void onConnectorHierarchyChange(
+ ConnectorHierarchyChangeEvent connectorHierarchyChangeEvent) {
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/GridFooter.java b/client/src/com/vaadin/client/ui/grid/GridFooter.java
new file mode 100644
index 0000000000..e798139b9a
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/GridFooter.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2000-2014 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.core.client.Scheduler;
+
+/**
+ * Represents the footer section of a Grid. The footer is always empty.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridFooter extends GridStaticSection<GridFooter.FooterRow> {
+
+ /**
+ * A single row in a grid Footer section.
+ *
+ */
+ public class FooterRow extends GridStaticSection.StaticRow<FooterCell> {
+
+ @Override
+ protected FooterCell createCell() {
+ return new FooterCell();
+ }
+ }
+
+ /**
+ * A single cell in a grid Footer row. Has a textual caption.
+ *
+ */
+ public class FooterCell extends GridStaticSection.StaticCell {
+ }
+
+ private boolean markAsDirty = false;
+
+ @Override
+ protected FooterRow createRow() {
+ return new FooterRow();
+ }
+
+ @Override
+ protected void requestSectionRefresh() {
+ markAsDirty = true;
+
+ /*
+ * Defer the refresh so if we multiple times call refreshSection() (for
+ * example when updating cell values) we only get one actual refresh in
+ * the end.
+ */
+ Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (markAsDirty) {
+ markAsDirty = false;
+ getGrid().refreshFooter();
+ }
+ }
+ });
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/GridHeader.java b/client/src/com/vaadin/client/ui/grid/GridHeader.java
new file mode 100644
index 0000000000..2867ae4d1c
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/GridHeader.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2000-2014 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.Arrays;
+import java.util.Collection;
+
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.client.BrowserEvents;
+
+/**
+ * Represents the header section of a Grid. A header consists of a single header
+ * row containing a header cell for each column. Each cell has a simple textual
+ * caption.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridHeader extends GridStaticSection<GridHeader.HeaderRow> {
+
+ /**
+ * A single row in a grid header section.
+ *
+ */
+ public class HeaderRow extends GridStaticSection.StaticRow<HeaderCell> {
+
+ private boolean isDefault = false;
+
+ protected void setDefault(boolean isDefault) {
+ this.isDefault = isDefault;
+ }
+
+ public boolean isDefault() {
+ return isDefault;
+ }
+
+ @Override
+ protected HeaderCell createCell() {
+ return new HeaderCell();
+ }
+ }
+
+ /**
+ * A single cell in a grid header row. Has a textual caption.
+ *
+ */
+ public class HeaderCell extends GridStaticSection.StaticCell {
+ }
+
+ private HeaderRow defaultRow;
+
+ private boolean markAsDirty = false;
+
+ @Override
+ public void removeRow(int index) {
+ HeaderRow removedRow = getRow(index);
+ super.removeRow(index);
+ if (removedRow == defaultRow) {
+ setDefaultRow(null);
+ }
+ }
+
+ /**
+ * Sets the default row of this header. The default row is a special header
+ * row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * this header does not contain the row
+ */
+ public void setDefaultRow(HeaderRow row) {
+ if (row == defaultRow) {
+ return;
+ }
+ if (row != null && !getRows().contains(row)) {
+ throw new IllegalArgumentException(
+ "Cannot set a default row that does not exist in the container");
+ }
+ if (defaultRow != null) {
+ defaultRow.setDefault(false);
+ }
+ if (row != null) {
+ row.setDefault(true);
+ }
+ defaultRow = row;
+ requestSectionRefresh();
+ }
+
+ /**
+ * Returns the current default row of this header. The default row is a
+ * special header row providing a user interface for sorting columns.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultRow() {
+ return defaultRow;
+ }
+
+ @Override
+ protected HeaderRow createRow() {
+ return new HeaderRow();
+ }
+
+ @Override
+ protected void requestSectionRefresh() {
+ markAsDirty = true;
+
+ /*
+ * Defer the refresh so if we multiple times call refreshSection() (for
+ * example when updating cell values) we only get one actual refresh in
+ * the end.
+ */
+ Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ if (markAsDirty) {
+ markAsDirty = false;
+ getGrid().refreshHeader();
+ }
+ }
+ });
+ }
+
+ /**
+ * Returns the events consumed by the header
+ *
+ * @return a collection of BrowserEvents
+ */
+ public Collection<String> getConsumedEvents() {
+ return Arrays.asList(BrowserEvents.TOUCHSTART, BrowserEvents.TOUCHMOVE,
+ BrowserEvents.TOUCHEND, BrowserEvents.TOUCHCANCEL,
+ BrowserEvents.CLICK);
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/GridStaticSection.java b/client/src/com/vaadin/client/ui/grid/GridStaticSection.java
new file mode 100644
index 0000000000..8c9ada46d0
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/GridStaticSection.java
@@ -0,0 +1,532 @@
+/*
+ * Copyright 2000-2014 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.HashSet;
+import java.util.List;
+
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+
+/**
+ * Abstract base class for Grid header and footer sections.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @param <ROWTYPE>
+ * the type of the rows in the section
+ */
+abstract class GridStaticSection<ROWTYPE extends GridStaticSection.StaticRow<?>> {
+
+ /**
+ * A header or footer cell. Has a simple textual caption.
+ *
+ */
+ static class StaticCell {
+
+ private Object content = null;
+
+ private int colspan = 1;
+
+ private GridStaticSection<?> section;
+
+ private GridStaticCellType type = GridStaticCellType.TEXT;
+
+ /**
+ * Sets the text displayed in this cell.
+ *
+ * @param text
+ * a plain text caption
+ */
+ public void setText(String text) {
+ this.content = text;
+ this.type = GridStaticCellType.TEXT;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the text displayed in this cell.
+ *
+ * @return the plain text caption
+ */
+ public String getText() {
+ if (type != GridStaticCellType.TEXT) {
+ throw new IllegalStateException(
+ "Cannot fetch Text from a cell with type " + type);
+ }
+ return (String) content;
+ }
+
+ protected GridStaticSection<?> getSection() {
+ assert section != null;
+ return section;
+ }
+
+ protected void setSection(GridStaticSection<?> section) {
+ this.section = section;
+ }
+
+ /**
+ * Returns the amount of columns the cell spans. By default is 1.
+ *
+ * @return The amount of columns the cell spans.
+ */
+ public int getColspan() {
+ return colspan;
+ }
+
+ /**
+ * Sets the amount of columns the cell spans. Must be more or equal to
+ * 1. By default is 1.
+ *
+ * @param colspan
+ * the colspan to set
+ */
+ public void setColspan(int colspan) {
+ if (colspan < 1) {
+ throw new IllegalArgumentException(
+ "Colspan cannot be less than 1");
+ }
+
+ this.colspan = colspan;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the html inside the cell.
+ *
+ * @throws IllegalStateException
+ * if trying to retrive HTML from a cell with a type other
+ * than {@link Type#HTML}.
+ * @return the html content of the cell.
+ */
+ public String getHtml() {
+ if (type != GridStaticCellType.HTML) {
+ throw new IllegalStateException(
+ "Cannot fetch HTML from a cell with type " + type);
+ }
+ return (String) content;
+ }
+
+ /**
+ * Sets the content of the cell to the provided html. All previous
+ * content is discarded and the cell type is set to {@link Type#HTML}.
+ *
+ * @param html
+ * The html content of the cell
+ */
+ public void setHtml(String html) {
+ this.content = html;
+ this.type = GridStaticCellType.HTML;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the widget in the cell.
+ *
+ * @throws IllegalStateException
+ * if the cell is not {@link Type#WIDGET}
+ *
+ * @return the widget in the cell
+ */
+ public Widget getWidget() {
+ if (type != GridStaticCellType.WIDGET) {
+ throw new IllegalStateException(
+ "Cannot fetch Widget from a cell with type " + type);
+ }
+ return (Widget) content;
+ }
+
+ /**
+ * Set widget as the content of the cell. The type of the cell becomes
+ * {@link Type#WIDGET}. All previous content is discarded.
+ *
+ * @param widget
+ * The widget to add to the cell. Should not be previously
+ * attached anywhere (widget.getParent == null).
+ */
+ public void setWidget(Widget widget) {
+ this.content = widget;
+ this.type = GridStaticCellType.WIDGET;
+ section.requestSectionRefresh();
+ }
+
+ /**
+ * Returns the type of the cell.
+ *
+ * @return the type of content the cell contains.
+ */
+ public GridStaticCellType getType() {
+ return type;
+ }
+ }
+
+ /**
+ * Abstract base class for Grid header and footer rows.
+ *
+ * @param <CELLTYPE>
+ * the type of the cells in the row
+ */
+ abstract static class StaticRow<CELLTYPE extends StaticCell> {
+
+ private List<CELLTYPE> cells = new ArrayList<CELLTYPE>();
+
+ private GridStaticSection<?> section;
+
+ private Collection<List<CELLTYPE>> cellGroups = new HashSet<List<CELLTYPE>>();
+
+ /**
+ * Returns the cell at the given position in this row.
+ *
+ * @param index
+ * the position of the cell
+ * @return the cell at the index
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public CELLTYPE getCell(int index) {
+ return cells.get(index);
+ }
+
+ /**
+ * Merges cells in a row
+ *
+ * @param cells
+ * The cells to be merged
+ * @return The first cell of the merged cells
+ */
+ protected CELLTYPE join(List<CELLTYPE> cells) {
+ assert cells.size() > 1 : "You cannot merge less than 2 cells together";
+
+ // Ensure no cell is already grouped
+ for (CELLTYPE cell : cells) {
+ if (getCellGroupForCell(cell) != null) {
+ throw new IllegalStateException("Cell " + cell.getText()
+ + " is already grouped.");
+ }
+ }
+
+ // Ensure continuous range
+ int firstCellIndex = this.cells.indexOf(cells.get(0));
+ for (int i = 0; i < cells.size(); i++) {
+ if (this.cells.get(firstCellIndex + i) != cells.get(i)) {
+ throw new IllegalStateException(
+ "Cell range must be a continous range");
+ }
+ }
+
+ // Create a new group
+ cellGroups.add(new ArrayList<CELLTYPE>(cells));
+
+ // Calculates colspans, triggers refresh on section implicitly
+ calculateColspans();
+
+ // Returns first cell of group
+ return cells.get(0);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param columns
+ * The columns which header should be merged
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(GridColumn<?, ?>... columns) {
+ assert columns.length > 1 : "You cannot merge less than 2 columns together";
+
+ // Convert columns to cells
+ List<CELLTYPE> cells = new ArrayList<CELLTYPE>();
+ for (GridColumn<?, ?> c : columns) {
+ int index = getSection().getGrid().getColumns().indexOf(c);
+ cells.add(this.cells.get(index));
+ }
+
+ return join(cells);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param cells
+ * The cells to merge. Must be from the same row.
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(CELLTYPE... cells) {
+ return join(Arrays.asList(cells));
+ }
+
+ private List<CELLTYPE> getCellGroupForCell(CELLTYPE cell) {
+ for (List<CELLTYPE> group : cellGroups) {
+ if (group.contains(cell)) {
+ return group;
+ }
+ }
+ return null;
+ }
+
+ void calculateColspans() {
+
+ // Reset all cells
+ for (CELLTYPE cell : cells) {
+ cell.setColspan(1);
+ }
+
+ // Set colspan for grouped cells
+ for (List<CELLTYPE> group : cellGroups) {
+
+ int firstVisibleColumnInGroup = -1;
+ int lastVisibleColumnInGroup = -1;
+ int hiddenInsideGroup = 0;
+
+ /*
+ * To be able to calculate the colspan correctly we need to two
+ * things; find the first visible cell in the group which will
+ * get the colspan assigned to and find the amount of columns
+ * which should be spanned.
+ *
+ * To do that we iterate through all cells, marking into memory
+ * when we find the first visible cell, when we find the last
+ * visible cell and how many cells are hidden in between.
+ */
+ for (int i = 0; i < group.size(); i++) {
+ CELLTYPE cell = group.get(i);
+ int cellIndex = this.cells.indexOf(cell);
+ boolean columnVisible = getSection().getGrid()
+ .getColumn(cellIndex).isVisible();
+ if (columnVisible) {
+ lastVisibleColumnInGroup = i;
+ if (firstVisibleColumnInGroup == -1) {
+ firstVisibleColumnInGroup = i;
+ }
+ } else if (firstVisibleColumnInGroup != -1) {
+ hiddenInsideGroup++;
+ }
+ }
+
+ if (firstVisibleColumnInGroup == -1
+ || lastVisibleColumnInGroup == -1
+ || firstVisibleColumnInGroup == lastVisibleColumnInGroup) {
+ // No cells in group
+ continue;
+ }
+
+ /*
+ * Assign colspan to first cell in group.
+ */
+ CELLTYPE firstVisibleCell = group
+ .get(firstVisibleColumnInGroup);
+ firstVisibleCell.setColspan(lastVisibleColumnInGroup
+ - firstVisibleColumnInGroup - hiddenInsideGroup + 1);
+ }
+
+ }
+
+ protected void addCell(int index) {
+ CELLTYPE cell = createCell();
+ cell.setSection(getSection());
+ cells.add(index, cell);
+ }
+
+ protected void removeCell(int index) {
+ cells.remove(index);
+ }
+
+ protected abstract CELLTYPE createCell();
+
+ protected GridStaticSection<?> getSection() {
+ return section;
+ }
+
+ protected void setSection(GridStaticSection<?> section) {
+ this.section = section;
+ }
+ }
+
+ private Grid<?> grid;
+
+ private List<ROWTYPE> rows = new ArrayList<ROWTYPE>();
+
+ private boolean visible = true;
+
+ /**
+ * Creates and returns a new instance of the row type.
+ *
+ * @return the created row
+ */
+ protected abstract ROWTYPE createRow();
+
+ /**
+ * Informs the grid that this section should be re-rendered.
+ * <p>
+ * <b>Note</b> that re-render means calling update() on each cell,
+ * preAttach()/postAttach()/preDetach()/postDetach() is not called as the
+ * cells are not removed from the DOM.
+ */
+ protected abstract void requestSectionRefresh();
+
+ /**
+ * Sets the visibility of the whole section.
+ *
+ * @param visible
+ * true to show this section, false to hide
+ */
+ public void setVisible(boolean visible) {
+ this.visible = visible;
+ requestSectionRefresh();
+ }
+
+ /**
+ * Returns the visibility of this section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return visible;
+ }
+
+ /**
+ * Inserts a new row at the given position.
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public ROWTYPE addRow(int index) {
+ ROWTYPE row = createRow();
+ row.setSection(this);
+ for (int i = 0; i < getGrid().getColumnCount(); ++i) {
+ row.addCell(i);
+ }
+ rows.add(index, row);
+
+ requestSectionRefresh();
+ return row;
+ }
+
+ /**
+ * Adds a new row at the top of this section.
+ *
+ * @return the new row
+ */
+ public ROWTYPE prependRow() {
+ return addRow(0);
+ }
+
+ /**
+ * Adds a new row at the bottom of this section.
+ *
+ * @return the new row
+ */
+ public ROWTYPE appendRow() {
+ return addRow(rows.size());
+ }
+
+ /**
+ * Removes the row at the given position.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public void removeRow(int index) {
+ rows.remove(index);
+ requestSectionRefresh();
+ }
+
+ /**
+ * Removes the given row from the section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ */
+ public void removeRow(ROWTYPE row) {
+ try {
+ removeRow(rows.indexOf(row));
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException(
+ "Section does not contain the given row");
+ }
+ }
+
+ /**
+ * Returns the row at the given position.
+ *
+ * @param index
+ * the position of the row
+ * @return the row with the given index
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public ROWTYPE getRow(int index) {
+ try {
+ return rows.get(index);
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException("Row with index " + index
+ + " does not exist");
+ }
+ }
+
+ /**
+ * Returns the number of rows in this section.
+ *
+ * @return the number of rows
+ */
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ protected List<ROWTYPE> getRows() {
+ return rows;
+ }
+
+ protected int getVisibleRowCount() {
+ return isVisible() ? getRowCount() : 0;
+ }
+
+ protected void addColumn(GridColumn<?, ?> column, int index) {
+ for (ROWTYPE row : rows) {
+ row.addCell(index);
+ }
+ }
+
+ protected void removeColumn(int index) {
+ for (ROWTYPE row : rows) {
+ row.removeCell(index);
+ }
+ }
+
+ protected void setGrid(Grid<?> grid) {
+ this.grid = grid;
+ }
+
+ protected Grid<?> getGrid() {
+ assert grid != null;
+ return grid;
+ }
+}
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..4db5efd0fc
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/PositionFunction.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2000-2014 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
+ * @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..787a145326
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Renderer.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2000-2014 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
+ * @author Vaadin Ltd
+ */
+public interface Renderer<T> {
+
+ /**
+ * Called whenever the {@link Grid} updates a cell
+ *
+ * @param cell
+ * The cell. Note that the cell is a flyweight and should not be
+ * stored outside of the method as it will change.
+ *
+ * @param data
+ * The column data object
+ */
+ void render(FlyweightCell 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..6419a98574
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/Row.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2000-2014 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.TableRowElement;
+
+/**
+ * A representation of a row in an {@link Escalator}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface Row {
+ /**
+ * 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 TableRowElement 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..d0fb0db103
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/RowContainer.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 2000-2014 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 the rows in each of the sections (header, body and
+ * footer) in an {@link Escalator}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @see Escalator#getHeader()
+ * @see Escalator#getBody()
+ * @see Escalator#getFooter()
+ */
+public interface RowContainer {
+
+ /**
+ * An arbitrary pixel height of a row, before any autodetection for the row
+ * height has been made.
+ * */
+ public static final int INITIAL_DEFAULT_ROW_HEIGHT = 20;
+
+ /**
+ * 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();
+
+ /**
+ * The default height of the rows in this RowContainer.
+ *
+ * @param px
+ * the default height in pixels of the rows in this RowContainer
+ * @throws IllegalArgumentException
+ * if <code>px &lt; 1</code>
+ * @see #getDefaultRowHeight()
+ */
+ public void setDefaultRowHeight(int px) throws IllegalArgumentException;
+
+ /**
+ * Returns the default height of the rows in this RowContainer.
+ * <p>
+ * This value will be equal to {@link #INITIAL_DEFAULT_ROW_HEIGHT} if the
+ * {@link Escalator} has not yet had a chance to autodetect the row height,
+ * or no explicit value has yet given via {@link #setDefaultRowHeight(int)}
+ *
+ * @return the default height of the rows in this RowContainer, in pixels
+ * @see #setDefaultRowHeight(int)
+ */
+ public int getDefaultRowHeight();
+
+ /**
+ * Returns the cell object which contains information about the cell the
+ * element is in.
+ *
+ * @param element
+ * The element to get the cell for. If element is not present in
+ * row container then <code>null</code> is returned.
+ *
+ * @return the cell of the element, or <code>null</code> if element is not
+ * present in the {@link RowContainer}.
+ */
+ public Cell getCell(Element element);
+
+ /**
+ * Gets the row element with given logical index. For lazy loaded containers
+ * such as Escalators BodyRowContainer visibility should be checked before
+ * calling this function. See {@link Escalator#getVisibleRowRange()}.
+ *
+ * @param index
+ * the logical index of the element to retrieve
+ * @return the element at position {@code index}
+ * @throws IndexOutOfBoundsException
+ * if {@code index} is not valid within container
+ * @throws IllegalStateException
+ * if {@code index} is currently not available in the DOM
+ */
+ public Element getRowElement(int index) throws IndexOutOfBoundsException,
+ IllegalStateException;
+
+ /**
+ * Returns the root element of RowContainer
+ *
+ * @return RowContainer root element
+ */
+ public Element getElement();
+}
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..c5c5e45ca8
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2000-2014 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
+ * @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..6aa165fe04
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2014 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
+ * @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..5a54f5fc68
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java
@@ -0,0 +1,639 @@
+/*
+ * Copyright 2000-2014 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.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HandlerManager;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.EventListener;
+import com.google.gwt.user.client.Timer;
+
+/**
+ * An element-like bundle representing a configurable and visual scrollbar in
+ * one axis.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @see VerticalScrollbarBundle
+ * @see HorizontalScrollbarBundle
+ */
+abstract class ScrollbarBundle {
+
+ private class TemporaryResizer extends Object {
+ private static final int TEMPORARY_RESIZE_DELAY = 1000;
+
+ private final Timer timer = new Timer() {
+ @Override
+ public void run() {
+ internalSetScrollbarThickness(1);
+ }
+ };
+
+ public void show() {
+ internalSetScrollbarThickness(OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX);
+ timer.schedule(TEMPORARY_RESIZE_DELAY);
+ }
+ }
+
+ /**
+ * A means to listen to when the scrollbar handle in a
+ * {@link ScrollbarBundle} either appears or is removed.
+ */
+ public interface VisibilityHandler extends EventHandler {
+ /**
+ * This method is called whenever the scrollbar handle's visibility is
+ * changed in a {@link ScrollbarBundle}.
+ *
+ * @param event
+ * the {@link VisibilityChangeEvent}
+ */
+ void visibilityChanged(VisibilityChangeEvent event);
+ }
+
+ public static class VisibilityChangeEvent extends
+ GwtEvent<VisibilityHandler> {
+ public static final Type<VisibilityHandler> TYPE = new Type<ScrollbarBundle.VisibilityHandler>() {
+ @Override
+ public String toString() {
+ return "VisibilityChangeEvent";
+ }
+ };
+
+ private final boolean isScrollerVisible;
+
+ private VisibilityChangeEvent(boolean isScrollerVisible) {
+ this.isScrollerVisible = isScrollerVisible;
+ }
+
+ /**
+ * Checks whether the scroll handle is currently visible or not
+ *
+ * @return <code>true</code> if the scroll handle is currently visible.
+ * <code>false</code> if not.
+ */
+ public boolean isScrollerVisible() {
+ return isScrollerVisible;
+ }
+
+ @Override
+ public Type<VisibilityHandler> getAssociatedType() {
+ return TYPE;
+ }
+
+ @Override
+ protected void dispatch(VisibilityHandler handler) {
+ handler.visibilityChanged(this);
+ }
+ }
+
+ /**
+ * 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;
+
+ /**
+ * The allowed value inaccuracy when comparing two double-typed pixel
+ * values.
+ * <p>
+ * Since we're comparing pixels on a screen, epsilon must be less than 1.
+ * 0.49 was deemed a perfectly fine and beautifully round number.
+ */
+ private static final double PIXEL_EPSILON = 0.49d;
+
+ /**
+ * 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
+ protected int internalGetScrollSize() {
+ return scrollSizeElement.getOffsetHeight();
+ }
+
+ @Override
+ protected void internalSetOffsetSize(double px) {
+ root.getStyle().setHeight(px, Unit.PX);
+ }
+
+ @Override
+ public double 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
+ protected int internalGetScrollSize() {
+ return scrollSizeElement.getOffsetWidth();
+ }
+
+ @Override
+ protected void internalSetOffsetSize(double px) {
+ root.getStyle().setWidth(px, Unit.PX);
+ }
+
+ @Override
+ public double 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 double scrollPos = 0;
+ private double maxScrollPos = 0;
+
+ private boolean scrollHandleIsVisible = false;
+
+ /** @deprecarted access via {@link #getHandlerManager()} instead. */
+ @Deprecated
+ private HandlerManager handlerManager;
+
+ private TemporaryResizer invisibleScrollbarTemporaryResizer = new TemporaryResizer();
+
+ private ScrollbarBundle() {
+ root.appendChild(scrollSizeElement);
+ }
+
+ protected abstract int internalGetScrollSize();
+
+ /**
+ * 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.
+ * <p>
+ * <em>Note:</em> Even though {@code double} values are used, they are
+ * currently only used as integers as large {@code int} (or small but fast
+ * {@code long}). This means, all values are truncated to zero decimal
+ * places.
+ *
+ * @param delta
+ * the delta in pixels to change the scroll position by
+ */
+ public final void setScrollPosByDelta(double 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(double px);
+
+ /**
+ * Sets the length of the scrollbar.
+ * <p>
+ * <em>Note:</em> Even though {@code double} values are used, they are
+ * currently only used as integers as large {@code int} (or small but fast
+ * {@code long}). This means, all values are truncated to zero decimal
+ * places.
+ *
+ * @param px
+ * the length of the scrollbar in pixels
+ */
+ public final void setOffsetSize(double px) {
+ internalSetOffsetSize(Math.max(0, truncate(px)));
+ forceScrollbar(showsScrollHandle());
+ recalculateMaxScrollPos();
+ fireVisibilityChangeIfNeeded();
+ }
+
+ /**
+ * 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 double getOffsetSize();
+
+ /**
+ * Sets the scroll position of the scrollbar in the axis the scrollbar is
+ * representing.
+ * <p>
+ * <em>Note:</em> Even though {@code double} values are used, they are
+ * currently only used as integers as large {@code int} (or small but fast
+ * {@code long}). This means, all values are truncated to zero decimal
+ * places.
+ *
+ * @param px
+ * the new scroll position in pixels
+ */
+ public final void setScrollPos(double px) {
+ double oldScrollPos = scrollPos;
+ scrollPos = Math.max(0, Math.min(maxScrollPos, truncate(px)));
+
+ if (!pixelValuesEqual(oldScrollPos, scrollPos)) {
+ if (isInvisibleScrollbar) {
+ invisibleScrollbarTemporaryResizer.show();
+ }
+
+ /*
+ * This is where the value needs to be converted into an integer no
+ * matter how we flip it, since GWT expects an integer value.
+ * There's no point making a JSNI method that accepts doubles as the
+ * scroll position, since the browsers themselves don't support such
+ * large numbers (as of today, 25.3.2014). This double-ranged is
+ * only facilitating future virtual scrollbars.
+ */
+ internalSetScrollPos(toInt32(scrollPos));
+ }
+ }
+
+ /**
+ * Truncates a double such that no decimal places are retained.
+ * <p>
+ * E.g. {@code trunc(2.3d) == 2.0d} and {@code trunc(-2.3d) == -2.0d}.
+ *
+ * @param num
+ * the double value to be truncated
+ * @return the {@code num} value without any decimal digits
+ */
+ private static double truncate(double num) {
+ if (num > 0) {
+ return Math.floor(num);
+ } else {
+ return Math.ceil(num);
+ }
+ }
+
+ /**
+ * Modifies the element's scroll position (scrollTop or scrollLeft).
+ * <p>
+ * <em>Note:</em> The parameter here is a type of integer (instead of a
+ * double) by design. The browsers internally convert all double values into
+ * an integer value. To make this fact explicit, this API has chosen to
+ * force integers already at this level.
+ *
+ * @param px
+ * integer pixel value to scroll to
+ */
+ 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 double getScrollPos() {
+ assert internalGetScrollPos() == toInt32(scrollPos) : "calculated scroll position ("
+ + toInt32(scrollPos)
+ + ") did not match the DOM element scroll position ("
+ + internalGetScrollPos() + ")";
+ return scrollPos;
+ }
+
+ /**
+ * Retrieves the element's scroll position (scrollTop or scrollLeft).
+ * <p>
+ * <em>Note:</em> The parameter here is a type of integer (instead of a
+ * double) by design. The browsers internally convert all double values into
+ * an integer value. To make this fact explicit, this API has chosen to
+ * force integers already at this level.
+ *
+ * @return integer pixel value of the scroll position
+ */
+ 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.
+ * <p>
+ * <em>Note:</em> Even though {@code double} values are used, they are
+ * currently only used as integers as large {@code int} (or small but fast
+ * {@code long}). This means, all values are truncated to zero decimal
+ * places.
+ *
+ * @param px
+ * the number of pixels the scrollbar should be able to scroll
+ * through
+ */
+ public final void setScrollSize(double px) {
+ internalSetScrollSize(toInt32(Math.max(0, truncate(px))));
+ forceScrollbar(showsScrollHandle());
+ recalculateMaxScrollPos();
+ fireVisibilityChangeIfNeeded();
+ }
+
+ /**
+ * 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 double getScrollSize() {
+ return internalGetScrollSize();
+ }
+
+ /**
+ * 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);
+
+ if (isInvisibleScrollbar) {
+ Event.sinkEvents(root, Event.ONSCROLL);
+ Event.setEventListener(root, new EventListener() {
+ @Override
+ public void onBrowserEvent(Event event) {
+ invisibleScrollbarTemporaryResizer.show();
+ }
+ });
+ } else {
+ Event.sinkEvents(root, 0);
+ Event.setEventListener(root, null);
+ }
+
+ internalSetScrollbarThickness(Math.max(1, 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() {
+ double scrollSize = getScrollSize();
+ double 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.
+ */
+ private final void updateScrollPosFromDom() {
+ scrollPos = internalGetScrollPos();
+ }
+
+ protected HandlerManager getHandlerManager() {
+ if (handlerManager == null) {
+ handlerManager = new HandlerManager(this);
+ }
+ return handlerManager;
+ }
+
+ /**
+ * Adds handler for the scrollbar handle visibility.
+ *
+ * @param handler
+ * the {@link VisibilityHandler} to add
+ * @return {@link HandlerRegistration} used to remove the handler
+ */
+ public HandlerRegistration addVisibilityHandler(
+ final VisibilityHandler handler) {
+ return getHandlerManager().addHandler(VisibilityChangeEvent.TYPE,
+ handler);
+ }
+
+ private void fireVisibilityChangeIfNeeded() {
+ final boolean oldHandleIsVisible = scrollHandleIsVisible;
+ scrollHandleIsVisible = showsScrollHandle();
+ if (oldHandleIsVisible != scrollHandleIsVisible) {
+ final VisibilityChangeEvent event = new VisibilityChangeEvent(
+ scrollHandleIsVisible);
+ getHandlerManager().fireEvent(event);
+ }
+ }
+
+ /**
+ * Converts a double into an integer by JavaScript's terms.
+ * <p>
+ * Implementation copied from {@link Element#toInt32(double)}.
+ *
+ * @param val
+ * the double value to convert into an integer
+ * @return the double value converted to an integer
+ */
+ private static native int toInt32(double val)
+ /*-{
+ return val | 0;
+ }-*/;
+
+ /**
+ * Compares two double values with the error margin of
+ * {@link #PIXEL_EPSILON} (i.e. {@value #PIXEL_EPSILON})
+ *
+ * @param num1
+ * the first value for which to compare equality
+ * @param num2
+ * the second value for which to compare equality
+ */
+ private static boolean pixelValuesEqual(final double num1, final double num2) {
+ return Math.abs(num1 - num2) <= PIXEL_EPSILON;
+ }
+}
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..ef021a496a
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright 2000-2014 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.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.shared.util.SharedUtil;
+
+/**
+ * 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
+ * @author Vaadin Ltd
+ */
+public class ListDataSource<T> implements DataSource<T> {
+
+ private class RowHandleImpl extends RowHandle<T> {
+
+ private final T row;
+
+ public RowHandleImpl(T row) {
+ this.row = row;
+ }
+
+ @Override
+ public T getRow() {
+ /*
+ * We'll cheat here and don't throw an IllegalStateException even if
+ * this isn't pinned, because we know that the reference never gets
+ * stale.
+ */
+ return row;
+ }
+
+ @Override
+ public void pin() {
+ // NOOP, really
+ }
+
+ @Override
+ public void unpin() throws IllegalStateException {
+ /*
+ * Just to make things easier for everyone, we won't throw the
+ * exception, even in illegal situations.
+ */
+ }
+
+ @Override
+ protected boolean equalsExplicit(Object obj) {
+ if (obj instanceof ListDataSource.RowHandleImpl) {
+ /*
+ * Java prefers AbstractRemoteDataSource<?>.RowHandleImpl. I
+ * like the @SuppressWarnings more (keeps the line length in
+ * check.)
+ */
+ @SuppressWarnings("unchecked")
+ RowHandleImpl rhi = (RowHandleImpl) obj;
+ return SharedUtil.equals(row, rhi.row);
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected int hashCodeExplicit() {
+ return row.hashCode();
+ }
+ }
+
+ /**
+ * 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
+ @SuppressWarnings("hiding")
+ 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());
+ changeHandler.dataAvailable(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());
+ changeHandler.dataAvailable(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");
+ }
+ changeHandler.dataAvailable(firstRowIndex, numberOfRows);
+ }
+
+ @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;
+ }
+
+ @Override
+ public RowHandle<T> getHandle(T row) throws IllegalStateException {
+ assert ds.contains(row) : "This data source doesn't contain the row "
+ + row;
+ return new RowHandleImpl(row);
+ }
+
+ /**
+ * Sort entire container according to a {@link Comparator}.
+ *
+ * @param comparator
+ * a comparator object, which compares two data source entries
+ * (beans/pojos)
+ */
+ public void sort(Comparator<T> comparator) {
+ Collections.sort(ds, comparator);
+ if (changeHandler != null) {
+ changeHandler.dataUpdated(0, ds.size());
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/datasources/ListSorter.java b/client/src/com/vaadin/client/ui/grid/datasources/ListSorter.java
new file mode 100644
index 0000000000..9e643825e9
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/datasources/ListSorter.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2000-2014 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.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.GridColumn;
+import com.vaadin.client.ui.grid.sort.SortEvent;
+import com.vaadin.client.ui.grid.sort.SortEventHandler;
+import com.vaadin.client.ui.grid.sort.SortOrder;
+import com.vaadin.shared.ui.grid.SortDirection;
+
+/**
+ * Provides sorting facility from Grid for the {@link ListDataSource} in-memory
+ * data source.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @param <T>
+ * Grid row data type
+ */
+public class ListSorter<T> {
+
+ private Grid<T> grid;
+ private Map<GridColumn<?, T>, Comparator<?>> comparators;
+ private HandlerRegistration sortHandlerRegistration;
+
+ public ListSorter(Grid<T> grid) {
+
+ if (grid == null) {
+ throw new IllegalArgumentException("Grid can not be null");
+ }
+
+ this.grid = grid;
+ comparators = new HashMap<GridColumn<?, T>, Comparator<?>>();
+
+ sortHandlerRegistration = grid
+ .addSortHandler(new SortEventHandler<T>() {
+ @Override
+ public void sort(SortEvent<T> event) {
+ ListSorter.this.sort(event.getOrder());
+ }
+ });
+ }
+
+ /**
+ * Detach this Sorter from the Grid. This unregisters the sort event handler
+ * which was used to apply sorting to the ListDataSource.
+ */
+ public void removeFromGrid() {
+ sortHandlerRegistration.removeHandler();
+ }
+
+ /**
+ * Assign or remove a comparator for a column. This comparator method, if
+ * defined, is always used in favour of 'natural' comparison of objects
+ * (i.e. the compareTo of objects implementing the Comparable interface,
+ * which includes all standard data classes like String, Number derivatives
+ * and Dates). Any existing comparator can be removed by passing in a
+ * non-null GridColumn and a null Comparator.
+ *
+ * @param column
+ * a grid column. May not be null.
+ * @param comparator
+ * comparator method for the values returned by the grid column.
+ * If null, any existing comparator is removed.
+ */
+ public <C> void setComparator(GridColumn<C, T> column,
+ Comparator<C> comparator) {
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Column reference can not be null");
+ }
+ if (comparator == null) {
+ comparators.remove(column);
+ } else {
+ comparators.put(column, comparator);
+ }
+ }
+
+ /**
+ * Retrieve the comparator assigned for a specific grid column.
+ *
+ * @param column
+ * a grid column. May not be null.
+ * @return a comparator, or null if no comparator for the specified grid
+ * column has been set.
+ */
+ @SuppressWarnings("unchecked")
+ public <C> Comparator<C> getComparator(GridColumn<C, T> column) {
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Column reference can not be null");
+ }
+ return (Comparator<C>) comparators.get(column);
+ }
+
+ /**
+ * Remove all comparator mappings. Useful if the data source has changed but
+ * this Sorter is being re-used.
+ */
+ public void clearComparators() {
+ comparators.clear();
+ }
+
+ /**
+ * Apply sorting to the current ListDataSource.
+ *
+ * @param order
+ * the sort order list provided by the grid sort event
+ */
+ private void sort(final List<SortOrder> order) {
+ DataSource<T> ds = grid.getDataSource();
+ if (!(ds instanceof ListDataSource)) {
+ throw new IllegalStateException("Grid " + grid
+ + " data source is not a ListDataSource!");
+ }
+
+ ((ListDataSource<T>) ds).sort(new Comparator<T>() {
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public int compare(T a, T b) {
+
+ for (SortOrder o : order) {
+
+ GridColumn column = o.getColumn();
+ Comparator cmp = ListSorter.this.comparators.get(column);
+ int result = 0;
+ Object value_a = column.getValue(a);
+ Object value_b = column.getValue(b);
+ if (cmp != null) {
+ result = cmp.compare(value_a, value_b);
+ } else {
+ if (!(value_a instanceof Comparable)) {
+ throw new IllegalStateException("Column " + column
+ + " has no assigned comparator and value "
+ + value_a + " isn't naturally comparable");
+ }
+ result = ((Comparable) value_a).compareTo(value_b);
+ }
+
+ if (result != 0) {
+ return o.getDirection() == SortDirection.ASCENDING ? result
+ : -result;
+ }
+ }
+
+ if (order.size() > 0) {
+ return order.get(0).getDirection() == SortDirection.ASCENDING ? a
+ .hashCode() - b.hashCode()
+ : b.hashCode() - a.hashCode();
+ }
+ return a.hashCode() - b.hashCode();
+ }
+ });
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/AbstractGridKeyEventHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/AbstractGridKeyEventHandler.java
new file mode 100644
index 0000000000..28a85924fa
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/AbstractGridKeyEventHandler.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Base interface of all handlers for {@link AbstractGridKeyEvent}s.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract interface AbstractGridKeyEventHandler extends EventHandler {
+
+ public abstract interface GridKeyDownHandler<T> extends
+ AbstractGridKeyEventHandler {
+ public void onKeyDown(GridKeyDownEvent<T> event);
+ }
+
+ public abstract interface GridKeyUpHandler<T> extends
+ AbstractGridKeyEventHandler {
+ public void onKeyUp(GridKeyUpEvent<T> event);
+ }
+
+ public abstract interface GridKeyPressHandler<T> extends
+ AbstractGridKeyEventHandler {
+ public void onKeyPress(GridKeyPressEvent<T> event);
+ }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyDownHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyDownHandler.java
new file mode 100644
index 0000000000..a3b76ea5d7
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyDownHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyDownHandler;
+
+/**
+ * Handler for {@link GridKeyDownEvent}s that happen when active cell is in the
+ * body of the Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface BodyKeyDownHandler<T> extends GridKeyDownHandler<T> {
+}
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyPressHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyPressHandler.java
new file mode 100644
index 0000000000..5548994cf9
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyPressHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyPressHandler;
+
+/**
+ * Handler for {@link GridKeyPressEvent}s that happen when active cell is in the
+ * body of the Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface BodyKeyPressHandler<T> extends GridKeyPressHandler<T> {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyUpHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyUpHandler.java
new file mode 100644
index 0000000000..33b4fc81fe
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/BodyKeyUpHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyUpHandler;
+
+/**
+ * Handler for {@link GridKeyUpEvent}s that happen when active cell is in the
+ * body of the Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface BodyKeyUpHandler<T> extends GridKeyUpHandler<T> {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyDownHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyDownHandler.java
new file mode 100644
index 0000000000..e90f52e736
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyDownHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyDownHandler;
+
+/**
+ * Handler for {@link GridKeyDownEvent}s that happen when active cell is in the
+ * footer of the Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface FooterKeyDownHandler<T> extends GridKeyDownHandler<T> {
+}
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyPressHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyPressHandler.java
new file mode 100644
index 0000000000..58f48f36f5
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyPressHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyPressHandler;
+
+/**
+ * Handler for {@link GridKeyPressEvent}s that happen when active cell is in the
+ * footer of the Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface FooterKeyPressHandler<T> extends GridKeyPressHandler<T> {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyUpHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyUpHandler.java
new file mode 100644
index 0000000000..d6bcddf710
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/FooterKeyUpHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyUpHandler;
+
+/**
+ * Handler for {@link GridKeyUpEvent}s that happen when active cell is in the
+ * footer of the Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface FooterKeyUpHandler<T> extends GridKeyUpHandler<T> {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyDownEvent.java b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyDownEvent.java
new file mode 100644
index 0000000000..8af65dbf49
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyDownEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.Grid.AbstractGridKeyEvent;
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyDownHandler;
+
+/**
+ * Represents native key down event in Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridKeyDownEvent<T> extends
+ AbstractGridKeyEvent<T, GridKeyDownHandler<T>> {
+
+ public GridKeyDownEvent(Grid<T> grid) {
+ super(grid);
+ }
+
+ @Override
+ protected void dispatch(GridKeyDownHandler<T> handler) {
+ super.dispatch(handler);
+ if ((activeSection == GridSection.BODY && handler instanceof BodyKeyDownHandler)
+ || (activeSection == GridSection.HEADER && handler instanceof HeaderKeyDownHandler)
+ || (activeSection == GridSection.FOOTER && handler instanceof FooterKeyDownHandler)) {
+ handler.onKeyDown(this);
+ }
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.KEYDOWN;
+ }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyPressEvent.java b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyPressEvent.java
new file mode 100644
index 0000000000..6f06bc6674
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyPressEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.Grid.AbstractGridKeyEvent;
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyPressHandler;
+
+/**
+ * Represents native key press event in Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridKeyPressEvent<T> extends
+ AbstractGridKeyEvent<T, GridKeyPressHandler<T>> {
+
+ public GridKeyPressEvent(Grid<T> grid) {
+ super(grid);
+ }
+
+ @Override
+ protected void dispatch(GridKeyPressHandler<T> handler) {
+ super.dispatch(handler);
+ if ((activeSection == GridSection.BODY && handler instanceof BodyKeyPressHandler)
+ || (activeSection == GridSection.HEADER && handler instanceof HeaderKeyPressHandler)
+ || (activeSection == GridSection.FOOTER && handler instanceof FooterKeyPressHandler)) {
+ handler.onKeyPress(this);
+ }
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.KEYPRESS;
+ }
+
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyUpEvent.java b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyUpEvent.java
new file mode 100644
index 0000000000..d289dbae98
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/GridKeyUpEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.Grid.AbstractGridKeyEvent;
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyUpHandler;
+
+/**
+ * Represents native key up event in Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridKeyUpEvent<T> extends
+ AbstractGridKeyEvent<T, GridKeyUpHandler<T>> {
+
+ public GridKeyUpEvent(Grid<T> grid) {
+ super(grid);
+ }
+
+ @Override
+ protected void dispatch(GridKeyUpHandler<T> handler) {
+ super.dispatch(handler);
+ if ((activeSection == GridSection.BODY && handler instanceof BodyKeyUpHandler)
+ || (activeSection == GridSection.HEADER && handler instanceof HeaderKeyUpHandler)
+ || (activeSection == GridSection.FOOTER && handler instanceof FooterKeyUpHandler)) {
+ handler.onKeyUp(this);
+ }
+ }
+
+ @Override
+ protected String getBrowserEventType() {
+ return BrowserEvents.KEYUP;
+ }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyDownHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyDownHandler.java
new file mode 100644
index 0000000000..28c9a8e056
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyDownHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyDownHandler;
+
+/**
+ * Handler for {@link GridKeyDownEvent}s that happen when active cell is in the
+ * header of the Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface HeaderKeyDownHandler<T> extends GridKeyDownHandler<T> {
+}
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyPressHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyPressHandler.java
new file mode 100644
index 0000000000..607c30493d
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyPressHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyPressHandler;
+
+/**
+ * Handler for {@link GridKeyPressEvent}s that happen when active cell is in the
+ * header of the Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface HeaderKeyPressHandler<T> extends GridKeyPressHandler<T> {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyUpHandler.java b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyUpHandler.java
new file mode 100644
index 0000000000..bfa3dde79b
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/keyevents/HeaderKeyUpHandler.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2000-2014 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.keyevents;
+
+import com.vaadin.client.ui.grid.keyevents.AbstractGridKeyEventHandler.GridKeyUpHandler;
+
+/**
+ * Handler for {@link GridKeyUpEvent}s that happen when active cell is in the
+ * header of the Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface HeaderKeyUpHandler<T> extends GridKeyUpHandler<T> {
+} \ No newline at end of file
diff --git a/client/src/com/vaadin/client/ui/grid/renderers/AbstractRendererConnector.java b/client/src/com/vaadin/client/ui/grid/renderers/AbstractRendererConnector.java
new file mode 100644
index 0000000000..cad5af97df
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/AbstractRendererConnector.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2000-2014 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.json.client.JSONValue;
+import com.vaadin.client.ServerConnector;
+import com.vaadin.client.Util;
+import com.vaadin.client.communication.JsonDecoder;
+import com.vaadin.client.extensions.AbstractExtensionConnector;
+import com.vaadin.client.metadata.NoDataException;
+import com.vaadin.client.metadata.Type;
+import com.vaadin.client.metadata.TypeData;
+import com.vaadin.client.metadata.TypeDataStore;
+import com.vaadin.client.ui.grid.GridConnector;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * An abstract base class for renderer connectors. A renderer connector is used
+ * to link a client-side {@link Renderer} to a server-side
+ * {@link com.vaadin.ui.components.grid.Renderer Renderer}. As a connector, it
+ * can use the regular Vaadin RPC and shared state mechanism to pass additional
+ * state and information between the client and the server. This base class
+ * itself only uses the basic
+ * {@link com.vaadin.shared.communication.SharedState SharedState} and no RPC
+ * interfaces.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class AbstractRendererConnector<T> extends
+ AbstractExtensionConnector {
+
+ private Renderer<T> renderer = null;
+
+ private final Type presentationType = TypeDataStore
+ .getPresentationType(this.getClass());
+
+ protected AbstractRendererConnector() {
+ if (presentationType == null) {
+ throw new IllegalStateException(
+ "No presentation type found for "
+ + Util.getSimpleName(this)
+ + ". This may be caused by some unspecified problem in widgetset compilation.");
+ }
+ }
+
+ /**
+ * Returns the renderer associated with this renderer connector.
+ * <p>
+ * A subclass of AbstractRendererConnector should override this method as
+ * shown below. The framework uses
+ * {@link com.google.gwt.core.client.GWT#create(Class) GWT.create(Class)} to
+ * create a renderer based on the return type of the overridden method, but
+ * only if {@link #createRenderer()} is not overridden as well:
+ *
+ * <pre>
+ * public MyRenderer getRenderer() {
+ * return (MyRenderer) super.getRenderer();
+ * }
+ * </pre>
+ *
+ * @return the renderer bound to this connector
+ */
+ public Renderer<T> getRenderer() {
+ if (renderer == null) {
+ renderer = createRenderer();
+ }
+ return renderer;
+ }
+
+ /**
+ * Creates a new Renderer instance associated with this renderer connector.
+ * <p>
+ * You should typically not override this method since the framework by
+ * default generates an implementation that uses {@link GWT#create(Class)}
+ * to create a renderer of the same type as returned by the most specific
+ * override of {@link #getRenderer()}. If you do override the method, you
+ * can't call <code>super.createRenderer()</code> since the metadata needed
+ * for that implementation is not generated if there's an override of the
+ * method.
+ *
+ * @return a new renderer to be used with this connector
+ */
+ protected Renderer<T> createRenderer() {
+ // TODO generate type data
+ Type type = TypeData.getType(getClass());
+ try {
+ Type rendererType = type.getMethod("getRenderer").getReturnType();
+ @SuppressWarnings("unchecked")
+ Renderer<T> instance = (Renderer<T>) rendererType.createInstance();
+ return instance;
+ } catch (NoDataException e) {
+ throw new IllegalStateException(
+ "Default implementation of createRenderer() does not work for "
+ + Util.getSimpleName(this)
+ + ". This might be caused by explicitely using "
+ + "super.createRenderer() or some unspecified "
+ + "problem with the widgetset compilation.", e);
+ }
+ }
+
+ /**
+ * Decodes the given JSON value into a value of type T so it can be passed
+ * to the {@link #getRenderer() renderer}.
+ *
+ * @param value
+ * the value to decode
+ * @return the decoded value of {@code value}
+ */
+ public T decode(JSONValue value) {
+ @SuppressWarnings("unchecked")
+ T decodedValue = (T) JsonDecoder.decodeValue(presentationType, value,
+ null, getConnection());
+ return decodedValue;
+ }
+
+ @Override
+ @Deprecated
+ protected void extend(ServerConnector target) {
+ // NOOP
+ }
+
+ /**
+ * Gets the row key for a row index.
+ * <p>
+ * In case this renderer wants be able to identify a row in such a way that
+ * the server also understands it, the row key is used for that. Rows are
+ * identified by unified keys between the client and the server.
+ *
+ * @param index
+ * the row index for which to get the row key
+ * @return the row key for the row at {@code index}
+ */
+ protected String getRowKey(int index) {
+ final ServerConnector parent = getParent();
+ if (parent instanceof GridConnector) {
+ return ((GridConnector) parent).getRowKey(index);
+ } else {
+ throw new IllegalStateException("Renderers can only be used "
+ + "with a Grid.");
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/renderers/ComplexRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/ComplexRenderer.java
new file mode 100644
index 0000000000..f0c95e2ddf
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/ComplexRenderer.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2000-2014 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.Collection;
+
+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.Style.Visibility;
+import com.vaadin.client.ui.grid.Cell;
+import com.vaadin.client.ui.grid.FlyweightCell;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * Base class for renderers that needs initialization and destruction logic
+ * (override {@link #init(FlyweightCell) and #destroy(FlyweightCell) } and event
+ * handling (see {@link #onBrowserEvent(Cell, NativeEvent)},
+ * {@link #getConsumedEvents()} and {@link #onActivate()}.
+ *
+ * <p>
+ * Also provides a helper method for hiding the cell contents by overriding
+ * {@link #setContentVisible(FlyweightCell, boolean)}
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class ComplexRenderer<T> implements Renderer<T> {
+
+ /**
+ * Called at initialization stage. Perform any initialization here e.g.
+ * attach handlers, attach widgets etc.
+ *
+ * @param cell
+ * The cell. Note that the cell is not to be stored outside of
+ * the method as the cell install will change. See
+ * {@link FlyweightCell}
+ */
+ public void init(FlyweightCell cell) {
+ // Implement if needed
+ }
+
+ /**
+ * Called after the cell is deemed to be destroyed and no longer used by the
+ * Grid. Called after the cell element is detached from the DOM.
+ *
+ * @param cell
+ * The cell. Note that the cell is not to be stored outside of
+ * the method as the cell install will change. See
+ * {@link FlyweightCell}
+ */
+ public void destroy(FlyweightCell cell) {
+ // Implement if needed
+ }
+
+ /**
+ * Returns the events that the renderer should consume. These are also the
+ * events that the Grid will pass to
+ * {@link #onBrowserEvent(Cell, NativeEvent)} when they occur.
+ * <code>null</code> if no events are consumed
+ *
+ * @return the consumed events, or null if no events are consumed
+ *
+ * @see com.google.gwt.dom.client.BrowserEvents
+ */
+ public Collection<String> getConsumedEvents() {
+ return null;
+ }
+
+ /**
+ * Called whenever a registered event is triggered in the column the
+ * renderer renders.
+ * <p>
+ * The events that triggers this needs to be returned by the
+ * {@link #getConsumedEvents()} method.
+ * <p>
+ * Returns boolean telling if the event has been completely handled and
+ * should not cause any other actions.
+ *
+ * @param cell
+ * Object containing information about the cell the event was
+ * triggered on.
+ *
+ * @param event
+ * The original DOM event
+ * @return true if event should not be handled by grid
+ */
+ public boolean onBrowserEvent(Cell cell, NativeEvent event) {
+ return false;
+ }
+
+ /**
+ * Used by Grid to toggle whether to show actual data or just an empty
+ * placeholder while data is loading. This method is invoked whenever a cell
+ * changes between data being available and data missing.
+ * <p>
+ * Default implementation hides content by setting visibility: hidden to all
+ * elements inside the cell. Text nodes are left as is - renderers that add
+ * such to the root element need to implement explicit support hiding them.
+ *
+ * @param cell
+ * The cell
+ * @param hasData
+ * Has the cell content been loaded from the data source
+ *
+ */
+ public void setContentVisible(FlyweightCell cell, boolean hasData) {
+ Element cellElement = cell.getElement();
+ for (int n = 0; n < cellElement.getChildCount(); n++) {
+ Node node = cellElement.getChild(n);
+ if (Element.is(node)) {
+ Element e = Element.as(node);
+ if (hasData) {
+ e.getStyle().clearVisibility();
+ } else {
+ e.getStyle().setVisibility(Visibility.HIDDEN);
+ }
+ }
+ }
+ }
+
+ /**
+ * Called when the cell is "activated" by pressing <code>enter</code>,
+ * double clicking or performing a double tap on the cell.
+ *
+ * @return <code>true</code> if event was handled and should not be
+ * interpreted as a generic gesture by Grid.
+ */
+ public boolean onActivate() {
+ return false;
+ }
+
+ /**
+ * Called when the renderer is deemed to be destroyed and no longer used by
+ * the Grid.
+ */
+ public void destroy() {
+ // Implement if needed
+ }
+}
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..fc7d3ac833
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2000-2014 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.DateTimeFormat.PredefinedFormat;
+import com.google.gwt.i18n.client.TimeZone;
+import com.vaadin.client.ui.grid.FlyweightCell;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * A renderer for rendering dates into cells
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class DateRenderer implements Renderer<Date> {
+
+ private DateTimeFormat format = DateTimeFormat
+ .getFormat(PredefinedFormat.DATE_TIME_SHORT);
+
+ // Calendar is unavailable for GWT
+ @SuppressWarnings("deprecation")
+ private TimeZone timeZone = TimeZone.createTimeZone(new Date()
+ .getTimezoneOffset());
+
+ @Override
+ public void render(FlyweightCell 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/DateRendererConnector.java b/client/src/com/vaadin/client/ui/grid/renderers/DateRendererConnector.java
new file mode 100644
index 0000000000..52ae7d9b6b
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/DateRendererConnector.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2014 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.shared.ui.Connect;
+
+/**
+ * A connector for {@link com.vaadin.ui.components.grid.renderers.DateRenderer
+ * DateRenderer}.
+ * <p>
+ * The server-side Renderer operates on dates, but the data is serialized as a
+ * string, and displayed as-is on the client side. This is to be able to support
+ * the server's locale.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.components.grid.renderers.DateRenderer.class)
+public class DateRendererConnector extends TextRendererConnector {
+ // No implementation needed
+}
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..36c5d2bb0f
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2000-2014 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.FlyweightCell;
+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
+ * @author Vaadin Ltd
+ * @see SafeHtmlUtils#fromSafeConstant(String)
+ */
+public class HtmlRenderer implements Renderer<String> {
+
+ @Override
+ public void render(FlyweightCell 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..aa23bc2370
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2000-2014 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.FlyweightCell;
+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
+ * @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 render(FlyweightCell cell, Number number) {
+ cell.getElement().setInnerText(format.format(number));
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/renderers/NumberRendererConnector.java b/client/src/com/vaadin/client/ui/grid/renderers/NumberRendererConnector.java
new file mode 100644
index 0000000000..cba29d0690
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/NumberRendererConnector.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2014 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.shared.ui.Connect;
+
+/**
+ * A connector for
+ * {@link com.vaadin.ui.components.grid.renderers.NumberRenderer NumberRenderer}
+ * .
+ * <p>
+ * The server-side Renderer operates on numbers, but the data is serialized as a
+ * string, and displayed as-is on the client side. This is to be able to support
+ * the server's locale.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.components.grid.renderers.NumberRenderer.class)
+public class NumberRendererConnector extends TextRendererConnector {
+ // no implementation needed
+}
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..d2f3520c43
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2014 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.FlyweightCell;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * Renderer that renders text into a cell.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class TextRenderer implements Renderer<String> {
+
+ @Override
+ public void render(FlyweightCell cell, String text) {
+ cell.getElement().setInnerText(text);
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/renderers/TextRendererConnector.java b/client/src/com/vaadin/client/ui/grid/renderers/TextRendererConnector.java
new file mode 100644
index 0000000000..9ec609ae06
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/TextRendererConnector.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2014 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.shared.ui.Connect;
+
+/**
+ * A connector for {@link TextRenderer}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.components.grid.renderers.TextRenderer.class)
+public class TextRendererConnector extends AbstractRendererConnector<String> {
+
+ @Override
+ public TextRenderer getRenderer() {
+ return (TextRenderer) super.getRenderer();
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/renderers/UnsafeHtmlRendererConnector.java b/client/src/com/vaadin/client/ui/grid/renderers/UnsafeHtmlRendererConnector.java
new file mode 100644
index 0000000000..1d4a8c0384
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/UnsafeHtmlRendererConnector.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2014 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.FlyweightCell;
+import com.vaadin.client.ui.grid.Renderer;
+import com.vaadin.shared.ui.Connect;
+
+/**
+ * A connector for {@link UnsafeHtmlRenderer}
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@Connect(com.vaadin.ui.components.grid.renderers.HtmlRenderer.class)
+public class UnsafeHtmlRendererConnector extends
+ AbstractRendererConnector<String> {
+
+ public static class UnsafeHtmlRenderer implements Renderer<String> {
+ @Override
+ public void render(FlyweightCell cell, String data) {
+ cell.getElement().setInnerHTML(data);
+ }
+ }
+
+ @Override
+ public UnsafeHtmlRenderer getRenderer() {
+ return (UnsafeHtmlRenderer) super.getRenderer();
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/renderers/WidgetRenderer.java b/client/src/com/vaadin/client/ui/grid/renderers/WidgetRenderer.java
new file mode 100644
index 0000000000..b7cd72600a
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/renderers/WidgetRenderer.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2000-2014 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.user.client.ui.Widget;
+import com.vaadin.client.Util;
+import com.vaadin.client.ui.grid.FlyweightCell;
+
+/**
+ * A renderer for rendering widgets into cells.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @param <T>
+ * the row data type
+ * @param <W>
+ * the Widget type
+ */
+public abstract class WidgetRenderer<T, W extends Widget> extends
+ ComplexRenderer<T> {
+
+ /**
+ * Creates a widget to attach to a cell. The widgets will be attached to the
+ * cell after the cell element has been attached to DOM.
+ *
+ * @return widget to attach to a cell. All returned instances should be new
+ * widget instances without a parent.
+ */
+ public abstract W createWidget();
+
+ @Override
+ public void render(FlyweightCell cell, T data) {
+ W w = Util.findWidget(cell.getElement().getFirstChildElement(), null);
+ assert w != null : "Widget not found in cell (" + cell.getColumn()
+ + "," + cell.getRow() + ")";
+ render(cell, data, w);
+ }
+
+ /**
+ * Renders a cell with a widget. This provides a way to update any
+ * information in the widget that is cell specific. Do not detach the Widget
+ * here, it will be done automatically by the Grid when the widget is no
+ * longer needed.
+ *
+ * @param cell
+ * the cell to render
+ * @param data
+ * the data of the cell
+ * @param widget
+ * the widget embedded in the cell
+ */
+ public abstract void render(FlyweightCell cell, T data, W widget);
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/AbstractRowHandleSelectionModel.java b/client/src/com/vaadin/client/ui/grid/selection/AbstractRowHandleSelectionModel.java
new file mode 100644
index 0000000000..f55229d86c
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/selection/AbstractRowHandleSelectionModel.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+
+/**
+ * An abstract class that adds a consistent API for common methods that's needed
+ * by Vaadin's server-based selection models to work.
+ * <p>
+ * <em>Note:</em> This should be an interface instead of an abstract class, if
+ * only we could define protected methods in an interface.
+ *
+ * @author Vaadin Ltd
+ * @param <T>
+ * The grid's row type
+ */
+public abstract class AbstractRowHandleSelectionModel<T> implements
+ SelectionModel<T> {
+ /**
+ * Select a row, based on its
+ * {@link com.vaadin.client.data.DataSource.RowHandle RowHandle}.
+ * <p>
+ * <em>Note:</em> this method may not fire selection change events.
+ *
+ * @param handle
+ * the handle to select by
+ * @return <code>true</code> iff the selection state was changed by this
+ * call
+ * @throws UnsupportedOperationException
+ * if the selection model does not support either handles or
+ * selection
+ */
+ protected abstract boolean selectByHandle(RowHandle<T> handle);
+
+ /**
+ * Deselect a row, based on its
+ * {@link com.vaadin.client.data.DataSource.RowHandle RowHandle}.
+ * <p>
+ * <em>Note:</em> this method may not fire selection change events.
+ *
+ * @param handle
+ * the handle to deselect by
+ * @return <code>true</code> iff the selection state was changed by this
+ * call
+ * @throws UnsupportedOperationException
+ * if the selection model does not support either handles or
+ * deselection
+ */
+ protected abstract boolean deselectByHandle(RowHandle<T> handle)
+ throws UnsupportedOperationException;
+}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/HasSelectionChangeHandlers.java b/client/src/com/vaadin/client/ui/grid/selection/HasSelectionChangeHandlers.java
new file mode 100644
index 0000000000..342c426b55
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/selection/HasSelectionChangeHandlers.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import com.google.gwt.event.shared.HandlerRegistration;
+
+/**
+ * Marker interface for widgets that fires selection change events.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+public interface HasSelectionChangeHandlers<T> {
+
+ /**
+ * Register a selection change handler.
+ * <p>
+ * This handler is called whenever a
+ * {@link com.vaadin.ui.components.grid.selection.SelectionModel
+ * SelectionModel} detects a change in selection state.
+ *
+ * @param handler
+ * a {@link SelectionChangeHandler}
+ * @return a handler registration object, which can be used to remove the
+ * handler.
+ */
+ public HandlerRegistration addSelectionChangeHandler(
+ SelectionChangeHandler<T> handler);
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java b/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java
new file mode 100644
index 0000000000..a3030e3a1f
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java
@@ -0,0 +1,764 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+
+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.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.dom.client.InputElement;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.TableElement;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Event.NativePreviewEvent;
+import com.google.gwt.user.client.Event.NativePreviewHandler;
+import com.vaadin.client.Util;
+import com.vaadin.client.data.AbstractRemoteDataSource;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.ui.grid.Cell;
+import com.vaadin.client.ui.grid.DataAvailableEvent;
+import com.vaadin.client.ui.grid.DataAvailableHandler;
+import com.vaadin.client.ui.grid.FlyweightCell;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.keyevents.BodyKeyDownHandler;
+import com.vaadin.client.ui.grid.keyevents.BodyKeyUpHandler;
+import com.vaadin.client.ui.grid.keyevents.GridKeyDownEvent;
+import com.vaadin.client.ui.grid.keyevents.GridKeyUpEvent;
+import com.vaadin.client.ui.grid.renderers.ComplexRenderer;
+import com.vaadin.client.ui.grid.selection.SelectionModel.Multi.Batched;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+
+/* This class will probably not survive the final merge of all selection functionality. */
+public class MultiSelectionRenderer<T> extends ComplexRenderer<Boolean> {
+
+ /** The size of the autoscroll area, both top and bottom. */
+ private static final int SCROLL_AREA_GRADIENT_PX = 100;
+
+ /** The maximum number of pixels per second to autoscroll. */
+ private static final int SCROLL_TOP_SPEED_PX_SEC = 500;
+
+ /**
+ * The minimum area where the grid doesn't scroll while the pointer is
+ * pressed.
+ */
+ private static final int MIN_NO_AUTOSCROLL_AREA_PX = 50;
+
+ /**
+ * This class's main objective is to listen when to stop autoscrolling, and
+ * make sure everything stops accordingly.
+ */
+ private class TouchEventHandler implements NativePreviewHandler {
+ @Override
+ public void onPreviewNativeEvent(final NativePreviewEvent event) {
+ switch (event.getTypeInt()) {
+ case Event.ONTOUCHSTART: {
+ if (event.getNativeEvent().getTouches().length() == 1) {
+ /*
+ * Something has dropped a touchend/touchcancel and the
+ * scroller is most probably running amok. Let's cancel it
+ * and pretend that everything's going as expected
+ *
+ * Because this is a preview, this code is run before the
+ * event handler in MultiSelectionRenderer.onBrowserEvent.
+ * Therefore, we can simply kill everything and let that
+ * method restart things as they should.
+ */
+ autoScrollHandler.stop();
+
+ /*
+ * Related TODO: investigate why iOS seems to ignore a
+ * touchend/touchcancel when frames are dropped, and/or if
+ * something can be done about that.
+ */
+ }
+ break;
+ }
+
+ case Event.ONTOUCHMOVE:
+ event.cancel();
+ break;
+
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ /*
+ * Remember: targetElement is always where touchstart started,
+ * not where the finger is pointing currently.
+ */
+ final Element targetElement = Element.as(event.getNativeEvent()
+ .getEventTarget());
+ if (isInFirstColumn(targetElement)) {
+ removeNativeHandler();
+ event.cancel();
+ }
+ break;
+ }
+ }
+
+ private boolean isInFirstColumn(final Element element) {
+ if (element == null) {
+ return false;
+ }
+ final Element tbody = getTbodyElement();
+
+ if (tbody == null || !tbody.isOrHasChild(element)) {
+ return false;
+ }
+
+ /*
+ * The null-parent in the while clause is in the case where element
+ * is an immediate tr child in the tbody. Should never happen in
+ * internal code, but hey...
+ */
+ Element cursor = element;
+ while (cursor.getParentElement() != null
+ && cursor.getParentElement().getParentElement() != tbody) {
+ cursor = cursor.getParentElement();
+ }
+
+ final Element tr = cursor.getParentElement();
+ return tr.getFirstChildElement().equals(cursor);
+ }
+ }
+
+ /**
+ * This class's responsibility is to
+ * <ul>
+ * <li>scroll the table while a pointer is kept in a scrolling zone and
+ * <li>select rows whenever a pointer is "activated" on a selection cell
+ * </ul>
+ * <p>
+ * <em>Techical note:</em> This class is an AnimationCallback because we
+ * need a timer: when the finger is kept in place while the grid scrolls, we
+ * still need to be able to make new selections. So, instead of relying on
+ * events (which won't be fired, since the pointer isn't necessarily
+ * moving), we do this check on each frame while the pointer is "active"
+ * (mouse is pressed, finger is on screen).
+ */
+ private class AutoScrollerAndSelector implements AnimationCallback {
+
+ /**
+ * If the acceleration gradient area is smaller than this, autoscrolling
+ * will be disabled (it becomes too quick to accelerate to be usable).
+ */
+ private static final int GRADIENT_MIN_THRESHOLD_PX = 10;
+
+ /**
+ * The speed at which the gradient area recovers, once scrolling in that
+ * direction has started.
+ */
+ private static final int SCROLL_AREA_REBOUND_PX_PER_SEC = 1;
+ private static final double SCROLL_AREA_REBOUND_PX_PER_MS = SCROLL_AREA_REBOUND_PX_PER_SEC / 1000.0d;
+
+ /**
+ * The lowest y-coordinate on the {@link Event#getClientY() client} from
+ * where we need to start scrolling towards the top.
+ */
+ private int topBound = -1;
+
+ /**
+ * The highest y-coordinate on the {@link Event#getClientY() client}
+ * from where we need to scrolling towards the bottom.
+ */
+ private int bottomBound = -1;
+
+ /**
+ * <code>true</code> if the pointer is selecting, <code>false</code> if
+ * the pointer is deselecting.
+ */
+ private final boolean selectionPaint;
+
+ /**
+ * The area where the selection acceleration takes place. If &lt;
+ * {@link #GRADIENT_MIN_THRESHOLD_PX}, autoscrolling is disabled
+ */
+ private final int gradientArea;
+
+ /**
+ * The number of pixels per seconds we currently are scrolling (negative
+ * is towards the top, positive is towards the bottom).
+ */
+ private double scrollSpeed = 0;
+
+ private double prevTimestamp = 0;
+
+ /**
+ * This field stores fractions of pixels to scroll, to make sure that
+ * we're able to scroll less than one px per frame.
+ */
+ private double pixelsToScroll = 0.0d;
+
+ /** Should this animator be running. */
+ private boolean running = false;
+
+ /** The handle in which this instance is running. */
+ private AnimationHandle handle;
+
+ /** The pointer's pageX coordinate. */
+ private int pageX;
+
+ /** The pointer's pageY coordinate. */
+ private int pageY;
+
+ /** The logical index of the row that was most recently modified. */
+ private int logicalRow = -1;
+
+ /** @see #doScrollAreaChecks(int) */
+ private int finalTopBound;
+
+ /** @see #doScrollAreaChecks(int) */
+ private int finalBottomBound;
+
+ private boolean scrollAreaShouldRebound = false;
+
+ private AbstractRemoteDataSource<T> remoteDataSource = null;
+ private Batched<T> batchedSelectionModel = null;
+
+ public AutoScrollerAndSelector(final int topBound,
+ final int bottomBound, final int gradientArea,
+ final boolean selectionPaint) {
+ finalTopBound = topBound;
+ finalBottomBound = bottomBound;
+ this.gradientArea = gradientArea;
+ this.selectionPaint = selectionPaint;
+ }
+
+ @Override
+ public void execute(final double timestamp) {
+ final double timeDiff = timestamp - prevTimestamp;
+ prevTimestamp = timestamp;
+
+ reboundScrollArea(timeDiff);
+
+ pixelsToScroll += scrollSpeed * (timeDiff / 1000.0d);
+ final int intPixelsToScroll = (int) pixelsToScroll;
+ pixelsToScroll -= intPixelsToScroll;
+
+ if (intPixelsToScroll != 0) {
+ grid.setScrollTop(grid.getScrollTop() + intPixelsToScroll);
+ }
+
+ @SuppressWarnings("hiding")
+ int logicalRow = getLogicalRowIndex(Util.getElementFromPoint(pageX,
+ pageY));
+ if (logicalRow != -1 && logicalRow != this.logicalRow) {
+ this.logicalRow = logicalRow;
+ setSelected(logicalRow, selectionPaint);
+
+ if (remoteDataSource != null && batchedSelectionModel != null) {
+ Collection<T> pinneds = batchedSelectionModel
+ .getSelectedRowsBatch();
+ pinneds.addAll(batchedSelectionModel
+ .getDeselectedRowsBatch());
+ remoteDataSource.transactionPin(pinneds);
+ }
+ }
+
+ reschedule();
+ }
+
+ /**
+ * If the scroll are has been offset by the pointer starting out there,
+ * move it back a bit
+ */
+ private void reboundScrollArea(double timeDiff) {
+ if (!scrollAreaShouldRebound) {
+ return;
+ }
+
+ int reboundPx = (int) Math.ceil(SCROLL_AREA_REBOUND_PX_PER_MS
+ * timeDiff);
+ if (topBound < finalTopBound) {
+ topBound += reboundPx;
+ topBound = Math.min(topBound, finalTopBound);
+ updateScrollSpeed(pageY);
+ } else if (bottomBound > finalBottomBound) {
+ bottomBound -= reboundPx;
+ bottomBound = Math.max(bottomBound, finalBottomBound);
+ updateScrollSpeed(pageY);
+ }
+ }
+
+ private void updateScrollSpeed(final int pointerPageY) {
+
+ final double ratio;
+ if (pointerPageY < topBound) {
+ final double distance = pointerPageY - topBound;
+ ratio = Math.max(-1, distance / gradientArea);
+ }
+
+ else if (pointerPageY > bottomBound) {
+ final double distance = pointerPageY - bottomBound;
+ ratio = Math.min(1, distance / gradientArea);
+ }
+
+ else {
+ ratio = 0;
+ }
+
+ scrollSpeed = ratio * SCROLL_TOP_SPEED_PX_SEC;
+ }
+
+ @SuppressWarnings("deprecation")
+ public void start(int logicalRowIndex) {
+ running = true;
+ setSelected(logicalRowIndex, selectionPaint);
+ logicalRow = logicalRowIndex;
+ reschedule();
+
+ DataSource<T> dataSource = grid.getDataSource();
+ SelectionModel<T> selectionModel = grid.getSelectionModel();
+ if (dataSource instanceof AbstractRemoteDataSource
+ && selectionModel instanceof Batched) {
+ this.remoteDataSource = (AbstractRemoteDataSource<T>) dataSource;
+ this.batchedSelectionModel = (Batched<T>) selectionModel;
+
+ Collection<T> pinneds = batchedSelectionModel
+ .getSelectedRowsBatch();
+ pinneds.addAll(batchedSelectionModel.getDeselectedRowsBatch());
+ remoteDataSource.transactionPin(pinneds);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ public void stop() {
+ running = false;
+
+ // split into two lines because of Java generics not playing nice.
+ @SuppressWarnings("unchecked")
+ Collection<T> emptySet = (Collection<T>) Collections.emptySet();
+ remoteDataSource.transactionPin(emptySet);
+ remoteDataSource = null;
+ batchedSelectionModel = null;
+
+ if (handle != null) {
+ handle.cancel();
+ handle = null;
+ }
+ }
+
+ private void reschedule() {
+ if (running && gradientArea >= GRADIENT_MIN_THRESHOLD_PX) {
+ handle = AnimationScheduler.get().requestAnimationFrame(this,
+ grid.getElement());
+ }
+ }
+
+ @SuppressWarnings("hiding")
+ public void updatePointerCoords(int pageX, int pageY) {
+ doScrollAreaChecks(pageY);
+ updateScrollSpeed(pageY);
+ this.pageX = pageX;
+ this.pageY = pageY;
+ }
+
+ /**
+ * This method checks whether the first pointer event started in an area
+ * that would start scrolling immediately, and does some actions
+ * accordingly.
+ * <p>
+ * If it is, that scroll area will be offset "beyond" the pointer (above
+ * if pointer is towards the top, otherwise below).
+ * <p>
+ * <span style="font-size:smaller">*) This behavior will change in
+ * future patches (henrik paul 2.7.2014)</span>
+ */
+ private void doScrollAreaChecks(int pageY) {
+ /*
+ * The first run makes sure that neither scroll position is
+ * underneath the finger, but offset to either direction from
+ * underneath the pointer.
+ */
+ if (topBound == -1) {
+ topBound = Math.min(finalTopBound, pageY);
+ bottomBound = Math.max(finalBottomBound, pageY);
+ }
+
+ /*
+ * Subsequent runs make sure that the scroll area grows (but doesn't
+ * shrink) with the finger, but no further than the final bound.
+ */
+ else {
+ int oldTopBound = topBound;
+ if (topBound < finalTopBound) {
+ topBound = Math.max(topBound,
+ Math.min(finalTopBound, pageY));
+ }
+
+ int oldBottomBound = bottomBound;
+ if (bottomBound > finalBottomBound) {
+ bottomBound = Math.min(bottomBound,
+ Math.max(finalBottomBound, pageY));
+ }
+
+ final boolean topDidNotMove = oldTopBound == topBound;
+ final boolean bottomDidNotMove = oldBottomBound == bottomBound;
+ final boolean wasVerticalMovement = pageY != this.pageY;
+ scrollAreaShouldRebound = (topDidNotMove && bottomDidNotMove && wasVerticalMovement);
+ }
+ }
+ }
+
+ /**
+ * This class makes sure that pointer movemenets are registered and
+ * delegated to the autoscroller so that it can:
+ * <ul>
+ * <li>modify the speed in which we autoscroll.
+ * <li>"paint" a new row with the selection.
+ * </ul>
+ * Essentially, when a pointer is pressed on the selection column, a native
+ * preview handler is registered (so that selection gestures can happen
+ * outside of the selection column). The handler itself makes sure that it's
+ * detached when the pointer is "lifted".
+ */
+ private class AutoScrollHandler {
+ private AutoScrollerAndSelector autoScroller;
+
+ /** The registration info for {@link #scrollPreviewHandler} */
+ private HandlerRegistration handlerRegistration;
+
+ private final NativePreviewHandler scrollPreviewHandler = new NativePreviewHandler() {
+ @Override
+ public void onPreviewNativeEvent(final NativePreviewEvent event) {
+ if (autoScroller == null) {
+ stop();
+ return;
+ }
+
+ final NativeEvent nativeEvent = event.getNativeEvent();
+ int pageY = 0;
+ int pageX = 0;
+ switch (event.getTypeInt()) {
+ case Event.ONMOUSEMOVE:
+ case Event.ONTOUCHMOVE:
+ pageY = Util.getTouchOrMouseClientY(nativeEvent);
+ pageX = Util.getTouchOrMouseClientX(nativeEvent);
+ autoScroller.updatePointerCoords(pageX, pageY);
+ break;
+ case Event.ONMOUSEUP:
+ case Event.ONTOUCHEND:
+ case Event.ONTOUCHCANCEL:
+ stop();
+ break;
+ }
+ }
+ };
+
+ /**
+ * The top bound, as calculated from the {@link Event#getClientY()
+ * client} coordinates.
+ */
+ private int topBound = -1;
+
+ /**
+ * The bottom bound, as calculated from the {@link Event#getClientY()
+ * client} coordinates.
+ */
+ private int bottomBound = -1;
+
+ /** The size of the autoscroll acceleration area. */
+ private int gradientArea;
+
+ public void start(int logicalRowIndex) {
+
+ SelectionModel<T> model = grid.getSelectionModel();
+ if (model instanceof Batched) {
+ Batched<?> batchedModel = (Batched<?>) model;
+ batchedModel.startBatchSelect();
+ }
+
+ /*
+ * bounds are updated whenever the autoscroll cycle starts, to make
+ * sure that the widget hasn't changed in size, moved around, or
+ * whatnot.
+ */
+ updateScrollBounds();
+
+ assert handlerRegistration == null : "handlerRegistration was not null";
+ assert autoScroller == null : "autoScroller was not null";
+ handlerRegistration = Event
+ .addNativePreviewHandler(scrollPreviewHandler);
+
+ autoScroller = new AutoScrollerAndSelector(topBound, bottomBound,
+ gradientArea, !isSelected(logicalRowIndex));
+ autoScroller.start(logicalRowIndex);
+ }
+
+ private void updateScrollBounds() {
+ final Element root = Element.as(grid.getElement());
+ final Element tableWrapper = Element.as(root.getChild(2));
+ final TableElement table = TableElement.as(tableWrapper
+ .getFirstChildElement());
+ final Element thead = table.getTHead();
+ final Element tfoot = table.getTFoot();
+
+ /*
+ * GWT _does_ have an "Element.getAbsoluteTop()" that takes both the
+ * client top and scroll compensation into account, but they're
+ * calculated wrong for our purposes, so this does something
+ * similar, but only suitable for us.
+ *
+ * Also, this should be a bit faster, since the scroll compensation
+ * is calculated only once and used in two places.
+ */
+
+ final int topBorder = getClientTop(root) + thead.getOffsetHeight();
+ final int bottomBorder = getClientTop(tfoot);
+
+ final int scrollCompensation = getScrollCompensation();
+ topBound = scrollCompensation + topBorder + SCROLL_AREA_GRADIENT_PX;
+ bottomBound = scrollCompensation + bottomBorder
+ - SCROLL_AREA_GRADIENT_PX;
+ gradientArea = SCROLL_AREA_GRADIENT_PX;
+
+ // modify bounds if they're too tightly packed
+ if (bottomBound - topBound < MIN_NO_AUTOSCROLL_AREA_PX) {
+ int adjustment = MIN_NO_AUTOSCROLL_AREA_PX
+ - (bottomBound - topBound);
+ topBound -= adjustment / 2;
+ bottomBound += adjustment / 2;
+ gradientArea -= adjustment / 2;
+ }
+ }
+
+ /** Get the "top" of an element in relation to "client" coordinates. */
+ private int getClientTop(final Element e) {
+ Element cursor = e;
+ int top = 0;
+ while (cursor != null) {
+ top += cursor.getOffsetTop();
+ cursor = cursor.getOffsetParent();
+ }
+ return top;
+ }
+
+ private int getScrollCompensation() {
+ Element cursor = grid.getElement();
+ int scroll = 0;
+ while (cursor != null) {
+ scroll -= cursor.getScrollTop();
+ cursor = cursor.getParentElement();
+ }
+
+ return scroll;
+ }
+
+ public void stop() {
+ if (handlerRegistration != null) {
+ handlerRegistration.removeHandler();
+ handlerRegistration = null;
+ }
+
+ if (autoScroller != null) {
+ autoScroller.stop();
+ autoScroller = null;
+ }
+
+ SelectionModel<T> model = grid.getSelectionModel();
+ if (model instanceof Batched) {
+ Batched<?> batchedModel = (Batched<?>) model;
+ batchedModel.commitBatchSelect();
+ }
+
+ removeNativeHandler();
+ }
+ }
+
+ private class SpaceKeyDownSelectHandler implements BodyKeyDownHandler<T> {
+
+ private HandlerRegistration scrollHandler = null;
+ private boolean spaceDown = false;
+
+ @Override
+ public void onKeyDown(GridKeyDownEvent<T> event) {
+ if (event.getNativeKeyCode() != KeyCodes.KEY_SPACE || spaceDown) {
+ return;
+ }
+
+ spaceDown = true;
+ Cell active = event.getActiveCell();
+ final int rowIndex = active.getRow();
+
+ if (scrollHandler != null) {
+ scrollHandler.removeHandler();
+ scrollHandler = null;
+ }
+
+ scrollHandler = grid
+ .addDataAvailableHandler(new DataAvailableHandler() {
+
+ @Override
+ public void onDataAvailable(DataAvailableEvent event) {
+ if (event.getAvailableRows().contains(rowIndex)) {
+ setSelected(rowIndex, !isSelected(rowIndex));
+ scrollHandler.removeHandler();
+ scrollHandler = null;
+ }
+ }
+ });
+ grid.scrollToRow(rowIndex, ScrollDestination.ANY);
+ }
+
+ }
+
+ private static final String LOGICAL_ROW_PROPERTY_INT = "vEscalatorLogicalRow";
+
+ private final Grid<T> grid;
+ private HandlerRegistration nativePreviewHandlerRegistration;
+ private final SpaceKeyDownSelectHandler handler = new SpaceKeyDownSelectHandler();
+ private HandlerRegistration spaceDown;
+ private HandlerRegistration spaceUp;
+
+ private final AutoScrollHandler autoScrollHandler = new AutoScrollHandler();
+
+ public MultiSelectionRenderer(final Grid<T> grid) {
+ this.grid = grid;
+ spaceDown = grid.addKeyDownHandler(handler);
+ spaceUp = grid.addKeyUpHandler(new BodyKeyUpHandler<T>() {
+
+ @Override
+ public void onKeyUp(GridKeyUpEvent<T> event) {
+ if (event.getNativeKeyCode() == KeyCodes.KEY_SPACE) {
+ handler.spaceDown = false;
+ }
+ }
+ });
+ }
+
+ @Override
+ public void destroy() {
+ spaceDown.removeHandler();
+ spaceUp.removeHandler();
+ if (nativePreviewHandlerRegistration != null) {
+ removeNativeHandler();
+ }
+ }
+
+ @Override
+ public void render(final FlyweightCell cell, final Boolean data) {
+ /*
+ * FIXME: Once https://dev.vaadin.com/review/#/c/3670/ is merged
+ * (init/destroy), split this method. Also, remove all event preview
+ * handlers on detach, to avoid hanging events.
+ */
+
+ final InputElement checkbox = InputElement.as(DOM.createInputCheck());
+ checkbox.setChecked(data.booleanValue());
+ checkbox.setPropertyInt(LOGICAL_ROW_PROPERTY_INT, cell.getRow());
+ cell.getElement().removeAllChildren();
+ cell.getElement().appendChild(checkbox);
+ }
+
+ @Override
+ public Collection<String> getConsumedEvents() {
+ final HashSet<String> events = new HashSet<String>();
+
+ /*
+ * this column's first interest is only to attach a NativePreventHandler
+ * that does all the magic. These events are the beginning of that
+ * cycle.
+ */
+ events.add(BrowserEvents.MOUSEDOWN);
+ events.add(BrowserEvents.TOUCHSTART);
+
+ return events;
+ }
+
+ @Override
+ public boolean onBrowserEvent(final Cell cell, final NativeEvent event) {
+ if (BrowserEvents.TOUCHSTART.equals(event.getType())
+ || BrowserEvents.MOUSEDOWN.equals(event.getType())) {
+ injectNativeHandler();
+ int logicalRowIndex = getLogicalRowIndex(Element.as(event
+ .getEventTarget()));
+ autoScrollHandler.start(logicalRowIndex);
+ event.preventDefault();
+ event.stopPropagation();
+ return true;
+ } else {
+ throw new IllegalStateException("received unexpected event: "
+ + event.getType());
+ }
+ }
+
+ private void injectNativeHandler() {
+ removeNativeHandler();
+ nativePreviewHandlerRegistration = Event
+ .addNativePreviewHandler(new TouchEventHandler());
+ }
+
+ private void removeNativeHandler() {
+ if (nativePreviewHandlerRegistration != null) {
+ nativePreviewHandlerRegistration.removeHandler();
+ nativePreviewHandlerRegistration = null;
+ }
+ }
+
+ private int getLogicalRowIndex(final Element target) {
+ /*
+ * We can't simply go backwards until we find a <tr> first element,
+ * because of the table-in-table scenario. We need to, unfortunately, go
+ * up from our known root.
+ */
+ final Element tbody = getTbodyElement();
+ Element tr = tbody.getFirstChildElement();
+ while (tr != null) {
+ if (tr.isOrHasChild(target)) {
+ final Element td = tr.getFirstChildElement();
+ assert td != null : "Cell has disappeared";
+
+ final Element checkbox = td.getFirstChildElement();
+ assert checkbox != null : "Checkbox has disappeared";
+
+ return checkbox.getPropertyInt(LOGICAL_ROW_PROPERTY_INT);
+ }
+ tr = tr.getNextSiblingElement();
+ }
+ return -1;
+ }
+
+ private Element getTbodyElement() {
+ final Element root = grid.getElement();
+ final Element tablewrapper = Element.as(root.getChild(2));
+ if (tablewrapper != null) {
+ final TableElement table = TableElement.as(tablewrapper
+ .getFirstChildElement());
+ return table.getTBodies().getItem(0);
+ } else {
+ return null;
+ }
+ }
+
+ protected boolean isSelected(final int logicalRow) {
+ return grid.isSelected(grid.getDataSource().getRow(logicalRow));
+ }
+
+ protected void setSelected(final int logicalRow, final boolean select) {
+ T row = grid.getDataSource().getRow(logicalRow);
+ if (select) {
+ grid.select(row);
+ } else {
+ grid.deselect(row);
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeEvent.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeEvent.java
new file mode 100644
index 0000000000..4bebaf0fbb
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeEvent.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.client.ui.grid.Grid;
+
+/**
+ * Event object describing a change in Grid row selection state.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@SuppressWarnings("rawtypes")
+public class SelectionChangeEvent<T> extends GwtEvent<SelectionChangeHandler> {
+
+ private static final Type<SelectionChangeHandler> eventType = new Type<SelectionChangeHandler>();
+
+ private final Grid<T> grid;
+ private final List<T> added;
+ private final List<T> removed;
+ private final boolean batched;
+
+ /**
+ * Creates an event with a single added or removed row.
+ *
+ * @param grid
+ * grid reference, used for getSource
+ * @param added
+ * the added row, or <code>null</code> if a row was not added
+ * @param removed
+ * the removed row, or <code>null</code> if a row was not removed
+ * @param batched
+ * whether or not this selection change event is triggered during
+ * a batched selection/deselection action
+ * @see SelectionModel.Multi.Batched
+ */
+ public SelectionChangeEvent(Grid<T> grid, T added, T removed,
+ boolean batched) {
+ this.grid = grid;
+ this.batched = batched;
+
+ if (added != null) {
+ this.added = Collections.singletonList(added);
+ } else {
+ this.added = Collections.emptyList();
+ }
+
+ if (removed != null) {
+ this.removed = Collections.singletonList(removed);
+ } else {
+ this.removed = Collections.emptyList();
+ }
+ }
+
+ /**
+ * Creates an event where several rows have been added or removed.
+ *
+ * @param grid
+ * Grid reference, used for getSource
+ * @param added
+ * a collection of added rows, or <code>null</code> if no rows
+ * were added
+ * @param removed
+ * a collection of removed rows, or <code>null</code> if no rows
+ * were removed
+ * @param batched
+ * whether or not this selection change event is triggered during
+ * a batched selection/deselection action
+ * @see SelectionModel.Multi.Batched
+ */
+ public SelectionChangeEvent(Grid<T> grid, Collection<T> added,
+ Collection<T> removed, boolean batched) {
+ this.grid = grid;
+ this.batched = batched;
+
+ if (added != null) {
+ this.added = new ArrayList<T>(added);
+ } else {
+ this.added = Collections.emptyList();
+ }
+
+ if (removed != null) {
+ this.removed = new ArrayList<T>(removed);
+ } else {
+ this.removed = Collections.emptyList();
+ }
+ }
+
+ /**
+ * Get a reference to the Grid object that fired this event.
+ *
+ * @return a grid reference
+ */
+ @Override
+ public Grid<T> getSource() {
+ return grid;
+ }
+
+ /**
+ * Get all rows added to the selection since the last
+ * {@link SelectionChangeEvent}.
+ *
+ * @return a collection of added rows. Empty collection if no rows were
+ * added.
+ */
+ public Collection<T> getAdded() {
+ return Collections.unmodifiableCollection(added);
+ }
+
+ /**
+ * Get all rows removed from the selection since the last
+ * {@link SelectionChangeEvent}.
+ *
+ * @return a collection of removed rows. Empty collection if no rows were
+ * removed.
+ */
+ public Collection<T> getRemoved() {
+ return Collections.unmodifiableCollection(removed);
+ }
+
+ /**
+ * Gets a type identifier for this event.
+ *
+ * @return a {@link Type} identifier.
+ */
+ public static Type<SelectionChangeHandler> getType() {
+ return eventType;
+ }
+
+ @Override
+ public Type<SelectionChangeHandler> getAssociatedType() {
+ return eventType;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void dispatch(SelectionChangeHandler handler) {
+ handler.onSelectionChange(this);
+ }
+
+ /**
+ * Checks if this selection change event is fired during a batched
+ * selection/deselection operation.
+ *
+ * @return <code>true</code> iff this event is fired during a batched
+ * selection/deselection operation
+ */
+ public boolean isBatchedSelection() {
+ return batched;
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeHandler.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeHandler.java
new file mode 100644
index 0000000000..a469f5af1f
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionChangeHandler.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for {@link SelectionChangeEvent}s.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @param <T>
+ * The row data type
+ */
+public interface SelectionChangeHandler<T> extends EventHandler {
+
+ /**
+ * Called when a selection model's selection state is changed.
+ *
+ * @param event
+ * a selection change event, containing info about rows that have
+ * been added to or removed from the selection.
+ */
+ public void onSelectionChange(SelectionChangeEvent<T> event);
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java
new file mode 100644
index 0000000000..55336ce3da
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.Collection;
+
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * Common interface for all selection models.
+ * <p>
+ * Selection models perform tracking of selected rows in the Grid, as well as
+ * dispatching events when the selection state changes.
+ *
+ * @author Vaadin Ltd
+ * @since
+ * @param <T>
+ * Grid's row type
+ */
+public interface SelectionModel<T> {
+
+ /**
+ * Return true if the provided row is considered selected under the
+ * implementing selection model.
+ *
+ * @param row
+ * row object instance
+ * @return <code>true</code>, if the row given as argument is considered
+ * selected.
+ */
+ public boolean isSelected(T row);
+
+ /**
+ * Return the {@link Renderer} responsible for rendering the selection
+ * column.
+ *
+ * @return a renderer instance. If null is returned, a selection column will
+ * not be drawn.
+ */
+ public Renderer<Boolean> getSelectionColumnRenderer();
+
+ /**
+ * Tells this SelectionModel which Grid it belongs to.
+ * <p>
+ * Implementations are free to have this be a no-op. This method is called
+ * internally by Grid.
+ *
+ * @param grid
+ * a {@link Grid} instance
+ */
+ public void setGrid(Grid<T> grid);
+
+ /**
+ * Resets the SelectionModel to the initial state.
+ * <p>
+ * This method can be called internally, for example, when the attached
+ * Grid's data source changes.
+ */
+ public void reset();
+
+ /**
+ * Returns a Collection containing all selected rows.
+ *
+ * @return a non-null collection.
+ */
+ public Collection<T> getSelectedRows();
+
+ /**
+ * Selection model that allows a maximum of one row to be selected at any
+ * one time.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface Single<T> extends SelectionModel<T> {
+
+ /**
+ * Selects a row.
+ *
+ * @param row
+ * a {@link Grid} row object
+ * @return true, if this row as not previously selected.
+ */
+ public boolean select(T row);
+
+ /**
+ * Deselects a row.
+ * <p>
+ * This is a no-op unless {@link row} is the currently selected row.
+ *
+ * @param row
+ * a {@link Grid} row object
+ * @return true, if the currently selected row was deselected.
+ */
+ public boolean deselect(T row);
+
+ /**
+ * Returns the currently selected row.
+ *
+ * @return a {@link Grid} row object or null, if nothing is selected.
+ */
+ public T getSelectedRow();
+
+ }
+
+ /**
+ * Selection model that allows for several rows to be selected at once.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface Multi<T> extends SelectionModel<T> {
+
+ /**
+ * A multi selection model that can send selections and deselections in
+ * a batch, instead of committing them one-by-one.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface Batched<T> extends Multi<T> {
+ /**
+ * Starts a batch selection.
+ * <p>
+ * Any commands to any select or deselect method will be batched
+ * into one, and a final selection event will be fired when
+ * {@link #commitBatchSelect()} is called.
+ * <p>
+ * <em>Note:</em> {@link SelectionChangeEvent SelectionChangeEvents}
+ * will still be fired for each selection/deselection. You should
+ * check whether the event is a part of a batch or not with
+ * {@link SelectionChangeEvent#isBatchedSelection()}.
+ */
+ public void startBatchSelect();
+
+ /**
+ * Commits and ends a batch selection.
+ * <p>
+ * Any and all selections and deselections since the last invocation
+ * of {@link #startBatchSelect()} will be fired at once as one
+ * collated {@link SelectionChangeEvent}.
+ */
+ public void commitBatchSelect();
+
+ /**
+ * Checks whether or not a batch has been started.
+ *
+ * @return <code>true</code> iff a batch has been started
+ */
+ public boolean isBeingBatchSelected();
+
+ /**
+ * Gets all the rows that would become selected in this batch.
+ *
+ * @return a collection of the rows that would become selected
+ */
+ public Collection<T> getSelectedRowsBatch();
+
+ /**
+ * Gets all the rows that would become deselected in this batch.
+ *
+ * @return a collection of the rows that would become deselected
+ */
+ public Collection<T> getDeselectedRowsBatch();
+ }
+
+ /**
+ * Selects one or more rows.
+ *
+ * @param rows
+ * {@link Grid} row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean select(T... rows);
+
+ /**
+ * Deselects one or more rows.
+ *
+ * @param rows
+ * Grid row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean deselect(T... rows);
+
+ /**
+ * De-selects all rows.
+ *
+ * @return true, if any row was previously selected.
+ */
+ public boolean deselectAll();
+
+ /**
+ * Select all rows in a {@link Collection}.
+ *
+ * @param rows
+ * a collection of Grid row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean select(Collection<T> rows);
+
+ /**
+ * Deselect all rows in a {@link Collection}.
+ *
+ * @param rows
+ * a collection of Grid row objects
+ * @return true, if the set of selected rows was changed.
+ */
+ public boolean deselect(Collection<T> rows);
+
+ }
+
+ /**
+ * Interface for a selection model that does not allow anything to be
+ * selected.
+ *
+ * @param <T>
+ * type parameter corresponding with Grid row type
+ */
+ public interface None<T> extends SelectionModel<T> {
+
+ }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java
new file mode 100644
index 0000000000..b6ecc945e2
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * Multi-row selection model.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+public class SelectionModelMulti<T> extends AbstractRowHandleSelectionModel<T>
+ implements SelectionModel.Multi.Batched<T> {
+
+ private final LinkedHashSet<RowHandle<T>> selectedRows;
+ private Renderer<Boolean> renderer;
+ private Grid<T> grid;
+
+ private boolean batchStarted = false;
+ private final LinkedHashSet<RowHandle<T>> selectionBatch = new LinkedHashSet<RowHandle<T>>();
+ private final LinkedHashSet<RowHandle<T>> deselectionBatch = new LinkedHashSet<RowHandle<T>>();
+
+ public SelectionModelMulti() {
+ grid = null;
+ renderer = null;
+ selectedRows = new LinkedHashSet<RowHandle<T>>();
+ }
+
+ @Override
+ public boolean isSelected(T row) {
+ return isSelectedByHandle(grid.getDataSource().getHandle(row));
+ }
+
+ @Override
+ public Renderer<Boolean> getSelectionColumnRenderer() {
+ return renderer;
+ }
+
+ @Override
+ public void setGrid(Grid<T> grid) {
+ if (grid == null) {
+ throw new IllegalArgumentException("Grid cannot be null");
+ }
+
+ if (this.grid == null) {
+ this.grid = grid;
+ } else {
+ throw new IllegalStateException(
+ "Grid reference cannot be reassigned");
+ }
+
+ this.renderer = new MultiSelectionRenderer<T>(grid);
+ }
+
+ @Override
+ public boolean select(T... rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+ return select(Arrays.asList(rows));
+ }
+
+ @Override
+ public boolean deselect(T... rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+ return deselect(Arrays.asList(rows));
+ }
+
+ @Override
+ public boolean deselectAll() {
+ if (selectedRows.size() > 0) {
+
+ @SuppressWarnings("unchecked")
+ final LinkedHashSet<RowHandle<T>> selectedRowsClone = (LinkedHashSet<RowHandle<T>>) selectedRows
+ .clone();
+ SelectionChangeEvent<T> event = new SelectionChangeEvent<T>(grid,
+ null, getSelectedRows(), isBeingBatchSelected());
+ selectedRows.clear();
+
+ if (isBeingBatchSelected()) {
+ selectionBatch.clear();
+ deselectionBatch.clear();
+ deselectionBatch.addAll(selectedRowsClone);
+ }
+
+ grid.fireEvent(event);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean select(Collection<T> rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+
+ Set<T> added = new LinkedHashSet<T>();
+
+ for (T row : rows) {
+ RowHandle<T> handle = grid.getDataSource().getHandle(row);
+ if (selectByHandle(handle)) {
+ added.add(row);
+ }
+ }
+
+ if (added.size() > 0) {
+ grid.fireEvent(new SelectionChangeEvent<T>(grid, added, null,
+ isBeingBatchSelected()));
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean deselect(Collection<T> rows) {
+ if (rows == null) {
+ throw new IllegalArgumentException("Rows cannot be null");
+ }
+
+ Set<T> removed = new LinkedHashSet<T>();
+
+ for (T row : rows) {
+ RowHandle<T> handle = grid.getDataSource().getHandle(row);
+ if (deselectByHandle(handle)) {
+ removed.add(row);
+ }
+ }
+
+ if (removed.size() > 0) {
+ grid.fireEvent(new SelectionChangeEvent<T>(grid, null, removed,
+ isBeingBatchSelected()));
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean isSelectedByHandle(RowHandle<T> handle) {
+ return selectedRows.contains(handle);
+ }
+
+ @Override
+ protected boolean selectByHandle(RowHandle<T> handle) {
+ if (selectedRows.add(handle)) {
+ handle.pin();
+
+ if (isBeingBatchSelected()) {
+ deselectionBatch.remove(handle);
+ selectionBatch.add(handle);
+ }
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ protected boolean deselectByHandle(RowHandle<T> handle) {
+ if (selectedRows.remove(handle)) {
+ if (!isBeingBatchSelected()) {
+ handle.unpin();
+ } else {
+ selectionBatch.remove(handle);
+ deselectionBatch.add(handle);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public Collection<T> getSelectedRows() {
+ Set<T> selected = new LinkedHashSet<T>();
+ for (RowHandle<T> handle : selectedRows) {
+ selected.add(handle.getRow());
+ }
+ return Collections.unmodifiableSet(selected);
+ }
+
+ @Override
+ public void reset() {
+ deselectAll();
+ }
+
+ @Override
+ public void startBatchSelect() {
+ assert !isBeingBatchSelected() : "Batch has already been started";
+ batchStarted = true;
+ }
+
+ @Override
+ public void commitBatchSelect() {
+ assert isBeingBatchSelected() : "Batch was never started";
+ if (!isBeingBatchSelected()) {
+ return;
+ }
+
+ batchStarted = false;
+
+ final Collection<T> added = getSelectedRowsBatch();
+ selectionBatch.clear();
+
+ final Collection<T> removed = getDeselectedRowsBatch();
+ deselectionBatch.clear();
+
+ grid.fireEvent(new SelectionChangeEvent<T>(grid, added, removed,
+ isBeingBatchSelected()));
+ }
+
+ @Override
+ public boolean isBeingBatchSelected() {
+ return batchStarted;
+ }
+
+ @Override
+ public Collection<T> getSelectedRowsBatch() {
+ return rowHandlesToRows(selectionBatch);
+ }
+
+ @Override
+ public Collection<T> getDeselectedRowsBatch() {
+ return rowHandlesToRows(deselectionBatch);
+ }
+
+ private ArrayList<T> rowHandlesToRows(Collection<RowHandle<T>> rowHandles) {
+ ArrayList<T> rows = new ArrayList<T>(rowHandles.size());
+ for (RowHandle<T> handle : rowHandles) {
+ rows.add(handle.getRow());
+ }
+ return rows;
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java
new file mode 100644
index 0000000000..8192237da0
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * No-row selection model.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+public class SelectionModelNone<T> extends AbstractRowHandleSelectionModel<T>
+ implements SelectionModel.None<T> {
+
+ @Override
+ public boolean isSelected(T row) {
+ return false;
+ }
+
+ @Override
+ public Renderer<Boolean> getSelectionColumnRenderer() {
+ return null;
+ }
+
+ @Override
+ public void setGrid(Grid<T> grid) {
+ // noop
+ }
+
+ @Override
+ public void reset() {
+ // noop
+ }
+
+ @Override
+ public Collection<T> getSelectedRows() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ protected boolean selectByHandle(RowHandle<T> handle)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("This selection model "
+ + "does not support selection");
+ }
+
+ @Override
+ protected boolean deselectByHandle(RowHandle<T> handle)
+ throws UnsupportedOperationException {
+ throw new UnsupportedOperationException("This selection model "
+ + "does not support deselection");
+ }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java
new file mode 100644
index 0000000000..d63b371c4d
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.client.data.DataSource.RowHandle;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.Renderer;
+
+/**
+ * Single-row selection model.
+ *
+ * @author Vaadin Ltd
+ * @since
+ */
+public class SelectionModelSingle<T> extends AbstractRowHandleSelectionModel<T>
+ implements SelectionModel.Single<T> {
+
+ private Grid<T> grid;
+ private RowHandle<T> selectedRow;
+ private Renderer<Boolean> renderer;
+
+ @Override
+ public boolean isSelected(T row) {
+ return selectedRow != null
+ && selectedRow.equals(grid.getDataSource().getHandle(row));
+ }
+
+ @Override
+ public Renderer<Boolean> getSelectionColumnRenderer() {
+ return renderer;
+ }
+
+ @Override
+ public void setGrid(Grid<T> grid) {
+ if (grid == null) {
+ throw new IllegalArgumentException("Grid cannot be null");
+ }
+
+ if (this.grid == null) {
+ this.grid = grid;
+ } else {
+ throw new IllegalStateException(
+ "Grid reference cannot be reassigned");
+ }
+ renderer = new MultiSelectionRenderer<T>(grid);
+ }
+
+ @Override
+ public boolean select(T row) {
+
+ if (row == null) {
+ throw new IllegalArgumentException("Row cannot be null");
+ }
+
+ T removed = getSelectedRow();
+ if (selectByHandle(grid.getDataSource().getHandle(row))) {
+ grid.fireEvent(new SelectionChangeEvent<T>(grid, row, removed,
+ false));
+
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean deselect(T row) {
+
+ if (row == null) {
+ throw new IllegalArgumentException("Row cannot be null");
+ }
+
+ if (isSelected(row)) {
+ deselectByHandle(selectedRow);
+ grid.fireEvent(new SelectionChangeEvent<T>(grid, null, row, false));
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public T getSelectedRow() {
+ return (selectedRow != null ? selectedRow.getRow() : null);
+ }
+
+ @Override
+ public void reset() {
+ if (selectedRow != null) {
+ deselect(getSelectedRow());
+ }
+ }
+
+ @Override
+ public Collection<T> getSelectedRows() {
+ if (getSelectedRow() != null) {
+ return Collections.singleton(getSelectedRow());
+ }
+ return Collections.emptySet();
+ }
+
+ @Override
+ protected boolean selectByHandle(RowHandle<T> handle) {
+ if (handle != null && !handle.equals(selectedRow)) {
+ deselectByHandle(selectedRow);
+ selectedRow = handle;
+ selectedRow.pin();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ protected boolean deselectByHandle(RowHandle<T> handle) {
+ if (handle != null && handle.equals(selectedRow)) {
+ selectedRow.unpin();
+ selectedRow = null;
+ return true;
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/sort/Sort.java b/client/src/com/vaadin/client/ui/grid/sort/Sort.java
new file mode 100644
index 0000000000..00658c4375
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/sort/Sort.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2000-2014 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.sort;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.client.ui.grid.GridColumn;
+import com.vaadin.shared.ui.grid.SortDirection;
+
+/**
+ * Fluid Sort descriptor object.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @param T
+ * grid data type
+ */
+public class Sort {
+
+ private final Sort previous;
+ private final SortOrder order;
+ private final int count;
+
+ /**
+ * Basic constructor, used by the {@link #by(GridColumn)} and
+ * {@link #by(GridColumn, SortDirection)} methods.
+ *
+ * @param column
+ * a grid column
+ * @param direction
+ * a sort direction
+ */
+ private Sort(GridColumn<?, ?> column, SortDirection direction) {
+ previous = null;
+ count = 1;
+ order = new SortOrder(column, direction);
+ }
+
+ /**
+ * Extension constructor. Performs object equality checks on all previous
+ * Sort objects in the chain to make sure that the column being passed in
+ * isn't already used earlier (which would indicate a bug). If the column
+ * has been used before, this constructor throws an
+ * {@link IllegalStateException}.
+ *
+ * @param previous
+ * the sort instance that the new sort instance is to extend
+ * @param column
+ * a (previously unused) grid column reference
+ * @param direction
+ * a sort direction
+ */
+ private Sort(Sort previous, GridColumn<?, ?> column, SortDirection direction) {
+ this.previous = previous;
+ count = previous.count + 1;
+ order = new SortOrder(column, direction);
+
+ Sort s = previous;
+ while (s != null) {
+ if (s.order.getColumn() == column) {
+ throw new IllegalStateException(
+ "Can not sort along the same column twice");
+ }
+ s = s.previous;
+ }
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column in ascending
+ * order.
+ *
+ * @param column
+ * a grid column object reference
+ * @return a sort instance, typed to the grid data type
+ */
+ public static Sort by(GridColumn<?, ?> column) {
+ return by(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column.
+ *
+ * @param column
+ * a grid column object reference
+ * @param direction
+ * indicator of sort direction - either ascending or descending
+ * @return a sort instance, typed to the grid data type
+ */
+ public static Sort by(GridColumn<?, ?> column, SortDirection direction) {
+ return new Sort(column, direction);
+ }
+
+ /**
+ * Continue building a Sort order. The provided column is sorted in
+ * ascending order if the previously added columns have been evaluated as
+ * equals.
+ *
+ * @param column
+ * a grid column object reference
+ * @return a sort instance, typed to the grid data type
+ */
+ public Sort then(GridColumn<?, ?> column) {
+ return then(column, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Continue building a Sort order. The provided column is sorted in
+ * specified order if the previously added columns have been evaluated as
+ * equals.
+ *
+ * @param column
+ * a grid column object reference
+ * @param direction
+ * indicator of sort direction - either ascending or descending
+ * @return a sort instance, typed to the grid data type
+ */
+ public Sort then(GridColumn<?, ?> column, SortDirection direction) {
+ return new Sort(this, column, direction);
+ }
+
+ /**
+ * Build a sort order list. This method is called internally by Grid when
+ * calling {@link com.vaadin.client.ui.grid.Grid#sort(Sort)}, but can also
+ * be called manually to create a SortOrder list, which can also be provided
+ * directly to Grid.
+ *
+ * @return a sort order list.
+ */
+ public List<SortOrder> build() {
+
+ List<SortOrder> order = new ArrayList<SortOrder>(count);
+
+ Sort s = this;
+ for (int i = count - 1; i >= 0; --i) {
+ order.add(0, s.order);
+ s = s.previous;
+ }
+
+ return order;
+ }
+}
diff --git a/client/src/com/vaadin/client/ui/grid/sort/SortEvent.java b/client/src/com/vaadin/client/ui/grid/sort/SortEvent.java
new file mode 100644
index 0000000000..edbd84c4a5
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/sort/SortEvent.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2000-2014 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.sort;
+
+import java.util.List;
+
+import com.google.gwt.event.shared.GwtEvent;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.shared.ui.grid.SortEventOriginator;
+
+/**
+ * A sort event, fired by the Grid when it needs its data source to provide data
+ * sorted in a specific manner.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class SortEvent<T> extends GwtEvent<SortEventHandler<?>> {
+
+ private static final Type<SortEventHandler<?>> TYPE = new Type<SortEventHandler<?>>();
+
+ private final Grid<T> grid;
+ private final List<SortOrder> order;
+ private final SortEventOriginator originator;
+
+ /**
+ * Creates a new Sort Event. All provided parameters are final, and passed
+ * on as-is.
+ *
+ * @param grid
+ * a grid reference
+ * @param order
+ * an array dictating the desired sort order of the data source
+ * @param originator
+ * a value indicating where this event originated from
+ */
+ public SortEvent(Grid<T> grid, List<SortOrder> order,
+ SortEventOriginator originator) {
+ this.grid = grid;
+ this.order = order;
+ this.originator = originator;
+ }
+
+ @Override
+ public Type<SortEventHandler<?>> getAssociatedType() {
+ return TYPE;
+ }
+
+ /**
+ * Static access to the GWT event type identifier associated with this Event
+ * class
+ *
+ * @return a type object, uniquely describing this event type.
+ */
+ public static Type<SortEventHandler<?>> getType() {
+ return TYPE;
+ }
+
+ /**
+ * Get access to the Grid that fired this event
+ *
+ * @return the grid instance
+ */
+ @Override
+ public Grid<T> getSource() {
+ return grid;
+ }
+
+ /**
+ * Get access to the Grid that fired this event
+ *
+ * @return the grid instance
+ */
+ public Grid<T> getGrid() {
+ return grid;
+ }
+
+ /**
+ * Access the data source of the Grid that fired this event
+ *
+ * @return a data source instance
+ */
+ public DataSource<T> getDataSource() {
+ return grid.getDataSource();
+ }
+
+ /**
+ * Get the sort ordering that is to be applied to the Grid
+ *
+ * @return a list of sort order objects
+ */
+ public List<SortOrder> getOrder() {
+ return order;
+ }
+
+ /**
+ * Gets a value describing the originator of this event, i.e. what actions
+ * resulted in this event being fired.
+ *
+ * @return a sort event originator value
+ *
+ * @see SortEventOriginator
+ */
+ public SortEventOriginator getOriginator() {
+ return originator;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ protected void dispatch(SortEventHandler<?> handler) {
+ ((SortEventHandler<T>) handler).sort(this);
+ }
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/sort/SortEventHandler.java b/client/src/com/vaadin/client/ui/grid/sort/SortEventHandler.java
new file mode 100644
index 0000000000..57e7fc2ead
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/sort/SortEventHandler.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2014 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.sort;
+
+import com.google.gwt.event.shared.EventHandler;
+
+/**
+ * Handler for a Grid sort event, called when the Grid needs its data source to
+ * provide data sorted in a specific manner.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface SortEventHandler<T> extends EventHandler {
+
+ /**
+ * Handle sorting of the Grid. This method is called when a re-sorting of
+ * the Grid's data is requested.
+ *
+ * @param event
+ * the sort event
+ */
+ public void sort(SortEvent<T> event);
+
+}
diff --git a/client/src/com/vaadin/client/ui/grid/sort/SortOrder.java b/client/src/com/vaadin/client/ui/grid/sort/SortOrder.java
new file mode 100644
index 0000000000..682beda793
--- /dev/null
+++ b/client/src/com/vaadin/client/ui/grid/sort/SortOrder.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2000-2014 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.sort;
+
+import com.vaadin.client.ui.grid.GridColumn;
+import com.vaadin.shared.ui.grid.SortDirection;
+
+/**
+ * Sort order descriptor. Contains column and direction references.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @param T
+ * grid data type
+ */
+public class SortOrder {
+
+ private final GridColumn<?, ?> column;
+ private final SortDirection direction;
+
+ /**
+ * Create a sort order descriptor.
+ *
+ * @param column
+ * a grid column descriptor object
+ * @param direction
+ * a sorting direction value (ascending or descending)
+ */
+ public SortOrder(GridColumn<?, ?> column, SortDirection direction) {
+ if (column == null) {
+ throw new IllegalArgumentException(
+ "Grid column reference can not be null!");
+ }
+ if (direction == null) {
+ throw new IllegalArgumentException(
+ "Direction value can not be null!");
+ }
+ this.column = column;
+ this.direction = direction;
+ }
+
+ /**
+ * Returns the {@link GridColumn} reference given in the constructor.
+ *
+ * @return a grid column reference
+ */
+ public GridColumn<?, ?> getColumn() {
+ return column;
+ }
+
+ /**
+ * Returns the {@link SortDirection} value given in the constructor.
+ *
+ * @return a sort direction value
+ */
+ public SortDirection getDirection() {
+ return direction;
+ }
+}
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..55a2b56ee2
--- /dev/null
+++ b/client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java
@@ -0,0 +1,192 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.Comparator;
+
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.ui.grid.datasources.ListDataSource;
+
+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();
+ }
+
+ @Test
+ public void sortColumn() {
+ ListDataSource<Integer> ds = new ListDataSource<Integer>(3, 4, 2, 3, 1);
+
+ // TODO Should be simplified to sort(). No point in providing these
+ // trivial comparators.
+ ds.sort(new Comparator<Integer>() {
+ @Override
+ public int compare(Integer o1, Integer o2) {
+ return o1.compareTo(o2);
+ }
+ });
+
+ assertTrue(Arrays.equals(ds.asList().toArray(), new Integer[] { 1, 2,
+ 3, 3, 4 }));
+ }
+
+}
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/Container.java b/server/src/com/vaadin/data/Container.java
index 8e99bac541..c58d37d5fe 100644
--- a/server/src/com/vaadin/data/Container.java
+++ b/server/src/com/vaadin/data/Container.java
@@ -582,6 +582,60 @@ public interface Container extends Serializable {
public Item addItemAt(int index, Object newItemId)
throws UnsupportedOperationException;
+ /**
+ * An <code>Event</code> object specifying information about the added
+ * items.
+ */
+ public interface ItemAddEvent extends ItemSetChangeEvent {
+
+ /**
+ * Gets the item id of the first added item.
+ *
+ * @return item id of the first added item
+ */
+ public Object getFirstItemId();
+
+ /**
+ * Gets the index of the first added item.
+ *
+ * @return index of the first added item
+ */
+ public int getFirstIndex();
+
+ /**
+ * Gets the number of the added items.
+ *
+ * @return the number of added items.
+ */
+ public int getAddedItemsCount();
+ }
+
+ /**
+ * An <code>Event</code> object specifying information about the removed
+ * items.
+ */
+ public interface ItemRemoveEvent extends ItemSetChangeEvent {
+ /**
+ * Gets the item id of the first removed item.
+ *
+ * @return item id of the first removed item
+ */
+ public Object getFirstItemId();
+
+ /**
+ * Gets the index of the first removed item.
+ *
+ * @return index of the first removed item
+ */
+ public int getFirstIndex();
+
+ /**
+ * Gets the number of the removed items.
+ *
+ * @return the number of removed items
+ */
+ public int getRemovedItemsCount();
+ }
}
/**
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java
new file mode 100644
index 0000000000..86fa11f62e
--- /dev/null
+++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java
@@ -0,0 +1,936 @@
+/*
+ * Copyright 2000-2014 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.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import com.google.gwt.thirdparty.guava.common.collect.BiMap;
+import com.google.gwt.thirdparty.guava.common.collect.HashBiMap;
+import com.vaadin.data.Container.Indexed;
+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.Property.ValueChangeEvent;
+import com.vaadin.data.Property.ValueChangeListener;
+import com.vaadin.data.Property.ValueChangeNotifier;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.server.AbstractExtension;
+import com.vaadin.server.ClientConnector;
+import com.vaadin.shared.data.DataProviderRpc;
+import com.vaadin.shared.data.DataProviderState;
+import com.vaadin.shared.data.DataRequestRpc;
+import com.vaadin.shared.ui.grid.GridState;
+import com.vaadin.shared.ui.grid.Range;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.GridColumn;
+import com.vaadin.ui.components.grid.Renderer;
+import com.vaadin.ui.components.grid.selection.SelectionChangeEvent;
+import com.vaadin.ui.components.grid.selection.SelectionChangeListener;
+
+/**
+ * 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
+ * @author Vaadin Ltd
+ */
+public class RpcDataProviderExtension extends AbstractExtension {
+
+ /**
+ * ItemId to Key to ItemId mapper.
+ * <p>
+ * This class is used when transmitting information about items in container
+ * related to Grid. It introduces a consistent way of mapping ItemIds and
+ * its container to a String that can be mapped back to ItemId.
+ * <p>
+ * <em>Technical note:</em> This class also keeps tabs on which indices are
+ * being shown/selected, and is able to clean up after itself once the
+ * itemId &lrarr; key mapping is not needed anymore. In other words, this
+ * doesn't leak memory.
+ */
+ public class DataProviderKeyMapper implements Serializable {
+ private final BiMap<Integer, Object> indexToItemId = HashBiMap.create();
+ private final BiMap<Object, String> itemIdToKey = HashBiMap.create();
+ private Set<Object> pinnedItemIds = new HashSet<Object>();
+ private Range activeRange = Range.withLength(0, 0);
+ private long rollingIndex = 0;
+
+ private DataProviderKeyMapper() {
+ // private implementation
+ }
+
+ void preActiveRowsChange(Range newActiveRange, int firstNewIndex,
+ List<?> itemIds) {
+ final Range[] removed = activeRange.partitionWith(newActiveRange);
+ final Range[] added = newActiveRange.partitionWith(activeRange);
+
+ removeActiveRows(removed[0]);
+ removeActiveRows(removed[2]);
+ addActiveRows(added[0], firstNewIndex, itemIds);
+ addActiveRows(added[2], firstNewIndex, itemIds);
+
+ activeRange = newActiveRange;
+ }
+
+ private void removeActiveRows(final Range deprecated) {
+ for (int i = deprecated.getStart(); i < deprecated.getEnd(); i++) {
+ final Integer ii = Integer.valueOf(i);
+ final Object itemId = indexToItemId.get(ii);
+
+ if (!isPinned(itemId)) {
+ itemIdToKey.remove(itemId);
+ indexToItemId.remove(ii);
+ }
+ }
+ }
+
+ private void addActiveRows(final Range added, int firstNewIndex,
+ List<?> newItemIds) {
+
+ for (int i = added.getStart(); i < added.getEnd(); i++) {
+
+ /*
+ * We might be in a situation we have an index <-> itemId entry
+ * already. This happens when something was selected, scrolled
+ * out of view and now we're scrolling it back into view. It's
+ * unnecessary to overwrite it in that case.
+ *
+ * Fun thought: considering branch prediction, it _might_ even
+ * be a bit faster to simply always run the code beyond this
+ * if-state. But it sounds too stupid (and most often too
+ * insignificant) to try out.
+ */
+ final Integer ii = Integer.valueOf(i);
+ if (indexToItemId.containsKey(ii)) {
+ continue;
+ }
+
+ /*
+ * We might be in a situation where we have an itemId <-> key
+ * entry already, but no index for it. This happens when
+ * something that is out of view is selected programmatically.
+ * In that case, we only want to add an index for that entry,
+ * and not overwrite the key.
+ */
+ final Object itemId = newItemIds.get(i - firstNewIndex);
+ if (!itemIdToKey.containsKey(itemId)) {
+ itemIdToKey.put(itemId, nextKey());
+ }
+ indexToItemId.put(ii, itemId);
+ }
+ }
+
+ private String nextKey() {
+ return String.valueOf(rollingIndex++);
+ }
+
+ String getKey(Object itemId) {
+ String key = itemIdToKey.get(itemId);
+ if (key == null) {
+ key = nextKey();
+ itemIdToKey.put(itemId, key);
+ }
+ return key;
+ }
+
+ /**
+ * Gets keys for a collection of item ids.
+ * <p>
+ * If the itemIds are currently cached, the existing keys will be used.
+ * Otherwise new ones will be created.
+ *
+ * @param itemIds
+ * the item ids for which to get keys
+ * @return keys for the {@code itemIds}
+ */
+ public List<String> getKeys(Collection<Object> itemIds) {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds can't be null");
+ }
+
+ ArrayList<String> keys = new ArrayList<String>(itemIds.size());
+ for (Object itemId : itemIds) {
+ keys.add(getKey(itemId));
+ }
+ return keys;
+ }
+
+ /**
+ * Gets the registered item id based on its key.
+ * <p>
+ * A key is used to identify a particular row on both a server and a
+ * client. This method can be used to get the item id for the row key
+ * that the client has sent.
+ *
+ * @param key
+ * the row key for which to retrieve an item id
+ * @return the item id corresponding to {@code key}
+ * @throws IllegalStateException
+ * if the key mapper does not have a record of {@code key} .
+ */
+ public Object getItemId(String key) throws IllegalStateException {
+ Object itemId = itemIdToKey.inverse().get(key);
+ if (itemId != null) {
+ return itemId;
+ } else {
+ throw new IllegalStateException("No item id for key " + key
+ + " found.");
+ }
+ }
+
+ /**
+ * Gets corresponding item ids for each of the keys in a collection.
+ *
+ * @param keys
+ * the keys for which to retrieve item ids
+ * @return a collection of item ids for the {@code keys}
+ * @throws IllegalStateException
+ * if one or more of keys don't have a corresponding item id
+ * in the cache
+ */
+ public Collection<Object> getItemIds(Collection<String> keys)
+ throws IllegalStateException {
+ if (keys == null) {
+ throw new IllegalArgumentException("keys may not be null");
+ }
+
+ ArrayList<Object> itemIds = new ArrayList<Object>(keys.size());
+ for (String key : keys) {
+ itemIds.add(getItemId(key));
+ }
+ return itemIds;
+ }
+
+ /**
+ * Pin an item id to be cached indefinitely.
+ * <p>
+ * Normally when an itemId is not an active row, it is discarded from
+ * the cache. Pinning an item id will make sure that it is kept in the
+ * cache.
+ * <p>
+ * In effect, while an item id is pinned, it always has the same key.
+ *
+ * @param itemId
+ * the item id to pin
+ * @throws IllegalStateException
+ * if {@code itemId} was already pinned
+ * @see #unpin(Object)
+ * @see #isPinned(Object)
+ * @see #getItemIds(Collection)
+ */
+ public void pin(Object itemId) throws IllegalStateException {
+ if (isPinned(itemId)) {
+ throw new IllegalStateException("Item id " + itemId
+ + " was pinned already");
+ }
+ pinnedItemIds.add(itemId);
+ }
+
+ /**
+ * Unpin an item id.
+ * <p>
+ * This cancels the effect of pinning an item id. If the item id is
+ * currently inactive, it will be immediately removed from the cache.
+ *
+ * @param itemId
+ * the item id to unpin
+ * @throws IllegalStateException
+ * if {@code itemId} was not pinned
+ * @see #pin(Object)
+ * @see #isPinned(Object)
+ * @see #getItemIds(Collection)
+ */
+ public void unpin(Object itemId) throws IllegalStateException {
+ if (!isPinned(itemId)) {
+ throw new IllegalStateException("Item id " + itemId
+ + " was not pinned");
+ }
+
+ pinnedItemIds.remove(itemId);
+ final Integer index = indexToItemId.inverse().get(itemId);
+ if (index == null || !activeRange.contains(index.intValue())) {
+ itemIdToKey.remove(itemId);
+ indexToItemId.remove(index);
+ }
+ }
+
+ /**
+ * Checks whether an item id is pinned or not.
+ *
+ * @param itemId
+ * the item id to check for pin status
+ * @return {@code true} iff the item id is currently pinned
+ */
+ public boolean isPinned(Object itemId) {
+ return pinnedItemIds.contains(itemId);
+ }
+
+ Object itemIdAtIndex(int index) {
+ return indexToItemId.inverse().get(Integer.valueOf(index));
+ }
+ }
+
+ /**
+ * 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
+ * required).
+ * <p>
+ * This bookeeping includes, but is not limited to:
+ * <ul>
+ * <li>listening to the currently visible {@link com.vaadin.data.Property
+ * Properties'} value changes on the server side and sending those back to
+ * the client; and
+ * <li>attaching and detaching {@link com.vaadin.ui.Component Components}
+ * from the Vaadin Component hierarchy.
+ * </ul>
+ */
+ private class ActiveRowHandler implements Serializable {
+ /**
+ * 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 cached 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 = container.getIdByIndex(i);
+ final Item item = container.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 = container.getIdByIndex(i);
+ final Item item = container.getItem(itemId);
+ final GridValueChangeListener listener = valueChangeListeners
+ .remove(itemId);
+
+ if (listener != null) {
+ for (final Object propertyId : item.getItemPropertyIds()) {
+ final Property<?> property = item
+ .getItemProperty(propertyId);
+ if (property instanceof ValueChangeNotifier) {
+ ((ValueChangeNotifier) property)
+ .removeValueChangeListener(listener);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Manages removed properties in active rows.
+ *
+ * @param removedPropertyIds
+ * the property ids that have been removed from the container
+ */
+ public void propertiesRemoved(@SuppressWarnings("unused")
+ 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 = container.getIdByIndex(i);
+ final Item item = container.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) {
+ updateRowData(container.indexOfId(itemId));
+ }
+ }
+
+ private final Indexed container;
+
+ private final ActiveRowHandler activeRowHandler = new ActiveRowHandler();
+
+ private final 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();
+ insertRowData(firstIndex, count);
+ }
+
+ else if (event instanceof ItemRemoveEvent) {
+ ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
+ int firstIndex = removeEvent.getFirstIndex();
+ int count = removeEvent.getRemovedItemsCount();
+ removeRowData(firstIndex, count);
+ }
+
+ else {
+ Range visibleRows = activeRowHandler.activeRange;
+ List<?> itemIds = container.getItemIds(visibleRows.getStart(),
+ visibleRows.length());
+
+ keyMapper.removeActiveRows(keyMapper.activeRange);
+ keyMapper.addActiveRows(visibleRows, visibleRows.getStart(),
+ itemIds);
+
+ pushRows(visibleRows.getStart(), itemIds);
+ activeRowHandler.setActiveRows(visibleRows.getStart(),
+ visibleRows.length());
+ }
+ }
+ };
+
+ private final DataProviderKeyMapper keyMapper = new DataProviderKeyMapper();
+
+ /**
+ * Creates a new data provider using the given container.
+ *
+ * @param container
+ * the container to make available
+ */
+ public RpcDataProviderExtension(Indexed container) {
+ this.container = container;
+
+ registerRpc(new DataRequestRpc() {
+ private Collection<String> allTemporarilyPinnedKeys = new ArrayList<String>();
+
+ @Override
+ public void requestRows(int firstRow, int numberOfRows,
+ int firstCachedRowIndex, int cacheSize,
+ List<String> temporarilyPinnedKeys) {
+
+ for (String key : temporarilyPinnedKeys) {
+ Object itemId = keyMapper.getItemId(key);
+ if (!keyMapper.isPinned(itemId)) {
+ keyMapper.pin(itemId);
+ }
+ }
+ allTemporarilyPinnedKeys.addAll(temporarilyPinnedKeys);
+
+ Range active = Range.withLength(firstRow, numberOfRows);
+ if (cacheSize != 0) {
+ Range cached = Range.withLength(firstCachedRowIndex,
+ cacheSize);
+ active = active.combineWith(cached);
+ }
+
+ List<?> itemIds = RpcDataProviderExtension.this.container
+ .getItemIds(firstRow, numberOfRows);
+ keyMapper.preActiveRowsChange(active, firstRow, itemIds);
+ pushRows(firstRow, itemIds);
+
+ activeRowHandler.setActiveRows(active.getStart(),
+ active.length());
+ }
+
+ @Override
+ public void releaseTemporarilyPinnedKeys() {
+ /*
+ * This needs to be done deferredly since the selection event
+ * comes after this RPC call.
+ */
+
+ final SelectionChangeListener listener = new SelectionChangeListener() {
+ @Override
+ public void selectionChange(SelectionChangeEvent event) {
+ for (String tempPinnedKey : allTemporarilyPinnedKeys) {
+ /*
+ * TODO: this could be moved into a field instead of
+ * inline to reduce indentations.
+ */
+
+ /*
+ * This works around the fact that when deselecting
+ * and leaping through the cache, the client tries
+ * to send a deselect event even though a row never
+ * was selected. So, it tries to unpin something
+ * that never was temporarily pinned.
+ *
+ * If the same thing would happen while selecting
+ * (instead of deselecting), the row would be
+ * pinned, not because of the temporary pinning, but
+ * because it's selected.
+ */
+ if (!keyMapper.isPinned(tempPinnedKey)) {
+ continue;
+ }
+
+ Object itemId = keyMapper.getItemId(tempPinnedKey);
+ Integer index = keyMapper.indexToItemId.inverse()
+ .get(itemId);
+ if (!getGrid().isSelected(itemId)
+ && !activeRowHandler.activeRange
+ .contains(index.intValue())) {
+ keyMapper.unpin(itemId);
+ }
+ }
+ allTemporarilyPinnedKeys = new ArrayList<String>();
+ getGrid().removeSelectionChangeListener(this);
+ }
+ };
+
+ getGrid().addSelectionChangeListener(listener);
+ }
+ });
+
+ getState().containerSize = container.size();
+
+ if (container instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) container)
+ .addItemSetChangeListener(itemListener);
+ }
+
+ }
+
+ private void pushRows(int firstRow, List<?> itemIds) {
+ Collection<?> propertyIds = container.getContainerPropertyIds();
+ JSONArray rows = new JSONArray();
+ for (Object itemId : itemIds) {
+ rows.put(getRowData(propertyIds, itemId));
+ }
+ String jsonString = rows.toString();
+ getRpcProxy(DataProviderRpc.class).setRowData(firstRow, jsonString);
+ }
+
+ private JSONObject getRowData(Collection<?> propertyIds, Object itemId) {
+ Item item = container.getItem(itemId);
+ String[] row = new String[propertyIds.size()];
+
+ JSONArray rowData = new JSONArray();
+
+ Grid grid = getGrid();
+ try {
+ for (Object propertyId : propertyIds) {
+ GridColumn column = grid.getColumn(propertyId);
+
+ Object propertyValue = item.getItemProperty(propertyId)
+ .getValue();
+ Object encodedValue = encodeValue(propertyValue,
+ column.getRenderer(), column.getConverter(),
+ grid.getLocale());
+
+ rowData.put(encodedValue);
+ }
+
+ final JSONObject rowObject = new JSONObject();
+ rowObject.put(GridState.JSONKEY_DATA, rowData);
+ rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId));
+ return rowObject;
+ } catch (final JSONException e) {
+ throw new RuntimeException("Grid was unable to serialize "
+ + "data for row (this should've been caught "
+ + "eariler by other Grid logic)", e);
+ }
+ }
+
+ @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>
+ */
+ private void insertRowData(int index, int count) {
+ getState().containerSize += count;
+ getRpcProxy(DataProviderRpc.class).insertRowData(index, count);
+
+ activeRowHandler.insertRows(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
+ * @param firstItemId
+ * the item id of the first removed item
+ */
+ private void removeRowData(int firstIndex, int count) {
+ getState().containerSize -= count;
+ getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count);
+
+ for (int i = 0; i < count; i++) {
+ Object itemId = keyMapper.itemIdAtIndex(firstIndex + i);
+ if (itemId != null) {
+ activeRowHandler.removeItemId(itemId);
+ }
+ }
+ }
+
+ /**
+ * 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);
+ JSONObject row = getRowData(container.getContainerPropertyIds(), itemId);
+ JSONArray rowArray = new JSONArray(Collections.singleton(row));
+ String jsonString = rowArray.toString();
+ getRpcProxy(DataProviderRpc.class).setRowData(index, jsonString);
+ }
+
+ @Override
+ public void setParent(ClientConnector parent) {
+ super.setParent(parent);
+ if (parent == null) {
+ // We're detached, release various listeners
+
+ activeRowHandler
+ .removeValueChangeListeners(activeRowHandler.activeRange);
+
+ if (container instanceof ItemSetChangeNotifier) {
+ ((ItemSetChangeNotifier) container)
+ .removeItemSetChangeListener(itemListener);
+ }
+
+ }
+ }
+
+ /**
+ * Informs this data provider that some of the properties have been removed
+ * from the container.
+ * <p>
+ * Please note that we could add our own
+ * {@link com.vaadin.data.Container.PropertySetChangeListener
+ * PropertySetChangeListener} to the container, but then we'd need to
+ * implement the same bookeeping for finding what's added and removed that
+ * Grid already does in its own listener.
+ *
+ * @param removedColumns
+ * a list of property ids for the removed columns
+ */
+ public void propertiesRemoved(List<Object> removedColumns) {
+ activeRowHandler.propertiesRemoved(removedColumns);
+ }
+
+ /**
+ * Informs this data provider that some of the properties have been added to
+ * the container.
+ * <p>
+ * Please note that we could add our own
+ * {@link com.vaadin.data.Container.PropertySetChangeListener
+ * PropertySetChangeListener} to the container, but then we'd need to
+ * implement the same bookeeping for finding what's added and removed that
+ * Grid already does in its own listener.
+ *
+ * @param addedPropertyIds
+ * a list of property ids for the added columns
+ */
+ public void propertiesAdded(HashSet<Object> addedPropertyIds) {
+ activeRowHandler.propertiesAdded(addedPropertyIds);
+ }
+
+ public DataProviderKeyMapper getKeyMapper() {
+ return keyMapper;
+ }
+
+ protected Grid getGrid() {
+ return (Grid) getParent();
+ }
+
+ /**
+ * Converts and encodes the given data model property value using the given
+ * converter and renderer. This method is public only for testing purposes.
+ *
+ * @param renderer
+ * the renderer to use
+ * @param converter
+ * the converter to use
+ * @param modelValue
+ * the value to convert and encode
+ * @param locale
+ * the locale to use in conversion
+ * @return an encoded value ready to be sent to the client
+ */
+ public static <T> Object encodeValue(Object modelValue,
+ Renderer<T> renderer, Converter<?, ?> converter, Locale locale) {
+ Class<T> presentationType = renderer.getPresentationType();
+ T presentationValue;
+
+ if (converter == null) {
+ try {
+ presentationValue = presentationType.cast(modelValue);
+ } catch (ClassCastException e) {
+ throw new Converter.ConversionException(
+ "Unable to convert value of type "
+ + modelValue.getClass().getName()
+ + " to presentation type "
+ + presentationType.getName()
+ + ". No converter is set and the types are not compatible.");
+ }
+ } else {
+ assert presentationType.isAssignableFrom(converter
+ .getPresentationType());
+ @SuppressWarnings("unchecked")
+ Converter<T, Object> safeConverter = (Converter<T, Object>) converter;
+ presentationValue = safeConverter.convertToPresentation(modelValue,
+ safeConverter.getPresentationType(), locale);
+ }
+
+ Object encodedValue = renderer.encode(presentationValue);
+
+ /*
+ * because this is a relatively heavy operation, we'll hide this behind
+ * an assert so that the check will be removed in production mode
+ */
+ assert jsonSupports(encodedValue) : "org.json.JSONObject does not know how to serialize objects of type "
+ + encodedValue.getClass().getName();
+ return encodedValue;
+ }
+
+ private static boolean jsonSupports(Object encodedValue) {
+ JSONObject jsonObject = new JSONObject();
+ try {
+ jsonObject.accumulate("test", encodedValue);
+ } catch (JSONException e) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/server/src/com/vaadin/data/util/AbstractBeanContainer.java b/server/src/com/vaadin/data/util/AbstractBeanContainer.java
index adf6313770..0559585e14 100644
--- a/server/src/com/vaadin/data/util/AbstractBeanContainer.java
+++ b/server/src/com/vaadin/data/util/AbstractBeanContainer.java
@@ -222,6 +222,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
@Override
public boolean removeAllItems() {
int origSize = size();
+ IDTYPE firstItem = getFirstVisibleItem();
internalRemoveAllItems();
@@ -234,7 +235,7 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
// fire event only if the visible view changed, regardless of whether
// filtered out items were removed or not
if (origSize != 0) {
- fireItemSetChange();
+ fireItemsRemoved(0, firstItem, origSize);
}
return true;
@@ -679,6 +680,8 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
protected void addAll(Collection<? extends BEANTYPE> collection)
throws IllegalStateException, IllegalArgumentException {
boolean modified = false;
+ int origSize = size();
+
for (BEANTYPE bean : collection) {
// TODO skipping invalid beans - should not allow them in javadoc?
if (bean == null
@@ -699,13 +702,22 @@ public abstract class AbstractBeanContainer<IDTYPE, BEANTYPE> extends
if (modified) {
// Filter the contents when all items have been added
if (isFiltered()) {
- filterAll();
- } else {
- fireItemSetChange();
+ doFilterContainer(!getFilters().isEmpty());
+ }
+ if (visibleNewItemsWasAdded(origSize)) {
+ // fire event about added items
+ int firstPosition = origSize;
+ IDTYPE firstItemId = getVisibleItemIds().get(firstPosition);
+ int affectedItems = size() - origSize;
+ fireItemsAdded(firstPosition, firstItemId, affectedItems);
}
}
}
+ private boolean visibleNewItemsWasAdded(int origSize) {
+ return size() > origSize;
+ }
+
/**
* Use the bean resolver to get the identifier for a bean.
*
diff --git a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java
index b19cddb021..5ddc11ec6f 100644
--- a/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java
+++ b/server/src/com/vaadin/data/util/AbstractInMemoryContainer.java
@@ -15,8 +15,10 @@
*/
package com.vaadin.data.util;
+import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
+import java.util.EventObject;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
@@ -146,6 +148,85 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE
}
}
+ private static abstract class BaseItemAddOrRemoveEvent extends
+ EventObject implements Serializable {
+ protected Object itemId;
+ protected int index;
+ protected int count;
+
+ public BaseItemAddOrRemoveEvent(Container source, Object itemId,
+ int index, int count) {
+ super(source);
+ this.itemId = itemId;
+ this.index = index;
+ this.count = count;
+ }
+
+ public Container getContainer() {
+ return (Container) getSource();
+ }
+
+ public Object getFirstItemId() {
+ return itemId;
+ }
+
+ public int getFirstIndex() {
+ return index;
+ }
+
+ public int getAffectedItemsCount() {
+ return count;
+ }
+ }
+
+ /**
+ * An <code>Event</code> object specifying information about the added
+ * items.
+ *
+ * <p>
+ * This class provides information about the first added item and the number
+ * of added items.
+ * </p>
+ */
+ protected static class BaseItemAddEvent extends
+ BaseItemAddOrRemoveEvent implements
+ Container.Indexed.ItemAddEvent {
+
+ public BaseItemAddEvent(Container source, Object itemId, int index,
+ int count) {
+ super(source, itemId, index, count);
+ }
+
+ @Override
+ public int getAddedItemsCount() {
+ return getAffectedItemsCount();
+ }
+ }
+
+ /**
+ * An <code>Event</code> object specifying information about the removed
+ * items.
+ *
+ * <p>
+ * This class provides information about the first removed item and the
+ * number of removed items.
+ * </p>
+ */
+ protected static class BaseItemRemoveEvent extends
+ BaseItemAddOrRemoveEvent implements
+ Container.Indexed.ItemRemoveEvent {
+
+ public BaseItemRemoveEvent(Container source, Object itemId,
+ int index, int count) {
+ super(source, itemId, index, count);
+ }
+
+ @Override
+ public int getRemovedItemsCount() {
+ return getAffectedItemsCount();
+ }
+ }
+
/**
* Get an item even if filtered out.
*
@@ -898,36 +979,69 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE
* Notify item set change listeners that an item has been added to the
* container.
*
- * Unless subclasses specify otherwise, the default notification indicates a
- * full refresh.
- *
* @param postion
- * position of the added item in the view (if visible)
+ * position of the added item in the view
* @param itemId
* id of the added item
* @param item
* the added item
*/
protected void fireItemAdded(int position, ITEMIDTYPE itemId, ITEMCLASS item) {
- fireItemSetChange();
+ fireItemsAdded(position, itemId, 1);
+ }
+
+ /**
+ * Notify item set change listeners that items has been added to the
+ * container.
+ *
+ * @param firstPosition
+ * position of the first visible added item in the view
+ * @param firstItemId
+ * id of the first visible added item
+ * @param numberOfItems
+ * the number of visible added items
+ */
+ protected void fireItemsAdded(int firstPosition, ITEMIDTYPE firstItemId,
+ int numberOfItems) {
+ BaseItemAddEvent addEvent = new BaseItemAddEvent(this,
+ firstItemId, firstPosition, numberOfItems);
+ fireItemSetChange(addEvent);
}
/**
* Notify item set change listeners that an item has been removed from the
* container.
*
- * Unless subclasses specify otherwise, the default notification indicates a
- * full refresh.
+ * @param position
+ * position of the removed item in the view prior to removal
*
- * @param postion
- * position of the removed item in the view prior to removal (if
- * was visible)
* @param itemId
* id of the removed item, of type {@link Object} to satisfy
* {@link Container#removeItem(Object)} API
*/
protected void fireItemRemoved(int position, Object itemId) {
- fireItemSetChange();
+ fireItemsRemoved(position, itemId, 1);
+ }
+
+ /**
+ * Notify item set change listeners that items has been removed from the
+ * container.
+ *
+ * @param firstPosition
+ * position of the first visible removed item in the view prior
+ * to removal
+ * @param firstItemId
+ * id of the first visible removed item, of type {@link Object}
+ * to satisfy {@link Container#removeItem(Object)} API
+ * @param numberOfItems
+ * the number of removed visible items
+ *
+ */
+ protected void fireItemsRemoved(int firstPosition, Object firstItemId,
+ int numberOfItems) {
+ BaseItemRemoveEvent removeEvent = new BaseItemRemoveEvent(this,
+ firstItemId, firstPosition, numberOfItems);
+ fireItemSetChange(removeEvent);
}
// visible and filtered item identifier lists
@@ -946,6 +1060,21 @@ public abstract class AbstractInMemoryContainer<ITEMIDTYPE, PROPERTYIDCLASS, ITE
}
/**
+ * Returns the item id of the first visible item after filtering. 'Null' is
+ * returned if there is no visible items.
+ *
+ * For internal use only.
+ *
+ * @return item id of the first visible item
+ */
+ protected ITEMIDTYPE getFirstVisibleItem() {
+ if (!getVisibleItemIds().isEmpty()) {
+ return getVisibleItemIds().get(0);
+ }
+ return null;
+ }
+
+ /**
* Returns true is the container has active filters.
*
* @return true if the container is currently filtered
diff --git a/server/src/com/vaadin/data/util/IndexedContainer.java b/server/src/com/vaadin/data/util/IndexedContainer.java
index 68960335d7..f9cc4c482a 100644
--- a/server/src/com/vaadin/data/util/IndexedContainer.java
+++ b/server/src/com/vaadin/data/util/IndexedContainer.java
@@ -226,6 +226,7 @@ public class IndexedContainer extends
@Override
public boolean removeAllItems() {
int origSize = size();
+ Object firstItem = getFirstVisibleItem();
internalRemoveAllItems();
@@ -235,7 +236,7 @@ public class IndexedContainer extends
// filtered out items were removed or not
if (origSize != 0) {
// Sends a change event
- fireItemSetChange();
+ fireItemsRemoved(0, firstItem, origSize);
}
return true;
@@ -620,8 +621,7 @@ public class IndexedContainer extends
@Override
protected void fireItemAdded(int position, Object itemId, Item item) {
if (position >= 0) {
- fireItemSetChange(new IndexedContainer.ItemSetChangeEvent(this,
- position));
+ super.fireItemAdded(position, itemId, item);
}
}
@@ -1211,4 +1211,5 @@ public class IndexedContainer extends
public Collection<Filter> getContainerFilters() {
return super.getContainerFilters();
}
+
}
diff --git a/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java b/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java
new file mode 100644
index 0000000000..d1cf77c24b
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/AbstractRenderer.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2000-2014 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 com.vaadin.server.AbstractClientConnector;
+import com.vaadin.server.AbstractExtension;
+
+/**
+ * An abstract base class for server-side Grid renderers.
+ * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. This class
+ * currently extends the AbstractExtension superclass, but this fact should be
+ * regarded as an implementation detail and subject to change in a future major
+ * or minor Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class AbstractRenderer<T> extends AbstractExtension implements
+ Renderer<T> {
+
+ private final Class<T> presentationType;
+
+ protected AbstractRenderer(Class<T> presentationType) {
+ this.presentationType = presentationType;
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected Class<Grid> getSupportedParentType() {
+ return Grid.class;
+ }
+
+ /**
+ * This method is inherited from AbstractExtension but should never be
+ * called directly with an AbstractRenderer.
+ */
+ @Deprecated
+ @Override
+ protected void extend(AbstractClientConnector target) {
+ super.extend(target);
+ }
+
+ @Override
+ public Class<T> getPresentationType() {
+ return presentationType;
+ }
+
+ /**
+ * Gets the item id for a row key.
+ * <p>
+ * A key is used to identify a particular row on both a server and a client.
+ * This method can be used to get the item id for the row key that the
+ * client has sent.
+ *
+ * @param key
+ * the row key for which to retrieve an item id
+ * @return the item id corresponding to {@code key}
+ */
+ protected Object getItemId(String key) {
+ if (getParent() instanceof Grid) {
+ Grid grid = (Grid) getParent();
+ return grid.getKeyMapper().getItemId(key);
+ } else {
+ throw new IllegalStateException(
+ "Renderers can be used only with Grid");
+ }
+ }
+}
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..3c115f9241
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/Grid.java
@@ -0,0 +1,1307 @@
+/*
+ * Copyright 2000-2014 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.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView;
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.PropertySetChangeEvent;
+import com.vaadin.data.Container.PropertySetChangeListener;
+import com.vaadin.data.Container.PropertySetChangeNotifier;
+import com.vaadin.data.Container.Sortable;
+import com.vaadin.data.RpcDataProviderExtension;
+import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper;
+import com.vaadin.server.KeyMapper;
+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.GridState.SharedSelectionMode;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.ScrollDestination;
+import com.vaadin.shared.ui.grid.SortDirection;
+import com.vaadin.shared.ui.grid.SortEventOriginator;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.HasComponents;
+import com.vaadin.ui.components.grid.GridFooter.FooterCell;
+import com.vaadin.ui.components.grid.GridFooter.FooterRow;
+import com.vaadin.ui.components.grid.GridHeader.HeaderCell;
+import com.vaadin.ui.components.grid.GridHeader.HeaderRow;
+import com.vaadin.ui.components.grid.selection.MultiSelectionModel;
+import com.vaadin.ui.components.grid.selection.NoSelectionModel;
+import com.vaadin.ui.components.grid.selection.SelectionChangeEvent;
+import com.vaadin.ui.components.grid.selection.SelectionChangeListener;
+import com.vaadin.ui.components.grid.selection.SelectionChangeNotifier;
+import com.vaadin.ui.components.grid.selection.SelectionModel;
+import com.vaadin.ui.components.grid.selection.SingleSelectionModel;
+import com.vaadin.ui.components.grid.sort.Sort;
+import com.vaadin.ui.components.grid.sort.SortOrder;
+import com.vaadin.util.ReflectTools;
+
+/**
+ * A grid component for displaying tabular data.
+ * <p>
+ * Grid is always bound to a {@link Container.Indexed}, but is not a
+ * {@code Container} of any kind in of itself. The contents of the given
+ * Container is displayed with the help of {@link Renderer Renderers}.
+ *
+ * <h3 id="grid-headers-and-footers">Headers and Footers</h3>
+ * <p>
+ *
+ *
+ * <h3 id="grid-converters-and-renderers">Converters and Renderers</h3>
+ * <p>
+ * Each column has its own {@link Renderer} that displays data into something
+ * that can be displayed in the browser. That data is first converted with a
+ * {@link com.vaadin.data.util.converter.Converter Converter} into something
+ * that the Renderer can process. This can also be an implicit step - if a
+ * column has a simple data type, like a String, no explicit assignment is
+ * needed.
+ * <p>
+ * Usually a renderer takes some kind of object, and converts it into a
+ * HTML-formatted string.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ * GridColumn column = grid.getColumn(STRING_DATE_PROPERTY);
+ * column.setConverter(new StringToDateConverter());
+ * column.setRenderer(new MyColorfulDateRenderer());
+ * </pre></code>
+ *
+ * <h3 id="grid-lazyloading">Lazy Loading</h3>
+ * <p>
+ * The data is accessed as it is needed by Grid and not any sooner. In other
+ * words, if the given Container is huge, but only the first few rows are
+ * displayed to the user, only those (and a few more, for caching purposes) are
+ * accessed.
+ *
+ * <h3 id="grid-selection-modes-and-models">Selection Modes and Models</h3>
+ * <p>
+ * Grid supports three selection <em>{@link SelectionMode modes}</em> (single,
+ * multi, none), and comes bundled with one
+ * <em>{@link SelectionModel model}</em> for each of the modes. The distinction
+ * between a selection mode and selection model is as follows: a <em>mode</em>
+ * essentially says whether you can have one, many or no rows selected. The
+ * model, however, has the behavioral details of each. A single selection model
+ * may require that the user deselects one row before selecting another one. A
+ * variant of a multiselect might have a configurable maximum of rows that may
+ * be selected. And so on.
+ * <p>
+ * <code><pre>
+ * Grid grid = new Grid(myContainer);
+ *
+ * // uses the bundled SingleSelectionModel class
+ * grid.setSelectionMode(SelectionMode.SINGLE);
+ *
+ * // changes the behavior to a custom selection model
+ * grid.setSelectionModel(new MyTwoSelectionModel());
+ * </pre></code>
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class Grid extends AbstractComponent implements SelectionChangeNotifier,
+ HasComponents {
+
+ /**
+ * Selection modes representing built-in {@link SelectionModel
+ * SelectionModels} that come bundled with {@link Grid}.
+ * <p>
+ * Passing one of these enums into
+ * {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling
+ * {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in
+ * implementations of {@link SelectionModel}.
+ *
+ * @see Grid#setSelectionMode(SelectionMode)
+ * @see Grid#setSelectionModel(SelectionModel)
+ */
+ public enum SelectionMode {
+ /** A SelectionMode that maps to {@link SingleSelectionModel} */
+ SINGLE {
+ @Override
+ protected SelectionModel createModel() {
+ return new SingleSelectionModel();
+ }
+
+ },
+
+ /** A SelectionMode that maps to {@link MultiSelectionModel} */
+ MULTI {
+ @Override
+ protected SelectionModel createModel() {
+ return new MultiSelectionModel();
+ }
+ },
+
+ /** A SelectionMode that maps to {@link NoSelectionModel} */
+ NONE {
+ @Override
+ protected SelectionModel createModel() {
+ return new NoSelectionModel();
+ }
+ };
+
+ protected abstract SelectionModel createModel();
+ }
+
+ /**
+ * 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 current sort order
+ */
+ private final List<SortOrder> sortOrder = new ArrayList<SortOrder>();
+
+ /**
+ * 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());
+ removeExtension(column.getRenderer());
+ }
+ datasourceExtension.propertiesRemoved(removedColumns);
+
+ // Add new columns
+ HashSet<Object> addedPropertyIds = new HashSet<Object>();
+ for (Object propertyId : properties) {
+ if (!columns.containsKey(propertyId)) {
+ appendColumn(propertyId);
+ addedPropertyIds.add(propertyId);
+ }
+ }
+ datasourceExtension.propertiesAdded(addedPropertyIds);
+
+ Object frozenPropertyId = columnKeys
+ .get(getState(false).lastFrozenColumnId);
+ if (!columns.containsKey(frozenPropertyId)) {
+ setLastFrozenPropertyId(null);
+ }
+ }
+ };
+
+ private RpcDataProviderExtension datasourceExtension;
+
+ /**
+ * The selection model that is currently in use. Never <code>null</code>
+ * after the constructor has been run.
+ */
+ private SelectionModel selectionModel;
+
+ /**
+ * The number of times to ignore selection state sync to the client.
+ * <p>
+ * This usually means that the client side has modified the selection. We
+ * still want to inform the listeners that the selection has changed, but we
+ * don't want to send those changes "back to the client".
+ */
+ private int ignoreSelectionClientSync = 0;
+
+ private final GridHeader header = new GridHeader(this);
+ private final GridFooter footer = new GridFooter(this);
+
+ private static final Method SELECTION_CHANGE_METHOD = ReflectTools
+ .findMethod(SelectionChangeListener.class, "selectionChange",
+ SelectionChangeEvent.class);
+
+ private static final Method SORT_ORDER_CHANGE_METHOD = ReflectTools
+ .findMethod(SortOrderChangeListener.class, "sortOrderChange",
+ SortOrderChangeEvent.class);
+
+ /**
+ * Creates a new Grid using the given datasource.
+ *
+ * @param datasource
+ * the data source for the grid
+ */
+ public Grid(final Container.Indexed datasource) {
+ setContainerDataSource(datasource);
+
+ setSelectionMode(SelectionMode.MULTI);
+ addSelectionChangeListener(new SelectionChangeListener() {
+ @Override
+ public void selectionChange(SelectionChangeEvent event) {
+ for (Object removedItemId : event.getRemoved()) {
+ getKeyMapper().unpin(removedItemId);
+ }
+
+ for (Object addedItemId : event.getAdded()) {
+ if (!getKeyMapper().isPinned(addedItemId)) {
+ getKeyMapper().pin(addedItemId);
+ }
+ }
+
+ List<String> keys = getKeyMapper().getKeys(getSelectedRows());
+
+ boolean markAsDirty = true;
+
+ /*
+ * If this clause is true, it means that the selection event
+ * originated from the client. This means that we don't want to
+ * send the changes back to the client (markAsDirty => false).
+ */
+ if (ignoreSelectionClientSync > 0) {
+ ignoreSelectionClientSync--;
+ markAsDirty = false;
+
+ try {
+
+ /*
+ * Make sure that the diffstate is aware of the
+ * "undirty" modification, so that the diffs are
+ * calculated correctly the next time we actually want
+ * to send the selection state to the client.
+ */
+ getUI().getConnectorTracker().getDiffState(Grid.this)
+ .put("selectedKeys", new JSONArray(keys));
+ } catch (JSONException e) {
+ throw new RuntimeException("Internal error", e);
+ }
+ }
+
+ getState(markAsDirty).selectedKeys = keys;
+ }
+ });
+
+ registerRpc(new GridServerRpc() {
+
+ @Override
+ public void selectionChange(List<String> selection) {
+ final HashSet<Object> newSelection = new HashSet<Object>(
+ getKeyMapper().getItemIds(selection));
+ final HashSet<Object> oldSelection = new HashSet<Object>(
+ getSelectedRows());
+
+ SetView<Object> addedItemIds = Sets.difference(newSelection,
+ oldSelection);
+ SetView<Object> removedItemIds = Sets.difference(oldSelection,
+ newSelection);
+
+ if (!removedItemIds.isEmpty()) {
+ /*
+ * Since these changes come from the client, we want to
+ * modify the selection model and get that event fired to
+ * all the listeners. One of the listeners is our internal
+ * selection listener, and this tells it not to send the
+ * selection event back to the client.
+ */
+ ignoreSelectionClientSync++;
+
+ if (removedItemIds.size() == 1) {
+ deselect(removedItemIds.iterator().next());
+ } else {
+ assert getSelectionModel() instanceof SelectionModel.Multi : "Got multiple deselections, but the selection model is not a SelectionModel.Multi";
+ ((SelectionModel.Multi) getSelectionModel())
+ .deselect(removedItemIds);
+ }
+ }
+
+ if (!addedItemIds.isEmpty()) {
+ /*
+ * Since these changes come from the client, we want to
+ * modify the selection model and get that event fired to
+ * all the listeners. One of the listeners is our internal
+ * selection listener, and this tells it not to send the
+ * selection event back to the client.
+ */
+ ignoreSelectionClientSync++;
+
+ if (addedItemIds.size() == 1) {
+ select(addedItemIds.iterator().next());
+ } else {
+ assert getSelectionModel() instanceof SelectionModel.Multi : "Got multiple selections, but the selection model is not a SelectionModel.Multi";
+ ((SelectionModel.Multi) getSelectionModel())
+ .select(addedItemIds);
+ }
+ }
+ }
+
+ @Override
+ public void sort(String[] columnIds, SortDirection[] directions,
+ SortEventOriginator originator) {
+ assert columnIds.length == directions.length;
+
+ List<SortOrder> order = new ArrayList<SortOrder>(
+ columnIds.length);
+ for (int i = 0; i < columnIds.length; i++) {
+ Object propertyId = getPropertyIdByColumnId(columnIds[i]);
+ order.add(new SortOrder(propertyId, directions[i]));
+ }
+
+ setSortOrder(order, originator);
+ }
+ });
+ }
+
+ /**
+ * 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 (datasourceExtension != null) {
+ removeExtension(datasourceExtension);
+ }
+
+ datasource = container;
+
+ //
+ // Adjust sort order
+ //
+
+ if (container instanceof Container.Sortable) {
+
+ // If the container is sortable, go through the current sort order
+ // and match each item to the sortable properties of the new
+ // container. If the new container does not support an item in the
+ // current sort order, that item is removed from the current sort
+ // order list.
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDatasource())
+ .getSortableContainerPropertyIds();
+
+ Iterator<SortOrder> i = sortOrder.iterator();
+ while (i.hasNext()) {
+ if (!sortableProps.contains(i.next().getPropertyId())) {
+ i.remove();
+ }
+ }
+
+ sort(SortEventOriginator.INTERNAL);
+ } else {
+
+ // If the new container is not sortable, we'll just re-set the sort
+ // order altogether.
+ clearSortOrder();
+ }
+
+ datasourceExtension = new RpcDataProviderExtension(container);
+ datasourceExtension.extend(this);
+
+ /*
+ * selectionModel == null when the invocation comes from the
+ * constructor.
+ */
+ if (selectionModel != null) {
+ selectionModel.reset();
+ }
+
+ // Listen to changes in properties and remove columns if needed
+ if (datasource instanceof PropertySetChangeNotifier) {
+ ((PropertySetChangeNotifier) datasource)
+ .addPropertySetChangeListener(propertyListener);
+ }
+ /*
+ * 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
+ HeaderRow row = getHeader().getDefaultRow();
+ for (Object propertyId : datasource.getContainerPropertyIds()) {
+ if (!columns.containsKey(propertyId)) {
+ GridColumn column = appendColumn(propertyId);
+
+ // Initial sorting is defined by container
+ if (datasource instanceof Sortable) {
+ column.setSortable(((Sortable) datasource)
+ .getSortableContainerPropertyIds().contains(
+ propertyId));
+ }
+
+ // Add by default property id as column header
+ row.getCell(propertyId).setText(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);
+ }
+
+ /**
+ * 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);
+
+ for (int i = 0; i < getHeader().getRowCount(); ++i) {
+ getHeader().getRow(i).addCell(datasourcePropertyId);
+ }
+
+ for (int i = 0; i < getFooter().getRowCount(); ++i) {
+ getFooter().getRow(i).addCell(datasourcePropertyId);
+ }
+
+ 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 scrollTo(Object itemId) throws IllegalArgumentException {
+ scrollTo(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 scrollTo(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();
+ }
+
+ /**
+ * Sets the number of rows that should be visible in Grid's body, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ * <p>
+ * If Grid is currently not in {@link HeightMode#ROW}, the given value is
+ * remembered, and applied once the mode is applied.
+ *
+ * @param rows
+ * The height in terms of number of rows displayed in Grid's
+ * body. If Grid doesn't contain enough rows, white space is
+ * displayed instead. If <code>null</code> is given, then Grid's
+ * height is undefined
+ * @throws IllegalArgumentException
+ * if {@code rows} is zero or less
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isInifinite(double)
+ * infinite}
+ * @throws IllegalArgumentException
+ * if {@code rows} is {@link Double#isNaN(double) NaN}
+ */
+ public void setHeightByRows(double rows) {
+ if (rows <= 0.0d) {
+ throw new IllegalArgumentException(
+ "More than zero rows must be shown.");
+ } else if (Double.isInfinite(rows)) {
+ throw new IllegalArgumentException(
+ "Grid doesn't support infinite heights");
+ } else if (Double.isNaN(rows)) {
+ throw new IllegalArgumentException("NaN is not a valid row count");
+ }
+
+ getState().heightByRows = rows;
+ }
+
+ /**
+ * Gets the amount of rows in Grid's body that are shown, while
+ * {@link #getHeightMode()} is {@link HeightMode#ROW}.
+ *
+ * @return the amount of rows that are being shown in Grid's body
+ * @see #setHeightByRows(double)
+ */
+ public double getHeightByRows() {
+ return getState(false).heightByRows;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * <em>Note:</em> This method will change the widget's size in the browser
+ * only if {@link #getHeightMode()} returns {@link HeightMode#CSS}.
+ *
+ * @see #setHeightMode(HeightMode)
+ */
+ @Override
+ public void setHeight(float height, Unit unit) {
+ super.setHeight(height, unit);
+ }
+
+ /**
+ * Defines the mode in which the Grid widget's height is calculated.
+ * <p>
+ * If {@link HeightMode#CSS} is given, Grid will respect the values given
+ * via a {@code setHeight}-method, and behave as a traditional Component.
+ * <p>
+ * If {@link HeightMode#ROW} is given, Grid will make sure that the body
+ * will display as many rows as {@link #getHeightByRows()} defines.
+ * <em>Note:</em> If headers/footers are inserted or removed, the widget
+ * will resize itself to still display the required amount of rows in its
+ * body. It also takes the horizontal scrollbar into account.
+ *
+ * @param heightMode
+ * the mode in to which Grid should be set
+ */
+ public void setHeightMode(HeightMode heightMode) {
+ /*
+ * This method is a workaround for the fact that Vaadin re-applies
+ * widget dimensions (height/width) on each state change event. The
+ * original design was to have setHeight an setHeightByRow be equals,
+ * and whichever was called the latest was considered in effect.
+ *
+ * But, because of Vaadin always calling setHeight on the widget, this
+ * approach doesn't work.
+ */
+
+ getState().heightMode = heightMode;
+ }
+
+ /**
+ * Returns the current {@link HeightMode} the Grid is in.
+ * <p>
+ * Defaults to {@link HeightMode#CSS}.
+ *
+ * @return the current HeightMode
+ */
+ public HeightMode getHeightMode() {
+ return getState(false).heightMode;
+ }
+
+ /* Selection related methods: */
+
+ /**
+ * Takes a new {@link SelectionModel} into use.
+ * <p>
+ * The SelectionModel that is previously in use will have all its items
+ * deselected.
+ * <p>
+ * If the given SelectionModel is already in use, this method does nothing.
+ *
+ * @param selectionModel
+ * the new SelectionModel to use
+ * @throws IllegalArgumentException
+ * if {@code selectionModel} is <code>null</code>
+ */
+ public void setSelectionModel(SelectionModel selectionModel)
+ throws IllegalArgumentException {
+ if (selectionModel == null) {
+ throw new IllegalArgumentException(
+ "Selection model may not be null");
+ }
+
+ if (this.selectionModel != selectionModel) {
+ // this.selectionModel is null on init
+ if (this.selectionModel != null) {
+ this.selectionModel.reset();
+ this.selectionModel.setGrid(null);
+ }
+
+ this.selectionModel = selectionModel;
+ this.selectionModel.setGrid(this);
+ this.selectionModel.reset();
+
+ if (selectionModel.getClass().equals(SingleSelectionModel.class)) {
+ getState().selectionMode = SharedSelectionMode.SINGLE;
+ } else if (selectionModel.getClass().equals(
+ MultiSelectionModel.class)) {
+ getState().selectionMode = SharedSelectionMode.MULTI;
+ } else if (selectionModel.getClass().equals(NoSelectionModel.class)) {
+ getState().selectionMode = SharedSelectionMode.NONE;
+ } else {
+ throw new UnsupportedOperationException("Grid currently "
+ + "supports only its own bundled selection models");
+ }
+ }
+ }
+
+ /**
+ * Returns the currently used {@link SelectionModel}.
+ *
+ * @return the currently used SelectionModel
+ */
+ public SelectionModel getSelectionModel() {
+ return selectionModel;
+ }
+
+ /**
+ * Changes the Grid's selection mode.
+ * <p>
+ * Grid supports three selection modes: multiselect, single select and no
+ * selection, and this is a conveniency method for choosing between one of
+ * them.
+ * <P>
+ * Technically, this method is a shortcut that can be used instead of
+ * calling {@code setSelectionModel} with a specific SelectionModel
+ * instance. Grid comes with three built-in SelectionModel classes, and the
+ * {@link SelectionMode} enum represents each of them.
+ * <p>
+ * Essentially, the two following method calls are equivalent:
+ * <p>
+ * <code><pre>
+ * grid.setSelectionMode(SelectionMode.MULTI);
+ * grid.setSelectionModel(new MultiSelectionMode());
+ * </pre></code>
+ *
+ *
+ * @param selectionMode
+ * the selection mode to switch to
+ * @return The {@link SelectionModel} instance that was taken into use
+ * @throws IllegalArgumentException
+ * if {@code selectionMode} is <code>null</code>
+ * @see SelectionModel
+ */
+ public SelectionModel setSelectionMode(final SelectionMode selectionMode)
+ throws IllegalArgumentException {
+ if (selectionMode == null) {
+ throw new IllegalArgumentException("selection mode may not be null");
+ }
+ final SelectionModel newSelectionModel = selectionMode.createModel();
+ setSelectionModel(newSelectionModel);
+ return newSelectionModel;
+ }
+
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ // keep this javadoc in sync with SelectionModel.isSelected
+ public boolean isSelected(Object itemId) {
+ return selectionModel.isSelected(itemId);
+ }
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ * <p>
+ * This method is a shorthand that is forwarded to the object that is
+ * returned by {@link #getSelectionModel()}.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ // keep this javadoc in sync with SelectionModel.getSelectedRows
+ public Collection<Object> getSelectedRows() {
+ return getSelectionModel().getSelectedRows();
+ }
+
+ /**
+ * Gets the item id of the currently selected item.
+ * <p>
+ * This method is a shorthand that is forwarded to the object that is
+ * returned by {@link #getSelectionModel()}. Only
+ * {@link SelectionModel.Single} is supported.
+ *
+ * @return the item id of the currently selected item, or <code>null</code>
+ * if nothing is selected
+ * @throws IllegalStateException
+ * if the object that is returned by
+ * {@link #getSelectionModel()} is not an instance of
+ * {@link SelectionModel.Single}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.getSelectedRow
+ public Object getSelectedRow() throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).getSelectedRow();
+ } else {
+ throw new IllegalStateException(Grid.class.getSimpleName()
+ + " does not support the 'getSelectedRow' shortcut method "
+ + "unless the selection model implements "
+ + SelectionModel.Single.class.getName()
+ + ". The current one does not ("
+ + selectionModel.getClass().getName() + ")");
+ }
+ }
+
+ /**
+ * Marks an item as selected.
+ * <p>
+ * This method is a shorthand that is forwarded to the object that is
+ * returned by {@link #getSelectionModel()}. Only
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi} are
+ * supported.
+ *
+ *
+ * @param itemIds
+ * the itemId to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might be that
+ * the implementation already had an item selected, and that
+ * needs to be explicitly deselected before re-selecting
+ * something
+ * @throws IllegalStateException
+ * if the object that is returned by
+ * {@link #getSelectionModel()} does not implement
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.select
+ public boolean select(Object itemId) throws IllegalArgumentException,
+ IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).select(itemId);
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).select(itemId);
+ } else {
+ throw new IllegalStateException(Grid.class.getSimpleName()
+ + " does not support the 'select' shortcut method "
+ + "unless the selection model implements "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + ". The current one does not ("
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Marks an item as deselected.
+ * <p>
+ * This method is a shorthand that is forwarded to the object that is
+ * returned by {@link #getSelectionModel()}. Only
+ * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+ * supported.
+ *
+ * @param itemId
+ * the itemId to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalArgumentException
+ * if the {@code itemId} doesn't exist in the currently active
+ * Container
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be that
+ * the implementation already had an item selected, and that
+ * needs to be explicitly deselected before re-selecting
+ * something
+ * @throws IllegalStateException
+ * if the object that is returned by
+ * {@link #getSelectionModel()} does not implement
+ * {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+ */
+ // keep this javadoc in sync with SelectionModel.Single.deselect
+ public boolean deselect(Object itemId) throws IllegalStateException {
+ if (selectionModel instanceof SelectionModel.Single) {
+ return ((SelectionModel.Single) selectionModel).deselect(itemId);
+ } else if (selectionModel instanceof SelectionModel.Multi) {
+ return ((SelectionModel.Multi) selectionModel).deselect(itemId);
+ } else {
+ throw new IllegalStateException(Grid.class.getSimpleName()
+ + " does not support the 'deselect' shortcut method "
+ + "unless the selection model implements "
+ + SelectionModel.Single.class.getName() + " or "
+ + SelectionModel.Multi.class.getName()
+ + ". The current one does not ("
+ + selectionModel.getClass().getName() + ").");
+ }
+ }
+
+ /**
+ * Fires a selection change event.
+ * <p>
+ * <strong>Note:</strong> This is not a method that should be called by
+ * application logic. This method is publicly accessible only so that
+ * {@link SelectionModel SelectionModels} would be able to inform Grid of
+ * these events.
+ *
+ * @param addedSelections
+ * the selections that were added by this event
+ * @param removedSelections
+ * the selections that were removed by this event
+ */
+ public void fireSelectionChangeEvent(Collection<Object> oldSelection,
+ Collection<Object> newSelection) {
+ fireEvent(new SelectionChangeEvent(this, oldSelection, newSelection));
+ }
+
+ @Override
+ public void addSelectionChangeListener(SelectionChangeListener listener) {
+ addListener(SelectionChangeEvent.class, listener,
+ SELECTION_CHANGE_METHOD);
+ }
+
+ @Override
+ public void removeSelectionChangeListener(SelectionChangeListener listener) {
+ removeListener(SelectionChangeEvent.class, listener,
+ SELECTION_CHANGE_METHOD);
+ }
+
+ /**
+ * Gets the
+ * {@link com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper
+ * DataProviderKeyMapper} being used by the data source.
+ *
+ * @return the key mapper being used by the data source
+ */
+ DataProviderKeyMapper getKeyMapper() {
+ return datasourceExtension.getKeyMapper();
+ }
+
+ /**
+ * Adds a renderer to this grid's connector hierarchy.
+ *
+ * @param renderer
+ * the renderer to add
+ */
+ void addRenderer(Renderer<?> renderer) {
+ addExtension(renderer);
+ }
+
+ /**
+ * Sets the current sort order using the fluid Sort API. Read the
+ * documentation for {@link Sort} for more information.
+ *
+ * @param s
+ * a sort instance
+ */
+ public void sort(Sort s) {
+ setSortOrder(s.build());
+ }
+
+ /**
+ * Sort this Grid in ascending order by a specified property.
+ *
+ * @param propertyId
+ * a property ID
+ */
+ public void sort(Object propertyId) {
+ sort(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Sort this Grid in user-specified {@link SortOrder} by a property.
+ *
+ * @param propertyId
+ * a property ID
+ * @param direction
+ * a sort order value (ascending/descending)
+ */
+ public void sort(Object propertyId, SortDirection direction) {
+ sort(Sort.by(propertyId, direction));
+ }
+
+ /**
+ * Clear the current sort order, and re-sort the grid.
+ */
+ public void clearSortOrder() {
+ sortOrder.clear();
+ sort(false);
+ }
+
+ /**
+ * Sets the sort order to use. This method throws
+ * {@link IllegalStateException} if the attached container is not a
+ * {@link Container.Sortable}, and {@link IllegalArgumentException} if a
+ * property in the list is not recognized by the container, or if the
+ * 'order' parameter is null.
+ *
+ * @param order
+ * a sort order list.
+ */
+ public void setSortOrder(List<SortOrder> order) {
+ setSortOrder(order, SortEventOriginator.API);
+ }
+
+ private void setSortOrder(List<SortOrder> order,
+ SortEventOriginator originator) {
+ if (!(getContainerDatasource() instanceof Container.Sortable)) {
+ throw new IllegalStateException(
+ "Attached container is not sortable (does not implement Container.Sortable)");
+ }
+
+ if (order == null) {
+ throw new IllegalArgumentException("Order list may not be null!");
+ }
+
+ sortOrder.clear();
+
+ Collection<?> sortableProps = ((Container.Sortable) getContainerDatasource())
+ .getSortableContainerPropertyIds();
+
+ for (SortOrder o : order) {
+ if (!sortableProps.contains(o.getPropertyId())) {
+ throw new IllegalArgumentException(
+ "Property "
+ + o.getPropertyId()
+ + " does not exist or is not sortable in the current container");
+ }
+ }
+
+ sortOrder.addAll(order);
+ sort(originator);
+ }
+
+ /**
+ * Get the current sort order list.
+ *
+ * @return a sort order list
+ */
+ public List<SortOrder> getSortOrder() {
+ return Collections.unmodifiableList(sortOrder);
+ }
+
+ /**
+ * Apply sorting to data source.
+ */
+ private void sort(SortEventOriginator originator) {
+
+ Container c = getContainerDatasource();
+ if (c instanceof Container.Sortable) {
+ Container.Sortable cs = (Container.Sortable) c;
+
+ final int items = sortOrder.size();
+ Object[] propertyIds = new Object[items];
+ boolean[] directions = new boolean[items];
+
+ String[] columnKeys = new String[items];
+ SortDirection[] stateDirs = new SortDirection[items];
+
+ for (int i = 0; i < items; ++i) {
+ SortOrder order = sortOrder.get(i);
+
+ columnKeys[i] = this.columnKeys.key(order.getPropertyId());
+ stateDirs[i] = order.getDirection();
+
+ propertyIds[i] = order.getPropertyId();
+ switch (order.getDirection()) {
+ case ASCENDING:
+ directions[i] = true;
+ break;
+ case DESCENDING:
+ directions[i] = false;
+ break;
+ default:
+ throw new IllegalArgumentException("getDirection() of "
+ + order + " returned an unexpected value");
+ }
+ }
+
+ cs.sort(propertyIds, directions);
+
+ fireEvent(new SortOrderChangeEvent(this, new ArrayList<SortOrder>(
+ sortOrder), originator));
+
+ getState().sortColumns = columnKeys;
+ getState(false).sortDirs = stateDirs;
+ } else {
+ throw new IllegalStateException(
+ "Container is not sortable (does not implement Container.Sortable)");
+ }
+ }
+
+ /**
+ * Adds a sort order change listener that gets notified when the sort order
+ * changes.
+ *
+ * @param listener
+ * the sort order change listener to add
+ */
+ public void addSortOrderChangeListener(SortOrderChangeListener listener) {
+ addListener(SortOrderChangeEvent.class, listener,
+ SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /**
+ * Removes a sort order change listener previously added using
+ * {@link #addSortOrderChangeListener(SortOrderChangeListener)}.
+ *
+ * @param listener
+ * the sort order change listener to remove
+ */
+ public void removeSortOrderChangeListener(SortOrderChangeListener listener) {
+ removeListener(SortOrderChangeEvent.class, listener,
+ SORT_ORDER_CHANGE_METHOD);
+ }
+
+ /**
+ * Returns the header section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the header
+ */
+ public GridHeader getHeader() {
+ return header;
+ }
+
+ /**
+ * Returns the footer section of this grid. The default header contains a
+ * single row displaying the column captions.
+ *
+ * @return the footer
+ */
+ public GridFooter getFooter() {
+ return footer;
+ }
+
+ @Override
+ public Iterator<Component> iterator() {
+ List<Component> componentList = new ArrayList<Component>();
+
+ GridHeader header = getHeader();
+ for (int i = 0; i < header.getRowCount(); ++i) {
+ HeaderRow row = header.getRow(i);
+ for (Object propId : datasource.getContainerPropertyIds()) {
+ HeaderCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ GridFooter footer = getFooter();
+ for (int i = 0; i < footer.getRowCount(); ++i) {
+ FooterRow row = footer.getRow(i);
+ for (Object propId : datasource.getContainerPropertyIds()) {
+ FooterCell cell = row.getCell(propId);
+ if (cell.getCellState().type == GridStaticCellType.WIDGET) {
+ componentList.add(cell.getComponent());
+ }
+ }
+ }
+
+ return componentList.iterator();
+ }
+}
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..0ef805eb2e
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/GridColumn.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright 2000-2014 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.data.util.converter.Converter;
+import com.vaadin.data.util.converter.ConverterUtil;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.shared.ui.grid.GridColumnState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.components.grid.renderers.TextRenderer;
+
+/**
+ * A column in the grid. Can be obtained by calling
+ * {@link Grid#getColumn(Object propertyId)}.
+ *
+ * @since
+ * @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;
+
+ private Converter<?, Object> converter;
+
+ /**
+ * A check for allowing the {@link #GridColumn(Grid, GridColumnState)
+ * constructor} to call {@link #setConverter(Converter)} with a
+ * <code>null</code>, even if model and renderer aren't compatible.
+ */
+ private boolean isFirstConverterAssignment = true;
+
+ /**
+ * 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;
+ internalSetRenderer(new TextRenderer());
+ }
+
+ /**
+ * 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 default row of header, null if no default row
+ *
+ * @throws IllegalStateException
+ * if the column no longer is attached to the grid
+ */
+ @Deprecated
+ 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
+ */
+ @Deprecated
+ public void setHeaderCaption(String caption) throws IllegalStateException {
+ checkColumnIsAttached();
+ state.header = caption;
+ }
+
+ /**
+ * 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
+ */
+ @Deprecated
+ public String getFooterCaption() throws IllegalStateException {
+ checkColumnIsAttached();
+ return getFooterCellState().text;
+ }
+
+ /**
+ * 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
+ */
+ @Deprecated
+ public void setFooterCaption(String caption) throws IllegalStateException {
+ checkColumnIsAttached();
+ getFooterCellState().text = caption;
+ state.footer = caption;
+ grid.markAsDirty();
+ }
+
+ private CellState getFooterCellState() {
+ int index = grid.getState().columns.indexOf(state);
+ return grid.getState().footer.rows.get(0).cells.get(index);
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Sets the renderer for this column.
+ * <p>
+ * If a suitable converter isn't defined explicitly, the session converter
+ * factory is used to find a compatible converter.
+ *
+ * @param renderer
+ * the renderer to use
+ * @throws IllegalArgumentException
+ * if no compatible converter could be found
+ *
+ * @see VaadinSession#getConverterFactory()
+ * @see ConverterUtil#getConverter(Class, Class, VaadinSession)
+ * @see #setConverter(Converter)
+ */
+ public void setRenderer(Renderer<?> renderer) {
+ if (!internalSetRenderer(renderer)) {
+ throw new IllegalArgumentException(
+ "Could not find a converter for converting from the model type "
+ + getModelType()
+ + " to the renderer presentation type "
+ + renderer.getPresentationType());
+ }
+ }
+
+ /**
+ * Sets the renderer for this column and the converter used to convert from
+ * the property value type to the renderer presentation type.
+ *
+ * @param renderer
+ * the renderer to use, cannot be null
+ * @param converter
+ * the converter to use
+ *
+ * @throws IllegalArgumentException
+ * if the renderer is already associated with a grid column
+ */
+ public <T> void setRenderer(Renderer<T> renderer,
+ Converter<? extends T, ?> converter) {
+ if (renderer.getParent() != null) {
+ throw new IllegalArgumentException(
+ "Cannot set a renderer that is already connected to a grid column");
+ }
+
+ if (getRenderer() != null) {
+ grid.removeExtension(getRenderer());
+ }
+
+ grid.addRenderer(renderer);
+ state.rendererConnector = renderer;
+ setConverter(converter);
+ }
+
+ /**
+ * Sets the converter used to convert from the property value type to the
+ * renderer presentation type.
+ *
+ * @param converter
+ * the converter to use, or {@code null} to not use any
+ * converters
+ * @throws IllegalArgumentException
+ * if the types are not compatible
+ */
+ public void setConverter(Converter<?, ?> converter)
+ throws IllegalArgumentException {
+ Class<?> modelType = getModelType();
+ if (converter != null) {
+ if (!converter.getModelType().isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException("The converter model type "
+ + converter.getModelType()
+ + " is not compatible with the property type "
+ + modelType);
+
+ } else if (!getRenderer().getPresentationType().isAssignableFrom(
+ converter.getPresentationType())) {
+ throw new IllegalArgumentException(
+ "The converter presentation type "
+ + converter.getPresentationType()
+ + " is not compatible with the renderer presentation type "
+ + getRenderer().getPresentationType());
+ }
+ }
+
+ else {
+ /*
+ * Since the converter is null (i.e. will be removed), we need to
+ * know that the renderer and model are compatible. If not, we can't
+ * allow for this to happen.
+ *
+ * The constructor is allowed to call this method with null without
+ * any compatibility checks, therefore we have a special case for
+ * it.
+ */
+
+ Class<?> rendererPresentationType = getRenderer()
+ .getPresentationType();
+ if (!isFirstConverterAssignment
+ && !rendererPresentationType.isAssignableFrom(modelType)) {
+ throw new IllegalArgumentException("Cannot remove converter, "
+ + "as renderer's presentation type "
+ + rendererPresentationType.getName() + " and column's "
+ + "model " + modelType.getName() + " type aren't "
+ + "directly with each other");
+ }
+ }
+
+ isFirstConverterAssignment = false;
+
+ @SuppressWarnings("unchecked")
+ Converter<?, Object> castConverter = (Converter<?, Object>) converter;
+ this.converter = castConverter;
+ }
+
+ /**
+ * Returns the renderer instance used by this column.
+ *
+ * @return the renderer
+ */
+ public Renderer<?> getRenderer() {
+ return (Renderer<?>) getState().rendererConnector;
+ }
+
+ /**
+ * Returns the converter instance used by this column.
+ *
+ * @return the converter
+ */
+ public Converter<?, ?> getConverter() {
+ return converter;
+ }
+
+ private <T> boolean internalSetRenderer(Renderer<T> renderer) {
+
+ Converter<? extends T, ?> converter;
+ if (isCompatibleWithProperty(renderer, getConverter())) {
+ // Use the existing converter (possibly none) if types compatible
+ converter = (Converter<? extends T, ?>) getConverter();
+ } else {
+ converter = ConverterUtil.getConverter(
+ renderer.getPresentationType(), getModelType(),
+ getSession());
+ }
+ setRenderer(renderer, converter);
+ return isCompatibleWithProperty(renderer, converter);
+ }
+
+ private VaadinSession getSession() {
+ UI ui = grid.getUI();
+ return ui != null ? ui.getSession() : null;
+ }
+
+ private boolean isCompatibleWithProperty(Renderer<?> renderer,
+ Converter<?, ?> converter) {
+ Class<?> type;
+ if (converter == null) {
+ type = getModelType();
+ } else {
+ type = converter.getPresentationType();
+ }
+ return renderer.getPresentationType().isAssignableFrom(type);
+ }
+
+ private Class<?> getModelType() {
+ return grid.getContainerDatasource().getType(
+ grid.getPropertyIdByColumnId(state.id));
+ }
+
+ /**
+ * Should sorting controls be available for the column
+ *
+ * @param sortable
+ * <code>true</code> if the sorting controls should be visible.
+ */
+ public void setSortable(boolean sortable) {
+ checkColumnIsAttached();
+ state.sortable = sortable;
+ grid.markAsDirty();
+ }
+
+ /**
+ * Are the sorting controls visible in the column header
+ */
+ public boolean isSortable() {
+ return state.sortable;
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/GridFooter.java b/server/src/com/vaadin/ui/components/grid/GridFooter.java
new file mode 100644
index 0000000000..0a28a481cf
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/GridFooter.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2000-2014 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 com.vaadin.shared.ui.grid.GridStaticSectionState;
+
+/**
+ * Represents the footer section of a Grid. By default Footer is not visible.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridFooter extends GridStaticSection<GridFooter.FooterRow> {
+
+ public class FooterRow extends GridStaticSection.StaticRow<FooterCell> {
+
+ protected FooterRow(GridStaticSection<?> section) {
+ super(section);
+ }
+
+ @Override
+ protected FooterCell createCell() {
+ return new FooterCell(this);
+ }
+
+ }
+
+ public class FooterCell extends GridStaticSection.StaticCell {
+
+ protected FooterCell(FooterRow row) {
+ super(row);
+ }
+ }
+
+ private final GridStaticSectionState footerState = new GridStaticSectionState();
+
+ protected GridFooter(Grid grid) {
+ this.grid = grid;
+ grid.getState(true).footer = footerState;
+ setVisible(false);
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return footerState;
+ }
+
+ @Override
+ protected FooterRow createRow() {
+ return new FooterRow(this);
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/components/grid/GridHeader.java b/server/src/com/vaadin/ui/components/grid/GridHeader.java
new file mode 100644
index 0000000000..9d7ec24a97
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/GridHeader.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2000-2014 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 com.vaadin.shared.ui.grid.GridStaticSectionState;
+
+/**
+ * Represents the header section of a Grid.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridHeader extends GridStaticSection<GridHeader.HeaderRow> {
+
+ public class HeaderRow extends GridStaticSection.StaticRow<HeaderCell> {
+
+ protected HeaderRow(GridStaticSection<?> section) {
+ super(section);
+ }
+
+ private void setDefaultRow(boolean value) {
+ getRowState().defaultRow = value;
+ }
+
+ @Override
+ protected HeaderCell createCell() {
+ return new HeaderCell(this);
+ }
+ }
+
+ public class HeaderCell extends GridStaticSection.StaticCell {
+
+ protected HeaderCell(HeaderRow row) {
+ super(row);
+ }
+ }
+
+ private HeaderRow defaultRow = null;
+ private final GridStaticSectionState headerState = new GridStaticSectionState();
+
+ protected GridHeader(Grid grid) {
+ this.grid = grid;
+ grid.getState(true).header = headerState;
+ HeaderRow row = createRow();
+ rows.add(row);
+ setDefaultRow(row);
+ getSectionState().rows.add(row.getRowState());
+ }
+
+ /**
+ * Sets the default row of this header. The default row is a special header
+ * row providing a user interface for sorting columns.
+ *
+ * @param row
+ * the new default row, or null for no default row
+ *
+ * @throws IllegalArgumentException
+ * this header does not contain the row
+ */
+ public void setDefaultRow(HeaderRow row) {
+ if (row == defaultRow) {
+ return;
+ }
+
+ if (row != null && !rows.contains(row)) {
+ throw new IllegalArgumentException(
+ "Cannot set a default row that does not exist in the section");
+ }
+
+ if (defaultRow != null) {
+ defaultRow.setDefaultRow(false);
+ }
+
+ if (row != null) {
+ row.setDefaultRow(true);
+ }
+
+ defaultRow = row;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the current default row of this header. The default row is a
+ * special header row providing a user interface for sorting columns.
+ *
+ * @return the default row or null if no default row set
+ */
+ public HeaderRow getDefaultRow() {
+ return defaultRow;
+ }
+
+ @Override
+ protected GridStaticSectionState getSectionState() {
+ return headerState;
+ }
+
+ @Override
+ protected HeaderRow createRow() {
+ return new HeaderRow(this);
+ }
+
+ @Override
+ public HeaderRow removeRow(int rowIndex) {
+ HeaderRow row = super.removeRow(rowIndex);
+ if (row == defaultRow) {
+ // Default Header Row was just removed.
+ setDefaultRow(null);
+ }
+ return row;
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/GridStaticSection.java b/server/src/com/vaadin/ui/components/grid/GridStaticSection.java
new file mode 100644
index 0000000000..eb098d0d4e
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/GridStaticSection.java
@@ -0,0 +1,425 @@
+/*
+ * Copyright 2000-2014 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.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.GridStaticSectionState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.CellState;
+import com.vaadin.shared.ui.grid.GridStaticSectionState.RowState;
+import com.vaadin.ui.Component;
+
+/**
+ * Abstract base class for Grid header and footer sections.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @param <ROWTYPE>
+ * the type of the rows in the section
+ */
+abstract class GridStaticSection<ROWTYPE extends GridStaticSection.StaticRow<?>>
+ implements Serializable {
+
+ /**
+ * Abstract base class for Grid header and footer rows.
+ *
+ * @param <CELLTYPE>
+ * the type of the cells in the row
+ */
+ abstract static class StaticRow<CELLTYPE extends StaticCell> implements
+ Serializable {
+
+ private RowState rowState = new RowState();
+ protected GridStaticSection<?> section;
+ private Map<Object, CELLTYPE> cells = new LinkedHashMap<Object, CELLTYPE>();
+ private Collection<List<CELLTYPE>> cellGroups = new HashSet<List<CELLTYPE>>();
+
+ protected StaticRow(GridStaticSection<?> section) {
+ this.section = section;
+ }
+
+ protected void addCell(Object propertyId) {
+ CELLTYPE cell = createCell();
+ cells.put(propertyId, cell);
+ rowState.cells.add(cell.getCellState());
+ }
+
+ /**
+ * Creates and returns a new instance of the cell type.
+ *
+ * @return the created cell
+ */
+ protected abstract CELLTYPE createCell();
+
+ protected RowState getRowState() {
+ return rowState;
+ }
+
+ /**
+ * Returns the cell at the given position in this row.
+ *
+ * @param propertyId
+ * the itemId of column
+ * @return the cell on given column
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public CELLTYPE getCell(Object propertyId) {
+ return cells.get(propertyId);
+ }
+
+ /**
+ * Merges cells in a row
+ *
+ * @param cells
+ * The cells to be merged
+ * @return The first cell of the merged cells
+ */
+ protected CELLTYPE join(List<CELLTYPE> cells) {
+ assert cells.size() > 1 : "You cannot merge less than 2 cells together";
+
+ // Ensure no cell is already grouped
+ for (CELLTYPE cell : cells) {
+ if (getCellGroupForCell(cell) != null) {
+ throw new IllegalStateException("Cell " + cell.getText()
+ + " is already grouped.");
+ }
+ }
+
+ // Ensure continuous range
+ Iterator<CELLTYPE> cellIterator = this.cells.values().iterator();
+ CELLTYPE current = null;
+ int firstIndex = 0;
+
+ while (cellIterator.hasNext()) {
+ current = cellIterator.next();
+ if (current == cells.get(0)) {
+ break;
+ }
+ firstIndex++;
+ }
+
+ for (int i = 1; i < cells.size(); ++i) {
+ current = cellIterator.next();
+
+ if (current != cells.get(i)) {
+ throw new IllegalStateException(
+ "Cell range must be a continous range");
+ }
+ }
+
+ // Create a new group
+ final ArrayList<CELLTYPE> cellGroup = new ArrayList<CELLTYPE>(cells);
+ cellGroups.add(cellGroup);
+
+ // Add group to state
+ List<Integer> stateGroup = new ArrayList<Integer>();
+ for (int i = 0; i < cells.size(); ++i) {
+ stateGroup.add(firstIndex + i);
+ }
+ rowState.cellGroups.add(stateGroup);
+ section.markAsDirty();
+
+ // Returns first cell of group
+ return cells.get(0);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param properties
+ * The column properties which header should be merged
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(Object... properties) {
+ List<CELLTYPE> cells = new ArrayList<CELLTYPE>();
+ for (int i = 0; i < properties.length; ++i) {
+ cells.add(getCell(properties[i]));
+ }
+
+ return join(cells);
+ }
+
+ /**
+ * Merges columns cells in a row
+ *
+ * @param cells
+ * The cells to merge. Must be from the same row.
+ * @return The remaining visible cell after the merge
+ */
+ public CELLTYPE join(CELLTYPE... cells) {
+ return join(Arrays.asList(cells));
+ }
+
+ private List<CELLTYPE> getCellGroupForCell(CELLTYPE cell) {
+ for (List<CELLTYPE> group : cellGroups) {
+ if (group.contains(cell)) {
+ return group;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * A header or footer cell. Has a simple textual caption.
+ */
+ abstract static class StaticCell implements Serializable {
+
+ private CellState cellState = new CellState();
+ private StaticRow<?> row;
+
+ protected StaticCell(StaticRow<?> row) {
+ this.row = row;
+ }
+
+ /**
+ * Gets the row where this cell is.
+ *
+ * @return row for this cell
+ */
+ public StaticRow<?> getRow() {
+ return row;
+ }
+
+ protected CellState getCellState() {
+ return cellState;
+ }
+
+ /**
+ * Sets the text displayed in this cell.
+ *
+ * @param text
+ * a plain text caption
+ */
+ public void setText(String text) {
+ cellState.text = text;
+ cellState.type = GridStaticCellType.TEXT;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the text displayed in this cell.
+ *
+ * @return the plain text caption
+ */
+ public String getText() {
+ if (cellState.type != GridStaticCellType.TEXT) {
+ throw new IllegalStateException(
+ "Cannot fetch Text from a cell with type "
+ + cellState.type);
+ }
+ return cellState.text;
+ }
+
+ /**
+ * Returns the HTML content displayed in this cell.
+ *
+ * @return the html
+ *
+ */
+ public String getHtml() {
+ if (cellState.type != GridStaticCellType.HTML) {
+ throw new IllegalStateException(
+ "Cannot fetch HTML from a cell with type "
+ + cellState.type);
+ }
+ return cellState.html;
+ }
+
+ /**
+ * Sets the HTML content displayed in this cell.
+ *
+ * @param html
+ * the html to set
+ */
+ public void setHtml(String html) {
+ cellState.html = html;
+ cellState.type = GridStaticCellType.HTML;
+ row.section.markAsDirty();
+ }
+
+ /**
+ * Returns the component displayed in this cell.
+ *
+ * @return the component
+ */
+ public Component getComponent() {
+ if (cellState.type != GridStaticCellType.WIDGET) {
+ throw new IllegalStateException(
+ "Cannot fetch Component from a cell with type "
+ + cellState.type);
+ }
+ return (Component) cellState.connector;
+ }
+
+ /**
+ * Sets the component displayed in this cell.
+ *
+ * @param component
+ * the component to set
+ */
+ public void setComponent(Component component) {
+ component.setParent(row.section.grid);
+ cellState.connector = component;
+ cellState.type = GridStaticCellType.WIDGET;
+ row.section.markAsDirty();
+ }
+ }
+
+ protected Grid grid;
+ protected List<ROWTYPE> rows = new ArrayList<ROWTYPE>();
+
+ /**
+ * Sets the visibility of the whole section.
+ *
+ * @param visible
+ * true to show this section, false to hide
+ */
+ public void setVisible(boolean visible) {
+ if (getSectionState().visible != visible) {
+ getSectionState().visible = visible;
+ markAsDirty();
+ }
+ }
+
+ /**
+ * Returns the visibility of this section.
+ *
+ * @return true if visible, false otherwise.
+ */
+ public boolean isVisible() {
+ return getSectionState().visible;
+ }
+
+ /**
+ * Removes the row at the given position.
+ *
+ * @param index
+ * the position of the row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public ROWTYPE removeRow(int rowIndex) {
+ ROWTYPE row = rows.remove(rowIndex);
+ getSectionState().rows.remove(rowIndex);
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Removes the given row from the section.
+ *
+ * @param row
+ * the row to be removed
+ *
+ * @throws IllegalArgumentException
+ * if the row does not exist in this section
+ */
+ public void removeRow(ROWTYPE row) {
+ try {
+ removeRow(rows.indexOf(row));
+ } catch (IndexOutOfBoundsException e) {
+ throw new IllegalArgumentException(
+ "Section does not contain the given row");
+ }
+ }
+
+ /**
+ * Gets row at given index.
+ *
+ * @param rowIndex
+ * 0 based index for row. Counted from top to bottom
+ * @return row at given index
+ */
+ public ROWTYPE getRow(int rowIndex) {
+ return rows.get(rowIndex);
+ }
+
+ /**
+ * Adds a new row at the top of this section.
+ *
+ * @return the new row
+ */
+ public ROWTYPE prependRow() {
+ return addRowAt(0);
+ }
+
+ /**
+ * Adds a new row at the bottom of this section.
+ *
+ * @return the new row
+ */
+ public ROWTYPE appendRow() {
+ return addRowAt(rows.size());
+ }
+
+ /**
+ * Inserts a new row at the given position.
+ *
+ * @param index
+ * the position at which to insert the row
+ * @return the new row
+ *
+ * @throws IndexOutOfBoundsException
+ * if the index is out of bounds
+ */
+ public ROWTYPE addRowAt(int index) {
+ ROWTYPE row = createRow();
+ rows.add(index, row);
+ getSectionState().rows.add(index, row.getRowState());
+
+ Indexed dataSource = grid.getContainerDatasource();
+ for (Object id : dataSource.getContainerPropertyIds()) {
+ row.addCell(id);
+ }
+
+ markAsDirty();
+ return row;
+ }
+
+ /**
+ * Gets the amount of rows in this section.
+ *
+ * @return row count
+ */
+ public int getRowCount() {
+ return rows.size();
+ }
+
+ protected abstract GridStaticSectionState getSectionState();
+
+ protected abstract ROWTYPE createRow();
+
+ /**
+ * Informs the grid that state has changed and it should be redrawn.
+ */
+ protected void markAsDirty() {
+ grid.markAsDirty();
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/Renderer.java b/server/src/com/vaadin/ui/components/grid/Renderer.java
new file mode 100644
index 0000000000..b9074fb9f7
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/Renderer.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2014 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 com.vaadin.server.ClientConnector;
+import com.vaadin.server.Extension;
+
+/**
+ * A ClientConnector for controlling client-side
+ * {@link com.vaadin.client.ui.grid.Renderer Grid renderers}. Renderers
+ * currently extend the Extension interface, but this fact should be regarded as
+ * an implementation detail and subject to change in a future major or minor
+ * Vaadin revision.
+ *
+ * @param <T>
+ * the type this renderer knows how to present
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface Renderer<T> extends Extension {
+
+ /**
+ * Returns the class literal corresponding to the presentation type T.
+ *
+ * @return the class literal of T
+ */
+ Class<T> getPresentationType();
+
+ /**
+ * Encodes the given value into a form that can be transferred to the
+ * client. The type of the returned value must be one of the types that are
+ * accepted by <a href=
+ * "http://www.json.org/javadoc/org/json/JSONObject.html#put%28java.lang.String,%20java.lang.Object%29"
+ * >{@code org.json.JSONObject#put(String, Object)}</a>.
+ *
+ * @param value
+ * the value to encode
+ * @return an encoded form of the given value
+ */
+ Object encode(T value);
+
+ /**
+ * This method is inherited from Extension but should never be called
+ * directly with a Renderer.
+ */
+ @Override
+ @Deprecated
+ void remove();
+
+ /**
+ * This method is inherited from Extension but should never be called
+ * directly with a Renderer.
+ */
+ @Override
+ @Deprecated
+ void setParent(ClientConnector parent);
+}
diff --git a/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java b/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java
new file mode 100644
index 0000000000..690fcdf1c4
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/SortOrderChangeEvent.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2014 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.List;
+
+import com.vaadin.shared.ui.grid.SortEventOriginator;
+import com.vaadin.ui.Component;
+import com.vaadin.ui.components.grid.sort.SortOrder;
+
+/**
+ * Event fired by {@link Grid} when the sort order has changed.
+ *
+ * @see SortOrderChangeListener
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class SortOrderChangeEvent extends Component.Event {
+
+ private final List<SortOrder> sortOrder;
+ private final SortEventOriginator originator;
+
+ /**
+ * Creates a new sort order change event for a grid and a sort order list.
+ *
+ * @param grid
+ * the grid from which the event originates
+ * @param sortOrder
+ * the new sort order list
+ * @param wasInitiatedByUser
+ * should be set to true if this event results from end-user
+ * interaction instead of an API call or side effect
+ */
+ public SortOrderChangeEvent(Grid grid, List<SortOrder> sortOrder,
+ SortEventOriginator originator) {
+ super(grid);
+ this.sortOrder = sortOrder;
+ this.originator = originator;
+ }
+
+ /**
+ * Gets the sort order list.
+ *
+ * @return the sort order list
+ */
+ public List<SortOrder> getSortOrder() {
+ return sortOrder;
+ }
+
+ /**
+ * Gets a value describing the originator of this event, i.e. what actions
+ * resulted in this event being fired.
+ *
+ * @return a sort event originator value
+ *
+ * @see SortEventOriginator
+ */
+ public SortEventOriginator getOriginator() {
+ return originator;
+ }
+
+}
diff --git a/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java b/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java
new file mode 100644
index 0000000000..82d7ba3108
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/SortOrderChangeListener.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2000-2014 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;
+
+/**
+ * Listener for sort order change events from {@link Grid}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface SortOrderChangeListener extends Serializable {
+ /**
+ * Called when the sort order has changed.
+ *
+ * @param event
+ * the sort order change event
+ */
+ public void sortOrderChange(SortOrderChangeEvent event);
+}
diff --git a/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java
new file mode 100644
index 0000000000..736b61d9e2
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/renderers/DateRenderer.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2000-2014 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.renderers;
+
+import java.text.DateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import com.vaadin.ui.components.grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting date values.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class DateRenderer extends AbstractRenderer<Date> {
+ private final Locale locale;
+ private final String formatString;
+ private final DateFormat dateFormat;
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the default locale.
+ */
+ public DateRenderer() {
+ this(Locale.getDefault());
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the {@link Date#toString()}
+ * representation for the given locale.
+ *
+ * @param locale
+ * the locale in which to present dates
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public DateRenderer(Locale locale) throws IllegalArgumentException {
+ this("%s", locale);
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the default locale.
+ *
+ * @param formatString
+ * the format string with which to format the date
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString) throws IllegalArgumentException {
+ this(formatString, Locale.getDefault());
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with the given string format, as
+ * displayed in the given locale.
+ *
+ * @param formatString
+ * the format string to format the date with
+ * @param locale
+ * the locale to use
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public DateRenderer(String formatString, Locale locale)
+ throws IllegalArgumentException {
+ super(Date.class);
+
+ if (formatString == null) {
+ throw new IllegalArgumentException("format string may not be null");
+ }
+
+ if (locale == null) {
+ throw new IllegalArgumentException("locale may not be null");
+ }
+
+ this.locale = locale;
+ this.formatString = formatString;
+ dateFormat = null;
+ }
+
+ /**
+ * Creates a new date renderer.
+ * <p>
+ * The renderer is configured to render with he given date format.
+ *
+ * @param dateFormat
+ * the date format to use when rendering dates
+ * @throws IllegalArgumentException
+ * if {@code dateFormat} is {@code null}
+ */
+ public DateRenderer(DateFormat dateFormat) throws IllegalArgumentException {
+ super(Date.class);
+ if (dateFormat == null) {
+ throw new IllegalArgumentException("date format may not be null");
+ }
+
+ locale = null;
+ formatString = null;
+ this.dateFormat = dateFormat;
+ }
+
+ @Override
+ public String encode(Date value) {
+ if (dateFormat != null) {
+ return dateFormat.format(value);
+ } else {
+ return String.format(locale, formatString, value);
+ }
+ }
+
+ @Override
+ public String toString() {
+ final String fieldInfo;
+ if (dateFormat != null) {
+ fieldInfo = "dateFormat: " + dateFormat.toString();
+ } else {
+ fieldInfo = "locale: " + locale + ", formatString: " + formatString;
+ }
+
+ return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo);
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java
new file mode 100644
index 0000000000..6439608c20
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/renderers/HtmlRenderer.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2000-2014 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.renderers;
+
+import com.vaadin.ui.components.grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting HTML content.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class HtmlRenderer extends AbstractRenderer<String> {
+ /**
+ * Creates a new HTML renderer.
+ */
+ public HtmlRenderer() {
+ super(String.class);
+ }
+
+ @Override
+ public String encode(String value) {
+ return value;
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java
new file mode 100644
index 0000000000..12fcfc890a
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/renderers/NumberRenderer.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2000-2014 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.renderers;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import com.vaadin.ui.components.grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting number values.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class NumberRenderer extends AbstractRenderer<Number> {
+ private final Locale locale;
+ private final NumberFormat numberFormat;
+ private final String formatString;
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the number's natural string
+ * representation in the default locale.
+ */
+ public NumberRenderer() {
+ this(Locale.getDefault());
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render the number as defined with the given
+ * number format.
+ *
+ * @param numberFormat
+ * the number format with which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code numberFormat} is {@code null}
+ */
+ public NumberRenderer(NumberFormat numberFormat)
+ throws IllegalArgumentException {
+ super(Number.class);
+
+ if (numberFormat == null) {
+ throw new IllegalArgumentException("Number format may not be null");
+ }
+
+ locale = null;
+ this.numberFormat = numberFormat;
+ formatString = null;
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the number's natural string
+ * representation in the given locale.
+ *
+ * @param locale
+ * the locale in which to display numbers
+ * @throws IllegalArgumentException
+ * if {@code locale} is {@code null}
+ */
+ public NumberRenderer(Locale locale) throws IllegalArgumentException {
+ this("%s", locale);
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the given format string in the
+ * default locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @throws IllegalArgumentException
+ * if {@code formatString} is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public NumberRenderer(String formatString) throws IllegalArgumentException {
+ this(formatString, Locale.getDefault());
+ }
+
+ /**
+ * Creates a new number renderer.
+ * <p>
+ * The renderer is configured to render with the given format string in the
+ * given locale.
+ *
+ * @param formatString
+ * the format string with which to format the number
+ * @param locale
+ * the locale in which to present numbers
+ * @throws IllegalArgumentException
+ * if either argument is {@code null}
+ * @see <a
+ * href="http://docs.oracle.com/javase/7/docs/api/java/util/Formatter.html#syntax">Format
+ * String Syntax</a>
+ */
+ public NumberRenderer(String formatString, Locale locale) {
+ super(Number.class);
+
+ if (formatString == null) {
+ throw new IllegalArgumentException("Format string may not be null");
+ }
+
+ if (locale == null) {
+ throw new IllegalArgumentException("Locale may not be null");
+ }
+
+ this.locale = locale;
+ numberFormat = null;
+ this.formatString = formatString;
+ }
+
+ @Override
+ public String encode(Number value) {
+ if (formatString != null && locale != null) {
+ return String.format(locale, formatString, value);
+ } else if (numberFormat != null) {
+ return numberFormat.format(value);
+ } else {
+ throw new IllegalStateException(String.format("Internal bug: "
+ + "%s is in an illegal state: "
+ + "[locale: %s, numberFormat: %s, formatString: %s]",
+ getClass().getSimpleName(), locale, numberFormat,
+ formatString));
+ }
+ }
+
+ @Override
+ public String toString() {
+ final String fieldInfo;
+ if (numberFormat != null) {
+ fieldInfo = "numberFormat: " + numberFormat.toString();
+ } else {
+ fieldInfo = "locale: " + locale + ", formatString: " + formatString;
+ }
+
+ return String.format("%s [%s]", getClass().getSimpleName(), fieldInfo);
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java b/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java
new file mode 100644
index 0000000000..61348a9e49
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/renderers/TextRenderer.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2014 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.renderers;
+
+import com.vaadin.ui.components.grid.AbstractRenderer;
+
+/**
+ * A renderer for presenting simple plain-text string values.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class TextRenderer extends AbstractRenderer<String> {
+
+ /**
+ * Creates a new text renderer
+ */
+ public TextRenderer() {
+ super(String.class);
+ }
+
+ @Override
+ public Object encode(String value) {
+ return value;
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java
new file mode 100644
index 0000000000..e153b8a4e4
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * A base class for SelectionModels that contains some of the logic that is
+ * reusable.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class AbstractSelectionModel implements SelectionModel {
+ protected final LinkedHashSet<Object> selection = new LinkedHashSet<Object>();
+ protected Grid grid = null;
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return selection.contains(itemId);
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return new ArrayList<Object>(selection);
+ }
+
+ @Override
+ public void setGrid(final Grid grid) {
+ this.grid = grid;
+ }
+
+ /**
+ * Fires a {@link SelectionChangeEvent} to all the
+ * {@link SelectionChangeListener SelectionChangeListeners} currently added
+ * to the Grid in which this SelectionModel is.
+ * <p>
+ * Note that this is only a helper method, and routes the call all the way
+ * to Grid. A {@link SelectionModel} is not a
+ * {@link SelectionChangeNotifier}
+ *
+ * @param oldSelection
+ * the complete {@link Collection} of the itemIds that were
+ * selected <em>before</em> this event happened
+ * @param newSelection
+ * the complete {@link Collection} of the itemIds that are
+ * selected <em>after</em> this event happened
+ */
+ protected void fireSelectionChangeEvent(
+ final Collection<Object> oldSelection,
+ final Collection<Object> newSelection) {
+ grid.fireSelectionChangeEvent(oldSelection, newSelection);
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java
new file mode 100644
index 0000000000..602e5ca169
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+
+import com.vaadin.data.Container.Indexed;
+
+/**
+ * A default implementation of a {@link SelectionModel.Multi}
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class MultiSelectionModel extends AbstractSelectionModel implements
+ SelectionModel.Multi {
+
+ @Override
+ public boolean select(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // select will fire the event
+ return select(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ @Override
+ public boolean select(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ final boolean hasSomeDifferingElements = !selection
+ .containsAll(itemIds);
+ if (hasSomeDifferingElements) {
+ final HashSet<Object> oldSelection = new HashSet<Object>(selection);
+ selection.addAll(itemIds);
+ fireSelectionChangeEvent(oldSelection, selection);
+ }
+ return hasSomeDifferingElements;
+ }
+
+ @Override
+ public boolean deselect(final Object... itemIds)
+ throws IllegalArgumentException {
+ if (itemIds != null) {
+ // deselect will fire the event
+ return deselect(Arrays.asList(itemIds));
+ } else {
+ throw new IllegalArgumentException(
+ "Vararg array of itemIds may not be null");
+ }
+ }
+
+ @Override
+ public boolean deselect(final Collection<?> itemIds)
+ throws IllegalArgumentException {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds may not be null");
+ }
+
+ final boolean hasCommonElements = !Collections.disjoint(itemIds,
+ selection);
+ if (hasCommonElements) {
+ final HashSet<Object> oldSelection = new HashSet<Object>(selection);
+ selection.removeAll(itemIds);
+ fireSelectionChangeEvent(oldSelection, selection);
+ }
+ return hasCommonElements;
+ }
+
+ @Override
+ public boolean selectAll() {
+ // select will fire the event
+ final Indexed container = grid.getContainerDatasource();
+ if (container != null) {
+ return select(container.getItemIds());
+ } else if (selection.isEmpty()) {
+ return false;
+ } else {
+ /*
+ * this should never happen (no container but has a selection), but
+ * I guess the only theoretically correct course of action...
+ */
+ return deselectAll();
+ }
+ }
+
+ @Override
+ public boolean deselectAll() {
+ // deselect will fire the event
+ return deselect(getSelectedRows());
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * The returned Collection is in <strong>order of selection</strong> &ndash;
+ * the item that was first selected will be first in the collection, and so
+ * on. Should an item have been selected twice without being deselected in
+ * between, it will have remained in its original position.
+ */
+ @Override
+ public Collection<Object> getSelectedRows() {
+ // overridden only for JavaDoc
+ return super.getSelectedRows();
+ }
+
+ /**
+ * Resets the selection model.
+ * <p>
+ * Equivalent to calling {@link #deselectAll()}
+ */
+ @Override
+ public void reset() {
+ deselectAll();
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java
new file mode 100644
index 0000000000..89c31398ea
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * A default implementation for a {@link SelectionModel.None}
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class NoSelectionModel implements SelectionModel.None {
+ @Override
+ public void setGrid(final Grid grid) {
+ // NOOP, not needed for anything
+ }
+
+ @Override
+ public boolean isSelected(final Object itemId) {
+ return false;
+ }
+
+ @Override
+ public Collection<Object> getSelectedRows() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Semantically resets the selection model.
+ * <p>
+ * Effectively a no-op.
+ */
+ @Override
+ public void reset() {
+ // NOOP
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java
new file mode 100644
index 0000000000..af6a37dfde
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.Collection;
+import java.util.EventObject;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * An event that specifies what in a selection has changed, and where the
+ * selection took place.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class SelectionChangeEvent extends EventObject {
+
+ private LinkedHashSet<Object> oldSelection;
+ private LinkedHashSet<Object> newSelection;
+
+ public SelectionChangeEvent(Grid source, Collection<Object> oldSelection,
+ Collection<Object> newSelection) {
+ super(source);
+ this.oldSelection = new LinkedHashSet<Object>(oldSelection);
+ this.newSelection = new LinkedHashSet<Object>(newSelection);
+ }
+
+ /**
+ * A {@link Collection} of all the itemIds that became selected.
+ * <p>
+ * <em>Note:</em> this excludes all itemIds that might have been previously
+ * selected.
+ *
+ * @return a Collection of the itemIds that became selected
+ */
+ public Set<Object> getAdded() {
+ return Sets.difference(newSelection, oldSelection);
+ }
+
+ /**
+ * A {@link Collection} of all the itemIds that became deselected.
+ * <p>
+ * <em>Note:</em> this excludes all itemIds that might have been previously
+ * deselected.
+ *
+ * @return a Collection of the itemIds that became deselected
+ */
+ public Set<Object> getRemoved() {
+ return Sets.difference(oldSelection, newSelection);
+ }
+
+ @Override
+ public Grid getSource() {
+ return (Grid) super.getSource();
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java
new file mode 100644
index 0000000000..0d10e8c74d
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.io.Serializable;
+
+/**
+ * The listener interface for receiving {@link SelectionChangeEvent
+ * SelectionChangeEvents}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface SelectionChangeListener extends Serializable {
+ /**
+ * Notifies the listener that the selection state has changed.
+ *
+ * @param event
+ * the selection change event
+ */
+ void selectionChange(SelectionChangeEvent event);
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java
new file mode 100644
index 0000000000..40cef965dd
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.io.Serializable;
+
+/**
+ * The interface for adding and removing listeners for
+ * {@link SelectionChangeEvent SelectionChangeEvents}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface SelectionChangeNotifier extends Serializable {
+ /**
+ * Registers a new selection change listener
+ *
+ * @param listener
+ * the listener to register
+ */
+ void addSelectionChangeListener(SelectionChangeListener listener);
+
+ /**
+ * Removes a previously registered selection change listener
+ *
+ * @param listener
+ * the listener to remove
+ */
+ void removeSelectionChangeListener(SelectionChangeListener listener);
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java
new file mode 100644
index 0000000000..60bb130ab1
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * The server-side interface that controls Grid's selection state.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface SelectionModel extends Serializable {
+ /**
+ * Checks whether an item is selected or not.
+ *
+ * @param itemId
+ * the item id to check for
+ * @return <code>true</code> iff the item is selected
+ */
+ boolean isSelected(Object itemId);
+
+ /**
+ * Returns a collection of all the currently selected itemIds.
+ *
+ * @return a collection of all the currently selected itemIds
+ */
+ Collection<Object> getSelectedRows();
+
+ /**
+ * Injects the current {@link Grid} instance into the SelectionModel.
+ * <p>
+ * <em>Note:</em> This method should not be called manually.
+ *
+ * @param grid
+ * the Grid in which the SelectionModel currently is, or
+ * <code>null</code> when a selection model is being detached
+ * from a Grid.
+ */
+ void setGrid(Grid grid);
+
+ /**
+ * Resets the SelectiomModel to an initial state.
+ * <p>
+ * Most often this means that the selection state is cleared, but
+ * implementations are free to interpret the "initial state" as they wish.
+ * Some, for example, may want to keep the first selected item as selected.
+ */
+ void reset();
+
+ /**
+ * A SelectionModel that supports multiple selections to be made.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter how
+ * the selection model is interacted with. In other words, if something is
+ * forbidden to do in e.g. the user interface, it must also be forbidden to
+ * do in the server-side and client-side APIs.
+ */
+ public interface Multi extends SelectionModel {
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only adds to
+ * it.
+ *
+ * @param itemIds
+ * the itemId(s) to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code>
+ * @see #deselect(Object...)
+ */
+ boolean select(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as selected.
+ * <p>
+ * This method does not clear any previous selection state, only adds to
+ * it.
+ *
+ * @param itemIds
+ * the itemIds to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if all the given itemIds already were
+ * selected
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code>
+ * @see #deselect(Collection)
+ */
+ boolean select(Collection<?> itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were selected
+ * previously
+ * @throws IllegalArgumentException
+ * if the <code>itemIds</code> varargs array is
+ * <code>null</code>
+ * @see #select(Object...)
+ */
+ boolean deselect(Object... itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks items as deselected.
+ *
+ * @param itemIds
+ * the itemId(s) to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if none the given itemIds were selected
+ * previously
+ * @throws IllegalArgumentException
+ * if <code>itemIds</code> is <code>null</code>
+ * @see #select(Collection)
+ */
+ boolean deselect(Collection<?> itemIds) throws IllegalArgumentException;
+
+ /**
+ * Marks all the items in the current Container as selected
+ *
+ * @return <code>true</code> iff some items were previously not selected
+ * @see #deselectAll()
+ */
+ boolean selectAll();
+
+ /**
+ * Marks all the items in the current Container as deselected
+ *
+ * @return <code>true</code> iff some items were previously selected
+ * @see #selectAll()
+ */
+ boolean deselectAll();
+ }
+
+ /**
+ * A SelectionModel that supports for only single rows to be selected at a
+ * time.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter how
+ * the selection model is interacted with. In other words, if something is
+ * forbidden to do in e.g. the user interface, it must also be forbidden to
+ * do in the server-side and client-side APIs.
+ */
+ public interface Single extends SelectionModel {
+ /**
+ * Marks an item as selected.
+ *
+ * @param itemIds
+ * the itemId to mark as selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalStateException
+ * if the selection was illegal. One such reason might be
+ * that the implementation already had an item selected, and
+ * that needs to be explicitly deselected before
+ * re-selecting something
+ * @see #deselect(Object)
+ */
+ boolean select(Object itemId) throws IllegalStateException;
+
+ /**
+ * Marks an item as deselected.
+ *
+ * @param itemId
+ * the itemId to remove from being selected
+ * @return <code>true</code> if the selection state changed.
+ * <code>false</code> if the itemId already was selected
+ * @throws IllegalStateException
+ * if the deselection was illegal. One such reason might be
+ * that the implementation enforces that an item is always
+ * selected
+ * @see #select(Object)
+ */
+ boolean deselect(Object itemId) throws IllegalStateException;
+
+ /**
+ * Gets the item id of the currently selected item.
+ *
+ * @return the item id of the currently selected item, or
+ * <code>null</code> if nothing is selected
+ */
+ Object getSelectedRow();
+ }
+
+ /**
+ * A SelectionModel that does not allow for rows to be selected.
+ * <p>
+ * This interface has a contract of having the same behavior, no matter how
+ * the selection model is interacted with. In other words, if the developer
+ * is unable to select something programmatically, it is not allowed for the
+ * end-user to select anything, either.
+ */
+ public interface None extends SelectionModel {
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always <code>false</code>.
+ */
+ @Override
+ public boolean isSelected(Object itemId);
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return always an empty collection.
+ */
+ @Override
+ public Collection<Object> getSelectedRows();
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java
new file mode 100644
index 0000000000..0f6e8a296d
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2000-2014 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.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * A default implementation of a {@link SelectionModel.Single}
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class SingleSelectionModel extends AbstractSelectionModel implements
+ SelectionModel.Single {
+ @Override
+ public boolean select(final Object itemId) {
+ final Object selectedRow = getSelectedRow();
+ final boolean modified = selection.add(itemId);
+ if (modified) {
+ final Collection<Object> deselected;
+ if (selectedRow != null) {
+ deselectInternal(selectedRow, false);
+ deselected = Collections.singleton(selectedRow);
+ } else {
+ deselected = Collections.emptySet();
+ }
+
+ fireSelectionChangeEvent(deselected, selection);
+ }
+
+ return modified;
+ }
+
+ @Override
+ public boolean deselect(final Object itemId) {
+ return deselectInternal(itemId, true);
+ }
+
+ private boolean deselectInternal(final Object itemId,
+ boolean fireEventIfNeeded) {
+ final boolean modified = selection.remove(itemId);
+ if (fireEventIfNeeded && modified) {
+ fireSelectionChangeEvent(Collections.singleton(itemId),
+ Collections.emptySet());
+ }
+ return modified;
+ }
+
+ @Override
+ public Object getSelectedRow() {
+ if (selection.isEmpty()) {
+ return null;
+ } else {
+ return selection.iterator().next();
+ }
+ }
+
+ /**
+ * Resets the selection state.
+ * <p>
+ * If an item is selected, it will become deselected.
+ */
+ @Override
+ public void reset() {
+ deselect(getSelectedRow());
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/sort/Sort.java b/server/src/com/vaadin/ui/components/grid/sort/Sort.java
new file mode 100644
index 0000000000..54831378b6
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/sort/Sort.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2000-2014 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.sort;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.vaadin.shared.ui.grid.SortDirection;
+
+/**
+ * Fluid Sort API. Provides a convenient, human-readable way of specifying
+ * multi-column sort order.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class Sort implements Serializable {
+
+ private final Sort previous;
+ private final SortOrder order;
+
+ /**
+ * Initial constructor, called by the static by() methods.
+ *
+ * @param propertyId
+ * a property ID, corresponding to a property in the data source
+ * @param direction
+ * a sort direction value
+ */
+ private Sort(Object propertyId, SortDirection direction) {
+ previous = null;
+ order = new SortOrder(propertyId, direction);
+ }
+
+ /**
+ * Chaining constructor, called by the non-static then() methods. This
+ * constructor links to the previous Sort object.
+ *
+ * @param previous
+ * the sort marker that comes before this one
+ * @param propertyId
+ * a property ID, corresponding to a property in the data source
+ * @param direction
+ * a sort direction value
+ */
+ private Sort(Sort previous, Object propertyId, SortDirection direction) {
+ this.previous = previous;
+ order = new SortOrder(propertyId, direction);
+
+ Sort s = previous;
+ while (s != null) {
+ if (s.order.getPropertyId() == propertyId) {
+ throw new IllegalStateException(
+ "Can not sort along the same property (" + propertyId
+ + ") twice!");
+ }
+ s = s.previous;
+ }
+
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column in ascending
+ * order.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @return a sort object
+ */
+ public static Sort by(Object propertyId) {
+ return by(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Start building a Sort order by sorting a provided column.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @param direction
+ * a sort direction value
+ * @return a sort object
+ */
+ public static Sort by(Object propertyId, SortDirection direction) {
+ return new Sort(propertyId, direction);
+ }
+
+ /**
+ * Continue building a Sort order. The provided property is sorted in
+ * ascending order if the previously added properties have been evaluated as
+ * equals.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @return a sort object
+ */
+ public Sort then(Object propertyId) {
+ return then(propertyId, SortDirection.ASCENDING);
+ }
+
+ /**
+ * Continue building a Sort order. The provided property is sorted in
+ * specified order if the previously added properties have been evaluated as
+ * equals.
+ *
+ * @param propertyId
+ * a property id, corresponding to a data source property
+ * @param direction
+ * a sort direction value
+ * @return a sort object
+ */
+ public Sort then(Object propertyId, SortDirection direction) {
+ return new Sort(this, propertyId, direction);
+ }
+
+ /**
+ * Build a sort order list, ready to be passed to Grid
+ *
+ * @return a sort order list.
+ */
+ public List<SortOrder> build() {
+
+ int count = 1;
+ Sort s = this;
+ while (s.previous != null) {
+ s = s.previous;
+ ++count;
+ }
+
+ List<SortOrder> order = new ArrayList<SortOrder>(count);
+
+ s = this;
+ do {
+ order.add(0, s.order);
+ s = s.previous;
+ } while (s != null);
+
+ return order;
+ }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java b/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java
new file mode 100644
index 0000000000..a76148fe0c
--- /dev/null
+++ b/server/src/com/vaadin/ui/components/grid/sort/SortOrder.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2000-2014 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.sort;
+
+import java.io.Serializable;
+
+import com.vaadin.shared.ui.grid.SortDirection;
+
+/**
+ * Sort order descriptor. Links together a {@link SortDirection} value and a
+ * Vaadin container property ID.
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public class SortOrder implements Serializable {
+
+ private final Object propertyId;
+ private final SortDirection direction;
+
+ /**
+ * Create a SortOrder object. Both arguments must be non-null.
+ *
+ * @param propertyId
+ * id of the data source property to sort by
+ * @param direction
+ * value indicating whether the property id should be sorted in
+ * ascending or descending order
+ */
+ public SortOrder(Object propertyId, SortDirection direction) {
+ if (propertyId == null) {
+ throw new IllegalArgumentException("Property ID can not be null!");
+ }
+ if (direction == null) {
+ throw new IllegalArgumentException(
+ "Direction value can not be null!");
+ }
+ this.propertyId = propertyId;
+ this.direction = direction;
+ }
+
+ /**
+ * Returns the property ID.
+ *
+ * @return a property ID
+ */
+ public Object getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Returns the {@link SortDirection} value.
+ *
+ * @return a sort direction value
+ */
+ public SortDirection getDirection() {
+ return direction;
+ }
+
+ @Override
+ public String toString() {
+ return propertyId + " " + direction;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + direction.hashCode();
+ result = prime * result + propertyId.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ } else if (obj == null) {
+ return false;
+ } else if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ SortOrder other = (SortOrder) obj;
+ if (direction != other.direction) {
+ return false;
+ } else if (!propertyId.equals(other.propertyId)) {
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java
index b58d962d96..463f0c92c1 100644
--- a/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java
+++ b/server/tests/src/com/vaadin/data/util/BeanItemContainerTest.java
@@ -10,8 +10,15 @@ import java.util.Map;
import org.junit.Assert;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+
import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
import com.vaadin.data.Item;
+import com.vaadin.data.util.filter.Compare;
/**
* Test basic functionality of BeanItemContainer.
@@ -727,4 +734,182 @@ public class BeanItemContainerTest extends AbstractBeanContainerTest {
assertNull(container.getContainerProperty(john, "address.street")
.getValue());
}
+
+ public void testItemAddedEvent() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class));
+ EasyMock.replay(addListener);
+
+ container.addItem(bean);
+
+ EasyMock.verify(addListener);
+ }
+
+ public void testItemAddedEvent_AddedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ container.addItem(bean);
+
+ assertEquals(bean, capturedEvent.getValue().getFirstItemId());
+ }
+
+ public void testItemAddedEvent_addItemAt_IndexOfAddedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ container.addItemAt(1, new Person(""));
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ public void testItemAddedEvent_addItemAfter_IndexOfAddedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ container.addItemAfter(bean, new Person(""));
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ public void testItemAddedEvent_amountOfAddedItems() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+ List<Person> beans = Arrays.asList(new Person("Jack"), new Person(
+ "John"));
+
+ container.addAll(beans);
+
+ assertEquals(2, capturedEvent.getValue().getAddedItemsCount());
+ }
+
+ public void testItemAddedEvent_someItemsAreFiltered_amountOfAddedItemsIsReducedByAmountOfFilteredItems() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+ List<Person> beans = Arrays.asList(new Person("Jack"), new Person(
+ "John"));
+ container.addFilter(new Compare.Equal("name", "John"));
+
+ container.addAll(beans);
+
+ assertEquals(1, capturedEvent.getValue().getAddedItemsCount());
+ }
+
+ public void testItemAddedEvent_someItemsAreFiltered_addedItemIsTheFirstVisibleItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+ List<Person> beans = Arrays.asList(new Person("Jack"), bean);
+ container.addFilter(new Compare.Equal("name", "John"));
+
+ container.addAll(beans);
+
+ assertEquals(bean, capturedEvent.getValue().getFirstItemId());
+ }
+
+ public void testItemRemovedEvent() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ removeListener.containerItemSetChange(EasyMock
+ .isA(ItemRemoveEvent.class));
+ EasyMock.replay(removeListener);
+
+ container.removeItem(bean);
+
+ EasyMock.verify(removeListener);
+ }
+
+ public void testItemRemovedEvent_RemovedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ Person bean = new Person("John");
+ container.addItem(bean);
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(bean);
+
+ assertEquals(bean, capturedEvent.getValue().getFirstItemId());
+ }
+
+ public void testItemRemovedEvent_indexOfRemovedItem() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ container.addItem(new Person("Jack"));
+ Person secondBean = new Person("John");
+ container.addItem(secondBean);
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(secondBean);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ public void testItemRemovedEvent_amountOfRemovedItems() {
+ BeanItemContainer<Person> container = new BeanItemContainer<Person>(
+ Person.class);
+ container.addItem(new Person("Jack"));
+ container.addItem(new Person("John"));
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeAllItems();
+
+ assertEquals(2, capturedEvent.getValue().getRemovedItemsCount());
+ }
+
+ private Capture<ItemAddEvent> captureAddEvent(
+ ItemSetChangeListener addListener) {
+ Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>();
+ addListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private Capture<ItemRemoveEvent> captureRemoveEvent(
+ ItemSetChangeListener removeListener) {
+ Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>();
+ removeListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private ItemSetChangeListener createListenerMockFor(
+ BeanItemContainer<Person> container) {
+ ItemSetChangeListener listener = EasyMock
+ .createNiceMock(ItemSetChangeListener.class);
+ container.addItemSetChangeListener(listener);
+ return listener;
+ }
}
diff --git a/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java b/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java
index eacee7e301..ddfee103c3 100644
--- a/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java
+++ b/server/tests/src/com/vaadin/data/util/TestIndexedContainer.java
@@ -4,6 +4,12 @@ import java.util.List;
import org.junit.Assert;
+import org.easymock.Capture;
+import org.easymock.EasyMock;
+
+import com.vaadin.data.Container.Indexed.ItemAddEvent;
+import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
+import com.vaadin.data.Container.ItemSetChangeListener;
import com.vaadin.data.Item;
public class TestIndexedContainer extends AbstractInMemoryContainerTest {
@@ -271,6 +277,113 @@ public class TestIndexedContainer extends AbstractInMemoryContainerTest {
counter.assertNone();
}
+ public void testItemAddedEvent() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ addListener.containerItemSetChange(EasyMock.isA(ItemAddEvent.class));
+ EasyMock.replay(addListener);
+
+ container.addItem();
+
+ EasyMock.verify(addListener);
+ }
+
+ public void testItemAddedEvent_AddedItem() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ Object itemId = container.addItem();
+
+ assertEquals(itemId, capturedEvent.getValue().getFirstItemId());
+ }
+
+ public void testItemAddedEvent_IndexOfAddedItem() {
+ IndexedContainer container = new IndexedContainer();
+ ItemSetChangeListener addListener = createListenerMockFor(container);
+ container.addItem();
+ Capture<ItemAddEvent> capturedEvent = captureAddEvent(addListener);
+ EasyMock.replay(addListener);
+
+ Object itemId = container.addItemAt(1);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ public void testItemRemovedEvent() {
+ IndexedContainer container = new IndexedContainer();
+ Object itemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ removeListener.containerItemSetChange(EasyMock
+ .isA(ItemRemoveEvent.class));
+ EasyMock.replay(removeListener);
+
+ container.removeItem(itemId);
+
+ EasyMock.verify(removeListener);
+ }
+
+ public void testItemRemovedEvent_RemovedItem() {
+ IndexedContainer container = new IndexedContainer();
+ Object itemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(itemId);
+
+ assertEquals(itemId, capturedEvent.getValue().getFirstItemId());
+ }
+
+ public void testItemRemovedEvent_indexOfRemovedItem() {
+ IndexedContainer container = new IndexedContainer();
+ container.addItem();
+ Object secondItemId = container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeItem(secondItemId);
+
+ assertEquals(1, capturedEvent.getValue().getFirstIndex());
+ }
+
+ public void testItemRemovedEvent_amountOfRemovedItems() {
+ IndexedContainer container = new IndexedContainer();
+ container.addItem();
+ container.addItem();
+ ItemSetChangeListener removeListener = createListenerMockFor(container);
+ Capture<ItemRemoveEvent> capturedEvent = captureRemoveEvent(removeListener);
+ EasyMock.replay(removeListener);
+
+ container.removeAllItems();
+
+ assertEquals(2, capturedEvent.getValue().getRemovedItemsCount());
+ }
+
+ private Capture<ItemAddEvent> captureAddEvent(
+ ItemSetChangeListener addListener) {
+ Capture<ItemAddEvent> capturedEvent = new Capture<ItemAddEvent>();
+ addListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private Capture<ItemRemoveEvent> captureRemoveEvent(
+ ItemSetChangeListener removeListener) {
+ Capture<ItemRemoveEvent> capturedEvent = new Capture<ItemRemoveEvent>();
+ removeListener.containerItemSetChange(EasyMock.capture(capturedEvent));
+ return capturedEvent;
+ }
+
+ private ItemSetChangeListener createListenerMockFor(
+ IndexedContainer container) {
+ ItemSetChangeListener listener = EasyMock
+ .createNiceMock(ItemSetChangeListener.class);
+ container.addItemSetChangeListener(listener);
+ return listener;
+ }
+
// Ticket 8028
public void testGetItemIdsRangeIndexOutOfBounds() {
IndexedContainer ic = new IndexedContainer();
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
new file mode 100644
index 0000000000..9ecf131c5b
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2000-2014 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.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.RpcDataProviderExtension;
+import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper;
+import com.vaadin.data.util.IndexedContainer;
+
+public class DataProviderExtension {
+ private RpcDataProviderExtension dataProvider;
+ private DataProviderKeyMapper keyMapper;
+ private Container.Indexed container;
+
+ private static final Object ITEM_ID1 = "itemid1";
+ private static final Object ITEM_ID2 = "itemid2";
+ private static final Object ITEM_ID3 = "itemid3";
+
+ private static final Object PROPERTY_ID1_STRING = "property1";
+
+ @Before
+ public void setup() {
+ container = new IndexedContainer();
+ populate(container);
+
+ dataProvider = new RpcDataProviderExtension(container);
+ keyMapper = dataProvider.getKeyMapper();
+ }
+
+ private static void populate(Indexed container) {
+ container.addContainerProperty(PROPERTY_ID1_STRING, String.class, "");
+ for (Object itemId : Arrays.asList(ITEM_ID1, ITEM_ID2, ITEM_ID3)) {
+ final Item item = container.addItem(itemId);
+ @SuppressWarnings("unchecked")
+ final Property<String> stringProperty = item
+ .getItemProperty(PROPERTY_ID1_STRING);
+ stringProperty.setValue(itemId.toString());
+ }
+ }
+
+ @Test
+ public void pinBasics() {
+ assertFalse("itemId1 should not start as pinned",
+ keyMapper.isPinned(ITEM_ID2));
+
+ keyMapper.pin(ITEM_ID1);
+ assertTrue("itemId1 should now be pinned", keyMapper.isPinned(ITEM_ID1));
+
+ keyMapper.unpin(ITEM_ID1);
+ assertFalse("itemId1 should not be pinned anymore",
+ keyMapper.isPinned(ITEM_ID2));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void doublePinning() {
+ keyMapper.pin(ITEM_ID1);
+ keyMapper.pin(ITEM_ID1);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void nonexistentUnpin() {
+ keyMapper.unpin(ITEM_ID1);
+ }
+}
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..d1c821cc54
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright 2000-2014 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(), grid.getHeader()
+ .getDefaultRow().getCell(propertyId).getText());
+ }
+ }
+
+ @Test
+ public void testModifyingColumnProperties() throws Exception {
+
+ // Modify first column
+ GridColumn column = grid.getColumn("column1");
+ assertNotNull(column);
+
+ 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.getHeader().isVisible());
+ assertTrue(state.header.visible);
+
+ grid.getHeader().setVisible(false);
+ assertFalse(grid.getHeader().isVisible());
+ assertFalse(state.header.visible);
+
+ grid.getHeader().setVisible(true);
+ assertTrue(grid.getHeader().isVisible());
+ assertTrue(state.header.visible);
+ }
+
+ @Test
+ public void testFooterVisibility() throws Exception {
+
+ assertFalse(grid.getFooter().isVisible());
+ assertFalse(state.footer.visible);
+
+ grid.getFooter().setVisible(true);
+ assertTrue(grid.getFooter().isVisible());
+ assertTrue(state.footer.visible);
+
+ grid.getFooter().setVisible(false);
+ assertFalse(grid.getFooter().isVisible());
+ assertFalse(state.footer.visible);
+ }
+
+ @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/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java
new file mode 100644
index 0000000000..7993d31295
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java
@@ -0,0 +1,306 @@
+/*
+ * 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.assertTrue;
+
+import java.util.Collection;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.Grid.SelectionMode;
+import com.vaadin.ui.components.grid.selection.SelectionChangeEvent;
+import com.vaadin.ui.components.grid.selection.SelectionChangeListener;
+import com.vaadin.ui.components.grid.selection.SelectionModel;
+
+/**
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridSelection {
+
+ private static class MockSelectionChangeListener implements
+ SelectionChangeListener {
+ private SelectionChangeEvent event;
+
+ @Override
+ public void selectionChange(final SelectionChangeEvent event) {
+ this.event = event;
+ }
+
+ public Collection<?> getAdded() {
+ return event.getAdded();
+ }
+
+ public Collection<?> getRemoved() {
+ return event.getRemoved();
+ }
+
+ public void clearEvent() {
+ /*
+ * This method is not strictly needed as the event will simply be
+ * overridden, but it's good practice, and makes the code more
+ * obvious.
+ */
+ event = null;
+ }
+
+ public boolean eventHasHappened() {
+ return event != null;
+ }
+ }
+
+ private Grid grid;
+ private MockSelectionChangeListener mockListener;
+
+ private final Object itemId1Present = "itemId1Present";
+ private final Object itemId2Present = "itemId2Present";
+
+ private final Object itemId1NotPresent = "itemId1NotPresent";
+ private final Object itemId2NotPresent = "itemId2NotPresent";
+
+ @Before
+ public void setup() {
+ final IndexedContainer container = new IndexedContainer();
+ container.addItem(itemId1Present);
+ container.addItem(itemId2Present);
+ for (int i = 2; i < 10; i++) {
+ container.addItem(new Object());
+ }
+
+ assertEquals("init size", 10, container.size());
+ assertTrue("itemId1Present", container.containsId(itemId1Present));
+ assertTrue("itemId2Present", container.containsId(itemId2Present));
+ assertFalse("itemId1NotPresent",
+ container.containsId(itemId1NotPresent));
+ assertFalse("itemId2NotPresent",
+ container.containsId(itemId2NotPresent));
+
+ grid = new Grid(container);
+
+ mockListener = new MockSelectionChangeListener();
+ grid.addSelectionChangeListener(mockListener);
+
+ assertFalse("eventHasHappened", mockListener.eventHasHappened());
+ }
+
+ @Test
+ public void defaultSelectionModeIsMulti() {
+ assertTrue(grid.getSelectionModel() instanceof SelectionModel.Multi);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void getSelectedRowThrowsExceptionMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.getSelectedRow();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void getSelectedRowThrowsExceptionNone() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.getSelectedRow();
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void selectThrowsExceptionNone() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.select(itemId1Present);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void deselectRowThrowsExceptionNone() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.deselect(itemId1Present);
+ }
+
+ @Test
+ public void selectionModeMapsToMulti() {
+ assertTrue(grid.setSelectionMode(SelectionMode.MULTI) instanceof SelectionModel.Multi);
+ }
+
+ @Test
+ public void selectionModeMapsToSingle() {
+ assertTrue(grid.setSelectionMode(SelectionMode.SINGLE) instanceof SelectionModel.Single);
+ }
+
+ @Test
+ public void selectionModeMapsToNone() {
+ assertTrue(grid.setSelectionMode(SelectionMode.NONE) instanceof SelectionModel.None);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void selectionModeNullThrowsException() {
+ grid.setSelectionMode(null);
+ }
+
+ @Test
+ public void noSelectModel_isSelected() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ assertFalse("itemId1Present", grid.isSelected(itemId1Present));
+ assertFalse("itemId1NotPresent", grid.isSelected(itemId1NotPresent));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void noSelectModel_getSelectedRow() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ grid.getSelectedRow();
+ }
+
+ @Test
+ public void noSelectModel_getSelectedRows() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ assertTrue(grid.getSelectedRows().isEmpty());
+ }
+
+ @Test
+ public void selectionCallsListenerMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ selectionCallsListener();
+ }
+
+ @Test
+ public void selectionCallsListenerSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ selectionCallsListener();
+ }
+
+ private void selectionCallsListener() {
+ grid.select(itemId1Present);
+ assertEquals("added size", 1, mockListener.getAdded().size());
+ assertEquals("added item", itemId1Present, mockListener.getAdded()
+ .iterator().next());
+ assertEquals("removed size", 0, mockListener.getRemoved().size());
+ }
+
+ @Test
+ public void deselectionCallsListenerMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ deselectionCallsListener();
+ }
+
+ @Test
+ public void deselectionCallsListenerSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ deselectionCallsListener();
+ }
+
+ private void deselectionCallsListener() {
+ grid.select(itemId1Present);
+ mockListener.clearEvent();
+
+ grid.deselect(itemId1Present);
+ assertEquals("removed size", 1, mockListener.getRemoved().size());
+ assertEquals("removed item", itemId1Present, mockListener.getRemoved()
+ .iterator().next());
+ assertEquals("removed size", 0, mockListener.getAdded().size());
+ }
+
+ @Test
+ public void deselectPresentButNotSelectedItemIdShouldntFireListenerMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ deselectPresentButNotSelectedItemIdShouldntFireListener();
+ }
+
+ @Test
+ public void deselectPresentButNotSelectedItemIdShouldntFireListenerSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ deselectPresentButNotSelectedItemIdShouldntFireListener();
+ }
+
+ private void deselectPresentButNotSelectedItemIdShouldntFireListener() {
+ grid.deselect(itemId1Present);
+ assertFalse(mockListener.eventHasHappened());
+ }
+
+ @Test
+ public void deselectNotPresentItemIdShouldNotThrowExceptionMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.deselect(itemId1NotPresent);
+ }
+
+ @Test
+ public void deselectNotPresentItemIdShouldNotThrowExceptionSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ grid.deselect(itemId1NotPresent);
+ }
+
+ @Test
+ public void selectNotPresentItemIdShouldNotThrowExceptionMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ grid.select(itemId1NotPresent);
+ }
+
+ @Test
+ public void selectNotPresentItemIdShouldNotThrowExceptionSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ grid.select(itemId1NotPresent);
+ }
+
+ @Test
+ public void selectAllMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ final SelectionModel.Multi select = (SelectionModel.Multi) grid
+ .getSelectionModel();
+ select.selectAll();
+ assertEquals("added size", 10, mockListener.getAdded().size());
+ assertEquals("removed size", 0, mockListener.getRemoved().size());
+ assertTrue("itemId1Present",
+ mockListener.getAdded().contains(itemId1Present));
+ assertTrue("itemId2Present",
+ mockListener.getAdded().contains(itemId2Present));
+ }
+
+ @Test
+ public void deselectAllMulti() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ final SelectionModel.Multi select = (SelectionModel.Multi) grid
+ .getSelectionModel();
+ select.selectAll();
+ mockListener.clearEvent();
+
+ select.deselectAll();
+ assertEquals("removed size", 10, mockListener.getRemoved().size());
+ assertEquals("added size", 0, mockListener.getAdded().size());
+ assertTrue("itemId1Present",
+ mockListener.getRemoved().contains(itemId1Present));
+ assertTrue("itemId2Present",
+ mockListener.getRemoved().contains(itemId2Present));
+ assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty());
+ }
+
+ @Test
+ public void reselectionDeselectsPreviousSingle() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ grid.select(itemId1Present);
+ mockListener.clearEvent();
+
+ grid.select(itemId2Present);
+ assertEquals("added size", 1, mockListener.getAdded().size());
+ assertEquals("removed size", 1, mockListener.getRemoved().size());
+ assertEquals("added item", itemId2Present, mockListener.getAdded()
+ .iterator().next());
+ assertEquals("removed item", itemId1Present, mockListener.getRemoved()
+ .iterator().next());
+ assertEquals("selectedRows is correct", itemId2Present,
+ grid.getSelectedRow());
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSection.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSection.java
new file mode 100644
index 0000000000..e89f6a8c6e
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridStaticSection.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2000-2014 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 org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.GridFooter;
+import com.vaadin.ui.components.grid.GridFooter.FooterRow;
+import com.vaadin.ui.components.grid.GridHeader;
+import com.vaadin.ui.components.grid.GridHeader.HeaderRow;
+
+public class GridStaticSection {
+
+ private Indexed dataSource = new IndexedContainer();
+ private Grid grid;
+
+ @Before
+ public void setUp() {
+ dataSource.addContainerProperty("firstName", String.class, "");
+ dataSource.addContainerProperty("lastName", String.class, "");
+ dataSource.addContainerProperty("streetAddress", String.class, "");
+ dataSource.addContainerProperty("zipCode", Integer.class, null);
+ grid = new Grid(dataSource);
+ }
+
+ @Test
+ public void testAddAndRemoveHeaders() {
+
+ final GridHeader section = grid.getHeader();
+ assertEquals(1, section.getRowCount());
+ section.prependRow();
+ assertEquals(2, section.getRowCount());
+ section.removeRow(0);
+ assertEquals(1, section.getRowCount());
+ section.removeRow(0);
+ assertEquals(0, section.getRowCount());
+ assertEquals(null, section.getDefaultRow());
+ HeaderRow row = section.appendRow();
+ assertEquals(1, section.getRowCount());
+ assertEquals(null, section.getDefaultRow());
+ section.setDefaultRow(row);
+ assertEquals(row, section.getDefaultRow());
+ }
+
+ @Test
+ public void testAddAndRemoveFooters() {
+ final GridFooter section = grid.getFooter();
+
+ // By default there are no footer rows
+ assertEquals(0, section.getRowCount());
+ FooterRow row = section.appendRow();
+
+ assertEquals(1, section.getRowCount());
+ section.prependRow();
+ assertEquals(2, section.getRowCount());
+ assertEquals(row, section.getRow(1));
+ section.removeRow(0);
+ assertEquals(1, section.getRowCount());
+ section.removeRow(0);
+ assertEquals(0, section.getRowCount());
+ }
+
+ @Test
+ public void testJoinHeaderCells() {
+ final GridHeader section = grid.getHeader();
+ HeaderRow mergeRow = section.prependRow();
+ mergeRow.join("firstName", "lastName").setText("Name");
+ mergeRow.join(mergeRow.getCell("streetAddress"),
+ mergeRow.getCell("zipCode"));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testJoinHeaderCellsIncorrectly() {
+ final GridHeader section = grid.getHeader();
+ HeaderRow mergeRow = section.prependRow();
+ mergeRow.join("firstName", "zipCode").setText("Name");
+ }
+
+ @Test
+ public void testJoinAllFooterrCells() {
+ final GridFooter section = grid.getFooter();
+ FooterRow mergeRow = section.prependRow();
+ mergeRow.join(dataSource.getContainerPropertyIds().toArray()).setText(
+ "All the stuff.");
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/RendererTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/RendererTest.java
new file mode 100644
index 0000000000..5583fc02e8
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/RendererTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2000-2014 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.assertNull;
+import static org.junit.Assert.assertSame;
+
+import java.util.Locale;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.RpcDataProviderExtension;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.data.util.converter.Converter;
+import com.vaadin.data.util.converter.Converter.ConversionException;
+import com.vaadin.data.util.converter.StringToIntegerConverter;
+import com.vaadin.server.VaadinSession;
+import com.vaadin.tests.util.AlwaysLockedVaadinSession;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.GridColumn;
+import com.vaadin.ui.components.grid.renderers.TextRenderer;
+
+public class RendererTest {
+
+ private static class TestBean {
+ int i = 42;
+ }
+
+ private static class ExtendedBean extends TestBean {
+ float f = 3.14f;
+ }
+
+ private static class TestRenderer extends TextRenderer {
+ @Override
+ public Object encode(String value) {
+ return "renderer(" + super.encode(value) + ")";
+ }
+ }
+
+ private static class TestConverter implements Converter<String, TestBean> {
+
+ @Override
+ public TestBean convertToModel(String value,
+ Class<? extends TestBean> targetType, Locale locale)
+ throws ConversionException {
+ return null;
+ }
+
+ @Override
+ public String convertToPresentation(TestBean value,
+ Class<? extends String> targetType, Locale locale)
+ throws ConversionException {
+ if (value instanceof ExtendedBean) {
+ return "ExtendedBean(" + value.i + ", "
+ + ((ExtendedBean) value).f + ")";
+ } else {
+ return "TestBean(" + value.i + ")";
+ }
+ }
+
+ @Override
+ public Class<TestBean> getModelType() {
+ return TestBean.class;
+ }
+
+ @Override
+ public Class<String> getPresentationType() {
+ return String.class;
+ }
+ }
+
+ private Grid grid;
+
+ private GridColumn foo;
+ private GridColumn bar;
+ private GridColumn baz;
+ private GridColumn bah;
+
+ @Before
+ public void setUp() {
+ VaadinSession.setCurrent(new AlwaysLockedVaadinSession(null));
+
+ IndexedContainer c = new IndexedContainer();
+
+ c.addContainerProperty("foo", Integer.class, 0);
+ c.addContainerProperty("bar", String.class, "");
+ c.addContainerProperty("baz", TestBean.class, null);
+ c.addContainerProperty("bah", ExtendedBean.class, null);
+
+ Object id = c.addItem();
+ Item item = c.getItem(id);
+ item.getItemProperty("foo").setValue(123);
+ item.getItemProperty("bar").setValue("321");
+ item.getItemProperty("baz").setValue(new TestBean());
+ item.getItemProperty("bah").setValue(new ExtendedBean());
+
+ grid = new Grid(c);
+
+ foo = grid.getColumn("foo");
+ bar = grid.getColumn("bar");
+ baz = grid.getColumn("baz");
+ bah = grid.getColumn("bah");
+ }
+
+ @Test
+ public void testDefaultRendererAndConverter() throws Exception {
+ assertSame(TextRenderer.class, foo.getRenderer().getClass());
+ assertSame(StringToIntegerConverter.class, foo.getConverter()
+ .getClass());
+
+ assertSame(TextRenderer.class, bar.getRenderer().getClass());
+ // String->String; converter not needed
+ assertNull(bar.getConverter());
+
+ assertSame(TextRenderer.class, baz.getRenderer().getClass());
+ // MyBean->String; converter not found
+ assertNull(baz.getConverter());
+ }
+
+ @Test
+ public void testFindCompatibleConverter() throws Exception {
+ foo.setRenderer(renderer());
+ assertSame(StringToIntegerConverter.class, foo.getConverter()
+ .getClass());
+
+ bar.setRenderer(renderer());
+ assertNull(bar.getConverter());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testCannotFindConverter() {
+ baz.setRenderer(renderer());
+ }
+
+ @Test
+ public void testExplicitConverter() throws Exception {
+ baz.setRenderer(renderer(), converter());
+ bah.setRenderer(renderer(), converter());
+ }
+
+ @Test
+ public void testEncoding() throws Exception {
+ assertEquals("42", render(foo, 42));
+ foo.setRenderer(renderer());
+ assertEquals("renderer(42)", render(foo, 42));
+
+ assertEquals("2.72", render(bar, "2.72"));
+ bar.setRenderer(new TestRenderer());
+ assertEquals("renderer(2.72)", render(bar, "2.72"));
+ }
+
+ @Test(expected = ConversionException.class)
+ public void testEncodingWithoutConverter() throws Exception {
+ render(baz, new TestBean());
+ }
+
+ @Test
+ public void testBeanEncoding() throws Exception {
+ baz.setRenderer(renderer(), converter());
+ bah.setRenderer(renderer(), converter());
+
+ assertEquals("renderer(TestBean(42))", render(baz, new TestBean()));
+ assertEquals("renderer(ExtendedBean(42, 3.14))",
+ render(baz, new ExtendedBean()));
+
+ assertEquals("renderer(ExtendedBean(42, 3.14))",
+ render(bah, new ExtendedBean()));
+ }
+
+ private TestConverter converter() {
+ return new TestConverter();
+ }
+
+ private TestRenderer renderer() {
+ return new TestRenderer();
+ }
+
+ private Object render(GridColumn column, Object value) {
+ return RpcDataProviderExtension.encodeValue(value,
+ column.getRenderer(), column.getConverter(), grid.getLocale());
+ }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java b/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java
new file mode 100644
index 0000000000..d3a9315e20
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/sort/SortTest.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 2000-2014 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.sort;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.shared.ui.grid.SortDirection;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.SortOrderChangeEvent;
+import com.vaadin.ui.components.grid.SortOrderChangeListener;
+import com.vaadin.ui.components.grid.sort.Sort;
+import com.vaadin.ui.components.grid.sort.SortOrder;
+
+public class SortTest {
+
+ class DummySortingIndexedContainer extends IndexedContainer {
+
+ private Object[] expectedProperties;
+ private boolean[] expectedAscending;
+ private boolean sorted = true;
+
+ @Override
+ public void sort(Object[] propertyId, boolean[] ascending) {
+ Assert.assertEquals(
+ "Different amount of expected and actual properties,",
+ expectedProperties.length, propertyId.length);
+ Assert.assertEquals(
+ "Different amount of expected and actual directions",
+ expectedAscending.length, ascending.length);
+ for (int i = 0; i < propertyId.length; ++i) {
+ Assert.assertEquals("Sorting properties differ",
+ expectedProperties[i], propertyId[i]);
+ Assert.assertEquals("Sorting directions differ",
+ expectedAscending[i], ascending[i]);
+ }
+ sorted = true;
+ }
+
+ public void expectedSort(Object[] properties, SortDirection[] directions) {
+ assert directions.length == properties.length : "Array dimensions differ";
+ expectedProperties = properties;
+ expectedAscending = new boolean[directions.length];
+ for (int i = 0; i < directions.length; ++i) {
+ expectedAscending[i] = (directions[i] == SortDirection.ASCENDING);
+ }
+ sorted = false;
+ }
+
+ public boolean isSorted() {
+ return sorted;
+ }
+ }
+
+ class RegisteringSortChangeListener implements SortOrderChangeListener {
+ private List<SortOrder> order;
+
+ @Override
+ public void sortOrderChange(SortOrderChangeEvent event) {
+ assert order == null : "The same listener was notified multipe times without checking";
+
+ order = event.getSortOrder();
+ }
+
+ public void assertEventFired(SortOrder... expectedOrder) {
+ Assert.assertEquals(Arrays.asList(expectedOrder), order);
+
+ // Reset for nest test
+ order = null;
+ }
+
+ }
+
+ private DummySortingIndexedContainer container;
+ private RegisteringSortChangeListener listener;
+ private Grid grid;
+
+ @Before
+ public void setUp() {
+ container = createContainer();
+ container.expectedSort(new Object[] {}, new SortDirection[] {});
+
+ listener = new RegisteringSortChangeListener();
+
+ grid = new Grid(container);
+ grid.addSortOrderChangeListener(listener);
+ }
+
+ @After
+ public void tearDown() {
+ Assert.assertTrue("Container was not sorted after the test.",
+ container.isSorted());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testInvalidSortDirection() {
+ Sort.by("foo", null);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void testSortOneColumnMultipleTimes() {
+ Sort.by("foo").then("bar").then("foo");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSortingByUnexistingProperty() {
+ grid.sort("foobar");
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testSortingByUnsortableProperty() {
+ container.addContainerProperty("foobar", Object.class, null);
+ grid.sort("foobar");
+ }
+
+ @Test
+ public void testGridDirectSortAscending() {
+ container.expectedSort(new Object[] { "foo" },
+ new SortDirection[] { SortDirection.ASCENDING });
+ grid.sort("foo");
+
+ listener.assertEventFired(new SortOrder("foo", SortDirection.ASCENDING));
+ }
+
+ @Test
+ public void testGridDirectSortDescending() {
+ container.expectedSort(new Object[] { "foo" },
+ new SortDirection[] { SortDirection.DESCENDING });
+ grid.sort("foo", SortDirection.DESCENDING);
+
+ listener.assertEventFired(new SortOrder("foo", SortDirection.DESCENDING));
+ }
+
+ @Test
+ public void testGridSortBy() {
+ container.expectedSort(new Object[] { "foo", "bar", "baz" },
+ new SortDirection[] { SortDirection.ASCENDING,
+ SortDirection.ASCENDING, SortDirection.DESCENDING });
+ grid.sort(Sort.by("foo").then("bar")
+ .then("baz", SortDirection.DESCENDING));
+
+ listener.assertEventFired(
+ new SortOrder("foo", SortDirection.ASCENDING), new SortOrder(
+ "bar", SortDirection.ASCENDING), new SortOrder("baz",
+ SortDirection.DESCENDING));
+
+ }
+
+ @Test
+ public void testChangeContainerAfterSorting() {
+ container.expectedSort(new Object[] { "foo", "bar", "baz" },
+ new SortDirection[] { SortDirection.ASCENDING,
+ SortDirection.ASCENDING, SortDirection.DESCENDING });
+ grid.sort(Sort.by("foo").then("bar")
+ .then("baz", SortDirection.DESCENDING));
+
+ listener.assertEventFired(
+ new SortOrder("foo", SortDirection.ASCENDING), new SortOrder(
+ "bar", SortDirection.ASCENDING), new SortOrder("baz",
+ SortDirection.DESCENDING));
+
+ container = new DummySortingIndexedContainer();
+ container.addContainerProperty("baz", String.class, "");
+ container.expectedSort(new Object[] { "baz" },
+ new SortDirection[] { SortDirection.DESCENDING });
+ grid.setContainerDataSource(container);
+
+ listener.assertEventFired(new SortOrder("baz", SortDirection.DESCENDING));
+
+ }
+
+ private DummySortingIndexedContainer createContainer() {
+ DummySortingIndexedContainer container = new DummySortingIndexedContainer();
+ container.addContainerProperty("foo", Integer.class, 0);
+ container.addContainerProperty("bar", Integer.class, 0);
+ container.addContainerProperty("baz", Integer.class, 0);
+ return container;
+ }
+}
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..21e299e68b
--- /dev/null
+++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2014 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.ClientRpc;
+
+/**
+ * RPC interface used for pushing container data to the client.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface DataProviderRpc extends ClientRpc {
+
+ /**
+ * Sends updated row data to a client.
+ * <p>
+ * rowDataJson represents a JSON array of JSON objects in the following
+ * format:
+ *
+ * <pre>
+ * [{
+ * "d": [COL_1_JSON, COL_2_json, ...],
+ * "k": "1"
+ * },
+ * ...
+ * ]
+ * </pre>
+ *
+ * where COL_INDEX is the index of the column (as a string), and COL_n_JSON
+ * is valid JSON of the column's data.
+ *
+ * @param firstRowIndex
+ * the index of the first updated row
+ * @param rowDataJson
+ * the updated row data
+ * @see com.vaadin.shared.ui.grid.GridState#JSONKEY_DATA
+ * @see com.vaadin.ui.components.grid.Renderer#encode(Object)
+ */
+ public void setRowData(int firstRowIndex, String rowDataJson);
+
+ /**
+ * 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..76d68e8352
--- /dev/null
+++ b/shared/src/com/vaadin/shared/data/DataProviderState.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2000-2014 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
+ * @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..000d196be9
--- /dev/null
+++ b/shared/src/com/vaadin/shared/data/DataRequestRpc.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2014 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.ServerRpc;
+
+/**
+ * RPC interface used for requesting container data to the client.
+ *
+ * @since
+ * @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
+ * @param firstCachedRowIndex
+ * the index of the first cached row
+ * @param cacheSize
+ * the number of cached rows
+ * @param temporarilyPinnedKeys
+ * the keys that should remain pinned, even if some of these
+ * would fall out of the cache range
+ */
+ public void requestRows(int firstRowIndex, int numberOfRows,
+ int firstCachedRowIndex, int cacheSize,
+ List<String> temporarilyPinnedKeys);
+
+ /**
+ * Informs the back-end that the temporarily pinned keys in
+ * {@link #requestRows(int, int, int, int, List)} may be released.
+ */
+ public void releaseTemporarilyPinnedKeys();
+}
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..2ef0dfc3f8
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2000-2014 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
+ * @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..ade9e87f36
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2000-2014 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
+ * @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..b73e7cffd5
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridColumnState.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2000-2014 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 com.vaadin.shared.Connector;
+
+/**
+ * Column state DTO for transferring column properties from the server to the
+ * client
+ *
+ * @since
+ * @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
+ */
+ @Deprecated
+ public String header;
+
+ /**
+ * Footer caption for the column
+ */
+ @Deprecated
+ 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;
+
+ public Connector rendererConnector;
+
+ /**
+ * Are sorting indicators shown for a column. Default is false.
+ */
+ public boolean sortable = false;
+}
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..1ee79a5d37
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridConstants.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2000-2014 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;
+
+/**
+ * Container class for common constants and default values used by the Grid
+ * component.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public final class GridConstants implements Serializable {
+
+ /**
+ * Default padding in pixels when scrolling programmatically, without an
+ * explicitly defined padding value.
+ */
+ public static final int DEFAULT_PADDING = 0;
+
+ /**
+ * Delay before a long tap action is triggered. Number in milliseconds.
+ */
+ public static final int LONG_TAP_DELAY = 500;
+
+ /**
+ * The threshold in pixels a finger can move while long tapping.
+ */
+ public static final int LONG_TAP_THRESHOLD = 3;
+}
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..fd671e30a7
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2000-2014 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.List;
+
+import com.vaadin.shared.communication.ServerRpc;
+
+/**
+ * Client-to-server RPC interface for the Grid component
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public interface GridServerRpc extends ServerRpc {
+ void selectionChange(List<String> newSelection);
+
+ void sort(String[] columnIds, SortDirection[] directions,
+ SortEventOriginator originator);
+}
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..d687dd8e48
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2000-2014 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;
+import com.vaadin.shared.annotations.DelegateToWidget;
+
+/**
+ * The shared state for the {@link com.vaadin.ui.components.grid.Grid} component
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridState extends AbstractComponentState {
+
+ /**
+ * A description of which of the three bundled SelectionModels is currently
+ * in use.
+ * <p>
+ * Used as a data transfer object instead of the client/server ones, because
+ * they don't know about each others classes.
+ *
+ * @see com.vaadin.ui.components.grid.Grid.SelectionMode
+ * @see com.vaadin.client.ui.grid.Grid.SelectionMode
+ */
+ public enum SharedSelectionMode {
+ /**
+ * Representation of a single selection mode
+ *
+ * @see com.vaadin.ui.components.grid.Grid.SelectionMode#SINGLE
+ * @see com.vaadin.client.ui.grid.Grid.SelectionMode#SINGLE
+ */
+ SINGLE,
+
+ /**
+ * Representation of a multiselection mode
+ *
+ * @see com.vaadin.ui.components.grid.Grid.SelectionMode#MULTI
+ * @see com.vaadin.client.ui.grid.Grid.SelectionMode#MULTI
+ */
+ MULTI,
+
+ /**
+ * Representation of a no-selection mode
+ *
+ * @see com.vaadin.ui.components.grid.Grid.SelectionMode#NONE
+ * @see com.vaadin.client.ui.grid.Grid.SelectionMode#NONE
+ */
+ NONE;
+ }
+
+ /**
+ * The default value for height-by-rows for both GWT widgets
+ * {@link com.vaadin.ui.components.grid Grid} and
+ * {@link com.vaadin.client.ui.grid.Escalator Escalator}
+ */
+ public static final double DEFAULT_HEIGHT_BY_ROWS = 10.0d;
+
+ /**
+ * The key in which a row's data can be found
+ *
+ * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String)
+ */
+ public static final String JSONKEY_DATA = "d";
+
+ /**
+ * The key in which a row's own key can be found
+ *
+ * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String)
+ */
+ public static final String JSONKEY_ROWKEY = "k";
+
+ {
+ // 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>();
+
+ public GridStaticSectionState header = new GridStaticSectionState();
+
+ public GridStaticSectionState footer = new GridStaticSectionState();
+
+ /**
+ * The id for the last frozen column.
+ *
+ * @see GridColumnState#id
+ */
+ public String lastFrozenColumnId = null;
+
+ /** The height of the Grid in terms of body rows. */
+ @DelegateToWidget
+ public double heightByRows = DEFAULT_HEIGHT_BY_ROWS;
+
+ /** The mode by which Grid defines its height. */
+ @DelegateToWidget
+ public HeightMode heightMode = HeightMode.CSS;
+
+ // instantiated just to avoid NPEs
+ public List<String> selectedKeys = new ArrayList<String>();
+
+ public SharedSelectionMode selectionMode;
+
+ /** Keys of the currently sorted columns */
+ public String[] sortColumns = new String[0];
+
+ /** Directions for each sorted column */
+ public SortDirection[] sortDirs = new SortDirection[0];
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java b/shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java
new file mode 100644
index 0000000000..eae4bc8da4
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridStaticCellType.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2000-2014 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 content type of a Cell in a GridStaticSection.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public enum GridStaticCellType {
+ /**
+ * Text content
+ */
+ TEXT,
+
+ /**
+ * HTML content
+ */
+ HTML,
+
+ /**
+ * Widget content
+ */
+ WIDGET;
+} \ No newline at end of file
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java b/shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java
new file mode 100644
index 0000000000..c3c373b5af
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridStaticSectionState.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2000-2014 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;
+
+import com.vaadin.shared.Connector;
+
+/**
+ * Shared state for Grid headers and footers.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridStaticSectionState implements Serializable {
+
+ public static class CellState implements Serializable {
+ public String text = "";
+
+ public String html = "";
+
+ public Connector connector = null;
+
+ public GridStaticCellType type = GridStaticCellType.TEXT;
+ }
+
+ public static class RowState implements Serializable {
+ public List<CellState> cells = new ArrayList<CellState>();
+
+ public boolean defaultRow = false;
+
+ public List<List<Integer>> cellGroups = new ArrayList<List<Integer>>();
+ }
+
+ public List<RowState> rows = new ArrayList<RowState>();
+
+ public boolean visible = true;
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/HeightMode.java b/shared/src/com/vaadin/shared/ui/grid/HeightMode.java
new file mode 100644
index 0000000000..228fcbf0f4
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/HeightMode.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2000-2014 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;
+
+/**
+ * The modes for height calculation that are supported by Grid (
+ * {@link com.vaadin.client.ui.grid.Grid client} and
+ * {@link com.vaadin.ui.components.grid.Grid server}) /
+ * {@link com.vaadin.client.ui.grid.Escalator Escalator}.
+ *
+ * @since
+ * @author Vaadin Ltd
+ * @see com.vaadin.client.ui.grid.Grid#setHeightMode(HeightMode)
+ * @see com.vaadin.ui.components.grid.Grid#setHeightMode(HeightMode)
+ * @see com.vaadin.client.ui.grid.Escalator#setHeightMode(HeightMode)
+ */
+public enum HeightMode {
+ /**
+ * The height of the Component or Widget is defined by a CSS-like value.
+ * (e.g. "100px", "50em" or "25%")
+ */
+ CSS,
+
+ /**
+ * The height of the Component or Widget in question is defined by a number
+ * of rows.
+ */
+ ROW;
+}
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..2054845320
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/Range.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright 2000-2014 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;
+
+/**
+ * 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
+ * @author Vaadin Ltd
+ */
+public final class Range implements Serializable {
+ 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()));
+ }
+
+ /**
+ * Creates a range that is expanded the given amounts in both ends.
+ *
+ * @param startDelta
+ * the amount to expand by in the beginning of the range
+ * @param endDelta
+ * the amount to expand by in the end of the range
+ *
+ * @return an expanded range
+ *
+ * @throws IllegalArgumentException
+ * if the new range would have <code>start &gt; end</code>
+ */
+ public Range expand(int startDelta, int endDelta)
+ throws IllegalArgumentException {
+ return Range.between(getStart() - startDelta, getEnd() + endDelta);
+ }
+
+ /**
+ * Limits this range to be within the bounds of the provided range.
+ * <p>
+ * This is basically an optimized way of calculating
+ * <code>{@link #partitionWith(Range)}[1]</code> without the overhead of
+ * defining the parts that do not overlap.
+ * <p>
+ * If the two ranges do not intersect, an empty range is returned. There are
+ * no guarantees about the position of that range.
+ *
+ * @param bounds
+ * the bounds that the returned range should be limited to
+ * @return a bounded range
+ */
+ public Range restrictTo(Range bounds) {
+ boolean startWithin = getStart() >= bounds.getStart();
+ boolean endWithin = getEnd() <= bounds.getEnd();
+
+ if (startWithin) {
+ if (endWithin) {
+ return this;
+ } else {
+ return Range.between(getStart(), bounds.getEnd());
+ }
+ } else {
+ if (endWithin) {
+ return Range.between(bounds.getStart(), getEnd());
+ } else {
+ return bounds;
+ }
+ }
+ }
+}
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..43d5fcc21b
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2014 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
+ * @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/src/com/vaadin/shared/ui/grid/SortDirection.java b/shared/src/com/vaadin/shared/ui/grid/SortDirection.java
new file mode 100644
index 0000000000..0b4eafc37f
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/SortDirection.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2014 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;
+
+/**
+ * Describes sorting direction for a Grid column
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public enum SortDirection implements Serializable {
+
+ /**
+ * Ascending (e.g. A-Z, 1..9) sort order
+ */
+ ASCENDING,
+
+ /**
+ * Descending (e.g. Z-A, 9..1) sort order
+ */
+ DESCENDING
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/SortEventOriginator.java b/shared/src/com/vaadin/shared/ui/grid/SortEventOriginator.java
new file mode 100644
index 0000000000..acdd46ea5b
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/SortEventOriginator.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2000-2014 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;
+
+/**
+ * Identifier for the originator of a sort event or sort order change event.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public enum SortEventOriginator {
+
+ /**
+ * This event was the result of an API call.
+ */
+ API,
+
+ /**
+ * This event was the result of a user interacting with the UI.
+ */
+ USER,
+
+ /**
+ * This event resulted as a side-effect of an internal event.
+ */
+ INTERNAL
+
+}
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..ab67b22d0b
--- /dev/null
+++ b/shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java
@@ -0,0 +1,406 @@
+/*
+ * 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);
+ }
+
+ @Test
+ public void expand_basic() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = r1.expand(2, 3);
+
+ assertEquals(Range.between(3, 13), r2);
+ }
+
+ @Test
+ public void expand_negativeLegal() {
+ Range r1 = Range.between(5, 10);
+
+ Range r2 = r1.expand(-2, -2);
+ assertEquals(Range.between(7, 8), r2);
+
+ Range r3 = r1.expand(-3, -2);
+ assertEquals(Range.between(8, 8), r3);
+
+ Range r4 = r1.expand(3, -8);
+ assertEquals(Range.between(2, 2), r4);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void expand_negativeIllegal1() {
+ Range r1 = Range.between(5, 10);
+
+ // Should throw because the start would contract beyond the end
+ r1.expand(-3, -3);
+
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void expand_negativeIllegal2() {
+ Range r1 = Range.between(5, 10);
+
+ // Should throw because the end would contract beyond the start
+ r1.expand(3, -9);
+ }
+
+ @Test
+ public void restrictTo_fullyInside() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = Range.between(4, 11);
+
+ Range r3 = r1.restrictTo(r2);
+ assertTrue(r1 == r3);
+ }
+
+ @Test
+ public void restrictTo_fullyOutside() {
+ Range r1 = Range.between(4, 11);
+ Range r2 = Range.between(5, 10);
+
+ Range r3 = r1.restrictTo(r2);
+ assertTrue(r2 == r3);
+ }
+
+ public void restrictTo_notInterstecting() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = Range.between(15, 20);
+
+ Range r3 = r1.restrictTo(r2);
+ assertTrue("Non-intersecting ranges should produce an empty result",
+ r3.isEmpty());
+
+ Range r4 = r2.restrictTo(r1);
+ assertTrue("Non-intersecting ranges should produce an empty result",
+ r4.isEmpty());
+ }
+
+ public void restrictTo_startOutside() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = Range.between(7, 15);
+
+ Range r3 = r1.restrictTo(r2);
+
+ assertEquals(Range.between(7, 10), r3);
+ }
+
+ public void restrictTo_endOutside() {
+ Range r1 = Range.between(5, 10);
+ Range r2 = Range.between(4, 7);
+
+ Range r3 = r1.restrictTo(r2);
+
+ assertEquals(Range.between(5, 7), r3);
+ }
+
+}
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..f7af6a57e5
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java
@@ -0,0 +1,319 @@
+/*
+ * Copyright 2000-2014 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;
+
+@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";
+
+ public static final String REMOVE_ROWS_OFFSET = "rro";
+ public static final String REMOVE_ROWS_AMOUNT = "rra";
+ public static final String REMOVE_ROWS_BUTTON = "rrb";
+
+ 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();
+ removeRowsOffset.setId(REMOVE_ROWS_OFFSET);
+ removeRowsLayout.addComponent(removeRowsOffset);
+ final TextField removeRowsAmount = new TextField();
+ removeRowsAmount.setId(REMOVE_ROWS_AMOUNT);
+ 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);
+ }
+ }) {
+ {
+ setId(REMOVE_ROWS_BUTTON);
+ }
+ });
+ 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();
+ }
+ }));
+
+ addComponent(new Button("Randomize row heights",
+ new Button.ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ grid.randomizeDefaultRowHeight();
+ }
+ }));
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return null;
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return null;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/BasicEscalatorTest.java b/uitest/src/com/vaadin/tests/components/grid/BasicEscalatorTest.java
new file mode 100644
index 0000000000..ba0b718f35
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/BasicEscalatorTest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2000-2014 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.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class BasicEscalatorTest extends MultiBrowserTest {
+
+ private static final int SLEEP = 300;
+
+ private static final Pattern ROW_PATTERN = Pattern
+ .compile("Row (\\d+): \\d+,\\d+");
+
+ @Test
+ public void testInitialState() throws Exception {
+ openTestURL();
+
+ WebElement cell1 = getBodyRowCell(0, 0);
+ assertEquals("Top left body cell had unexpected content", "Row 0: 0,0",
+ cell1.getText());
+
+ WebElement cell2 = getBodyRowCell(15, 3);
+ assertEquals("Lower merged cell had unexpected content", "Cell: 3,15",
+ cell2.getText());
+ }
+
+ @Test
+ public void testScroll() throws Exception {
+ openTestURL();
+
+ /*
+ * let the DOM stabilize itself. TODO: remove once waitForVaadin
+ * supports lazy loaded components
+ */
+ Thread.sleep(100);
+
+ setScrollTop(getVerticalScrollbar(), 1000);
+ assertBodyCellWithContentIsFound("Row 50: 0,50");
+ }
+
+ @Test
+ public void testLastRow() throws Exception {
+ openTestURL();
+
+ /*
+ * let the DOM stabilize itself. TODO: remove once waitForVaadin
+ * supports lazy loaded components
+ */
+ Thread.sleep(100);
+
+ // scroll to bottom
+ setScrollTop(getVerticalScrollbar(), 100000000);
+
+ /*
+ * this test does not test DOM reordering, therefore we don't rely on
+ * child indices - we simply seek by content.
+ */
+ assertBodyCellWithContentIsFound("Row 99: 0,99");
+ }
+
+ @Test
+ public void testNormalRowHeight() throws Exception {
+ /*
+ * This is tested with screenshots instead of CSS queries, since some
+ * browsers report dimensions differently from each other, which is
+ * uninteresting for our purposes
+ */
+ openTestURL();
+ compareScreen("normalHeight");
+ }
+
+ @Test
+ public void testModifiedRowHeight() throws Exception {
+ /*
+ * This is tested with screenshots instead of CSS queries, since some
+ * browsers report dimensions differently from each other, which is
+ * uninteresting for our purposes
+ */
+ openTestURLWithTheme("reindeer-tests");
+ compareScreen("modifiedHeight");
+ }
+
+ private void assertBodyCellWithContentIsFound(String cellContent) {
+ String xpath = "//tbody/tr/td[.='" + cellContent + "']";
+ try {
+ assertNotNull("received a null element with \"" + xpath + "\"",
+ getDriver().findElement(By.xpath(xpath)));
+ } catch (NoSuchElementException e) {
+ fail("Could not find '" + xpath + "'");
+ }
+ }
+
+ private WebElement getBodyRowCell(int row, int col) {
+ return getDriver().findElement(
+ By.xpath("//tbody/tr[@class='v-escalator-row'][" + (row + 1)
+ + "]/td[" + (col + 1) + "]"));
+ }
+
+ private void openTestURLWithTheme(String themeName) {
+ String testUrl = getTestUrl();
+ testUrl += (testUrl.contains("?")) ? "&" : "?";
+ testUrl += "theme=" + themeName;
+ getDriver().get(testUrl);
+ }
+
+ private Object executeScript(String script, WebElement element) {
+ @SuppressWarnings("hiding")
+ final WebDriver driver = getDriver();
+ if (driver instanceof JavascriptExecutor) {
+ final JavascriptExecutor je = (JavascriptExecutor) driver;
+ return je.executeScript(script, element);
+ } else {
+ throw new IllegalStateException("current driver "
+ + getDriver().getClass().getName() + " is not a "
+ + JavascriptExecutor.class.getSimpleName());
+ }
+ }
+
+ @Test
+ public void domIsInitiallySorted() throws Exception {
+ openTestURL();
+
+ final List<WebElement> rows = getBodyRows();
+ assertTrue("no body rows found", !rows.isEmpty());
+ for (int i = 0; i < rows.size(); i++) {
+ String text = rows.get(i).getText();
+ String expected = "Row " + i;
+ assertTrue("Expected \"" + expected + "...\" but was " + text,
+ text.startsWith(expected));
+ }
+ }
+
+ @Test
+ public void domIsSortedAfterInsert() throws Exception {
+ openTestURL();
+
+ final int rowsToInsert = 5;
+ final int offset = 5;
+ insertRows(offset, rowsToInsert);
+
+ final List<WebElement> rows = getBodyRows();
+ int i = 0;
+ for (; i < offset + rowsToInsert; i++) {
+ final String expectedStart = "Row " + i;
+ final String text = rows.get(i).getText();
+ assertTrue("Expected \"" + expectedStart + "...\" but was " + text,
+ text.startsWith(expectedStart));
+ }
+
+ for (; i < rows.size(); i++) {
+ final String expectedStart = "Row " + (i - rowsToInsert);
+ final String text = rows.get(i).getText();
+ assertTrue("(post insert) Expected \"" + expectedStart
+ + "...\" but was " + text, text.startsWith(expectedStart));
+ }
+ }
+
+ @Test
+ public void domIsSortedAfterRemove() throws Exception {
+ openTestURL();
+
+ final int rowsToRemove = 5;
+ final int offset = 5;
+ removeRows(offset, rowsToRemove);
+
+ final List<WebElement> rows = getBodyRows();
+ int i = 0;
+ for (; i < offset; i++) {
+ final String expectedStart = "Row " + i;
+ final String text = rows.get(i).getText();
+ assertTrue("Expected " + expectedStart + "... but was " + text,
+ text.startsWith(expectedStart));
+ }
+
+ /*
+ * We check only up to 10, since after that, the indices are again
+ * reset, because new rows have been generated. The row numbers that
+ * they are given depends on the widget size, and it's too fragile to
+ * rely on some special assumptions on that.
+ */
+ for (; i < 10; i++) {
+ final String expectedStart = "Row " + (i + rowsToRemove);
+ final String text = rows.get(i).getText();
+ assertTrue("(post remove) Expected " + expectedStart
+ + "... but was " + text, text.startsWith(expectedStart));
+ }
+ }
+
+ @Test
+ public void domIsSortedAfterScroll() throws Exception {
+ openTestURL();
+ setScrollTop(getVerticalScrollbar(), 500);
+
+ /*
+ * Let the DOM reorder itself.
+ *
+ * TODO TestBench currently doesn't know when Grid's DOM structure is
+ * stable. There are some plans regarding implementing support for this,
+ * so this test case can (should) be modified once that's implemented.
+ */
+ sleep(SLEEP);
+
+ List<WebElement> rows = getBodyRows();
+ int firstRowNumber = parseFirstRowNumber(rows);
+
+ for (int i = 0; i < rows.size(); i++) {
+ final String expectedStart = "Row " + (i + firstRowNumber);
+ final String text = rows.get(i).getText();
+ assertTrue("(post remove) Expected " + expectedStart
+ + "... but was " + text, text.startsWith(expectedStart));
+ }
+ }
+
+ private static int parseFirstRowNumber(List<WebElement> rows)
+ throws NumberFormatException {
+ final WebElement firstRow = rows.get(0);
+ final String firstRowText = firstRow.getText();
+ final Matcher matcher = ROW_PATTERN.matcher(firstRowText);
+ if (!matcher.find()) {
+ fail("could not find " + ROW_PATTERN.pattern() + " in \""
+ + firstRowText + "\"");
+ }
+ final String number = matcher.group(1);
+ return Integer.parseInt(number);
+ }
+
+ private void insertRows(final int offset, final int amount) {
+ final WebElement offsetInput = vaadinElementById(BasicEscalator.INSERT_ROWS_OFFSET);
+ offsetInput.sendKeys(String.valueOf(offset), Keys.RETURN);
+
+ final WebElement amountInput = vaadinElementById(BasicEscalator.INSERT_ROWS_AMOUNT);
+ amountInput.sendKeys(String.valueOf(amount), Keys.RETURN);
+
+ final WebElement button = vaadinElementById(BasicEscalator.INSERT_ROWS_BUTTON);
+ button.click();
+ }
+
+ private void removeRows(final int offset, final int amount) {
+ final WebElement offsetInput = vaadinElementById(BasicEscalator.REMOVE_ROWS_OFFSET);
+ offsetInput.sendKeys(String.valueOf(offset), Keys.RETURN);
+
+ final WebElement amountInput = vaadinElementById(BasicEscalator.REMOVE_ROWS_AMOUNT);
+ amountInput.sendKeys(String.valueOf(amount), Keys.RETURN);
+
+ final WebElement button = vaadinElementById(BasicEscalator.REMOVE_ROWS_BUTTON);
+ button.click();
+ }
+
+ private void setScrollTop(WebElement element, long px) {
+ executeScript("arguments[0].scrollTop = " + px, element);
+ }
+
+ private List<WebElement> getBodyRows() {
+ return getDriver().findElements(By.xpath("//tbody/tr/td[1]"));
+ }
+
+ private WebElement getVerticalScrollbar() {
+ return getDriver().findElement(
+ By.xpath("//div["
+ + "contains(@class, 'v-escalator-scroller-vertical')"
+ + "]"));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java b/uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java
new file mode 100644
index 0000000000..d217829bcb
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/CustomRenderer.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2014 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.annotations.Widgetset;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.Grid.SelectionMode;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class CustomRenderer extends AbstractTestUI {
+
+ private static final Object INT_ARRAY_PROPERTY = "int array";
+ private static final Object VOID_PROPERTY = "void";
+
+ static final Object ITEM_ID = "itemId1";
+ static final String DEBUG_LABEL_ID = "debuglabel";
+ static final String INIT_DEBUG_LABEL_CAPTION = "Debug label placeholder";
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ IndexedContainer container = new IndexedContainer();
+ container.addContainerProperty(INT_ARRAY_PROPERTY, int[].class,
+ new int[] {});
+ container.addContainerProperty(VOID_PROPERTY, Void.class, null);
+
+ Item item = container.addItem(ITEM_ID);
+
+ @SuppressWarnings("unchecked")
+ Property<int[]> propertyIntArray = item
+ .getItemProperty(INT_ARRAY_PROPERTY);
+ propertyIntArray.setValue(new int[] { 1, 1, 2, 3, 5, 8, 13 });
+
+ Label debugLabel = new Label(INIT_DEBUG_LABEL_CAPTION);
+ debugLabel.setId(DEBUG_LABEL_ID);
+
+ Grid grid = new Grid(container);
+ grid.getColumn(INT_ARRAY_PROPERTY).setRenderer(new IntArrayRenderer());
+ grid.getColumn(VOID_PROPERTY).setRenderer(
+ new RowAwareRenderer(debugLabel));
+ grid.setSelectionMode(SelectionMode.NONE);
+ addComponent(grid);
+ addComponent(debugLabel);
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Verifies that renderers operating on other data than "
+ + "just Strings also work ";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return Integer.valueOf(13334);
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java b/uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java
new file mode 100644
index 0000000000..571a929c7e
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/CustomRendererTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2000-2014 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 java.util.List;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.LabelElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class CustomRendererTest extends MultiBrowserTest {
+ @Test
+ public void testIntArrayIsRendered() throws Exception {
+ openTestURL();
+
+ GridElement grid = findGrid();
+ assertEquals("1 :: 1 :: 2 :: 3 :: 5 :: 8 :: 13", grid.getCell(0, 0)
+ .getText());
+ }
+
+ @Test
+ public void testRowAwareRenderer() throws Exception {
+ openTestURL();
+
+ GridElement grid = findGrid();
+ assertEquals("Click me!", grid.getCell(0, 1).getText());
+ assertEquals(CustomRenderer.INIT_DEBUG_LABEL_CAPTION, findDebugLabel()
+ .getText());
+
+ grid.getCell(0, 1).click();
+ assertEquals("row: 0, key: 0", grid.getCell(0, 1).getText());
+ assertEquals("key: 0, itemId: " + CustomRenderer.ITEM_ID,
+ findDebugLabel().getText());
+ }
+
+ private GridElement findGrid() {
+ List<GridElement> elements = $(GridElement.class).all();
+ return elements.get(0);
+ }
+
+ private LabelElement findDebugLabel() {
+ return $(LabelElement.class).id(CustomRenderer.DEBUG_LABEL_ID);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java b/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java
new file mode 100644
index 0000000000..fd3c8d5b2f
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2000-2014 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.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elements.LabelElement;
+import com.vaadin.testbench.elements.NativeButtonElement;
+import com.vaadin.testbench.elements.NativeSelectElement;
+import com.vaadin.testbench.elements.ServerClass;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererConnector.Renderers;
+import com.vaadin.tests.widgetset.server.grid.GridClientColumnRenderers;
+
+/**
+ * Tests Grid client side renderers
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@TestCategory("grid")
+public class GridClientRenderers extends MultiBrowserTest {
+
+ private static final double SLEEP_MULTIPLIER = 1.2;
+ private int latency = 0;
+
+ @Override
+ protected Class<?> getUIClass() {
+ return GridClientColumnRenderers.class;
+ }
+
+ @Override
+ protected String getDeploymentPath() {
+ String path = super.getDeploymentPath();
+ if (latency > 0) {
+ path += (path.contains("?") ? "&" : "?") + "latency=" + latency;
+ }
+ return path;
+ }
+
+ @ServerClass("com.vaadin.tests.widgetset.server.grid.GridClientColumnRenderers.GridController")
+ public static class MyClientGridElement extends GridElement {
+ }
+
+ @Override
+ public void setup() throws Exception {
+ latency = 0; // reset
+ super.setup();
+ }
+
+ @Test
+ public void addWidgetRenderer() throws Exception {
+ openTestURL();
+
+ // Add widget renderer column
+ $(NativeSelectElement.class).first().selectByText(
+ Renderers.WIDGET_RENDERER.toString());
+ $(NativeButtonElement.class).caption("Add").first().click();
+
+ // Click the button in cell 1,1
+ TestBenchElement cell = getGrid().getCell(1, 2);
+ WebElement gwtButton = cell.findElement(By.tagName("button"));
+ gwtButton.click();
+
+ // Should be an alert visible
+ assertEquals("Button did not contain text \"Clicked\"", "Clicked",
+ gwtButton.getText());
+ }
+
+ @Test
+ public void detachAndAttachGrid() {
+ openTestURL();
+
+ // Add widget renderer column
+ $(NativeSelectElement.class).first().selectByText(
+ Renderers.WIDGET_RENDERER.toString());
+ $(NativeButtonElement.class).caption("Add").first().click();
+
+ // Detach and re-attach the Grid
+ $(NativeButtonElement.class).caption("DetachAttach").first().click();
+
+ // Click the button in cell 1,1
+ TestBenchElement cell = getGrid().getCell(1, 2);
+ WebElement gwtButton = cell.findElement(By.tagName("button"));
+ gwtButton.click();
+
+ // Should be an alert visible
+ assertEquals("Button did not contain text \"Clicked\"",
+ gwtButton.getText(), "Clicked");
+ }
+
+ @Test
+ public void rowsWithDataHasStyleName() throws Exception {
+
+ // Simulate network latency with 2000ms
+ latency = 2000;
+
+ openTestURL();
+
+ sleep((int) (latency * SLEEP_MULTIPLIER));
+
+ TestBenchElement row = getGrid().getRow(51);
+ String className = row.getAttribute("class");
+ assertFalse(
+ "Row should not yet contain style name v-grid-row-has-data",
+ className.contains("v-grid-row-has-data"));
+
+ // Wait for data to arrive
+ sleep((int) (latency * SLEEP_MULTIPLIER));
+
+ row = getGrid().getRow(51);
+ className = row.getAttribute("class");
+ assertTrue("Row should now contain style name v-grid-row-has-data",
+ className.contains("v-grid-row-has-data"));
+ }
+
+ @Test
+ public void complexRendererSetVisibleContent() throws Exception {
+
+ DesiredCapabilities desiredCapabilities = getDesiredCapabilities();
+
+ // Simulate network latency with 2000ms
+ latency = 2000;
+ if (BrowserUtil.isIE8(desiredCapabilities)) {
+ // IE8 is slower than other browsers. Bigger latency is needed for
+ // stability in this test.
+ latency = 3000;
+ }
+
+ // Chrome uses RGB instead of RGBA
+ String colorRed = "rgba(255, 0, 0, 1)";
+ String colorWhite = "rgba(255, 255, 255, 1)";
+ if (BrowserUtil.isChrome(desiredCapabilities)) {
+ colorRed = "rgb(255, 0, 0)";
+ colorWhite = "rgb(255, 255, 255)";
+ }
+
+ openTestURL();
+
+ // Test initial renderering with contentVisible = False
+ TestBenchElement cell = getGrid().getCell(51, 1);
+ String backgroundColor = cell.getCssValue("backgroundColor");
+ assertEquals("Background color was not red.", colorRed, backgroundColor);
+
+ // data arrives...
+ sleep((int) (latency * SLEEP_MULTIPLIER));
+
+ // Content becomes visible
+ cell = getGrid().getCell(51, 1);
+ backgroundColor = cell.getCssValue("backgroundColor");
+ assertNotEquals("Background color was red.", colorRed, backgroundColor);
+
+ // scroll down, new cells becomes contentVisible = False
+ getGrid().scrollToRow(60);
+
+ // Cell should be red (setContentVisible set cell red)
+ cell = getGrid().getCell(55, 1);
+ backgroundColor = cell.getCssValue("backgroundColor");
+ assertEquals("Background color was not red.", colorRed, backgroundColor);
+
+ // data arrives...
+ sleep((int) (latency * SLEEP_MULTIPLIER));
+
+ // Cell should no longer be red
+ backgroundColor = cell.getCssValue("backgroundColor");
+ assertEquals("Background color was not white", colorWhite,
+ backgroundColor);
+ }
+
+ @Test
+ public void testSortingEvent() throws Exception {
+ openTestURL();
+
+ $(NativeButtonElement.class).caption("Trigger sorting event").first()
+ .click();
+
+ String consoleText = $(LabelElement.class).id("testDebugConsole")
+ .getText();
+
+ assertTrue("Console text as expected",
+ consoleText.contains("Columns: 1, order: Column 1: ASCENDING"));
+
+ }
+
+ @Test
+ public void testListSorter() throws Exception {
+ openTestURL();
+
+ $(NativeButtonElement.class).caption("Shuffle").first().click();
+
+ GridElement gridElem = $(MyClientGridElement.class).first();
+
+ // XXX: DANGER! We'll need to know how many rows the Grid has!
+ // XXX: Currently, this is impossible; hence the hardcoded value of 70.
+
+ boolean shuffled = false;
+ for (int i = 1, l = 70; i < l; ++i) {
+
+ String str_a = gridElem.getCell(i - 1, 0).getAttribute("innerHTML");
+ String str_b = gridElem.getCell(i, 0).getAttribute("innerHTML");
+
+ int value_a = Integer.parseInt(str_a);
+ int value_b = Integer.parseInt(str_b);
+
+ if (value_a > value_b) {
+ shuffled = true;
+ break;
+ }
+ }
+ assertTrue("Grid shuffled", shuffled);
+
+ $(NativeButtonElement.class).caption("Test sorting").first().click();
+
+ for (int i = 1, l = 70; i < l; ++i) {
+
+ String str_a = gridElem.getCell(i - 1, 0).getAttribute("innerHTML");
+ String str_b = gridElem.getCell(i, 0).getAttribute("innerHTML");
+
+ int value_a = Integer.parseInt(str_a);
+ int value_b = Integer.parseInt(str_b);
+
+ if (value_a > value_b) {
+ assertTrue("Grid sorted", false);
+ }
+ }
+ }
+
+ private GridElement getGrid() {
+ return $(MyClientGridElement.class).first();
+ }
+
+ private void addColumn(Renderers renderer) {
+ // Add widget renderer column
+ $(NativeSelectElement.class).first().selectByText(renderer.toString());
+ $(NativeButtonElement.class).caption("Add").first().click();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColspans.java b/uitest/src/com/vaadin/tests/components/grid/GridColspans.java
new file mode 100644
index 0000000000..be12c2bcb2
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridColspans.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2000-2014 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.Container.Indexed;
+import com.vaadin.data.Item;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.components.AbstractTestUI;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.GridFooter;
+import com.vaadin.ui.components.grid.GridFooter.FooterRow;
+import com.vaadin.ui.components.grid.GridHeader;
+import com.vaadin.ui.components.grid.GridHeader.HeaderRow;
+import com.vaadin.ui.components.grid.renderers.NumberRenderer;
+
+public class GridColspans extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+ Indexed dataSource = new IndexedContainer();
+ Grid grid;
+
+ dataSource.addContainerProperty("firstName", String.class, "");
+ dataSource.addContainerProperty("lastName", String.class, "");
+ dataSource.addContainerProperty("streetAddress", String.class, "");
+ dataSource.addContainerProperty("zipCode", Integer.class, null);
+ dataSource.addContainerProperty("city", String.class, "");
+ Item i = dataSource.addItem(0);
+ i.getItemProperty("firstName").setValue("Rudolph");
+ i.getItemProperty("lastName").setValue("Reindeer");
+ i.getItemProperty("streetAddress").setValue("Ruukinkatu 2-4");
+ i.getItemProperty("zipCode").setValue(20540);
+ i.getItemProperty("city").setValue("Turku");
+ grid = new Grid(dataSource);
+ grid.setWidth("600px");
+ grid.getColumn("zipCode").setRenderer(new NumberRenderer());
+ addComponent(grid);
+
+ GridHeader header = grid.getHeader();
+ HeaderRow row = header.prependRow();
+ row.join("firstName", "lastName").setText("Full Name");
+ row.join("streetAddress", "zipCode", "city").setText("Address");
+ header.prependRow()
+ .join(dataSource.getContainerPropertyIds().toArray())
+ .setText("All the stuff");
+
+ GridFooter footer = grid.getFooter();
+ FooterRow footerRow = footer.appendRow();
+ footerRow.join("firstName", "lastName").setText("Full Name");
+ footerRow.join("streetAddress", "zipCode", "city").setText("Address");
+ footer.appendRow().join(dataSource.getContainerPropertyIds().toArray())
+ .setText("All the stuff");
+
+ footer.setVisible(true);
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Grid header and footer colspans";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return 13334;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java b/uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java
new file mode 100644
index 0000000000..dad9399466
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridColspansTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2000-2014 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 java.io.IOException;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridColspansTest extends MultiBrowserTest {
+
+ @Test
+ public void testHeaderColSpans() {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+ assertEquals("5", grid.getHeaderCell(0, 1).getAttribute("colspan"));
+ assertEquals("2", grid.getHeaderCell(1, 1).getAttribute("colspan"));
+ assertEquals("3", grid.getHeaderCell(1, 3).getAttribute("colspan"));
+ }
+
+ @Test
+ public void testFooterColSpans() {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+ assertEquals("5", grid.getFooterCell(1, 1).getAttribute("colspan"));
+ assertEquals("2", grid.getFooterCell(0, 1).getAttribute("colspan"));
+ assertEquals("3", grid.getFooterCell(0, 3).getAttribute("colspan"));
+ }
+
+ @Test
+ public void testActiveHeaderColumnsWithNavigation() throws IOException {
+ openTestURL();
+
+ GridElement grid = $(GridElement.class).first();
+ grid.getCell(0, 1).click();
+
+ compareScreen("beforeNavigation");
+
+ for (int i = 1; i <= 6; ++i) {
+ assertEquals(true, grid.getFooterCell(1, 1).isActiveHeader());
+ assertEquals(i < 3, grid.getFooterCell(0, 1).isActiveHeader());
+ assertEquals(i >= 3, grid.getFooterCell(0, 3).isActiveHeader());
+ assertEquals(true, grid.getHeaderCell(0, 1).isActiveHeader());
+ assertEquals(i < 3, grid.getHeaderCell(1, 1).isActiveHeader());
+ assertEquals(i >= 3, grid.getHeaderCell(1, 3).isActiveHeader());
+ new Actions(getDriver()).sendKeys(Keys.ARROW_RIGHT).perform();
+ }
+
+ compareScreen("afterNavigation");
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridElement.java b/uitest/src/com/vaadin/tests/components/grid/GridElement.java
new file mode 100644
index 0000000000..bd8cad45c6
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridElement.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2000-2014 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.List;
+
+import org.openqa.selenium.NoSuchElementException;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.testbench.By;
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.testbench.elements.AbstractComponentElement;
+import com.vaadin.testbench.elements.AbstractElement;
+import com.vaadin.testbench.elements.ServerClass;
+
+/**
+ * TestBench Element API for Grid
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@ServerClass("com.vaadin.ui.components.grid.Grid")
+public class GridElement extends AbstractComponentElement {
+
+ public static class GridCellElement extends AbstractElement {
+
+ private String ACTIVE_CLASS_NAME = "-cell-active";
+ private String ACTIVE_HEADER_CLASS_NAME = "-header-active";
+
+ public boolean isActive() {
+ return getAttribute("class").contains(ACTIVE_CLASS_NAME);
+ }
+
+ public boolean isActiveHeader() {
+ return getAttribute("class").contains(ACTIVE_HEADER_CLASS_NAME);
+ }
+ }
+
+ public static class GridRowElement extends AbstractElement {
+
+ private String ACTIVE_CLASS_NAME = "-row-active";
+ private String SELECTED_CLASS_NAME = "-row-selected";
+
+ public boolean isActive() {
+ return getAttribute("class").contains(ACTIVE_CLASS_NAME);
+ }
+
+ @Override
+ public boolean isSelected() {
+ return getAttribute("class").contains(SELECTED_CLASS_NAME);
+ }
+ }
+
+ /**
+ * Scrolls Grid element so that wanted row is displayed
+ *
+ * @param index
+ * Target row
+ */
+ public void scrollToRow(int index) {
+ try {
+ getSubPart("#cell[" + index + "]");
+ } catch (NoSuchElementException e) {
+ // Expected, ignore it.
+ }
+ }
+
+ /**
+ * Gets cell element with given row and column index.
+ *
+ * @param rowIndex
+ * Row index
+ * @param colIndex
+ * Column index
+ * @return Cell element with given indices.
+ */
+ public GridCellElement getCell(int rowIndex, int colIndex) {
+ scrollToRow(rowIndex);
+ return getSubPart("#cell[" + rowIndex + "][" + colIndex + "]").wrap(
+ GridCellElement.class);
+ }
+
+ /**
+ * Gets row element with given row index.
+ *
+ * @param index
+ * Row index
+ * @return Row element with given index.
+ */
+ public GridRowElement getRow(int index) {
+ scrollToRow(index);
+ return getSubPart("#cell[" + index + "]").wrap(GridRowElement.class);
+ }
+
+ /**
+ * Gets header cell element with given row and column index.
+ *
+ * @param rowIndex
+ * Row index
+ * @param colIndex
+ * Column index
+ * @return Header cell element with given indices.
+ */
+ public GridCellElement getHeaderCell(int rowIndex, int colIndex) {
+ return getSubPart("#header[" + rowIndex + "][" + colIndex + "]").wrap(
+ GridCellElement.class);
+ }
+
+ /**
+ * Gets footer cell element with given row and column index.
+ *
+ * @param rowIndex
+ * Row index
+ * @param colIndex
+ * Column index
+ * @return Footer cell element with given indices.
+ */
+ public GridCellElement getFooterCell(int rowIndex, int colIndex) {
+ return getSubPart("#footer[" + rowIndex + "][" + colIndex + "]").wrap(
+ GridCellElement.class);
+ }
+
+ /**
+ * Gets list of header cell elements on given row.
+ *
+ * @param rowIndex
+ * Row index
+ * @return Header cell elements on given row.
+ */
+ public List<GridCellElement> getHeaderCells(int rowIndex) {
+ List<GridCellElement> headers = new ArrayList<GridCellElement>();
+ for (TestBenchElement e : TestBenchElement.wrapElements(
+ getSubPart("#header[" + rowIndex + "]").findElements(
+ By.xpath("./th")), getCommandExecutor())) {
+ headers.add(e.wrap(GridCellElement.class));
+ }
+ return headers;
+ }
+
+ /**
+ * Gets list of header cell elements on given row.
+ *
+ * @param rowIndex
+ * Row index
+ * @return Header cell elements on given row.
+ */
+ public List<GridCellElement> getFooterCells(int rowIndex) {
+ List<GridCellElement> footers = new ArrayList<GridCellElement>();
+ for (TestBenchElement e : TestBenchElement.wrapElements(
+ getSubPart("#footer[" + rowIndex + "]").findElements(
+ By.xpath("./td")), getCommandExecutor())) {
+ footers.add(e.wrap(GridCellElement.class));
+ }
+ return footers;
+ }
+
+ /**
+ * Get header row count
+ *
+ * @return Header row count
+ */
+ public int getHeaderCount() {
+ return getSubPart("#header").findElements(By.xpath("./tr")).size();
+ }
+
+ /**
+ * Get footer row count
+ *
+ * @return Footer row count
+ */
+ public int getFooterCount() {
+ return getSubPart("#footer").findElements(By.xpath("./tr")).size();
+ }
+
+ /**
+ * Get a header row by index
+ *
+ * @param rowIndex
+ * Row index
+ * @return The th element of the row
+ */
+ public WebElement getHeaderRow(int rowIndex) {
+ return getSubPart("#header[" + rowIndex + "]");
+ }
+
+ /**
+ * Get a footer row by index
+ *
+ * @param rowIndex
+ * Row index
+ * @return The tr element of the row
+ */
+ public WebElement getFooterRow(int rowIndex) {
+ return getSubPart("#footer[" + rowIndex + "]");
+ }
+
+ /**
+ * Get the vertical scroll element
+ *
+ * @return The element representing the vertical scrollbar
+ */
+ public WebElement getVerticalScroller() {
+ List<WebElement> rootElements = findElements(By.xpath("./div"));
+ return rootElements.get(0);
+ }
+
+ /**
+ * Get the horizontal scroll element
+ *
+ * @return The element representing the horizontal scrollbar
+ */
+ public WebElement getHorizontalScroller() {
+ List<WebElement> rootElements = findElements(By.xpath("./div"));
+ return rootElements.get(1);
+ }
+
+ /**
+ * Get the header element
+ *
+ * @return The thead element
+ */
+ public WebElement getHeader() {
+ return getSubPart("#header");
+ }
+
+ /**
+ * Get the body element
+ *
+ * @return the tbody element
+ */
+ public WebElement getBody() {
+ return getSubPart("#cell");
+ }
+
+ /**
+ * Get the footer element
+ *
+ * @return the tfoot element
+ */
+ public WebElement getFooter() {
+ return getSubPart("#footer");
+ }
+
+ /**
+ * Get the element wrapping the table element
+ *
+ * @return The element that wraps the table element
+ */
+ public WebElement getTableWrapper() {
+ List<WebElement> rootElements = findElements(By.xpath("./div"));
+ return rootElements.get(2);
+ }
+
+ /**
+ * Helper function to get Grid subparts wrapped correctly
+ *
+ * @param subPartSelector
+ * SubPart to be used in ComponentLocator
+ * @return SubPart element wrapped in TestBenchElement class
+ */
+ private TestBenchElement getSubPart(String subPartSelector) {
+ return (TestBenchElement) findElement(By.vaadin(subPartSelector));
+ }
+
+}
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..dd86d616b9
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridScrolling.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2000-2014 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.scrollTo(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/components/grid/GridSingleColumn.java b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumn.java
new file mode 100644
index 0000000000..75b83ea3aa
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumn.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2014 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.tests.components.AbstractTestUI;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.Grid.SelectionMode;
+import com.vaadin.ui.components.grid.GridColumn;
+
+public class GridSingleColumn extends AbstractTestUI {
+
+ @Override
+ protected void setup(VaadinRequest request) {
+
+ IndexedContainer indexedContainer = new IndexedContainer();
+ indexedContainer.addContainerProperty("column1", String.class, "");
+
+ for (int i = 0; i < 100; i++) {
+ Item addItem = indexedContainer.addItem(i);
+ addItem.getItemProperty("column1").setValue("cell");
+ }
+
+ Grid grid = new Grid(indexedContainer);
+ grid.setSelectionMode(SelectionMode.NONE);
+
+ GridColumn column = grid.getColumn("column1");
+
+ column.setHeaderCaption("Header");
+
+ addComponent(grid);
+ }
+
+ @Override
+ protected String getTestDescription() {
+ return "Tests a single column grid";
+ }
+
+ @Override
+ protected Integer getTicketNumber() {
+ return null;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java
new file mode 100644
index 0000000000..2e062f36c6
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/GridSingleColumnTest.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2000-2014 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.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+
+import org.junit.Ignore;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public class GridSingleColumnTest extends MultiBrowserTest {
+
+ /*
+ * TODO unignore once column header captions are reimplemented
+ */
+ @Test
+ @Ignore
+ public void headerIsVisible() {
+ openTestURL();
+
+ WebElement header = getDriver().findElement(
+ By.className("v-grid-header"));
+ WebElement cell = header.findElement(By.className("v-grid-cell"));
+ assertThat(cell.getText(), is("Header"));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java b/uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java
new file mode 100644
index 0000000000..142c370e13
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/IntArrayRenderer.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2000-2014 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 org.json.JSONArray;
+import org.json.JSONException;
+
+import com.vaadin.ui.components.grid.AbstractRenderer;
+
+public class IntArrayRenderer extends AbstractRenderer<int[]> {
+ public IntArrayRenderer() {
+ super(int[].class);
+ }
+
+ @Override
+ public Object encode(int[] value) {
+ try {
+ return new JSONArray(value);
+ } catch (JSONException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java b/uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java
new file mode 100644
index 0000000000..f55f5f064c
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/RowAwareRenderer.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2000-2014 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 org.json.JSONObject;
+
+import com.vaadin.tests.widgetset.client.grid.RowAwareRendererConnector.RowAwareRendererRpc;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.components.grid.AbstractRenderer;
+
+public class RowAwareRenderer extends AbstractRenderer<Void> {
+ public RowAwareRenderer(final Label debugLabel) {
+ super(Void.class);
+ registerRpc(new RowAwareRendererRpc() {
+ @Override
+ public void clicky(String key) {
+ Object itemId = getItemId(key);
+ debugLabel.setValue("key: " + key + ", itemId: " + itemId);
+ }
+ });
+ }
+
+ @Override
+ public Object encode(Void value) {
+ return JSONObject.NULL;
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java
new file mode 100644
index 0000000000..4c5e703b82
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeatures.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.UI;
+
+/**
+ * Initializer shell for GridClientBasicFeatures test application
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@Widgetset(TestingWidgetSet.NAME)
+public class GridBasicClientFeatures extends UI {
+
+ public class GridTestComponent extends AbstractComponent {
+ }
+
+ @Override
+ protected void init(VaadinRequest request) {
+ setContent(new GridTestComponent());
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java
new file mode 100644
index 0000000000..e3318fe650
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicClientFeaturesTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.Dimension;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+
+/**
+ * Variant of GridBasicFeaturesTest to be used with GridBasicClientFeatures.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class GridBasicClientFeaturesTest extends GridBasicFeaturesTest {
+
+ @Override
+ protected Class<?> getUIClass() {
+ return GridBasicClientFeatures.class;
+ }
+
+ @Override
+ protected void selectMenu(String menuCaption) {
+ WebElement menuElement = getMenuElement(menuCaption);
+ Dimension size = menuElement.getSize();
+ new Actions(getDriver()).moveToElement(menuElement, size.width - 10,
+ size.height / 2).perform();
+ }
+
+ private WebElement getMenuElement(String menuCaption) {
+ return getDriver().findElement(
+ By.xpath("//td[text() = '" + menuCaption + "']"));
+ }
+
+ @Override
+ protected void selectMenuPath(String... menuCaptions) {
+ new Actions(getDriver()).moveToElement(getMenuElement(menuCaptions[0]))
+ .click().perform();
+ for (int i = 1; i < menuCaptions.length - 1; ++i) {
+ selectMenu(menuCaptions[i]);
+ new Actions(getDriver()).moveByOffset(20, 0).perform();
+ }
+ new Actions(getDriver())
+ .moveToElement(
+ getMenuElement(menuCaptions[menuCaptions.length - 1]))
+ .click().perform();
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java
new file mode 100644
index 0000000000..d54b1838ea
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeatures.java
@@ -0,0 +1,701 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures;
+
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.shared.ui.grid.GridStaticCellType;
+import com.vaadin.shared.ui.grid.HeightMode;
+import com.vaadin.shared.ui.grid.SortDirection;
+import com.vaadin.tests.components.AbstractComponentTest;
+import com.vaadin.ui.Button;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.Grid.SelectionMode;
+import com.vaadin.ui.components.grid.GridColumn;
+import com.vaadin.ui.components.grid.GridFooter;
+import com.vaadin.ui.components.grid.GridFooter.FooterCell;
+import com.vaadin.ui.components.grid.GridHeader;
+import com.vaadin.ui.components.grid.GridHeader.HeaderCell;
+import com.vaadin.ui.components.grid.GridHeader.HeaderRow;
+import com.vaadin.ui.components.grid.SortOrderChangeEvent;
+import com.vaadin.ui.components.grid.SortOrderChangeListener;
+import com.vaadin.ui.components.grid.renderers.DateRenderer;
+import com.vaadin.ui.components.grid.renderers.HtmlRenderer;
+import com.vaadin.ui.components.grid.renderers.NumberRenderer;
+import com.vaadin.ui.components.grid.sort.Sort;
+import com.vaadin.ui.components.grid.sort.SortOrder;
+
+/**
+ * Tests the basic features like columns, footers and headers
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridBasicFeatures extends AbstractComponentTest<Grid> {
+
+ private static final int MANUALLY_FORMATTED_COLUMNS = 5;
+ public static final int COLUMNS = 12;
+ public static final int ROWS = 1000;
+
+ private int columnGroupRows = 0;
+ private IndexedContainer ds;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected Grid constructComponent() {
+
+ // Build data source
+ ds = new IndexedContainer() {
+ @Override
+ public List<Object> getItemIds(int startIndex, int numberOfIds) {
+ log("Requested items " + startIndex + " - "
+ + (startIndex + numberOfIds));
+ return super.getItemIds(startIndex, numberOfIds);
+ }
+ };
+
+ {
+ int col = 0;
+ for (; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; col++) {
+ ds.addContainerProperty(getColumnProperty(col), String.class,
+ "");
+ }
+
+ ds.addContainerProperty(getColumnProperty(col++), Integer.class,
+ Integer.valueOf(0));
+ ds.addContainerProperty(getColumnProperty(col++), Date.class,
+ new Date());
+ ds.addContainerProperty(getColumnProperty(col++), String.class, "");
+
+ // Random numbers
+ ds.addContainerProperty(getColumnProperty(col++), Integer.class, 0);
+ ds.addContainerProperty(getColumnProperty(col++), Integer.class, 0);
+
+ }
+
+ {
+ Random rand = new Random();
+ rand.setSeed(13334);
+ long timestamp = 0;
+ for (int row = 0; row < ROWS; row++) {
+ Item item = ds.addItem(Integer.valueOf(row));
+ int col = 0;
+ for (; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; col++) {
+ item.getItemProperty(getColumnProperty(col)).setValue(
+ "(" + row + ", " + col + ")");
+ }
+ item.getItemProperty(getColumnProperty(col++)).setValue(
+ Integer.valueOf(row));
+ item.getItemProperty(getColumnProperty(col++)).setValue(
+ new Date(timestamp));
+ timestamp += 91250000; // a bit over a day, just to get
+ // variation
+ item.getItemProperty(getColumnProperty(col++)).setValue(
+ "<b>" + row + "</b>");
+
+ // Random numbers
+ item.getItemProperty(getColumnProperty(col++)).setValue(
+ rand.nextInt());
+ // Random between 0 - 5 to test multisorting
+ item.getItemProperty(getColumnProperty(col++)).setValue(
+ rand.nextInt(5));
+ }
+ }
+
+ // Create grid
+ Grid grid = new Grid(ds);
+
+ {
+ int col = grid.getContainerDatasource().getContainerPropertyIds()
+ .size()
+ - MANUALLY_FORMATTED_COLUMNS;
+ grid.getColumn(getColumnProperty(col++)).setRenderer(
+ new NumberRenderer(new DecimalFormat("0,000.00",
+ DecimalFormatSymbols.getInstance(new Locale("fi",
+ "FI")))));
+ grid.getColumn(getColumnProperty(col++)).setRenderer(
+ new DateRenderer(new SimpleDateFormat("dd.MM.yy HH:mm")));
+ grid.getColumn(getColumnProperty(col++)).setRenderer(
+ new HtmlRenderer());
+ grid.getColumn(getColumnProperty(col++)).setRenderer(
+ new NumberRenderer());
+ grid.getColumn(getColumnProperty(col++)).setRenderer(
+ new NumberRenderer());
+ }
+
+ // Create footer
+ GridFooter footer = grid.getFooter();
+ footer.appendRow();
+ footer.setVisible(false);
+
+ // Add footer values (header values are automatically created)
+ for (int col = 0; col < COLUMNS; col++) {
+ footer.getRow(0).getCell(getColumnProperty(col))
+ .setText("Footer " + col);
+ }
+
+ // Set varying column widths
+ for (int col = 0; col < COLUMNS; col++) {
+ grid.getColumn(getColumnProperty(col)).setWidth(100 + col * 50);
+ }
+
+ grid.addSortOrderChangeListener(new SortOrderChangeListener() {
+ @Override
+ public void sortOrderChange(SortOrderChangeEvent event) {
+
+ String origin;
+ switch (event.getOriginator()) {
+ case API:
+ origin = "API";
+ break;
+ case INTERNAL:
+ origin = "INTERNAL";
+ break;
+ case USER:
+ origin = "USER";
+ break;
+ default:
+ origin = "!!! ERROR !!!";
+ break;
+ }
+
+ log("Sort order: " + event.getSortOrder() + " by " + origin);
+ }
+ });
+
+ grid.setSelectionMode(SelectionMode.NONE);
+
+ createGridActions();
+
+ createColumnActions();
+
+ createHeaderActions();
+
+ createFooterActions();
+
+ createRowActions();
+
+ addHeightActions();
+
+ 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"));
+
+ LinkedHashMap<String, SelectionMode> selectionModes = new LinkedHashMap<String, Grid.SelectionMode>();
+ selectionModes.put("single", SelectionMode.SINGLE);
+ selectionModes.put("multi", SelectionMode.MULTI);
+ selectionModes.put("none", SelectionMode.NONE);
+ createSelectAction("Selection mode", "State", selectionModes, "none",
+ new Command<Grid, Grid.SelectionMode>() {
+ @Override
+ public void execute(Grid grid, SelectionMode selectionMode,
+ Object data) {
+ grid.setSelectionMode(selectionMode);
+ }
+ });
+
+ LinkedHashMap<String, List<SortOrder>> sortableProperties = new LinkedHashMap<String, List<SortOrder>>();
+ for (Object propertyId : ds.getSortableContainerPropertyIds()) {
+ sortableProperties.put(propertyId + ", ASC", Sort.by(propertyId)
+ .build());
+ sortableProperties.put(propertyId + ", DESC",
+ Sort.by(propertyId, SortDirection.DESCENDING).build());
+ }
+ createSelectAction("Sort by column", "State", sortableProperties,
+ "Column 9, ascending", new Command<Grid, List<SortOrder>>() {
+ @Override
+ public void execute(Grid grid, List<SortOrder> sortOrder,
+ Object data) {
+ grid.setSortOrder(sortOrder);
+ }
+ });
+ }
+
+ protected void createHeaderActions() {
+ createCategory("Header", null);
+
+ createBooleanAction("Visible", "Header", true,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid grid, Boolean value, Object data) {
+ grid.getHeader().setVisible(value);
+ }
+ });
+
+ LinkedHashMap<String, String> defaultRows = new LinkedHashMap<String, String>();
+ defaultRows.put("Top", "Top");
+ defaultRows.put("Bottom", "Bottom");
+ defaultRows.put("Unset", "Unset");
+
+ createMultiClickAction("Default row", "Header", defaultRows,
+ new Command<Grid, String>() {
+
+ @Override
+ public void execute(Grid grid, String value, Object data) {
+ HeaderRow defaultRow = null;
+ GridHeader header = grid.getHeader();
+ if (value.equals("Top")) {
+ defaultRow = header.getRow(0);
+ } else if (value.equals("Bottom")) {
+ defaultRow = header.getRow(header.getRowCount() - 1);
+ }
+ header.setDefaultRow(defaultRow);
+ }
+
+ }, defaultRows.get("Top"));
+
+ createClickAction("Prepend row", "Header", new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.getHeader().prependRow();
+ }
+
+ }, null);
+ createClickAction("Append row", "Header", new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.getHeader().appendRow();
+ }
+
+ }, null);
+
+ createClickAction("Remove top row", "Header",
+ new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.getHeader().removeRow(0);
+ }
+
+ }, null);
+ createClickAction("Remove bottom row", "Header",
+ new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.getHeader().removeRow(
+ grid.getHeader().getRowCount() - 1);
+ }
+
+ }, null);
+ }
+
+ protected void createFooterActions() {
+ createCategory("Footer", null);
+
+ createBooleanAction("Visible", "Footer", false,
+ new Command<Grid, Boolean>() {
+
+ @Override
+ public void execute(Grid grid, Boolean value, Object data) {
+ grid.getFooter().setVisible(value);
+ }
+ });
+
+ createClickAction("Prepend row", "Footer", new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.getFooter().prependRow();
+ }
+
+ }, null);
+ createClickAction("Append row", "Footer", new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.getFooter().appendRow();
+ }
+
+ }, null);
+
+ createClickAction("Remove top row", "Footer",
+ new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.getFooter().removeRow(0);
+ }
+
+ }, null);
+ createClickAction("Remove bottom row", "Footer",
+ new Command<Grid, Object>() {
+
+ @Override
+ public void execute(Grid grid, Object value, Object data) {
+ grid.getFooter().removeRow(
+ grid.getFooter().getRowCount() - 1);
+ }
+
+ }, null);
+ }
+
+ 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(
+ getColumnProperty((Integer) data));
+ }
+ }, null, c);
+
+ createClickAction("Freeze", getColumnProperty(c),
+ new Command<Grid, String>() {
+
+ @Override
+ public void execute(Grid grid, String value, Object data) {
+ grid.setLastFrozenPropertyId(getColumnProperty((Integer) data));
+ }
+ }, null, c);
+
+ createBooleanAction("Sortable", 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.setSortable(value);
+ }
+ }, 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);
+ }
+
+ LinkedHashMap<String, GridStaticCellType> defaultRows = new LinkedHashMap<String, GridStaticCellType>();
+ defaultRows.put("Text Header", GridStaticCellType.TEXT);
+ defaultRows.put("Html Header ", GridStaticCellType.HTML);
+ defaultRows.put("Widget Header", GridStaticCellType.WIDGET);
+
+ createMultiClickAction("Header Type", getColumnProperty(c),
+ defaultRows, new Command<Grid, GridStaticCellType>() {
+
+ @Override
+ public void execute(Grid grid,
+ GridStaticCellType value, Object columnIndex) {
+ final Object propertyId = (new ArrayList(grid
+ .getContainerDatasource()
+ .getContainerPropertyIds())
+ .get((Integer) columnIndex));
+ final HeaderCell cell = grid.getHeader()
+ .getDefaultRow().getCell(propertyId);
+ switch (value) {
+ case TEXT:
+ cell.setText("Text Header");
+ break;
+ case HTML:
+ cell.setHtml("HTML Header");
+ break;
+ case WIDGET:
+ cell.setComponent(new Button("Button Header",
+ new ClickListener() {
+
+ @Override
+ public void buttonClick(
+ ClickEvent event) {
+ log("Button clicked!");
+ }
+ }));
+ default:
+ break;
+ }
+ }
+
+ }, c);
+
+ defaultRows = new LinkedHashMap<String, GridStaticCellType>();
+ defaultRows.put("Text Footer", GridStaticCellType.TEXT);
+ defaultRows.put("Html Footer", GridStaticCellType.HTML);
+ defaultRows.put("Widget Footer", GridStaticCellType.WIDGET);
+
+ createMultiClickAction("Footer Type", getColumnProperty(c),
+ defaultRows, new Command<Grid, GridStaticCellType>() {
+
+ @Override
+ public void execute(Grid grid,
+ GridStaticCellType value, Object columnIndex) {
+ final Object propertyId = (new ArrayList(grid
+ .getContainerDatasource()
+ .getContainerPropertyIds())
+ .get((Integer) columnIndex));
+ final FooterCell cell = grid.getFooter().getRow(0)
+ .getCell(propertyId);
+ switch (value) {
+ case TEXT:
+ cell.setText("Text Footer");
+ break;
+ case HTML:
+ cell.setHtml("HTML Footer");
+ break;
+ case WIDGET:
+ cell.setComponent(new Button("Button Footer",
+ new ClickListener() {
+
+ @Override
+ public void buttonClick(
+ ClickEvent event) {
+ log("Button clicked!");
+ }
+ }));
+ default:
+ break;
+ }
+ }
+
+ }, c);
+ }
+ }
+
+ private static String getColumnProperty(int c) {
+ return "Column " + c;
+ }
+
+ 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>() {
+ @SuppressWarnings("unchecked")
+ @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++) {
+ Property<?> property = item
+ .getItemProperty(getColumnProperty(i));
+ if (property.getType().equals(String.class)) {
+ ((Property<String>) property)
+ .setValue("modified: " + i);
+ }
+ }
+ }
+ }, null);
+
+ createClickAction("Modify first row (getContainerProperty)",
+ "Body rows", new Command<Grid, String>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ Object firstItemId = ds.getIdByIndex(0);
+ for (Object containerPropertyId : ds
+ .getContainerPropertyIds()) {
+ Property<?> property = ds.getContainerProperty(
+ firstItemId, containerPropertyId);
+ if (property.getType().equals(String.class)) {
+ ((Property<String>) property)
+ .setValue("modified: "
+ + containerPropertyId);
+ }
+ }
+ }
+ }, null);
+
+ createBooleanAction("Select first row", "Body rows", false,
+ new Command<Grid, Boolean>() {
+ @Override
+ public void execute(Grid grid, Boolean select, Object data) {
+ final Object firstItemId = grid
+ .getContainerDatasource().firstItemId();
+ if (select.booleanValue()) {
+ grid.select(firstItemId);
+ } else {
+ grid.deselect(firstItemId);
+ }
+ }
+ });
+
+ createClickAction("Remove all rows", "Body rows",
+ new Command<Grid, String>() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ ds.removeAllItems();
+ }
+ }, null);
+ }
+
+ @SuppressWarnings("boxing")
+ protected void addHeightActions() {
+ createCategory("Height by Rows", "Size");
+
+ createBooleanAction("HeightMode Row", "Size", false,
+ new Command<Grid, Boolean>() {
+ @Override
+ public void execute(Grid c, Boolean heightModeByRows,
+ Object data) {
+ c.setHeightMode(heightModeByRows ? HeightMode.ROW
+ : HeightMode.CSS);
+ }
+ }, null);
+
+ addActionForHeightByRows(1d / 3d);
+ addActionForHeightByRows(2d / 3d);
+
+ for (double i = 1; i < 5; i++) {
+ addActionForHeightByRows(i);
+ addActionForHeightByRows(i + 1d / 3d);
+ addActionForHeightByRows(i + 2d / 3d);
+ }
+
+ Command<Grid, String> sizeCommand = new Command<Grid, String>() {
+ @Override
+ public void execute(Grid grid, String height, Object data) {
+ grid.setHeight(height);
+ }
+ };
+
+ createCategory("Height", "Size");
+ // header 20px + scrollbar 16px = 36px baseline
+ createClickAction("86px (no drag scroll select)", "Height",
+ sizeCommand, "86px");
+ createClickAction("96px (drag scroll select limit)", "Height",
+ sizeCommand, "96px");
+ createClickAction("106px (drag scroll select enabled)", "Height",
+ sizeCommand, "106px");
+ }
+
+ private void addActionForHeightByRows(final Double i) {
+ DecimalFormat df = new DecimalFormat("0.00");
+ createClickAction(df.format(i) + " rows", "Height by Rows",
+ new Command<Grid, String>() {
+ @Override
+ public void execute(Grid c, String value, Object data) {
+ c.setHeightByRows(i);
+ }
+ }, 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/basicfeatures/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java
new file mode 100644
index 0000000000..6ef0ab5006
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/GridBasicFeaturesTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.JavascriptExecutor;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.interactions.Actions;
+import org.openqa.selenium.remote.DesiredCapabilities;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.tests.annotations.TestCategory;
+import com.vaadin.tests.components.grid.GridElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+@TestCategory("grid")
+public abstract class GridBasicFeaturesTest extends MultiBrowserTest {
+
+ @Override
+ protected DesiredCapabilities getDesiredCapabilities() {
+ DesiredCapabilities dCap = super.getDesiredCapabilities();
+ if (BrowserUtil.isIE(dCap)) {
+ dCap.setCapability("requireWindowFocus", true);
+ }
+ return super.getDesiredCapabilities();
+ }
+
+ @Override
+ protected Class<?> getUIClass() {
+ return GridBasicFeatures.class;
+ }
+
+ protected void selectSubMenu(String menuCaption) {
+ selectMenu(menuCaption);
+ new Actions(getDriver()).moveByOffset(100, 0).build().perform();
+ }
+
+ protected void selectMenu(String menuCaption) {
+ getDriver().findElement(
+ By.xpath("//span[text() = '" + menuCaption + "']")).click();
+ }
+
+ protected void selectMenuPath(String... menuCaptions) {
+ selectMenu(menuCaptions[0]);
+ for (int i = 1; i < menuCaptions.length; i++) {
+ selectSubMenu(menuCaptions[i]);
+ }
+ }
+
+ protected GridElement getGridElement() {
+ return ((TestBenchElement) findElement(By.id("testComponent")))
+ .wrap(GridElement.class);
+ }
+
+ protected void scrollGridVerticallyTo(double px) {
+ executeScript("arguments[0].scrollTop = " + px,
+ getGridVerticalScrollbar());
+ }
+
+ protected List<TestBenchElement> getGridHeaderRowCells() {
+ List<TestBenchElement> headerCells = new ArrayList<TestBenchElement>();
+ for (int i = 0; i < getGridElement().getHeaderCount(); ++i) {
+ headerCells.addAll(getGridElement().getHeaderCells(i));
+ }
+ return headerCells;
+ }
+
+ protected List<TestBenchElement> getGridFooterRowCells() {
+ List<TestBenchElement> footerCells = new ArrayList<TestBenchElement>();
+ for (int i = 0; i < getGridElement().getFooterCount(); ++i) {
+ footerCells.addAll(getGridElement().getFooterCells(i));
+ }
+ return footerCells;
+ }
+
+ private Object executeScript(String script, WebElement element) {
+ final WebDriver driver = getDriver();
+ if (driver instanceof JavascriptExecutor) {
+ final JavascriptExecutor je = (JavascriptExecutor) driver;
+ return je.executeScript(script, element);
+ } else {
+ throw new IllegalStateException("current driver "
+ + getDriver().getClass().getName() + " is not a "
+ + JavascriptExecutor.class.getSimpleName());
+ }
+ }
+
+ private WebElement getGridVerticalScrollbar() {
+ return getDriver()
+ .findElement(
+ By.xpath("//div[contains(@class, \"v-grid-scroller-vertical\")]"));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java
new file mode 100644
index 0000000000..ece9fdf7d7
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientColumnPropertiesTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+import com.vaadin.tests.widgetset.client.grid.GridBasicClientFeaturesWidget;
+
+public class GridClientColumnPropertiesTest extends GridBasicClientFeaturesTest {
+
+ @Test
+ public void initialColumnWidths() {
+ openTestURL();
+
+ for (int col = 0; col < GridBasicClientFeaturesWidget.COLUMNS; col++) {
+ int width = getGridElement().getCell(0, col).getSize().getWidth();
+ if (col <= 6) {
+ // Growing column widths
+ assertEquals(50 + col * 25, width);
+ } else {
+ assertEquals(100, width);
+ }
+ }
+ }
+
+ @Test
+ public void testChangingColumnWidth() {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 0", "Width", "50px");
+ int width = getGridElement().getCell(0, 0).getSize().getWidth();
+ assertEquals(50, width);
+
+ selectMenuPath("Component", "Columns", "Column 0", "Width", "200px");
+ width = getGridElement().getCell(0, 0).getSize().getWidth();
+ assertEquals(200, width);
+
+ selectMenuPath("Component", "Columns", "Column 0", "Width", "auto");
+ width = getGridElement().getCell(0, 0).getSize().getWidth();
+ assertEquals(100, width);
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java
new file mode 100644
index 0000000000..fe81380296
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientKeyEventsTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.By;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+
+public class GridClientKeyEventsTest extends GridBasicClientFeaturesTest {
+
+ private List<String> eventOrder = Arrays.asList("keydown", "keyup",
+ "keypress");
+
+ @Test
+ public void testBodyKeyEvents() throws IOException {
+ openTestURL();
+
+ getGridElement().getCell(2, 2).click();
+
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+
+ for (int i = 0; i < 3; ++i) {
+ assertEquals("Body key event handler was not called.", "(2, 2) "
+ + eventOrder.get(i) + " 13",
+ findElements(By.className("v-label")).get(i * 3).getText());
+
+ assertTrue("Header key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3 + 1)
+ .getText().isEmpty());
+ assertTrue("Footer key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3 + 2)
+ .getText().isEmpty());
+ }
+
+ }
+
+ @Test
+ public void testHeaderKeyEvents() throws IOException {
+ openTestURL();
+
+ getGridElement().getHeaderCell(0, 2).click();
+
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+
+ for (int i = 0; i < 3; ++i) {
+ assertEquals("Header key event handler was not called.", "(0, 2) "
+ + eventOrder.get(i) + " 13",
+ findElements(By.className("v-label")).get(i * 3 + 1)
+ .getText());
+
+ assertTrue("Body key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3).getText()
+ .isEmpty());
+ assertTrue("Footer key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3 + 2)
+ .getText().isEmpty());
+ }
+ }
+
+ @Test
+ public void testFooterKeyEvents() throws IOException {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+ getGridElement().getFooterCell(0, 2).click();
+
+ new Actions(getDriver()).sendKeys(Keys.ENTER).perform();
+
+ for (int i = 0; i < 3; ++i) {
+ assertEquals("Footer key event handler was not called.", "(0, 2) "
+ + eventOrder.get(i) + " 13",
+ findElements(By.className("v-label")).get(i * 3 + 2)
+ .getText());
+
+ assertTrue("Body key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3).getText()
+ .isEmpty());
+ assertTrue("Header key event handler got called unexpectedly.",
+ findElements(By.className("v-label")).get(i * 3 + 1)
+ .getText().isEmpty());
+
+ }
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java
new file mode 100644
index 0000000000..4b47837887
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridClientSelectionTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.client;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+
+public class GridClientSelectionTest extends GridBasicClientFeaturesTest {
+
+ @Test
+ public void testChangeSelectionMode() {
+ openTestURL();
+
+ selectMenuPath("Component", "State", "Selection mode", "none");
+ assertTrue("First column was selection column", getGridElement()
+ .getCell(0, 0).getText().equals("(0, 0)"));
+ selectMenuPath("Component", "State", "Selection mode", "multi");
+ assertTrue("First column was not selection column", getGridElement()
+ .getCell(0, 1).getText().equals("(0, 0)"));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java
new file mode 100644
index 0000000000..8124e5361f
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridFooterTest.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.tests.components.grid.GridElement.GridCellElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+
+public class GridFooterTest extends GridStaticSectionTest {
+
+ @Test
+ public void testDefaultFooter() {
+ openTestURL();
+
+ // Footer should have zero rows by default
+ assertFooterCount(0);
+ }
+
+ @Test
+ public void testFooterVisibility() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Visible");
+
+ assertFooterCount(0);
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ assertFooterCount(0);
+
+ selectMenuPath("Component", "Footer", "Visible");
+
+ assertFooterCount(1);
+ }
+
+ @Test
+ public void testAddRows() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ assertFooterCount(1);
+ assertFooterTexts(0, 0);
+
+ selectMenuPath("Component", "Footer", "Prepend row");
+
+ assertFooterCount(2);
+ assertFooterTexts(1, 0);
+ assertFooterTexts(0, 1);
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ assertFooterCount(3);
+ assertFooterTexts(1, 0);
+ assertFooterTexts(0, 1);
+ assertFooterTexts(2, 2);
+ }
+
+ @Test
+ public void testRemoveRows() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Prepend row");
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Footer", "Remove top row");
+
+ assertFooterCount(1);
+ assertFooterTexts(1, 0);
+
+ selectMenuPath("Component", "Footer", "Remove bottom row");
+ assertFooterCount(0);
+ }
+
+ @Test
+ public void joinColumnsByCells() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Footer", "Row 1", "Join column cells 0, 1");
+
+ GridCellElement spannedCell = getGridElement().getFooterCell(0, 0);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+
+ GridCellElement hiddenCell = getGridElement().getFooterCell(0, 1);
+ assertFalse(hiddenCell.isDisplayed());
+ }
+
+ @Test
+ public void joinColumnsByColumns() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Footer", "Row 1", "Join columns 1, 2");
+
+ GridCellElement spannedCell = getGridElement().getFooterCell(0, 1);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+
+ GridCellElement hiddenCell = getGridElement().getFooterCell(0, 2);
+ assertFalse(hiddenCell.isDisplayed());
+ }
+
+ @Test
+ public void joinAllColumnsInRow() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Footer", "Row 1", "Join all columns");
+
+ GridCellElement spannedCell = getGridElement().getFooterCell(0, 0);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("" + GridBasicFeatures.COLUMNS,
+ spannedCell.getAttribute("colspan"));
+
+ for (int columnIndex = 1; columnIndex < GridBasicFeatures.COLUMNS; columnIndex++) {
+ GridCellElement hiddenCell = getGridElement().getFooterCell(0,
+ columnIndex);
+ assertFalse(hiddenCell.isDisplayed());
+ }
+ }
+
+ @Test
+ public void testInitialCellTypes() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ GridCellElement textCell = getGridElement().getFooterCell(0, 0);
+ assertEquals("Footer (0,0)", textCell.getText());
+
+ GridCellElement widgetCell = getGridElement().getFooterCell(0, 1);
+ assertTrue(widgetCell.isElementPresent(By.className("gwt-HTML")));
+
+ GridCellElement htmlCell = getGridElement().getFooterCell(0, 2);
+ assertHTML("<b>Footer (0,2)</b>", htmlCell);
+ }
+
+ @Test
+ public void testDynamicallyChangingCellType() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Columns", "Column 0", "Footer Type",
+ "Widget Footer");
+ GridCellElement widgetCell = getGridElement().getFooterCell(0, 0);
+ assertTrue(widgetCell.isElementPresent(By.className("gwt-Button")));
+
+ selectMenuPath("Component", "Columns", "Column 1", "Footer Type",
+ "HTML Footer");
+ GridCellElement htmlCell = getGridElement().getFooterCell(0, 1);
+ assertHTML("<b>HTML Footer</b>", htmlCell);
+
+ selectMenuPath("Component", "Columns", "Column 2", "Footer Type",
+ "Text Footer");
+ GridCellElement textCell = getGridElement().getFooterCell(0, 2);
+ assertEquals("Text Footer", textCell.getText());
+ }
+
+ @Test
+ public void testCellWidgetInteraction() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Append row");
+
+ selectMenuPath("Component", "Columns", "Column 0", "Footer Type",
+ "Widget Footer");
+ GridCellElement widgetCell = getGridElement().getFooterCell(0, 0);
+ WebElement button = widgetCell.findElement(By.className("gwt-Button"));
+
+ assertNotEquals("Clicked", button.getText());
+
+ button.click();
+
+ assertEquals("Clicked", button.getText());
+ }
+
+ private void assertFooterCount(int count) {
+ assertEquals("footer count", count, getGridElement().getFooterCount());
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java
new file mode 100644
index 0000000000..c528571a2e
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridHeaderTest.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.tests.components.grid.GridElement.GridCellElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+
+public class GridHeaderTest extends GridStaticSectionTest {
+
+ @Test
+ public void testDefaultHeader() throws Exception {
+ openTestURL();
+
+ assertHeaderCount(1);
+ assertHeaderTexts(0, 0);
+ }
+
+ @Test
+ public void testHeaderVisibility() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Visible");
+
+ assertHeaderCount(0);
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ assertHeaderCount(0);
+
+ selectMenuPath("Component", "Header", "Visible");
+
+ assertHeaderCount(2);
+ }
+
+ @Test
+ public void testHeaderCaptions() throws Exception {
+ openTestURL();
+
+ assertHeaderTexts(0, 0);
+ }
+
+ @Test
+ public void testHeadersWithInvisibleColumns() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 1", "Visible");
+ selectMenuPath("Component", "Columns", "Column 3", "Visible");
+
+ List<TestBenchElement> cells = getGridHeaderRowCells();
+ assertEquals(GridBasicFeatures.COLUMNS - 2, cells.size());
+
+ assertText("Header (0,0)", cells.get(0));
+ assertHTML("<b>Header (0,2)</b>", cells.get(1));
+ assertHTML("<b>Header (0,4)</b>", cells.get(2));
+
+ selectMenuPath("Component", "Columns", "Column 3", "Visible");
+
+ cells = getGridHeaderRowCells();
+ assertEquals(GridBasicFeatures.COLUMNS - 1, cells.size());
+
+ assertText("Header (0,0)", cells.get(0));
+ assertHTML("<b>Header (0,2)</b>", cells.get(1));
+ assertText("Header (0,3)", cells.get(2));
+ assertHTML("<b>Header (0,4)</b>", cells.get(3));
+ }
+
+ @Test
+ public void testAddRows() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ assertHeaderCount(2);
+ assertHeaderTexts(0, 0);
+ assertHeaderTexts(1, 1);
+
+ selectMenuPath("Component", "Header", "Prepend row");
+
+ assertHeaderCount(3);
+ assertHeaderTexts(2, 0);
+ assertHeaderTexts(0, 1);
+ assertHeaderTexts(1, 2);
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ assertHeaderCount(4);
+ assertHeaderTexts(2, 0);
+ assertHeaderTexts(0, 1);
+ assertHeaderTexts(1, 2);
+ assertHeaderTexts(3, 3);
+ }
+
+ @Test
+ public void testRemoveRows() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Prepend row");
+ selectMenuPath("Component", "Header", "Append row");
+
+ selectMenuPath("Component", "Header", "Remove top row");
+
+ assertHeaderCount(2);
+ assertHeaderTexts(0, 0);
+ assertHeaderTexts(2, 1);
+
+ selectMenuPath("Component", "Header", "Remove bottom row");
+ assertHeaderCount(1);
+ assertHeaderTexts(0, 0);
+ }
+
+ @Test
+ public void testDefaultRow() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 0", "Sortable");
+
+ GridCellElement headerCell = getGridElement().getHeaderCell(0, 0);
+
+ headerCell.click();
+
+ assertTrue(hasClassName(headerCell, "sort-asc"));
+
+ headerCell.click();
+
+ assertFalse(hasClassName(headerCell, "sort-asc"));
+ assertTrue(hasClassName(headerCell, "sort-desc"));
+
+ selectMenuPath("Component", "Header", "Prepend row");
+ selectMenuPath("Component", "Header", "Default row", "Top");
+
+ assertFalse(hasClassName(headerCell, "sort-desc"));
+ headerCell = getGridElement().getHeaderCell(0, 0);
+ assertTrue(hasClassName(headerCell, "sort-desc"));
+
+ selectMenuPath("Component", "Header", "Default row", "Unset");
+
+ assertFalse(hasClassName(headerCell, "sort-desc"));
+ }
+
+ @Test
+ public void joinHeaderColumnsByCells() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ selectMenuPath("Component", "Header", "Row 2", "Join column cells 0, 1");
+
+ GridCellElement spannedCell = getGridElement().getHeaderCell(1, 0);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+
+ GridCellElement hiddenCell = getGridElement().getHeaderCell(1, 1);
+ assertFalse(hiddenCell.isDisplayed());
+ }
+
+ @Test
+ public void joinHeaderColumnsByColumns() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2");
+
+ GridCellElement spannedCell = getGridElement().getHeaderCell(1, 1);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+
+ GridCellElement hiddenCell = getGridElement().getHeaderCell(1, 2);
+ assertFalse(hiddenCell.isDisplayed());
+ }
+
+ @Test
+ public void joinAllColumnsInHeaderRow() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ selectMenuPath("Component", "Header", "Row 2", "Join all columns");
+
+ GridCellElement spannedCell = getGridElement().getHeaderCell(1, 0);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("" + GridBasicFeatures.COLUMNS,
+ spannedCell.getAttribute("colspan"));
+
+ for (int columnIndex = 1; columnIndex < GridBasicFeatures.COLUMNS; columnIndex++) {
+ GridCellElement hiddenCell = getGridElement().getHeaderCell(1,
+ columnIndex);
+ assertFalse(hiddenCell.isDisplayed());
+ }
+ }
+
+ @Test
+ public void hideFirstColumnInColspan() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ selectMenuPath("Component", "Header", "Row 2", "Join all columns");
+
+ int visibleColumns = GridBasicFeatures.COLUMNS;
+
+ GridCellElement spannedCell = getGridElement().getHeaderCell(1, 0);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("" + visibleColumns, spannedCell.getAttribute("colspan"));
+
+ selectMenuPath("Component", "Columns", "Column 0", "Visible");
+ visibleColumns--;
+
+ spannedCell = getGridElement().getHeaderCell(1, 0);
+ assertTrue(spannedCell.isDisplayed());
+ assertEquals("" + visibleColumns, spannedCell.getAttribute("colspan"));
+ }
+
+ @Test
+ public void multipleColspanAndMultipleHiddenColumns() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Header", "Append row");
+
+ // Join columns [1,2] and [3,4,5]
+ selectMenuPath("Component", "Header", "Row 2", "Join columns 1, 2");
+ GridCellElement spannedCell = getGridElement().getHeaderCell(1, 1);
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+
+ selectMenuPath("Component", "Header", "Row 2", "Join columns 3, 4, 5");
+ spannedCell = getGridElement().getHeaderCell(1, 3);
+ assertEquals("3", spannedCell.getAttribute("colspan"));
+
+ selectMenuPath("Component", "Columns", "Column 2", "Visible");
+ spannedCell = getGridElement().getHeaderCell(1, 1);
+ assertEquals("1", spannedCell.getAttribute("colspan"));
+
+ // Ensure the second colspan is preserved (shifts one index to the left)
+ spannedCell = getGridElement().getHeaderCell(1, 2);
+ assertEquals("3", spannedCell.getAttribute("colspan"));
+
+ selectMenuPath("Component", "Columns", "Column 4", "Visible");
+
+ // First reduced colspan is reduced
+ spannedCell = getGridElement().getHeaderCell(1, 1);
+ assertEquals("1", spannedCell.getAttribute("colspan"));
+
+ // Second colspan is also now reduced
+ spannedCell = getGridElement().getHeaderCell(1, 2);
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+
+ // Show columns again
+ selectMenuPath("Component", "Columns", "Column 2", "Visible");
+ selectMenuPath("Component", "Columns", "Column 4", "Visible");
+
+ spannedCell = getGridElement().getHeaderCell(1, 1);
+ assertEquals("2", spannedCell.getAttribute("colspan"));
+ spannedCell = getGridElement().getHeaderCell(1, 3);
+ assertEquals("3", spannedCell.getAttribute("colspan"));
+
+ }
+
+ @Test
+ public void testInitialCellTypes() throws Exception {
+ openTestURL();
+
+ GridCellElement textCell = getGridElement().getHeaderCell(0, 0);
+ assertEquals("Header (0,0)", textCell.getText());
+
+ GridCellElement widgetCell = getGridElement().getHeaderCell(0, 1);
+ assertTrue(widgetCell.isElementPresent(By.className("gwt-HTML")));
+
+ GridCellElement htmlCell = getGridElement().getHeaderCell(0, 2);
+ assertHTML("<b>Header (0,2)</b>", htmlCell);
+ }
+
+ @Test
+ public void testDynamicallyChangingCellType() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 0", "Header Type",
+ "Widget Header");
+ GridCellElement widgetCell = getGridElement().getHeaderCell(0, 0);
+ assertTrue(widgetCell.isElementPresent(By.className("gwt-Button")));
+
+ selectMenuPath("Component", "Columns", "Column 1", "Header Type",
+ "HTML Header");
+ GridCellElement htmlCell = getGridElement().getHeaderCell(0, 1);
+ assertHTML("<b>HTML Header</b>", htmlCell);
+
+ selectMenuPath("Component", "Columns", "Column 2", "Header Type",
+ "Text Header");
+ GridCellElement textCell = getGridElement().getHeaderCell(0, 2);
+ assertEquals("Text Header", textCell.getText());
+ }
+
+ @Test
+ public void testCellWidgetInteraction() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 0", "Header Type",
+ "Widget Header");
+ GridCellElement widgetCell = getGridElement().getHeaderCell(0, 0);
+ WebElement button = widgetCell.findElement(By.className("gwt-Button"));
+
+ button.click();
+
+ assertEquals("Clicked", button.getText());
+ }
+
+ @Test
+ public void widgetInSortableCellInteraction() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 0", "Header Type",
+ "Widget Header");
+
+ selectMenuPath("Component", "Columns", "Column 0", "Sortable");
+
+ GridCellElement widgetCell = getGridElement().getHeaderCell(0, 0);
+ WebElement button = widgetCell.findElement(By.className("gwt-Button"));
+
+ assertNotEquals("Clicked", button.getText());
+
+ button.click();
+
+ assertEquals("Clicked", button.getText());
+ }
+
+ private void assertHeaderCount(int count) {
+ assertEquals("header count", count, getGridElement().getHeaderCount());
+ }
+
+ private boolean hasClassName(TestBenchElement element, String name) {
+ return Arrays.asList(element.getAttribute("class").split(" "))
+ .contains(name);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java
new file mode 100644
index 0000000000..cc801bf870
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStaticSectionTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicClientFeaturesTest;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+
+/**
+ * Abstract base class for header and footer tests.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class GridStaticSectionTest extends GridBasicClientFeaturesTest {
+
+ protected void assertHeaderTexts(int headerId, int rowIndex) {
+ int i = 0;
+ for (TestBenchElement cell : getGridElement().getHeaderCells(rowIndex)) {
+
+ if (i % 3 == 0) {
+ assertText(String.format("Header (%d,%d)", headerId, i), cell);
+ } else if (i % 2 == 0) {
+ assertHTML(String.format("<b>Header (%d,%d)</b>", headerId, i),
+ cell);
+ } else {
+ assertHTML(String.format(
+ "<div class=\"gwt-HTML\">Header (%d,%d)</div>",
+ headerId, i), cell);
+ }
+
+ i++;
+ }
+ assertEquals("number of header columns", GridBasicFeatures.COLUMNS, i);
+ }
+
+ protected void assertFooterTexts(int footerId, int rowIndex) {
+ int i = 0;
+ for (TestBenchElement cell : getGridElement().getFooterCells(rowIndex)) {
+ if (i % 3 == 0) {
+ assertText(String.format("Footer (%d,%d)", footerId, i), cell);
+ } else if (i % 2 == 0) {
+ assertHTML(String.format("<b>Footer (%d,%d)</b>", footerId, i),
+ cell);
+ } else {
+ assertHTML(String.format(
+ "<div class=\"gwt-HTML\">Footer (%d,%d)</div>",
+ footerId, i), cell);
+ }
+ i++;
+ }
+ assertEquals("number of footer columns", GridBasicFeatures.COLUMNS, i);
+ }
+
+ protected static void assertText(String text, TestBenchElement e) {
+ // TBE.getText returns "" if the element is scrolled out of view
+ assertEquals(text, e.getAttribute("innerHTML"));
+ }
+
+ protected static void assertHTML(String text, TestBenchElement e) {
+ String html = e.getAttribute("innerHTML");
+
+ // IE 8 returns tags as upper case while other browsers do not, make the
+ // comparison non-casesensive
+ html = html.toLowerCase();
+ text = text.toLowerCase();
+
+ // IE 8 returns attributes without quotes, make the comparison without
+ // quotes
+ html = html.replaceAll("\"", "");
+ text = html.replaceAll("\"", "");
+
+ assertEquals(text, html);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java
new file mode 100644
index 0000000000..67e974bd0a
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/client/GridStylingTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.client;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.By;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+
+public class GridStylingTest extends GridStaticSectionTest {
+
+ @Test
+ public void testGridPrimaryStyle() throws Exception {
+ openTestURL();
+
+ validateStylenames("v-grid");
+ }
+
+ @Test
+ public void testChangingPrimaryStyleName() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "State", "Primary Stylename",
+ "v-custom-style");
+
+ validateStylenames("v-custom-style");
+ }
+
+ private void validateStylenames(String stylename) {
+
+ String classNames = getGridElement().getAttribute("class");
+ assertEquals(stylename, classNames);
+
+ classNames = getGridElement().getVerticalScroller().getAttribute(
+ "class");
+ assertTrue(classNames.contains(stylename + "-scroller"));
+ assertTrue(classNames.contains(stylename + "-scroller-vertical"));
+
+ classNames = getGridElement().getHorizontalScroller().getAttribute(
+ "class");
+ assertTrue(classNames.contains(stylename + "-scroller"));
+ assertTrue(classNames.contains(stylename + "-scroller-horizontal"));
+
+ classNames = getGridElement().getTableWrapper().getAttribute("class");
+ assertEquals(stylename + "-tablewrapper", classNames);
+
+ classNames = getGridElement().getHeader().getAttribute("class");
+ assertEquals(stylename + "-header", classNames);
+
+ for (int row = 0; row < getGridElement().getHeaderCount(); row++) {
+ classNames = getGridElement().getHeaderRow(row).getAttribute(
+ "class");
+ assertEquals(stylename + "-row", classNames);
+
+ for (int col = 0; col < GridBasicFeatures.COLUMNS; col++) {
+ classNames = getGridElement().getHeaderCell(row, col)
+ .getAttribute("class");
+ assertTrue(classNames.contains(stylename + "-cell"));
+
+ if (row == 0 && col == 0) {
+ assertTrue(classNames,
+ classNames.contains(stylename + "-header-active"));
+ }
+ }
+ }
+
+ classNames = getGridElement().getBody().getAttribute("class");
+ assertEquals(stylename + "-body", classNames);
+
+ int rowsInBody = getGridElement().getBody()
+ .findElements(By.tagName("tr")).size();
+ for (int row = 0; row < rowsInBody; row++) {
+ classNames = getGridElement().getRow(row).getAttribute("class");
+ assertTrue(classNames.contains(stylename + "-row"));
+ assertTrue(classNames.contains(stylename + "-row-has-data"));
+
+ for (int col = 0; col < GridBasicFeatures.COLUMNS; col++) {
+ classNames = getGridElement().getCell(row, col).getAttribute(
+ "class");
+ assertTrue(classNames.contains(stylename + "-cell"));
+
+ if (row == 0 && col == 0) {
+ assertTrue(classNames.contains(stylename + "-cell-active"));
+ }
+ }
+ }
+
+ classNames = getGridElement().getFooter().getAttribute("class");
+ assertEquals(stylename + "-footer", classNames);
+
+ for (int row = 0; row < getGridElement().getFooterCount(); row++) {
+ classNames = getGridElement().getFooterRow(row).getAttribute(
+ "class");
+ assertEquals(stylename + "-row", classNames);
+
+ for (int col = 0; col < GridBasicFeatures.COLUMNS; col++) {
+ classNames = getGridElement().getFooterCell(row, col)
+ .getAttribute("class");
+ assertTrue(classNames.contains(stylename + "-cell"));
+ }
+ }
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java
new file mode 100644
index 0000000000..0f9fd875d8
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridKeyboardNavigationTest.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.server;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.tests.components.grid.GridElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridKeyboardNavigationTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testCellActiveOnClick() {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+ assertTrue("Body cell 0, 0 is not active on init.", grid.getCell(0, 0)
+ .isActive());
+ grid.getCell(5, 2).click();
+ assertFalse("Body cell 0, 0 was still active after clicking", grid
+ .getCell(0, 0).isActive());
+ assertTrue("Body cell 5, 2 is not active after clicking",
+ grid.getCell(5, 2).isActive());
+ }
+
+ @Test
+ public void testCellNotActiveWhenRendererHandlesEvent() {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+ assertTrue("Body cell 0, 0 is not active on init.", grid.getCell(0, 0)
+ .isActive());
+ grid.getHeaderCell(0, 3).click();
+ assertFalse("Body cell 0, 0 is active after click on header.", grid
+ .getCell(0, 0).isActive());
+ assertTrue("Header cell 0, 3 is not active after click on header.",
+ grid.getHeaderCell(0, 3).isActive());
+ }
+
+ @Test
+ public void testSimpleKeyboardNavigation() {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+ grid.getCell(0, 0).click();
+
+ new Actions(getDriver()).sendKeys(Keys.ARROW_DOWN).perform();
+ assertTrue("Body cell 1, 0 is not active after keyboard navigation.",
+ grid.getCell(1, 0).isActive());
+
+ new Actions(getDriver()).sendKeys(Keys.ARROW_RIGHT).perform();
+ assertTrue("Body cell 1, 1 is not active after keyboard navigation.",
+ grid.getCell(1, 1).isActive());
+
+ int i;
+ for (i = 1; i < 40; ++i) {
+ new Actions(getDriver()).sendKeys(Keys.ARROW_DOWN).perform();
+ }
+
+ assertFalse("Grid has not scrolled with active cell",
+ isElementPresent(By.xpath("//td[text() = '(0, 0)']")));
+ assertTrue("Active cell is not visible",
+ isElementPresent(By.xpath("//td[text() = '(" + i + ", 0)']")));
+ assertTrue("Body cell " + i + ", 1 is not active", grid.getCell(i, 1)
+ .isActive());
+ }
+
+ @Test
+ public void testNavigateFromHeaderToBody() {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+ grid.scrollToRow(300);
+ new Actions(driver).moveToElement(grid.getHeaderCell(0, 7)).click()
+ .perform();
+ grid.scrollToRow(280);
+
+ assertTrue("Header cell is not active.", grid.getHeaderCell(0, 7)
+ .isActive());
+ new Actions(getDriver()).sendKeys(Keys.ARROW_DOWN).perform();
+ assertTrue("Body cell 280, 7 is not active", grid.getCell(280, 7)
+ .isActive());
+ }
+
+ @Test
+ public void testNavigationFromFooterToBody() {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Visible");
+
+ GridElement grid = getGridElement();
+ grid.scrollToRow(300);
+ grid.getFooterCell(0, 2).click();
+
+ assertTrue("Footer cell is not active.", grid.getFooterCell(0, 2)
+ .isActive());
+ new Actions(getDriver()).sendKeys(Keys.ARROW_UP).perform();
+ assertTrue("Body cell 300, 2 is not active", grid.getCell(300, 2)
+ .isActive());
+ }
+
+ @Test
+ public void testNavigateBetweenHeaderAndBodyWithTab() {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+ grid.getCell(10, 2).click();
+
+ assertTrue("Body cell 10, 2 is not active", grid.getCell(10, 2)
+ .isActive());
+ new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.TAB)
+ .keyUp(Keys.SHIFT).perform();
+ assertTrue("Header cell 0, 2 is not active", grid.getHeaderCell(0, 2)
+ .isActive());
+ new Actions(getDriver()).sendKeys(Keys.TAB).perform();
+ assertTrue("Body cell 10, 2 is not active", grid.getCell(10, 2)
+ .isActive());
+
+ // Navigate out of the Grid and try to navigate with arrow keys.
+ new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.TAB)
+ .sendKeys(Keys.TAB).keyUp(Keys.SHIFT).sendKeys(Keys.ARROW_DOWN)
+ .perform();
+ assertTrue("Header cell 0, 2 is not active", grid.getHeaderCell(0, 2)
+ .isActive());
+ }
+
+ @Test
+ public void testNavigateBetweenFooterAndBodyWithTab() {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Visible");
+
+ GridElement grid = getGridElement();
+ grid.getCell(10, 2).click();
+
+ assertTrue("Body cell 10, 2 is not active", grid.getCell(10, 2)
+ .isActive());
+ new Actions(getDriver()).sendKeys(Keys.TAB).perform();
+ assertTrue("Footer cell 0, 2 is not active", grid.getFooterCell(0, 2)
+ .isActive());
+ new Actions(getDriver()).keyDown(Keys.SHIFT).sendKeys(Keys.TAB)
+ .keyUp(Keys.SHIFT).perform();
+ assertTrue("Body cell 10, 2 is not active", grid.getCell(10, 2)
+ .isActive());
+
+ // Navigate out of the Grid and try to navigate with arrow keys.
+ new Actions(getDriver()).sendKeys(Keys.TAB).sendKeys(Keys.TAB)
+ .sendKeys(Keys.ARROW_UP).perform();
+ assertTrue("Footer cell 0, 2 is not active", grid.getFooterCell(0, 2)
+ .isActive());
+ }
+
+ @Test
+ public void testHomeEnd() throws Exception {
+ openTestURL();
+
+ getGridElement().getCell(100, 2).click();
+
+ new Actions(getDriver()).sendKeys(Keys.HOME).perform();
+ assertTrue("First row is not visible", getGridElement().getCell(0, 2)
+ .isDisplayed());
+
+ new Actions(getDriver()).sendKeys(Keys.END).perform();
+ assertTrue("Last row cell not visible",
+ getGridElement().getCell(GridBasicFeatures.ROWS - 1, 2)
+ .isDisplayed());
+ }
+
+ @Test
+ public void testPageUpPageDown() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Size", "HeightMode Row");
+
+ getGridElement().getCell(5, 2).click();
+
+ new Actions(getDriver()).sendKeys(Keys.PAGE_DOWN).perform();
+ assertTrue("Row 5 did not remain active", getGridElement()
+ .getCell(5, 2).isActive());
+ assertTrue("Row 20 did not become visible",
+ getGridElement().getCell(20, 2).isDisplayed());
+
+ new Actions(getDriver()).sendKeys(Keys.PAGE_DOWN).perform();
+ assertTrue("Row 5 did not remain active", getGridElement()
+ .getCell(5, 2).isActive());
+ assertTrue("Row 40 did not become visible",
+ getGridElement().getCell(40, 2).isDisplayed());
+
+ getGridElement().getCell(50, 2).click();
+
+ new Actions(getDriver()).sendKeys(Keys.PAGE_UP).perform();
+ assertTrue("Row 50 did not remain active",
+ getGridElement().getCell(50, 2).isActive());
+ assertTrue("Row 20 did not become visible",
+ getGridElement().getCell(20, 2).isDisplayed());
+
+ new Actions(getDriver()).sendKeys(Keys.PAGE_UP).perform();
+ assertTrue("Row 50 did not remain active",
+ getGridElement().getCell(50, 2).isActive());
+ assertTrue("Row 0 did not become visible",
+ getGridElement().getCell(0, 2).isDisplayed());
+
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java
new file mode 100644
index 0000000000..6e2ac91df2
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSelectionTest.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.server;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.tests.components.grid.GridElement;
+import com.vaadin.tests.components.grid.GridElement.GridRowElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridSelectionTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testSelectOnOff() throws Exception {
+ openTestURL();
+
+ setSelectionModelMulti();
+
+ assertFalse("row shouldn't start out as selected", getRow(0)
+ .isSelected());
+ toggleFirstRowSelection();
+ assertTrue("row should become selected", getRow(0).isSelected());
+ toggleFirstRowSelection();
+ assertFalse("row shouldn't remain selected", getRow(0).isSelected());
+ }
+
+ @Test
+ public void testSelectOnScrollOffScroll() throws Exception {
+ openTestURL();
+
+ setSelectionModelMulti();
+
+ assertFalse("row shouldn't start out as selected", getRow(0)
+ .isSelected());
+ toggleFirstRowSelection();
+ assertTrue("row should become selected", getRow(0).isSelected());
+
+ scrollGridVerticallyTo(10000); // make sure the row is out of cache
+ scrollGridVerticallyTo(0); // scroll it back into view
+
+ assertTrue("row should still be selected when scrolling "
+ + "back into view", getRow(0).isSelected());
+ }
+
+ @Test
+ public void testSelectScrollOnScrollOff() throws Exception {
+ openTestURL();
+
+ setSelectionModelMulti();
+
+ assertFalse("row shouldn't start out as selected", getRow(0)
+ .isSelected());
+
+ scrollGridVerticallyTo(10000); // make sure the row is out of cache
+ toggleFirstRowSelection();
+
+ scrollGridVerticallyTo(0); // scroll it back into view
+ assertTrue("row should still be selected when scrolling "
+ + "back into view", getRow(0).isSelected());
+
+ toggleFirstRowSelection();
+ assertFalse("row shouldn't remain selected", getRow(0).isSelected());
+ }
+
+ @Test
+ public void testSelectScrollOnOffScroll() throws Exception {
+ openTestURL();
+
+ setSelectionModelMulti();
+
+ assertFalse("row shouldn't start out as selected", getRow(0)
+ .isSelected());
+
+ scrollGridVerticallyTo(10000); // make sure the row is out of cache
+ toggleFirstRowSelection();
+ toggleFirstRowSelection();
+
+ scrollGridVerticallyTo(0); // make sure the row is out of cache
+ assertFalse("row shouldn't be selected when scrolling "
+ + "back into view", getRow(0).isSelected());
+ }
+
+ @Test
+ public void testSingleSelectionUpdatesFromServer() {
+ openTestURL();
+ setSelectionModelSingle();
+
+ GridElement grid = getGridElement();
+ assertFalse("First row was selected from start", grid.getRow(0)
+ .isSelected());
+ toggleFirstRowSelection();
+ assertTrue("First row was not selected.", getRow(0).isSelected());
+ grid.getCell(5, 0).click();
+ assertTrue("Fifth row was not selected.", getRow(5).isSelected());
+ assertFalse("First row was still selected.", getRow(0).isSelected());
+ grid.getCell(0, 0).click();
+ toggleFirstRowSelection();
+ assertFalse("First row was still selected.", getRow(0).isSelected());
+ assertFalse("Fifth row was still selected.", getRow(5).isSelected());
+
+ grid.scrollToRow(600);
+ grid.getCell(595, 0).click();
+ assertTrue("Row 595 was not selected.", getRow(595).isSelected());
+ toggleFirstRowSelection();
+ assertFalse("Row 595 was still selected.", getRow(595).isSelected());
+ assertTrue("First row was not selected.", getRow(0).isSelected());
+ }
+
+ @Test
+ public void testKeyboardSelection() {
+ openTestURL();
+ setSelectionModelMulti();
+
+ GridElement grid = getGridElement();
+ grid.getCell(3, 1).click();
+ new Actions(getDriver()).sendKeys(Keys.SPACE).perform();
+
+ assertTrue("Grid row 3 was not selected with space key.", grid
+ .getRow(3).isSelected());
+
+ new Actions(getDriver()).sendKeys(Keys.SPACE).perform();
+
+ assertTrue("Grid row 3 was not deselected with space key.", !grid
+ .getRow(3).isSelected());
+
+ grid.scrollToRow(500);
+
+ new Actions(getDriver()).sendKeys(Keys.SPACE).perform();
+
+ assertTrue("Grid row 3 was not selected with space key.", grid
+ .getRow(3).isSelected());
+
+ }
+
+ private void setSelectionModelMulti() {
+ selectMenuPath("Component", "State", "Selection mode", "multi");
+ }
+
+ private void setSelectionModelSingle() {
+ selectMenuPath("Component", "State", "Selection mode", "single");
+ }
+
+ @SuppressWarnings("static-method")
+ private boolean isSelected(TestBenchElement row) {
+ /*
+ * FIXME We probably should get a GridRow instead of a plain
+ * TestBenchElement, that has an "isSelected" thing integrated. (henrik
+ * paul 26.6.2014)
+ */
+ return row.getAttribute("class").contains("-row-selected");
+ }
+
+ private void toggleFirstRowSelection() {
+ selectMenuPath("Component", "Body rows", "Select first row");
+ }
+
+ private GridRowElement getRow(int i) {
+ return getGridElement().getRow(i);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java
new file mode 100644
index 0000000000..024be65e83
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridSortingTest.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.server;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+
+import org.junit.Test;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.interactions.Actions;
+
+import com.vaadin.tests.components.grid.GridElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeatures;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridSortingTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testProgrammaticSorting() throws IOException {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+
+ // Sorting by column 9 is sorting by row index that is represented as a
+ // String.
+ // First cells for first 3 rows are (9, 0), (99, 0) and (999, 0)
+ sortBy("Column 9, DESC");
+
+ // Verify that programmatic sorting calls are identified as originating
+ // from API
+ assertEquals("3. Sort order: [Column 9 DESCENDING] by API",
+ getLogRow(0));
+
+ assertTrue("Column 9 should have the sort-desc stylename", grid
+ .getHeaderCell(0, 9).getAttribute("class")
+ .contains("sort-desc"));
+
+ String row = "";
+ for (int i = 0; i < 3; ++i) {
+ row += "9";
+ assertEquals(
+ "Grid is not sorted by Column 9 using descending direction.",
+ "(" + row + ", 0)", grid.getCell(i, 0).getText());
+ }
+
+ // Column 10 is random numbers from Random with seed 13334
+ sortBy("Column 10, ASC");
+
+ assertFalse(
+ "Column 9 should no longer have the sort-desc stylename",
+ grid.getHeaderCell(0, 9).getAttribute("class")
+ .contains("sort-desc"));
+ assertTrue("Column 10 should have the sort-asc stylename", grid
+ .getHeaderCell(0, 10).getAttribute("class")
+ .contains("sort-asc"));
+
+ // Not cleaning up correctly causes exceptions when scrolling.
+ grid.scrollToRow(50);
+ assertFalse("Scrolling caused and exception when shuffled.",
+ getLogRow(0).contains("Exception"));
+
+ for (int i = 0; i < 5; ++i) {
+ assertGreater(
+ "Grid is not sorted by Column 10 using ascending direction",
+ Integer.parseInt(grid.getCell(i + 1, 10).getText()),
+ Integer.parseInt(grid.getCell(i, 10).getText()));
+
+ }
+
+ // Column 7 is row index as a number. Last three row are original rows
+ // 2, 1 and 0.
+ sortBy("Column 7, DESC");
+ for (int i = 0; i < 3; ++i) {
+ assertEquals(
+ "Grid is not sorted by Column 7 using descending direction",
+ "(" + i + ", 0)",
+ grid.getCell(GridBasicFeatures.ROWS - (i + 1), 0).getText());
+ }
+
+ assertFalse(
+ "Column 10 should no longer have the sort-asc stylename",
+ grid.getHeaderCell(0, 10).getAttribute("class")
+ .contains("sort-asc"));
+ assertTrue("Column 7 should have the sort-desc stylename", grid
+ .getHeaderCell(0, 7).getAttribute("class")
+ .contains("sort-desc"));
+
+ }
+
+ @Test
+ public void testUserSorting() throws InterruptedException {
+ openTestURL();
+
+ GridElement grid = getGridElement();
+
+ // Sorting by column 9 is sorting by row index that is represented as a
+ // String.
+ // First cells for first 3 rows are (9, 0), (99, 0) and (999, 0)
+
+ // Click header twice to sort descending
+ grid.getHeaderCell(0, 9).click();
+ grid.getHeaderCell(0, 9).click();
+ String row = "";
+ for (int i = 0; i < 3; ++i) {
+ row += "9";
+ assertEquals(
+ "Grid is not sorted by Column 9 using descending direction.",
+ "(" + row + ", 0)", grid.getCell(i, 0).getText());
+ }
+
+ assertEquals("2. Sort order: [Column 9 ASCENDING] by USER",
+ getLogRow(2));
+ assertEquals("4. Sort order: [Column 9 DESCENDING] by USER",
+ getLogRow(0));
+
+ // Column 10 is random numbers from Random with seed 13334
+ // Click header to sort ascending
+ grid.getHeaderCell(0, 10).click();
+
+ assertEquals("6. Sort order: [Column 10 ASCENDING] by USER",
+ getLogRow(0));
+
+ // Not cleaning up correctly causes exceptions when scrolling.
+ grid.scrollToRow(50);
+ assertFalse("Scrolling caused and exception when shuffled.",
+ getLogRow(0).contains("Exception"));
+
+ for (int i = 0; i < 5; ++i) {
+ assertGreater(
+ "Grid is not sorted by Column 10 using ascending direction",
+ Integer.parseInt(grid.getCell(i + 1, 10).getText()),
+ Integer.parseInt(grid.getCell(i, 10).getText()));
+
+ }
+
+ // Column 7 is row index as a number. Last three row are original rows
+ // 2, 1 and 0.
+ // Click header twice to sort descending
+ grid.getHeaderCell(0, 7).click();
+ grid.getHeaderCell(0, 7).click();
+ for (int i = 0; i < 3; ++i) {
+ assertEquals(
+ "Grid is not sorted by Column 7 using descending direction",
+ "(" + i + ", 0)",
+ grid.getCell(GridBasicFeatures.ROWS - (i + 1), 0).getText());
+ }
+
+ assertEquals("9. Sort order: [Column 7 ASCENDING] by USER",
+ getLogRow(3));
+ assertEquals("11. Sort order: [Column 7 DESCENDING] by USER",
+ getLogRow(1));
+ }
+
+ @Test
+ public void testUserMultiColumnSorting() {
+ openTestURL();
+
+ getGridElement().getHeaderCell(0, 0).click();
+ new Actions(driver).keyDown(Keys.SHIFT).perform();
+ getGridElement().getHeaderCell(0, 11).click();
+ new Actions(driver).keyUp(Keys.SHIFT).perform();
+
+ String prev = getGridElement().getCell(0, 11).getAttribute("innerHTML");
+ for (int i = 1; i <= 6; ++i) {
+ assertEquals("Column 11 should contain same values.", prev,
+ getGridElement().getCell(i, 11).getAttribute("innerHTML"));
+ }
+
+ prev = getGridElement().getCell(0, 0).getText();
+ for (int i = 1; i <= 6; ++i) {
+ assertTrue(
+ "Grid is not sorted by column 0.",
+ prev.compareTo(getGridElement().getCell(i, 0).getText()) < 0);
+ }
+
+ }
+
+ private void sortBy(String column) {
+ selectMenuPath("Component", "State", "Sort by column", column);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java
new file mode 100644
index 0000000000..19a68a87f4
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStaticSectionComponentTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.server;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+import com.vaadin.testbench.elements.ButtonElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridStaticSectionComponentTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testNativeButtonInHeader() throws IOException {
+ openTestURL();
+
+ selectMenuPath("Component", "Columns", "Column 1", "Header Type",
+ "Widget Header");
+
+ getGridElement().$(ButtonElement.class).first().click();
+
+ // Clicking also triggers sorting
+ assertEquals("2. Button clicked!", getLogRow(2));
+ }
+
+ @Test
+ public void testNativeButtonInFooter() throws IOException {
+ openTestURL();
+
+ selectMenuPath("Component", "Footer", "Visible");
+ selectMenuPath("Component", "Footer", "Append row");
+ selectMenuPath("Component", "Columns", "Column 1", "Footer Type",
+ "Widget Footer");
+
+ getGridElement().$(ButtonElement.class).first().click();
+
+ assertEquals("4. Button clicked!", getLogRow(0));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java
new file mode 100644
index 0000000000..7c5607d4e6
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/components/grid/basicfeatures/server/GridStructureTest.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2000-2014 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.basicfeatures.server;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNot.not;
+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.WebElement;
+
+import com.vaadin.testbench.TestBenchElement;
+import com.vaadin.tests.components.grid.basicfeatures.GridBasicFeaturesTest;
+
+public class GridStructureTest extends GridBasicFeaturesTest {
+
+ @Test
+ public void testHidingColumn() throws Exception {
+ openTestURL();
+
+ // Column 0 should be visible
+ List<TestBenchElement> cells = getGridHeaderRowCells();
+ assertEquals("Column 0", cells.get(0).getText());
+
+ // Hide column 0
+ selectMenuPath("Component", "Columns", "Column 0", "Visible");
+
+ // Column 1 should now be the first cell
+ cells = getGridHeaderRowCells();
+ assertEquals("Column 1", cells.get(0).getText());
+ }
+
+ @Test
+ public void testRemovingColumn() throws Exception {
+ openTestURL();
+
+ // Column 0 should be visible
+ List<TestBenchElement> cells = getGridHeaderRowCells();
+ assertEquals("Column 0", cells.get(0).getText());
+
+ // Hide column 0
+ selectMenuPath("Component", "Columns", "Column 0", "Remove");
+
+ // Column 1 should now be the first cell
+ cells = getGridHeaderRowCells();
+ assertEquals("Column 1", cells.get(0).getText());
+ }
+
+ @Test
+ public void testDataLoadingAfterRowRemoval() throws Exception {
+ openTestURL();
+
+ // Remove columns 2,3,4
+ selectMenuPath("Component", "Columns", "Column 2", "Remove");
+ selectMenuPath("Component", "Columns", "Column 3", "Remove");
+ selectMenuPath("Component", "Columns", "Column 4", "Remove");
+
+ // Scroll so new data is lazy loaded
+ scrollGridVerticallyTo(1000);
+
+ // Let lazy loading do its job
+ sleep(1000);
+
+ // Check that row is loaded
+ assertThat(getGridElement().getCell(11, 0).getText(), not("..."));
+ }
+
+ @Test
+ public void testFreezingColumn() throws Exception {
+ openTestURL();
+
+ // Freeze column 2
+ selectMenuPath("Component", "Columns", "Column 2", "Freeze");
+
+ WebElement cell = getGridElement().getCell(0, 0);
+ assertTrue(cell.getAttribute("class").contains("frozen"));
+
+ cell = getGridElement().getCell(0, 1);
+ assertTrue(cell.getAttribute("class").contains("frozen"));
+ }
+
+ @Test
+ public void testInitialColumnWidths() throws Exception {
+ openTestURL();
+
+ WebElement cell = getGridElement().getCell(0, 0);
+ assertEquals(100, cell.getSize().getWidth());
+
+ cell = getGridElement().getCell(0, 1);
+ assertEquals(150, cell.getSize().getWidth());
+
+ cell = getGridElement().getCell(0, 2);
+ assertEquals(200, cell.getSize().getWidth());
+ }
+
+ @Test
+ public void testColumnWidths() throws Exception {
+ openTestURL();
+
+ // Default column width is 100px
+ WebElement cell = getGridElement().getCell(0, 0);
+ assertEquals(100, cell.getSize().getWidth());
+
+ // Set first column to be 200px wide
+ selectMenuPath("Component", "Columns", "Column 0", "Column 0 Width",
+ "200px");
+
+ cell = getGridElement().getCell(0, 0);
+ assertEquals(200, cell.getSize().getWidth());
+
+ // Set second column to be 150px wide
+ selectMenuPath("Component", "Columns", "Column 1", "Column 1 Width",
+ "150px");
+ cell = getGridElement().getCell(0, 1);
+ assertEquals(150, cell.getSize().getWidth());
+
+ // Set first column to be auto sized (defaults to 100px currently)
+ selectMenuPath("Component", "Columns", "Column 0", "Column 0 Width",
+ "Auto");
+
+ cell = getGridElement().getCell(0, 0);
+ assertEquals(100, cell.getSize().getWidth());
+ }
+
+ @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", !isElementPresent(newRow));
+
+ selectMenuPath("Component", "Body rows", "Add first row");
+ assertTrue("Add row failed", isElementPresent(newRow));
+
+ selectMenuPath("Component", "Body rows", "Remove first row");
+ assertTrue("Remove row failed", !isElementPresent(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)",
+ getGridElement().getCell(0, 0).getText());
+
+ selectMenuPath("Component", "Body rows",
+ "Modify first row (getItemProperty)");
+ assertEquals("(First) modification with getItemProperty failed",
+ "modified: 0", getGridElement().getCell(0, 0).getText());
+
+ selectMenuPath("Component", "Body rows",
+ "Modify first row (getContainerProperty)");
+ assertEquals("(Second) modification with getItemProperty failed",
+ "modified: Column 0", getGridElement().getCell(0, 0).getText());
+ }
+
+ @Test
+ public void testRemovingAllItems() throws Exception {
+ openTestURL();
+
+ selectMenuPath("Component", "Body rows", "Remove all rows");
+
+ assertEquals(0, getGridElement().findElement(By.tagName("tbody"))
+ .findElements(By.tagName("tr")).size());
+ }
+
+ private void assertPrimaryStylename(String stylename) {
+ assertTrue(getGridElement().getAttribute("class").contains(stylename));
+
+ String tableWrapperStyleName = getGridElement().getTableWrapper()
+ .getAttribute("class");
+ assertTrue(tableWrapperStyleName.contains(stylename + "-tablewrapper"));
+
+ String hscrollStyleName = getGridElement().getHorizontalScroller()
+ .getAttribute("class");
+ assertTrue(hscrollStyleName.contains(stylename + "-scroller"));
+ assertTrue(hscrollStyleName
+ .contains(stylename + "-scroller-horizontal"));
+
+ String vscrollStyleName = getGridElement().getVerticalScroller()
+ .getAttribute("class");
+ assertTrue(vscrollStyleName.contains(stylename + "-scroller"));
+ assertTrue(vscrollStyleName.contains(stylename + "-scroller-vertical"));
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml b/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml
index 2c25c54e04..d23903f9db 100644
--- a/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml
+++ b/uitest/src/com/vaadin/tests/widgetset/TestingWidgetSet.gwt.xml
@@ -4,6 +4,8 @@
<!-- Inherit the DefaultWidgetSet -->
<inherits name="com.vaadin.DefaultWidgetSet" />
+ <inherits name="com.google.gwt.user.theme.standard.Standard" />
+
<replace-with class="com.vaadin.tests.widgetset.client.CustomUIConnector">
<when-type-is class="com.vaadin.client.ui.ui.UIConnector" />
</replace-with>
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesConnector.java
new file mode 100644
index 0000000000..b0841b69fb
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesConnector.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2000-2014 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.tests.components.grid.basicfeatures.GridBasicClientFeatures.GridTestComponent;
+
+/**
+ * Connector for the GridClientBasicFeatures ApplicationWidget
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+@Connect(GridTestComponent.class)
+public class GridBasicClientFeaturesConnector extends
+ AbstractComponentConnector {
+
+ @Override
+ public GridBasicClientFeaturesWidget getWidget() {
+ return (GridBasicClientFeaturesWidget) super.getWidget();
+ }
+
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java
new file mode 100644
index 0000000000..a7210236d4
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridBasicClientFeaturesWidget.java
@@ -0,0 +1,755 @@
+/*
+ * Copyright 2000-2014 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 java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HTML;
+import com.vaadin.client.ui.VLabel;
+import com.vaadin.client.ui.grid.Cell;
+import com.vaadin.client.ui.grid.FlyweightCell;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.Grid.AbstractGridKeyEvent;
+import com.vaadin.client.ui.grid.Grid.SelectionMode;
+import com.vaadin.client.ui.grid.GridColumn;
+import com.vaadin.client.ui.grid.GridFooter;
+import com.vaadin.client.ui.grid.GridFooter.FooterRow;
+import com.vaadin.client.ui.grid.GridHeader;
+import com.vaadin.client.ui.grid.GridHeader.HeaderRow;
+import com.vaadin.client.ui.grid.Renderer;
+import com.vaadin.client.ui.grid.datasources.ListDataSource;
+import com.vaadin.client.ui.grid.datasources.ListSorter;
+import com.vaadin.client.ui.grid.keyevents.BodyKeyDownHandler;
+import com.vaadin.client.ui.grid.keyevents.BodyKeyPressHandler;
+import com.vaadin.client.ui.grid.keyevents.BodyKeyUpHandler;
+import com.vaadin.client.ui.grid.keyevents.FooterKeyDownHandler;
+import com.vaadin.client.ui.grid.keyevents.FooterKeyPressHandler;
+import com.vaadin.client.ui.grid.keyevents.FooterKeyUpHandler;
+import com.vaadin.client.ui.grid.keyevents.GridKeyDownEvent;
+import com.vaadin.client.ui.grid.keyevents.GridKeyPressEvent;
+import com.vaadin.client.ui.grid.keyevents.GridKeyUpEvent;
+import com.vaadin.client.ui.grid.keyevents.HeaderKeyDownHandler;
+import com.vaadin.client.ui.grid.keyevents.HeaderKeyPressHandler;
+import com.vaadin.client.ui.grid.keyevents.HeaderKeyUpHandler;
+import com.vaadin.client.ui.grid.renderers.DateRenderer;
+import com.vaadin.client.ui.grid.renderers.HtmlRenderer;
+import com.vaadin.client.ui.grid.renderers.NumberRenderer;
+import com.vaadin.client.ui.grid.renderers.TextRenderer;
+import com.vaadin.tests.widgetset.client.grid.GridBasicClientFeaturesWidget.Data;
+
+/**
+ * Grid basic client features test application.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridBasicClientFeaturesWidget extends
+ PureGWTTestApplication<Grid<List<Data>>> {
+
+ public static enum Renderers {
+ TEXT_RENDERER, HTML_RENDERER, NUMBER_RENDERER, DATE_RENDERER;
+ }
+
+ private static final int MANUALLY_FORMATTED_COLUMNS = 5;
+ public static final int COLUMNS = 12;
+ public static final int ROWS = 1000;
+
+ private final Grid<List<Data>> grid;
+ private final List<List<Data>> data;
+ private final ListDataSource<List<Data>> ds;
+ private final ListSorter<List<Data>> sorter;
+
+ /**
+ * Our basic data object
+ */
+ public final static class Data {
+ Object value;
+ }
+
+ /**
+ * Convenience method for creating a list of Data objects to be used as a
+ * Row in the data source
+ *
+ * @param cols
+ * number of columns (items) to include in the row
+ * @return
+ */
+ private List<Data> createDataRow(int cols) {
+ List<Data> list = new ArrayList<Data>(cols);
+ for (int i = 0; i < cols; ++i) {
+ list.add(new Data());
+ }
+ data.add(list);
+ return list;
+ }
+
+ @SuppressWarnings("unchecked")
+ public GridBasicClientFeaturesWidget() {
+ super(new Grid<List<Data>>());
+
+ // Initialize data source
+ data = new ArrayList<List<Data>>();
+ {
+ Random rand = new Random();
+ rand.setSeed(13334);
+ long timestamp = 0;
+ for (int row = 0; row < ROWS; row++) {
+
+ List<Data> datarow = createDataRow(COLUMNS);
+ Data d;
+
+ int col = 0;
+ for (; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; ++col) {
+ d = datarow.get(col);
+ d.value = "(" + row + ", " + col + ")";
+ }
+
+ d = datarow.get(col++);
+ d.value = Integer.valueOf(row);
+
+ d = datarow.get(col++);
+ d.value = new Date(timestamp);
+ timestamp += 91250000; // a bit over a day, just to get
+ // variation
+
+ d = datarow.get(col++);
+ d.value = "<b>" + row + "</b>";
+
+ d = datarow.get(col++);
+ d.value = Integer.valueOf(rand.nextInt());
+
+ d = datarow.get(col++);
+ d.value = Integer.valueOf(rand.nextInt(5));
+ }
+ }
+
+ ds = new ListDataSource<List<Data>>(data);
+ grid = getTestedWidget();
+ grid.getElement().setId("testComponent");
+ grid.setDataSource(ds);
+ grid.setSelectionMode(SelectionMode.NONE);
+
+ sorter = new ListSorter<List<Data>>(grid);
+
+ // Create a bunch of grid columns
+
+ // Data source layout:
+ // text (String) * (COLUMNS - MANUALLY_FORMATTED_COLUMNS + 1) |
+ // rownumber (Integer) | some date (Date) | row number as HTML (String)
+ // | random value (Integer)
+
+ int col = 0;
+
+ // Text times COLUMNS - MANUALLY_FORMATTED_COLUMNS
+ for (col = 0; col < COLUMNS - MANUALLY_FORMATTED_COLUMNS; ++col) {
+
+ final int c = col;
+
+ GridColumn<String, List<Data>> column = new GridColumn<String, List<Data>>(
+ createRenderer(Renderers.TEXT_RENDERER)) {
+ @Override
+ public String getValue(List<Data> row) {
+ return (String) row.get(c).value;
+ }
+ };
+
+ column.setWidth(50 + c * 25);
+
+ grid.addColumn(column);
+
+ }
+
+ // Integer row number
+ {
+ final int c = col++;
+ grid.addColumn(new GridColumn<Integer, List<Data>>(
+ createRenderer(Renderers.NUMBER_RENDERER)) {
+ @Override
+ public Integer getValue(List<Data> row) {
+ return (Integer) row.get(c).value;
+ }
+ });
+ }
+
+ // Some date
+ {
+ final int c = col++;
+ grid.addColumn(new GridColumn<Date, List<Data>>(
+ createRenderer(Renderers.DATE_RENDERER)) {
+ @Override
+ public Date getValue(List<Data> row) {
+ return (Date) row.get(c).value;
+ }
+ });
+ }
+
+ // Row number as a HTML string
+ {
+ final int c = col++;
+ grid.addColumn(new GridColumn<String, List<Data>>(
+ createRenderer(Renderers.HTML_RENDERER)) {
+ @Override
+ public String getValue(List<Data> row) {
+ return (String) row.get(c).value;
+ }
+ });
+ }
+
+ // Random integer value
+ {
+ final int c = col++;
+ grid.addColumn(new GridColumn<Integer, List<Data>>(
+ createRenderer(Renderers.NUMBER_RENDERER)) {
+ @Override
+ public Integer getValue(List<Data> row) {
+ return (Integer) row.get(c).value;
+ }
+ });
+ }
+
+ // Random integer value between 0 and 5
+ {
+ final int c = col++;
+ grid.addColumn(new GridColumn<Integer, List<Data>>(
+ createRenderer(Renderers.NUMBER_RENDERER)) {
+ @Override
+ public Integer getValue(List<Data> row) {
+ return (Integer) row.get(c).value;
+ }
+ });
+ }
+
+ setHeaderTexts(grid.getHeader().getRow(0));
+
+ //
+ // Populate the menu
+ //
+
+ createStateMenu();
+ createColumnsMenu();
+ createHeaderMenu();
+ createFooterMenu();
+
+ grid.getElement().getStyle().setZIndex(0);
+ addNorth(grid, 400);
+
+ createKeyHandlers();
+ }
+
+ private void createStateMenu() {
+ String[] selectionModePath = { "Component", "State", "Selection mode" };
+ String[] primaryStyleNamePath = { "Component", "State",
+ "Primary Stylename" };
+
+ addMenuCommand("multi", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setSelectionMode(SelectionMode.MULTI);
+ }
+ }, selectionModePath);
+
+ addMenuCommand("single", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setSelectionMode(SelectionMode.SINGLE);
+ }
+ }, selectionModePath);
+
+ addMenuCommand("none", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setSelectionMode(SelectionMode.NONE);
+ }
+ }, selectionModePath);
+
+ addMenuCommand("v-grid", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setStylePrimaryName("v-grid");
+
+ }
+ }, primaryStyleNamePath);
+
+ addMenuCommand("v-escalator", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setStylePrimaryName("v-escalator");
+
+ }
+ }, primaryStyleNamePath);
+
+ addMenuCommand("v-custom-style", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.setStylePrimaryName("v-custom-style");
+
+ }
+ }, primaryStyleNamePath);
+
+ }
+
+ private void createColumnsMenu() {
+
+ for (int i = 0; i < COLUMNS; i++) {
+ final int index = i;
+ addMenuCommand("Visible", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getColumn(index).setVisible(
+ !grid.getColumn(index).isVisible());
+ }
+ }, "Component", "Columns", "Column " + i);
+ addMenuCommand("Sortable", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getColumn(index).setSortable(
+ !grid.getColumn(index).isSortable());
+ }
+ }, "Component", "Columns", "Column " + i);
+
+ addMenuCommand("auto", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getColumn(index).setWidth(-1);
+ }
+ }, "Component", "Columns", "Column " + i, "Width");
+ addMenuCommand("50px", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getColumn(index).setWidth(50);
+ }
+ }, "Component", "Columns", "Column " + i, "Width");
+ addMenuCommand("200px", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getColumn(index).setWidth(200);
+ }
+ }, "Component", "Columns", "Column " + i, "Width");
+
+ // Header types
+ addMenuCommand("Text Header", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getHeader().getRow(0).getCell(index)
+ .setText("Text Header");
+ }
+ }, "Component", "Columns", "Column " + i, "Header Type");
+ addMenuCommand("HTML Header", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getHeader().getRow(0).getCell(index)
+ .setHtml("<b>HTML Header</b>");
+ }
+ }, "Component", "Columns", "Column " + i, "Header Type");
+ addMenuCommand("Widget Header", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ final Button button = new Button("Button Header");
+ button.addClickHandler(new ClickHandler() {
+
+ @Override
+ public void onClick(ClickEvent event) {
+ button.setText("Clicked");
+ }
+ });
+ grid.getHeader().getRow(0).getCell(index).setWidget(button);
+ }
+ }, "Component", "Columns", "Column " + i, "Header Type");
+
+ // Footer types
+ addMenuCommand("Text Footer", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getFooter().getRow(0).getCell(index)
+ .setText("Text Footer");
+ }
+ }, "Component", "Columns", "Column " + i, "Footer Type");
+ addMenuCommand("HTML Footer", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ grid.getFooter().getRow(0).getCell(index)
+ .setHtml("<b>HTML Footer</b>");
+ }
+ }, "Component", "Columns", "Column " + i, "Footer Type");
+ addMenuCommand("Widget Footer", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ final Button button = new Button("Button Footer");
+ button.addClickHandler(new ClickHandler() {
+
+ @Override
+ public void onClick(ClickEvent event) {
+ button.setText("Clicked");
+ }
+ });
+ grid.getFooter().getRow(0).getCell(index).setWidget(button);
+ }
+ }, "Component", "Columns", "Column " + i, "Footer Type");
+ }
+ }
+
+ private int headerCounter = 0;
+ private int footerCounter = 0;
+
+ private void setHeaderTexts(HeaderRow row) {
+ for (int i = 0; i < COLUMNS; ++i) {
+ String caption = "Header (" + headerCounter + "," + i + ")";
+
+ // Lets use some different cell types
+ if (i % 3 == 0) {
+ row.getCell(i).setText(caption);
+ } else if (i % 2 == 0) {
+ row.getCell(i).setHtml("<b>" + caption + "</b>");
+ } else {
+ row.getCell(i).setWidget(new HTML(caption));
+ }
+ }
+ headerCounter++;
+ }
+
+ private void setFooterTexts(FooterRow row) {
+ for (int i = 0; i < COLUMNS; ++i) {
+ String caption = "Footer (" + footerCounter + "," + i + ")";
+
+ // Lets use some different cell types
+ if (i % 3 == 0) {
+ row.getCell(i).setText(caption);
+ } else if (i % 2 == 0) {
+ row.getCell(i).setHtml("<b>" + caption + "</b>");
+ } else {
+ row.getCell(i).setWidget(new HTML(caption));
+ }
+ }
+ footerCounter++;
+ }
+
+ private void createHeaderMenu() {
+ final GridHeader header = grid.getHeader();
+ final String[] menuPath = { "Component", "Header" };
+
+ addMenuCommand("Visible", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ header.setVisible(!header.isVisible());
+ }
+ }, menuPath);
+
+ addMenuCommand("Top", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ header.setDefaultRow(header.getRow(0));
+ }
+ }, "Component", "Header", "Default row");
+ addMenuCommand("Bottom", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ header.setDefaultRow(header.getRow(header.getRowCount() - 1));
+ }
+ }, "Component", "Header", "Default row");
+ addMenuCommand("Unset", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ header.setDefaultRow(null);
+ }
+ }, "Component", "Header", "Default row");
+
+ addMenuCommand("Prepend row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ configureHeaderRow(header.prependRow());
+ }
+ }, menuPath);
+ addMenuCommand("Append row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ configureHeaderRow(header.appendRow());
+ }
+ }, menuPath);
+ addMenuCommand("Remove top row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ header.removeRow(0);
+ }
+ }, menuPath);
+ addMenuCommand("Remove bottom row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ header.removeRow(header.getRowCount() - 1);
+ }
+ }, menuPath);
+
+ }
+
+ private void configureHeaderRow(final HeaderRow row) {
+ final GridHeader header = grid.getHeader();
+ setHeaderTexts(row);
+ String rowTitle = "Row " + header.getRowCount();
+ final String[] menuPath = { "Component", "Header", rowTitle };
+
+ addMenuCommand("Join column cells 0, 1", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(row.getCell(0), row.getCell(1));
+
+ }
+ }, menuPath);
+
+ addMenuCommand("Join columns 1, 2", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(grid.getColumn(1), grid.getColumn(2));
+
+ }
+ }, menuPath);
+
+ addMenuCommand("Join columns 3, 4, 5", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(grid.getColumn(3), grid.getColumn(4),
+ grid.getColumn(5));
+
+ }
+ }, menuPath);
+
+ addMenuCommand("Join all columns", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(grid.getColumns().toArray(
+ new GridColumn[grid.getColumnCount()]));
+
+ }
+ }, menuPath);
+ }
+
+ private void createFooterMenu() {
+ final GridFooter footer = grid.getFooter();
+ final String[] menuPath = { "Component", "Footer" };
+
+ addMenuCommand("Visible", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ footer.setVisible(!footer.isVisible());
+ }
+ }, menuPath);
+
+ addMenuCommand("Prepend row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ configureFooterRow(footer.prependRow());
+ }
+ }, menuPath);
+ addMenuCommand("Append row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ configureFooterRow(footer.appendRow());
+ }
+ }, menuPath);
+ addMenuCommand("Remove top row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ footer.removeRow(0);
+ }
+ }, menuPath);
+ addMenuCommand("Remove bottom row", new ScheduledCommand() {
+ @Override
+ public void execute() {
+ assert footer.getRowCount() > 0;
+ footer.removeRow(footer.getRowCount() - 1);
+ }
+ }, menuPath);
+ }
+
+ private void configureFooterRow(final FooterRow row) {
+ final GridFooter footer = grid.getFooter();
+ setFooterTexts(row);
+ String rowTitle = "Row " + footer.getRowCount();
+ final String[] menuPath = { "Component", "Footer", rowTitle };
+
+ addMenuCommand("Join column cells 0, 1", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(row.getCell(0), row.getCell(1));
+
+ }
+ }, menuPath);
+
+ addMenuCommand("Join columns 1, 2", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(grid.getColumn(1), grid.getColumn(2));
+
+ }
+ }, menuPath);
+
+ addMenuCommand("Join all columns", new ScheduledCommand() {
+
+ @Override
+ public void execute() {
+ row.join(grid.getColumns().toArray(
+ new GridColumn[grid.getColumnCount()]));
+
+ }
+ }, menuPath);
+ }
+
+ /**
+ * Creates a renderer for a {@link Renderers}
+ */
+ @SuppressWarnings("rawtypes")
+ private final Renderer createRenderer(Renderers renderer) {
+ switch (renderer) {
+ case TEXT_RENDERER:
+ return new TextRenderer();
+
+ case HTML_RENDERER:
+ return new HtmlRenderer() {
+
+ @Override
+ public void render(FlyweightCell cell, String htmlString) {
+ super.render(cell, "<b>" + htmlString + "</b>");
+ }
+ };
+
+ case NUMBER_RENDERER:
+ return new NumberRenderer<Integer>();
+
+ case DATE_RENDERER:
+ return new DateRenderer();
+
+ default:
+ return new TextRenderer();
+ }
+ }
+
+ /**
+ * Creates a collection of handlers for all the grid key events
+ */
+ private void createKeyHandlers() {
+ final List<VLabel> labels = new ArrayList<VLabel>();
+ for (int i = 0; i < 9; ++i) {
+ VLabel tmp = new VLabel();
+ addNorth(tmp, 20);
+ labels.add(tmp);
+ }
+
+ // Key Down Events
+ grid.addKeyDownHandler(new BodyKeyDownHandler<List<Data>>() {
+ private final VLabel label = labels.get(0);
+
+ @Override
+ public void onKeyDown(GridKeyDownEvent<List<Data>> event) {
+ updateLabel(label, event);
+ }
+ });
+
+ grid.addKeyDownHandler(new HeaderKeyDownHandler<List<Data>>() {
+ private final VLabel label = labels.get(1);
+
+ @Override
+ public void onKeyDown(GridKeyDownEvent<List<Data>> event) {
+ updateLabel(label, event);
+ }
+ });
+
+ grid.addKeyDownHandler(new FooterKeyDownHandler<List<Data>>() {
+ private final VLabel label = labels.get(2);
+
+ @Override
+ public void onKeyDown(GridKeyDownEvent<List<Data>> event) {
+ updateLabel(label, event);
+ }
+ });
+
+ // Key Up Events
+ grid.addKeyUpHandler(new BodyKeyUpHandler<List<Data>>() {
+ private final VLabel label = labels.get(3);
+
+ @Override
+ public void onKeyUp(GridKeyUpEvent<List<Data>> event) {
+ updateLabel(label, event);
+ }
+ });
+
+ grid.addKeyUpHandler(new HeaderKeyUpHandler<List<Data>>() {
+ private final VLabel label = labels.get(4);
+
+ @Override
+ public void onKeyUp(GridKeyUpEvent<List<Data>> event) {
+ updateLabel(label, event);
+ }
+ });
+
+ grid.addKeyUpHandler(new FooterKeyUpHandler<List<Data>>() {
+ private final VLabel label = labels.get(5);
+
+ @Override
+ public void onKeyUp(GridKeyUpEvent<List<Data>> event) {
+ updateLabel(label, event);
+ }
+ });
+
+ // Key Press Events
+ grid.addKeyPressHandler(new BodyKeyPressHandler<List<Data>>() {
+ private final VLabel label = labels.get(6);
+
+ @Override
+ public void onKeyPress(GridKeyPressEvent<List<Data>> event) {
+ updateLabel(label, event);
+ }
+ });
+
+ grid.addKeyPressHandler(new HeaderKeyPressHandler<List<Data>>() {
+ private final VLabel label = labels.get(7);
+
+ @Override
+ public void onKeyPress(GridKeyPressEvent<List<Data>> event) {
+ updateLabel(label, event);
+ }
+ });
+
+ grid.addKeyPressHandler(new FooterKeyPressHandler<List<Data>>() {
+ private final VLabel label = labels.get(8);
+
+ @Override
+ public void onKeyPress(GridKeyPressEvent<List<Data>> event) {
+ updateLabel(label, event);
+ }
+ });
+
+ }
+
+ private void updateLabel(VLabel label,
+ AbstractGridKeyEvent<List<Data>, ?> event) {
+ String type = event.getNativeEvent().getType();
+ Cell active = event.getActiveCell();
+ String coords = "(" + active.getRow() + ", " + active.getColumn() + ")";
+ String keyCode = "" + event.getNativeKeyCode();
+ label.setText(coords + " " + type + " " + keyCode);
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java
new file mode 100644
index 0000000000..7a9f8a06f5
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererConnector.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright 2000-2014 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 java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+import com.google.gwt.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.Window.Location;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HasWidgets;
+import com.vaadin.client.data.DataChangeHandler;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.client.ui.AbstractComponentConnector;
+import com.vaadin.client.ui.grid.FlyweightCell;
+import com.vaadin.client.ui.grid.Grid;
+import com.vaadin.client.ui.grid.GridColumn;
+import com.vaadin.client.ui.grid.Renderer;
+import com.vaadin.client.ui.grid.datasources.ListDataSource;
+import com.vaadin.client.ui.grid.datasources.ListSorter;
+import com.vaadin.client.ui.grid.renderers.ComplexRenderer;
+import com.vaadin.client.ui.grid.renderers.DateRenderer;
+import com.vaadin.client.ui.grid.renderers.HtmlRenderer;
+import com.vaadin.client.ui.grid.renderers.NumberRenderer;
+import com.vaadin.client.ui.grid.renderers.TextRenderer;
+import com.vaadin.client.ui.grid.renderers.WidgetRenderer;
+import com.vaadin.client.ui.grid.sort.Sort;
+import com.vaadin.client.ui.grid.sort.SortEvent;
+import com.vaadin.client.ui.grid.sort.SortEventHandler;
+import com.vaadin.client.ui.grid.sort.SortOrder;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.tests.widgetset.server.grid.GridClientColumnRenderers;
+
+@Connect(GridClientColumnRenderers.GridController.class)
+public class GridClientColumnRendererConnector extends
+ AbstractComponentConnector {
+
+ public static enum Renderers {
+ TEXT_RENDERER, WIDGET_RENDERER, HTML_RENDERER, NUMBER_RENDERER, DATE_RENDERER, CPLX_RENDERER;
+ }
+
+ /**
+ * Datasource for simulating network latency
+ */
+ private class DelayedDataSource implements DataSource<String> {
+
+ private DataSource<String> ds;
+ private int firstRowIndex = -1;
+ private int numberOfRows;
+ private DataChangeHandler dataChangeHandler;
+ private int latency;
+
+ public DelayedDataSource(DataSource<String> ds, int latency) {
+ this.ds = ds;
+ this.latency = latency;
+ }
+
+ @Override
+ public void ensureAvailability(final int firstRowIndex,
+ final int numberOfRows) {
+ new Timer() {
+
+ @Override
+ public void run() {
+ DelayedDataSource.this.firstRowIndex = firstRowIndex;
+ DelayedDataSource.this.numberOfRows = numberOfRows;
+ dataChangeHandler.dataUpdated(firstRowIndex, numberOfRows);
+ dataChangeHandler
+ .dataAvailable(firstRowIndex, numberOfRows);
+ }
+ }.schedule(latency);
+ }
+
+ @Override
+ public String getRow(int rowIndex) {
+ if (rowIndex >= firstRowIndex
+ && rowIndex <= firstRowIndex + numberOfRows) {
+ return ds.getRow(rowIndex);
+ }
+ return null;
+ }
+
+ @Override
+ public int getEstimatedSize() {
+ return ds.getEstimatedSize();
+ }
+
+ @Override
+ public void setDataChangeHandler(DataChangeHandler dataChangeHandler) {
+ this.dataChangeHandler = dataChangeHandler;
+ }
+
+ @Override
+ public RowHandle<String> getHandle(String row) {
+ // TODO Auto-generated method stub (henrik paul: 17.6.)
+ return null;
+ }
+ }
+
+ @Override
+ protected void init() {
+ Grid<String> grid = getWidget();
+ grid.setSelectionMode(Grid.SelectionMode.NONE);
+
+ // Generated some column data
+ List<String> columnData = new ArrayList<String>();
+ for (int i = 0; i < 100; i++) {
+ columnData.add(String.valueOf(i));
+ }
+
+ // Provide data as data source
+ if (Location.getParameter("latency") != null) {
+ grid.setDataSource(new DelayedDataSource(
+ new ListDataSource<String>(columnData), Integer
+ .parseInt(Location.getParameter("latency"))));
+ } else {
+ grid.setDataSource(new ListDataSource<String>(columnData));
+ }
+
+ // Add a column to display the data in
+ GridColumn<String, String> c = createColumnWithRenderer(Renderers.TEXT_RENDERER);
+ grid.addColumn(c);
+ grid.getHeader().getDefaultRow().getCell(0).setText("Column 1");
+
+ // Add another column with a custom complex renderer
+ c = createColumnWithRenderer(Renderers.CPLX_RENDERER);
+ grid.addColumn(c);
+ grid.getHeader().getDefaultRow().getCell(1).setText("Column 2");
+
+ // Add method for testing sort event firing
+ grid.addSortHandler(new SortEventHandler<String>() {
+ @Override
+ public void sort(SortEvent<String> event) {
+ Element console = Document.get().getElementById(
+ "testDebugConsole");
+ String text = "Client-side sort event received<br>"
+ + "Columns: " + event.getOrder().size() + ", order: ";
+ for (SortOrder order : event.getOrder()) {
+ int colIdx = getWidget().getColumns().indexOf(
+ order.getColumn());
+ String columnHeader = getWidget().getHeader()
+ .getDefaultRow().getCell(colIdx).getText();
+ text += columnHeader + ": "
+ + order.getDirection().toString();
+ }
+ console.setInnerHTML(text);
+ }
+ });
+
+ // Handle RPC calls
+ registerRpc(GridClientColumnRendererRpc.class,
+ new GridClientColumnRendererRpc() {
+
+ @Override
+ public void addColumn(Renderers renderer) {
+
+ if (renderer == Renderers.NUMBER_RENDERER) {
+ GridColumn<Number, String> numberColumn = createNumberColumnWithRenderer(renderer);
+ getWidget().addColumn(numberColumn);
+
+ } else if (renderer == Renderers.DATE_RENDERER) {
+ GridColumn<Date, String> dateColumn = createDateColumnWithRenderer(renderer);
+ getWidget().addColumn(dateColumn);
+
+ } else {
+ GridColumn<String, String> column = createColumnWithRenderer(renderer);
+ getWidget().addColumn(column);
+ }
+
+ int idx = getWidget().getColumnCount() - 1;
+ getWidget()
+ .getHeader()
+ .getDefaultRow()
+ .getCell(idx)
+ .setText(
+ "Column "
+ + String.valueOf(getWidget()
+ .getColumnCount() + 1));
+ }
+
+ @Override
+ public void detachAttach() {
+
+ // Detach
+ HasWidgets parent = (HasWidgets) getWidget()
+ .getParent();
+ parent.remove(getWidget());
+
+ // Re-attach
+ parent.add(getWidget());
+ }
+
+ @Override
+ public void triggerClientSorting() {
+ getWidget().sort(Sort.by(getWidget().getColumn(0)));
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void triggerClientSortingTest() {
+ Grid<String> grid = getWidget();
+ ListSorter<String> sorter = new ListSorter<String>(grid);
+
+ // Make sorter sort the numbers in natural order
+ sorter.setComparator(
+ (GridColumn<String, String>) grid.getColumn(0),
+ new Comparator<String>() {
+ @Override
+ public int compare(String o1, String o2) {
+ return Integer.parseInt(o1)
+ - Integer.parseInt(o2);
+ }
+ });
+
+ // Sort along column 0 in ascending order
+ grid.sort(grid.getColumn(0));
+
+ // Remove the sorter once we're done
+ sorter.removeFromGrid();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void shuffle() {
+ Grid<String> grid = getWidget();
+ ListSorter<String> shuffler = new ListSorter<String>(
+ grid);
+
+ // Make shuffler return random order
+ shuffler.setComparator(
+ (GridColumn<String, String>) grid.getColumn(0),
+ new Comparator<String>() {
+ @Override
+ public int compare(String o1, String o2) {
+ return com.google.gwt.user.client.Random
+ .nextInt(3) - 1;
+ }
+ });
+
+ // "Sort" (actually shuffle) along column 0
+ grid.sort(grid.getColumn(0));
+
+ // Remove the shuffler when we're done so that it
+ // doesn't interfere with further operations
+ shuffler.removeFromGrid();
+ }
+ });
+ }
+
+ /**
+ * Creates a a renderer for a {@link Renderers}
+ */
+ private Renderer createRenderer(Renderers renderer) {
+ switch (renderer) {
+ case TEXT_RENDERER:
+ return new TextRenderer();
+
+ case WIDGET_RENDERER:
+ return new WidgetRenderer<String, Button>() {
+
+ @Override
+ public Button createWidget() {
+ final Button button = new Button("");
+ button.addClickHandler(new ClickHandler() {
+
+ @Override
+ public void onClick(ClickEvent event) {
+ button.setText("Clicked");
+ }
+ });
+ return button;
+ }
+
+ @Override
+ public void render(FlyweightCell cell, String data,
+ Button button) {
+ button.setHTML(data);
+ }
+ };
+
+ case HTML_RENDERER:
+ return new HtmlRenderer() {
+
+ @Override
+ public void render(FlyweightCell cell, String htmlString) {
+ super.render(cell, "<b>" + htmlString + "</b>");
+ }
+ };
+
+ case NUMBER_RENDERER:
+ return new NumberRenderer<Long>();
+
+ case DATE_RENDERER:
+ return new DateRenderer();
+
+ case CPLX_RENDERER:
+ return new ComplexRenderer<String>() {
+
+ @Override
+ public void render(FlyweightCell cell, String data) {
+ cell.getElement().setInnerHTML("<span>" + data + "</span>");
+ cell.getElement().getStyle().clearBackgroundColor();
+ }
+
+ @Override
+ public void setContentVisible(FlyweightCell cell,
+ boolean hasData) {
+
+ // Visualize content visible property
+ cell.getElement().getStyle()
+ .setBackgroundColor(hasData ? "green" : "red");
+
+ super.setContentVisible(cell, hasData);
+ }
+ };
+
+ default:
+ return new TextRenderer();
+ }
+ }
+
+ private GridColumn<String, String> createColumnWithRenderer(
+ Renderers renderer) {
+ return new GridColumn<String, String>(createRenderer(renderer)) {
+
+ @Override
+ public String getValue(String row) {
+ return row;
+ }
+ };
+ }
+
+ private GridColumn<Number, String> createNumberColumnWithRenderer(
+ Renderers renderer) {
+ return new GridColumn<Number, String>(createRenderer(renderer)) {
+
+ @Override
+ public Number getValue(String row) {
+ return Long.parseLong(row);
+ }
+ };
+ }
+
+ private GridColumn<Date, String> createDateColumnWithRenderer(
+ Renderers renderer) {
+ return new GridColumn<Date, String>(createRenderer(renderer)) {
+
+ @Override
+ public Date getValue(String row) {
+ return new Date();
+ }
+ };
+ }
+
+ @Override
+ public Grid<String> getWidget() {
+ return (Grid<String>) super.getWidget();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java
new file mode 100644
index 0000000000..90eee9e1c6
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/GridClientColumnRendererRpc.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2000-2014 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;
+import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererConnector.Renderers;
+
+public interface GridClientColumnRendererRpc extends ClientRpc {
+
+ /**
+ * Adds a new column with a specific renderer to the grid
+ *
+ */
+ void addColumn(Renderers renderer);
+
+ /**
+ * Detaches and attaches the client side Grid
+ */
+ void detachAttach();
+
+ /**
+ * Used for client-side sorting API test
+ */
+ void triggerClientSorting();
+
+ /**
+ * @since
+ */
+ void triggerClientSortingTest();
+
+ /**
+ * @since
+ */
+ void shuffle();
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java
new file mode 100644
index 0000000000..d6873ac0a5
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/IntArrayRendererConnector.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2000-2014 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.grid.FlyweightCell;
+import com.vaadin.client.ui.grid.Renderer;
+import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector;
+import com.vaadin.shared.ui.Connect;
+
+@Connect(com.vaadin.tests.components.grid.IntArrayRenderer.class)
+public class IntArrayRendererConnector extends AbstractRendererConnector<int[]> {
+
+ public static class IntArrayRenderer implements Renderer<int[]> {
+ private static final String JOINER = " :: ";
+
+ @Override
+ public void render(FlyweightCell cell, int[] data) {
+ String text = "";
+ for (int i : data) {
+ text += i + JOINER;
+ }
+ if (!text.isEmpty()) {
+ text = text.substring(0, text.length() - JOINER.length());
+ }
+ cell.getElement().setInnerText(text);
+ }
+ }
+
+ @Override
+ public IntArrayRenderer getRenderer() {
+ return (IntArrayRenderer) super.getRenderer();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java
new file mode 100644
index 0000000000..e9c126f232
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/PureGWTTestApplication.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 2000-2014 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 java.util.ArrayList;
+import java.util.List;
+
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.DockLayoutPanel;
+import com.google.gwt.user.client.ui.LayoutPanel;
+import com.google.gwt.user.client.ui.MenuBar;
+import com.google.gwt.user.client.ui.Panel;
+import com.vaadin.client.ui.SubPartAware;
+
+/**
+ * Pure GWT Test Application base for testing features of a single widget;
+ * provides a menu system and convenience method for adding items to it.
+ *
+ * @since
+ * @author Vaadin Ltd
+ */
+public abstract class PureGWTTestApplication<T> extends DockLayoutPanel
+ implements SubPartAware {
+
+ /**
+ * Class describing a menu item with an associated action
+ */
+ public static class Command {
+ private final String title;
+ private final ScheduledCommand command;
+
+ /**
+ * Creates a Command object, which is used as an action entry in the
+ * Menu
+ *
+ * @param t
+ * a title string
+ * @param cmd
+ * a scheduled command that is executed when this item is
+ * selected
+ */
+ public Command(String t, ScheduledCommand cmd) {
+ title = t;
+ command = cmd;
+ }
+
+ /**
+ * Returns the title of this command item
+ *
+ * @return a title string
+ */
+ public final String getTitle() {
+ return title;
+ }
+
+ /**
+ * Returns the actual scheduled command of this command item
+ *
+ * @return a scheduled command
+ */
+ public final ScheduledCommand getCommand() {
+ return command;
+ }
+ }
+
+ /**
+ * A menu object, providing a complete system for building a hierarchical
+ * menu bar system.
+ */
+ public static class Menu {
+
+ private final String title;
+ private final MenuBar menubar;
+ private final List<Menu> children;
+ private final List<Command> items;
+
+ /**
+ * Create base-level menu, without a title. This is the root menu bar,
+ * which can be attached to a client application window. All other Menus
+ * should be added as child menus to this Menu, in order to maintain a
+ * nice hierarchy.
+ */
+ private Menu() {
+ title = "";
+ menubar = new MenuBar();
+ children = new ArrayList<Menu>();
+ items = new ArrayList<Command>();
+ }
+
+ /**
+ * Create a sub-menu, with a title.
+ *
+ * @param title
+ */
+ public Menu(String title) {
+ this.title = title;
+ menubar = new MenuBar(true);
+ children = new ArrayList<Menu>();
+ items = new ArrayList<Command>();
+ }
+
+ /**
+ * Return the GWT {@link MenuBar} object that provides the widget for
+ * this Menu
+ *
+ * @return a menubar object
+ */
+ public MenuBar getMenuBar() {
+ return menubar;
+ }
+
+ /**
+ * Returns the title of this menu entry
+ *
+ * @return a title string
+ */
+ public String getTitle() {
+ return title;
+ }
+
+ /**
+ * Adds a child menu entry to this menu. The title for this entry is
+ * taken from the Menu object argument.
+ *
+ * @param m
+ * another Menu object
+ */
+ public void addChildMenu(Menu m) {
+ menubar.addItem(m.title, m.menubar);
+ children.add(m);
+ }
+
+ /**
+ * Tests for the existence of a child menu by title at this level of the
+ * menu hierarchy
+ *
+ * @param title
+ * a title string
+ * @return true, if this menu has a direct child menu with the specified
+ * title, otherwise false
+ */
+ public boolean hasChildMenu(String title) {
+ return getChildMenu(title) != null;
+ }
+
+ /**
+ * Gets a reference to a child menu with a certain title, that is a
+ * direct child of this menu level.
+ *
+ * @param title
+ * a title string
+ * @return a Menu object with the specified title string, or null, if
+ * this menu doesn't have a direct child with the specified
+ * title.
+ */
+ public Menu getChildMenu(String title) {
+ for (Menu m : children) {
+ if (m.title.equals(title)) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Adds a command item to the menu. When the entry is clicked, the
+ * command is executed.
+ *
+ * @param cmd
+ * a command object.
+ */
+ public void addCommand(Command cmd) {
+ menubar.addItem(cmd.title, cmd.command);
+ items.add(cmd);
+ }
+
+ /**
+ * Tests for the existence of a {@link Command} that is the direct child
+ * of this level of menu.
+ *
+ * @param title
+ * the command's title
+ * @return true, if this menu level includes a command item with the
+ * specified title. Otherwise false.
+ */
+ public boolean hasCommand(String title) {
+ return getCommand(title) != null;
+ }
+
+ /**
+ * Gets a reference to a {@link Command} item that is the direct child
+ * of this level of menu.
+ *
+ * @param title
+ * the command's title
+ * @return a command, if found in this menu level, otherwise null.
+ */
+ public Command getCommand(String title) {
+ for (Command c : items) {
+ if (c.title.equals(title)) {
+ return c;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Base level menu object, provides visible menu bar
+ */
+ private final Menu menu;
+ private final T testedWidget;
+
+ /**
+ * This constructor creates the basic menu bar and adds it to the top of the
+ * parent {@link DockLayoutPanel}
+ */
+ protected PureGWTTestApplication(T widget) {
+ super(Unit.PX);
+ Panel menuPanel = new LayoutPanel();
+ menu = new Menu();
+ menuPanel.add(menu.getMenuBar());
+ addNorth(menuPanel, 25);
+ testedWidget = widget;
+ }
+
+ /**
+ * Connect an item to the menu structure
+ *
+ * @param cmd
+ * a scheduled command; see google's docs
+ * @param menupath
+ * path to the item
+ */
+ public void addMenuCommand(String title, ScheduledCommand cmd,
+ String... menupath) {
+ Menu m = createMenuPath(menupath);
+
+ m.addCommand(new Command(title, cmd));
+ }
+
+ /**
+ * Create a menu path, if one doesn't already exist, and return the last
+ * menu in the series.
+ *
+ * @param path
+ * a varargs list or array of strings describing a menu path,
+ * e.g. "File", "Recent", "User Files", which would result in the
+ * File menu having a submenu called "Recent" which would have a
+ * submenu called "User Files".
+ * @return the last Menu object specified by the path
+ */
+ private Menu createMenuPath(String... path) {
+ Menu m = menu;
+
+ for (String p : path) {
+ Menu sub = m.getChildMenu(p);
+
+ if (sub == null) {
+ sub = new Menu(p);
+ m.addChildMenu(sub);
+ }
+ m = sub;
+ }
+
+ return m;
+ }
+
+ @Override
+ public Element getSubPartElement(String subPart) {
+ if (testedWidget instanceof SubPartAware) {
+ return ((SubPartAware) testedWidget).getSubPartElement(subPart);
+ }
+ return null;
+ }
+
+ @Override
+ public String getSubPartName(Element subElement) {
+ if (testedWidget instanceof SubPartAware) {
+ return ((SubPartAware) testedWidget).getSubPartName(subElement);
+ }
+ return null;
+ }
+
+ /**
+ * Gets the tested widget.
+ *
+ * @return tested widget
+ */
+ public T getTestedWidget() {
+ return testedWidget;
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java b/uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java
new file mode 100644
index 0000000000..3880bacae2
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/RowAwareRendererConnector.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2000-2014 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 java.util.Arrays;
+import java.util.Collection;
+
+import com.google.gwt.dom.client.BrowserEvents;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.user.client.DOM;
+import com.vaadin.client.ui.grid.Cell;
+import com.vaadin.client.ui.grid.FlyweightCell;
+import com.vaadin.client.ui.grid.Renderer;
+import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector;
+import com.vaadin.client.ui.grid.renderers.ComplexRenderer;
+import com.vaadin.shared.communication.ServerRpc;
+import com.vaadin.shared.ui.Connect;
+
+@Connect(com.vaadin.tests.components.grid.RowAwareRenderer.class)
+public class RowAwareRendererConnector extends AbstractRendererConnector<Void> {
+ public interface RowAwareRendererRpc extends ServerRpc {
+ void clicky(String key);
+ }
+
+ public class RowAwareRenderer extends ComplexRenderer<Void> {
+
+ @Override
+ public Collection<String> getConsumedEvents() {
+ return Arrays.asList(BrowserEvents.CLICK);
+ }
+
+ @Override
+ public void init(FlyweightCell cell) {
+ DivElement div = DivElement.as(DOM.createDiv());
+ div.setAttribute("style",
+ "border: 1px solid red; background: pink;");
+ div.setInnerText("Click me!");
+ cell.getElement().appendChild(div);
+ }
+
+ @Override
+ public void render(FlyweightCell cell, Void data) {
+ // NOOP
+ }
+
+ @Override
+ public boolean onBrowserEvent(Cell cell, NativeEvent event) {
+ int row = cell.getRow();
+ String key = getRowKey(row);
+ getRpcProxy(RowAwareRendererRpc.class).clicky(key);
+ cell.getElement().setInnerText("row: " + row + ", key: " + key);
+ return true;
+ }
+ }
+
+ @Override
+ protected Renderer<Void> createRenderer() {
+ // cannot use the default createRenderer as RowAwareRenderer needs a
+ // reference to its connector - it has no "real" no-argument constructor
+ return new RowAwareRenderer();
+ }
+}
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..ae2799d228
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java
@@ -0,0 +1,48 @@
+/*
+ * 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();
+
+ void randomRowHeight();
+}
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..6dbff5ca66
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java
@@ -0,0 +1,138 @@
+/*
+ * 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.google.gwt.user.client.Random;
+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
+ * @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 void randomRowHeight() {
+ getWidget().getHeader().setDefaultRowHeight(
+ Random.nextInt(20) + 20);
+ getWidget().getBody().setDefaultRowHeight(
+ Random.nextInt(20) + 20);
+ getWidget().getFooter().setDefaultRowHeight(
+ Random.nextInt(20) + 20);
+ }
+ });
+ }
+
+ @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..ecbc59552b
--- /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
+ * @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..fbce00fc11
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java
@@ -0,0 +1,249 @@
+package com.vaadin.tests.widgetset.client.grid;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.gwt.user.client.Window.Location;
+import com.google.gwt.user.client.ui.Composite;
+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.FlyweightCell;
+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 abstract class TestEscalatorUpdater implements
+ EscalatorUpdater {
+
+ @Override
+ public void preAttach(Row row, Iterable<FlyweightCell> cellsToAttach) {
+ }
+
+ @Override
+ public void postAttach(Row row, Iterable<FlyweightCell> attachedCells) {
+ }
+
+ @Override
+ public void preDetach(Row row, Iterable<FlyweightCell> cellsToDetach) {
+ }
+
+ @Override
+ public void postDetach(Row row, Iterable<FlyweightCell> detachedCells) {
+ }
+ }
+
+ 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 TestEscalatorUpdater() {
+ @Override
+ public void update(final Row row,
+ final Iterable<FlyweightCell> cellsToUpdate) {
+ for (final FlyweightCell 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 TestEscalatorUpdater() {
+ @Override
+ public void update(final Row row,
+ final Iterable<FlyweightCell> cellsToUpdate) {
+ for (final FlyweightCell 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 TestEscalatorUpdater() {
+ private int i = 0;
+
+ public void renderCell(final FlyweightCell cell) {
+ final Integer columnName = columns.get(cell.getColumn());
+ final Integer rowName = rows.get(cell.getRow());
+ String cellInfo = columnName + "," + rowName;
+ if (shouldRenderPretty()) {
+ cellInfo += " (" + 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);
+ }
+
+ if (shouldRenderPretty()) {
+ 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++;
+ }
+
+ private boolean shouldRenderPretty() {
+ return Location.getQueryString().contains("pretty");
+ }
+
+ @Override
+ public void update(final Row row,
+ final Iterable<FlyweightCell> cellsToUpdate) {
+ for (final FlyweightCell 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 getBody() {
+ return escalator.getBody();
+ }
+
+ public RowContainer getFooter() {
+ return escalator.getFooter();
+ }
+
+ public void calculateColumnWidths() {
+ escalator.calculateColumnWidths();
+ }
+}
diff --git a/uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java b/uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java
new file mode 100644
index 0000000000..db931888bc
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/server/grid/GridClientColumnRenderers.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2000-2014 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 java.util.Arrays;
+
+import com.vaadin.annotations.Widgetset;
+import com.vaadin.server.VaadinRequest;
+import com.vaadin.shared.ui.label.ContentMode;
+import com.vaadin.tests.widgetset.TestingWidgetSet;
+import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererConnector.Renderers;
+import com.vaadin.tests.widgetset.client.grid.GridClientColumnRendererRpc;
+import com.vaadin.ui.AbstractComponent;
+import com.vaadin.ui.Button.ClickEvent;
+import com.vaadin.ui.Button.ClickListener;
+import com.vaadin.ui.CssLayout;
+import com.vaadin.ui.Label;
+import com.vaadin.ui.NativeButton;
+import com.vaadin.ui.NativeSelect;
+import com.vaadin.ui.UI;
+import com.vaadin.ui.VerticalLayout;
+
+@Widgetset(TestingWidgetSet.NAME)
+public class GridClientColumnRenderers extends UI {
+
+ /**
+ * Controls the grid on the client side
+ */
+ public static class GridController extends AbstractComponent {
+
+ private GridClientColumnRendererRpc rpc() {
+ return getRpcProxy(GridClientColumnRendererRpc.class);
+ }
+
+ /**
+ * Adds a new column with a renderer to the grid.
+ */
+ public void addColumn(Renderers renderer) {
+ rpc().addColumn(renderer);
+ }
+
+ /**
+ * Tests detaching and attaching grid
+ */
+ public void detachAttach() {
+ rpc().detachAttach();
+ }
+
+ /**
+ * @since
+ */
+ public void triggerClientSorting() {
+ rpc().triggerClientSorting();
+ }
+
+ /**
+ * @since
+ */
+ public void triggerClientSortingTest() {
+ rpc().triggerClientSortingTest();
+ }
+
+ /**
+ * @since
+ */
+ public void shuffle() {
+ rpc().shuffle();
+ }
+ }
+
+ @Override
+ protected void init(VaadinRequest request) {
+ final GridController controller = new GridController();
+ final CssLayout controls = new CssLayout();
+ final VerticalLayout content = new VerticalLayout();
+
+ content.addComponent(controller);
+ content.addComponent(controls);
+ setContent(content);
+
+ final NativeSelect select = new NativeSelect(
+ "Add Column with Renderer", Arrays.asList(Renderers.values()));
+ select.setValue(Renderers.TEXT_RENDERER);
+ select.setNullSelectionAllowed(false);
+ controls.addComponent(select);
+
+ NativeButton addColumnBtn = new NativeButton("Add");
+ addColumnBtn.addClickListener(new ClickListener() {
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ Renderers renderer = (Renderers) select.getValue();
+ controller.addColumn(renderer);
+ }
+ });
+ controls.addComponent(addColumnBtn);
+
+ NativeButton detachAttachBtn = new NativeButton("DetachAttach");
+ detachAttachBtn.addClickListener(new ClickListener() {
+
+ @Override
+ public void buttonClick(ClickEvent event) {
+ controller.detachAttach();
+ }
+ });
+ controls.addComponent(detachAttachBtn);
+
+ NativeButton shuffleButton = new NativeButton("Shuffle");
+ shuffleButton.addClickListener(new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ controller.shuffle();
+ }
+ });
+ controls.addComponent(shuffleButton);
+
+ NativeButton sortButton = new NativeButton("Trigger sorting event");
+ sortButton.addClickListener(new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ controller.triggerClientSorting();
+ }
+ });
+ controls.addComponent(sortButton);
+
+ NativeButton testSortingButton = new NativeButton("Test sorting");
+ testSortingButton.addClickListener(new ClickListener() {
+ @Override
+ public void buttonClick(ClickEvent event) {
+ controller.triggerClientSortingTest();
+ }
+ });
+ controls.addComponent(testSortingButton);
+
+ Label console = new Label();
+ console.setContentMode(ContentMode.HTML);
+ console.setId("testDebugConsole");
+ content.addComponent(console);
+ }
+}
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..0dbb60359d
--- /dev/null
+++ b/uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java
@@ -0,0 +1,96 @@
+/*
+ * 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
+ * @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();
+ }
+
+ public void randomizeDefaultRowHeight() {
+ rpc().randomRowHeight();
+ }
+}