Browse Source

Merge branch 'master' into grid

Change-Id: Ia9d156009a3f1b4e61f12eb415040670a52d7876
tags/7.2.0.beta1
Leif Åstrand 10 years ago
parent
commit
4420f52578
65 changed files with 14682 additions and 0 deletions
  1. 4
    0
      WebContent/VAADIN/themes/base/base.scss
  2. 117
    0
      WebContent/VAADIN/themes/base/escalator/escalator.scss
  3. 3
    0
      WebContent/VAADIN/themes/base/grid/grid.scss
  4. 4
    0
      WebContent/VAADIN/themes/reindeer-tests/styles.css
  5. 323
    0
      client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
  6. 59
    0
      client/src/com/vaadin/client/data/DataChangeHandler.java
  7. 76
    0
      client/src/com/vaadin/client/data/DataSource.java
  8. 81
    0
      client/src/com/vaadin/client/data/RpcDataSourceConnector.java
  9. 75
    0
      client/src/com/vaadin/client/ui/grid/Cell.java
  10. 146
    0
      client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java
  11. 184
    0
      client/src/com/vaadin/client/ui/grid/ColumnGroup.java
  12. 243
    0
      client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java
  13. 4042
    0
      client/src/com/vaadin/client/ui/grid/Escalator.java
  14. 66
    0
      client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java
  15. 205
    0
      client/src/com/vaadin/client/ui/grid/FlyweightCell.java
  16. 217
    0
      client/src/com/vaadin/client/ui/grid/FlyweightRow.java
  17. 1318
    0
      client/src/com/vaadin/client/ui/grid/Grid.java
  18. 54
    0
      client/src/com/vaadin/client/ui/grid/GridColumn.java
  19. 277
    0
      client/src/com/vaadin/client/ui/grid/GridConnector.java
  20. 118
    0
      client/src/com/vaadin/client/ui/grid/PositionFunction.java
  21. 43
    0
      client/src/com/vaadin/client/ui/grid/Renderer.java
  22. 55
    0
      client/src/com/vaadin/client/ui/grid/Row.java
  23. 156
    0
      client/src/com/vaadin/client/ui/grid/RowContainer.java
  24. 90
    0
      client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java
  25. 38
    0
      client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java
  26. 403
    0
      client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java
  27. 357
    0
      client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java
  28. 94
    0
      client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java
  29. 42
    0
      client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java
  30. 64
    0
      client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java
  31. 33
    0
      client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java
  32. 178
    0
      client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java
  33. 104
    0
      client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java
  34. 148
    0
      server/src/com/vaadin/data/RpcDataProviderExtension.java
  35. 165
    0
      server/src/com/vaadin/ui/components/grid/ColumnGroup.java
  36. 303
    0
      server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java
  37. 861
    0
      server/src/com/vaadin/ui/components/grid/Grid.java
  38. 216
    0
      server/src/com/vaadin/ui/components/grid/GridColumn.java
  39. 265
    0
      server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java
  40. 228
    0
      server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java
  41. 61
    0
      shared/src/com/vaadin/shared/data/DataProviderRpc.java
  42. 32
    0
      shared/src/com/vaadin/shared/data/DataProviderState.java
  43. 38
    0
      shared/src/com/vaadin/shared/data/DataRequestRpc.java
  44. 46
    0
      shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java
  45. 45
    0
      shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java
  46. 53
    0
      shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java
  47. 55
    0
      shared/src/com/vaadin/shared/ui/grid/GridColumnState.java
  48. 33
    0
      shared/src/com/vaadin/shared/ui/grid/GridConstants.java
  49. 39
    0
      shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
  50. 64
    0
      shared/src/com/vaadin/shared/ui/grid/GridState.java
  51. 378
    0
      shared/src/com/vaadin/shared/ui/grid/Range.java
  52. 55
    0
      shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java
  53. 318
    0
      shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java
  54. 176
    0
      uitest/src/com/vaadin/tests/components/grid/BasicEscalator.html
  55. 312
    0
      uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java
  56. 49
    0
      uitest/src/com/vaadin/tests/components/grid/BasicEscalatorTest.java
  57. 343
    0
      uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
  58. 373
    0
      uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java
  59. 111
    0
      uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java
  60. 115
    0
      uitest/src/com/vaadin/tests/components/grid/GridScrolling.java
  61. 48
    0
      uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java
  62. 138
    0
      uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java
  63. 29
    0
      uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java
  64. 220
    0
      uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java
  65. 96
    0
      uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java

+ 4
- 0
WebContent/VAADIN/themes/base/base.scss View File

@@ -15,8 +15,10 @@
@import "inlinedatefield/inlinedatefield.scss";
@import "dragwrapper/dragwrapper.scss";
@import "embedded/embedded.scss";
@import "escalator/escalator.scss";
@import "fonts/fonts.scss";
@import "formlayout/formlayout.scss";
@import "grid/grid.scss";
@import "gridlayout/gridlayout.scss";
@import "label/label.scss";
@import "link/link.scss";
@@ -90,7 +92,9 @@ $line-height: normal;
@include base-inline-datefield;
@include base-dragwrapper;
@include base-embedded;
@include base-escalator;
@include base-formlayout;
@include base-grid;
@include base-gridlayout;
@include base-label;
@include base-link;

+ 117
- 0
WebContent/VAADIN/themes/base/escalator/escalator.scss View File

@@ -0,0 +1,117 @@
@mixin base-escalator($primaryStyleName : v-escalator) {

$background-color: white;
$border-color: #aaa;

.#{$primaryStyleName} {
position: relative;
background-color: $background-color;
}

.#{$primaryStyleName}-scroller {
position: absolute;
overflow: auto;
z-index: 20;
}

.#{$primaryStyleName}-scroller-horizontal {
left: 0; /* Left position adjusted to align with frozen columns */
right: 0;
bottom: 0;
overflow-y: hidden;
-ms-overflow-y: hidden;
}

.#{$primaryStyleName}-scroller-vertical {
right: 0;
top: 0; /* this will be overridden by code, but it's a good default behavior */
bottom: 0; /* this will be overridden by code, but it's a good default behavior */
overflow-x: hidden;
-ms-overflow-x: hidden;
}

.#{$primaryStyleName}-tablewrapper {
position: absolute;
overflow: hidden;
}

.#{$primaryStyleName}-tablewrapper > table {
border-spacing: 0;
table-layout: fixed;
width: inherit; /* a decent default fallback */
}

.#{$primaryStyleName}-header,
.#{$primaryStyleName}-body,
.#{$primaryStyleName}-footer {
position: absolute;
left: 0;
width: inherit;
z-index: 10;
}

.#{$primaryStyleName}-header { top: 0; }
.#{$primaryStyleName}-footer { bottom: 0; }

.#{$primaryStyleName}-body {
z-index: 0;
top: 0;
.#{$primaryStyleName}-row {
position: absolute;
top: 0;
left: 0;
}
}

.#{$primaryStyleName}-row {
display: block;
.v-ie8 & {
/* IE8 doesn't let table rows be longer than body only with display block. Moar hax. */
float: left;
clear: left;
/*
* The inline style of margin-top from the <tbody> to offset the header's dimension is,
* for some strange reason, inherited into each contained <tr>.
* We need to cancel it:
*/
margin-top: 0;
}

> td, > th {
/* IE8 likes the bgcolor here instead of on the row */
background-color: $background-color;
}
}


.#{$primaryStyleName}-row {
width: inherit;
}

.#{$primaryStyleName}-cell {
display: block;
float: left;
border: 1px solid $border-color;
padding: 2px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow:hidden;
/*
* 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: 0;
}

}

+ 3
- 0
WebContent/VAADIN/themes/base/grid/grid.scss View File

@@ -0,0 +1,3 @@
@mixin base-grid($primaryStyleName : v-grid) {
@include base-escalator($primaryStyleName);
}

+ 4
- 0
WebContent/VAADIN/themes/reindeer-tests/styles.css View File

@@ -32,3 +32,7 @@
.popup-style .v-datefield-calendarpanel-body {
background: yellow;
}

#escalator .v-escalator-body .v-escalator-cell {
height: 50px;
}

+ 323
- 0
client/src/com/vaadin/client/data/AbstractRemoteDataSource.java View File

@@ -0,0 +1,323 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.data;

import java.util.HashMap;
import java.util.List;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.vaadin.client.Profiler;
import com.vaadin.shared.ui.grid.Range;

/**
* Base implementation for data sources that fetch data from a remote system.
* This class takes care of caching data and communicating with the data source
* user. An implementation of this class should override
* {@link #requestRows(int, int)} to trigger asynchronously loading of data.
* When data is received from the server, new row data should be passed to
* {@link #setRowData(int, List)}. {@link #setEstimatedSize(int)} should be used
* based on estimations of how many rows are available.
*
* @since 7.2
* @author Vaadin Ltd
* @param <T>
* the row type
*/
public abstract class AbstractRemoteDataSource<T> implements DataSource<T> {

private boolean requestPending = false;

private boolean coverageCheckPending = false;

private Range requestedAvailability = Range.between(0, 0);

private Range cached = Range.between(0, 0);

private final HashMap<Integer, T> rowCache = new HashMap<Integer, T>();

private DataChangeHandler dataChangeHandler;

private int estimatedSize;

private final ScheduledCommand coverageChecker = new ScheduledCommand() {
@Override
public void execute() {
coverageCheckPending = false;
checkCacheCoverage();
}
};

/**
* Sets the estimated number of rows in the data source.
*
* @param estimatedSize
* the estimated number of available rows
*/
protected void setEstimatedSize(int estimatedSize) {
// TODO update dataChangeHandler if size changes
this.estimatedSize = estimatedSize;
}

private void ensureCoverageCheck() {
if (!coverageCheckPending) {
coverageCheckPending = true;
Scheduler.get().scheduleDeferred(coverageChecker);
}
}

@Override
public void ensureAvailability(int firstRowIndex, int numberOfRows) {
requestedAvailability = Range.withLength(firstRowIndex, numberOfRows);

/*
* Don't request any data right away since the data might be included in
* a message that has been received but not yet fully processed.
*/
ensureCoverageCheck();
}

private void checkCacheCoverage() {
if (requestPending) {
// Anyone clearing requestPending should run this method again
return;
}

Profiler.enter("AbstractRemoteDataSource.checkCacheCoverage");

if (!requestedAvailability.intersects(cached) || cached.isEmpty()) {
/*
* Simple case: no overlap between cached data and needed data.
* Clear the cache and request new data
*/
rowCache.clear();
cached = Range.between(0, 0);

handleMissingRows(requestedAvailability);
} else {
discardStaleCacheEntries();

// Might need more rows -> request them
Range[] availabilityPartition = requestedAvailability
.partitionWith(cached);
handleMissingRows(availabilityPartition[0]);
handleMissingRows(availabilityPartition[2]);
}

Profiler.leave("AbstractRemoteDataSource.checkCacheCoverage");
}

private void discardStaleCacheEntries() {
Range[] cacheParition = cached.partitionWith(requestedAvailability);
dropFromCache(cacheParition[0]);
cached = cacheParition[1];
dropFromCache(cacheParition[2]);
}

private void dropFromCache(Range range) {
for (int i = range.getStart(); i < range.getEnd(); i++) {
rowCache.remove(Integer.valueOf(i));
}
}

private void handleMissingRows(Range range) {
if (range.isEmpty()) {
return;
}
requestPending = true;
requestRows(range.getStart(), range.length());
}

/**
* Triggers fetching rows from the remote data source.
* {@link #setRowData(int, List)} should be invoked with data for the
* requested rows when they have been received.
*
* @param firstRowIndex
* the index of the first row to fetch
* @param numberOfRows
* the number of rows to fetch
*/
protected abstract void requestRows(int firstRowIndex, int numberOfRows);

@Override
public int getEstimatedSize() {
return estimatedSize;
}

@Override
public T getRow(int rowIndex) {
return rowCache.get(Integer.valueOf(rowIndex));
}

@Override
public void setDataChangeHandler(DataChangeHandler dataChangeHandler) {
this.dataChangeHandler = dataChangeHandler;

if (dataChangeHandler != null && !cached.isEmpty()) {
// Push currently cached data to the implementation
dataChangeHandler.dataUpdated(cached.getStart(), cached.length());
}
}

/**
* Informs this data source that updated data has been sent from the server.
*
* @param firstRowIndex
* the index of the first received row
* @param rowData
* a list of rows, starting from <code>firstRowIndex</code>
*/
protected void setRowData(int firstRowIndex, List<T> rowData) {
requestPending = false;

Profiler.enter("AbstractRemoteDataSource.setRowData");

Range received = Range.withLength(firstRowIndex, rowData.size());

Range[] partition = received.partitionWith(requestedAvailability);

Range newUsefulData = partition[1];
if (!newUsefulData.isEmpty()) {
// Update the parts that are actually inside
for (int i = newUsefulData.getStart(); i < newUsefulData.getEnd(); i++) {
rowCache.put(Integer.valueOf(i), rowData.get(i - firstRowIndex));
}

if (dataChangeHandler != null) {
Profiler.enter("AbstractRemoteDataSource.setRowData notify dataChangeHandler");
dataChangeHandler.dataUpdated(newUsefulData.getStart(),
newUsefulData.length());
Profiler.leave("AbstractRemoteDataSource.setRowData notify dataChangeHandler");
}

// Potentially extend the range
if (cached.isEmpty()) {
cached = newUsefulData;
} else {
discardStaleCacheEntries();

/*
* everything might've become stale so we need to re-check for
* emptiness.
*/
if (!cached.isEmpty()) {
cached = cached.combineWith(newUsefulData);
} else {
cached = newUsefulData;
}
}
}

if (!partition[0].isEmpty() || !partition[2].isEmpty()) {
/*
* FIXME
*
* Got data that we might need in a moment if the container is
* updated before the widget settings. Support for this will be
* implemented later on.
*/
}

// Eventually check whether all needed rows are now available
ensureCoverageCheck();

Profiler.leave("AbstractRemoteDataSource.setRowData");
}

/**
* Informs this data source that the server has removed data.
*
* @param firstRowIndex
* the index of the first removed row
* @param count
* the number of removed rows, starting from
* <code>firstRowIndex</code>
*/
protected void removeRowData(int firstRowIndex, int count) {
Profiler.enter("AbstractRemoteDataSource.removeRowData");

// pack the cached data
for (int i = 0; i < count; i++) {
Integer oldIndex = Integer.valueOf(firstRowIndex + count + i);
if (rowCache.containsKey(oldIndex)) {
Integer newIndex = Integer.valueOf(firstRowIndex + i);
rowCache.put(newIndex, rowCache.remove(oldIndex));
}
}

Range removedRange = Range.withLength(firstRowIndex, count);
if (removedRange.intersects(cached)) {
Range[] partitions = cached.partitionWith(removedRange);
Range remainsBefore = partitions[0];
Range transposedRemainsAfter = partitions[2].offsetBy(-removedRange
.length());
cached = remainsBefore.combineWith(transposedRemainsAfter);
}
estimatedSize -= count;
dataChangeHandler.dataRemoved(firstRowIndex, count);
checkCacheCoverage();

Profiler.leave("AbstractRemoteDataSource.removeRowData");
}

/**
* Informs this data source that new data has been inserted from the server.
*
* @param firstRowIndex
* the destination index of the new row data
* @param count
* the number of rows inserted
*/
protected void insertRowData(int firstRowIndex, int count) {
Profiler.enter("AbstractRemoteDataSource.insertRowData");

if (cached.contains(firstRowIndex)) {
int oldCacheEnd = cached.getEnd();
/*
* We need to invalidate the cache from the inserted row onwards,
* since the cache wants to be a contiguous range. It doesn't
* support holes.
*
* If holes were supported, we could shift the higher part of
* "cached" and leave a hole the size of "count" in the middle.
*/
cached = cached.splitAt(firstRowIndex)[0];

for (int i = firstRowIndex; i < oldCacheEnd; i++) {
rowCache.remove(Integer.valueOf(i));
}
}

else if (firstRowIndex < cached.getStart()) {
Range oldCached = cached;
cached = cached.offsetBy(count);

for (int i = 0; i < rowCache.size(); i++) {
Integer oldIndex = Integer.valueOf(oldCached.getEnd() - i);
Integer newIndex = Integer.valueOf(cached.getEnd() - i);
rowCache.put(newIndex, rowCache.remove(oldIndex));
}
}

estimatedSize += count;
dataChangeHandler.dataAdded(firstRowIndex, count);
checkCacheCoverage();

Profiler.leave("AbstractRemoteDataSource.insertRowData");
}
}

+ 59
- 0
client/src/com/vaadin/client/data/DataChangeHandler.java View File

@@ -0,0 +1,59 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.data;

/**
* Callback interface used by {@link DataSource} to inform its user about
* updates to the data.
*
* @since 7.2
* @author Vaadin Ltd
*/
public interface DataChangeHandler {
/**
* Called when the contents of the data source has changed. If the number of
* rows has changed or if rows have been moved around,
* {@link #dataAdded(int, int)} or {@link #dataRemoved(int, int)} should
* ideally be used instead.
*
* @param firstRowIndex
* the index of the first changed row
* @param numberOfRows
* the number of changed rows
*/
public void dataUpdated(int firstRowIndex, int numberOfRows);

/**
* Called when rows have been removed from the data source.
*
* @param firstRowIndex
* the index that the first removed row had prior to removal
* @param numberOfRows
* the number of removed rows
*/
public void dataRemoved(int firstRowIndex, int numberOfRows);

/**
* Called when the new rows have been added to the container.
*
* @param firstRowIndex
* the index of the first added row
* @param numberOfRows
* the number of added rows
*/
public void dataAdded(int firstRowIndex, int numberOfRows);
}

+ 76
- 0
client/src/com/vaadin/client/data/DataSource.java View File

@@ -0,0 +1,76 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.data;

/**
* Source of data for widgets showing lazily loaded data based on indexable
* items (e.g. rows) of a specified type. The data source is a lazy view into a
* larger data set.
*
* @since 7.2
* @author Vaadin Ltd
* @param <T>
* the row type
*/
public interface DataSource<T> {
/**
* Informs the data source that data for the given range is needed. A data
* source only has one active region at a time, so calling this method
* discards the previously set range.
* <p>
* This method triggers lazy loading of data if necessary. The change
* handler registered using {@link #setDataChangeHandler(DataChangeHandler)}
* is informed when new data has been loaded.
*
* @param firstRowIndex
* the index of the first needed row
* @param numberOfRows
* the number of needed rows
*/
public void ensureAvailability(int firstRowIndex, int numberOfRows);

/**
* Retrieves the data for the row at the given index. If the row data is not
* available, returns <code>null</code>.
* <p>
* This method does not trigger loading of unavailable data.
* {@link #ensureAvailability(int, int)} should be used to signal what data
* will be needed.
*
* @param rowIndex
* the index of the row to retrieve data for
* @return data for the row; or <code>null</code> if no data is available
*/
public T getRow(int rowIndex);

/**
* Returns the current best guess for the number of rows in the container.
*
* @return the current estimation of the container size
*/
public int getEstimatedSize();

/**
* Sets a data change handler to inform when data is updated, added or
* removed.
*
* @param dataChangeHandler
* the data change handler
*/
public void setDataChangeHandler(DataChangeHandler dataChangeHandler);

}

+ 81
- 0
client/src/com/vaadin/client/data/RpcDataSourceConnector.java View File

@@ -0,0 +1,81 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.data;

import java.util.List;

import com.vaadin.client.ServerConnector;
import com.vaadin.client.extensions.AbstractExtensionConnector;
import com.vaadin.client.ui.grid.GridConnector;
import com.vaadin.shared.data.DataProviderRpc;
import com.vaadin.shared.data.DataProviderState;
import com.vaadin.shared.data.DataRequestRpc;
import com.vaadin.shared.ui.Connect;

/**
* Connects a Vaadin server-side container data source to a Grid. This is
* currently implemented as an Extension hardcoded to support a specific
* connector type. This will be changed once framework support for something
* more flexible has been implemented.
*
* @since 7.2
* @author Vaadin Ltd
*/
@Connect(com.vaadin.data.RpcDataProviderExtension.class)
public class RpcDataSourceConnector extends AbstractExtensionConnector {

private final AbstractRemoteDataSource<String[]> dataSource = new AbstractRemoteDataSource<String[]>() {
@Override
protected void requestRows(int firstRowIndex, int numberOfRows) {
getRpcProxy(DataRequestRpc.class).requestRows(firstRowIndex,
numberOfRows);
}
};

@Override
protected void extend(ServerConnector target) {
dataSource.setEstimatedSize(getState().containerSize);
((GridConnector) target).getWidget().setDataSource(dataSource);

registerRpc(DataProviderRpc.class, new DataProviderRpc() {
@Override
public void setRowData(int firstRow, List<String[]> rows) {
dataSource.setRowData(firstRow, rows);
}

@Override
public void removeRowData(int firstRow, int count) {
dataSource.removeRowData(firstRow, count);
}

@Override
public void insertRowData(int firstRow, int count) {
dataSource.insertRowData(firstRow, count);
}
});
}

/*
* (non-Javadoc)
*
* @see com.vaadin.client.ui.AbstractConnector#getState()
*/
@Override
public DataProviderState getState() {
return (DataProviderState) super.getState();
}
}

+ 75
- 0
client/src/com/vaadin/client/ui/grid/Cell.java View File

@@ -0,0 +1,75 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

import com.google.gwt.dom.client.Element;
import com.google.gwt.user.client.ui.HasOneWidget;

/**
* A representation of a single cell.
* <p>
* A Cell instance will be provided to the {@link EscalatorUpdater} responsible
* for rendering the cells in a certain {@link RowContainer}.
*
* @since 7.2
* @author Vaadin Ltd
*/
public interface Cell extends HasOneWidget {

/**
* Gets the index of the row this cell is in.
*
* @return the index of the row this cell is in
*/
public int getRow();

/**
* Gets the index of the column this cell is in.
*
* @return the index of the column this cell is in
*/
public int getColumn();

/**
* Gets the root element for this cell. The {@link EscalatorUpdater} may
* update the class names of the element, add inline styles and freely
* modify the contents.
* <p>
* Avoid modifying the dimensions, positioning or colspan of the cell
* element.
*
* @return The root element for this cell. Never <code>null</code>.
*/
public Element getElement();

/**
* Sets the column span of the cell.
* <p>
* This will overwrite any possible "colspan" attribute in the current
* element (i.e. the object returned by {@link #getElement()}). This will
* also handle internal bookkeeping, skip the rendering of any affected
* adjacent cells, and make sure that the current cell's dimensions are
* handled correctly.
*
* @param numberOfCells
* the number of cells to span to the right, or <code>1</code> to
* unset any column spans
* @throws IllegalArgumentException
* if <code>numberOfCells &lt; 1</code>
*/
public void setColSpan(int numberOfCells) throws IllegalArgumentException;
}

+ 146
- 0
client/src/com/vaadin/client/ui/grid/ColumnConfiguration.java View File

@@ -0,0 +1,146 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

/**
* A representation of the columns in an instance of {@link Escalator}.
*
* @since 7.2
* @author Vaadin Ltd
* @see Escalator#getColumnConfiguration()
*/
public interface ColumnConfiguration {

/**
* Removes columns at a certain index.
* <p>
* If any of the removed columns were frozen, the number of frozen columns
* will be reduced by the number of the removed columns that were frozen.
*
* @param index
* the index of the first column to be removed
* @param numberOfColumns
* the number of rows to remove, starting from the index
* @throws IndexOutOfBoundsException
* if any integer in the range
* <code>[index..(index+numberOfColumns)]</code> is not an
* existing column index.
* @throws IllegalArgumentException
* if <code>numberOfColumns</code> is less than 1.
*/
public void removeColumns(int index, int numberOfColumns)
throws IndexOutOfBoundsException, IllegalArgumentException;

/**
* Adds columns at a certain index.
* <p>
* The new columns will be inserted between the column at the index, and the
* column before (an index of 0 means that the columns are inserted at the
* beginning). Therefore, the columns at the index and afterwards will be
* moved to the right.
* <p>
* The contents of the inserted columns will be queried from the respective
* cell renderers in the header, body and footer.
* <p>
* If there are frozen columns and the first added column is to the left of
* the last frozen column, the number of frozen columns will be increased by
* the number of inserted columns.
* <p>
* <em>Note:</em> Only the contents of the inserted columns will be
* rendered. If inserting new columns affects the contents of existing
* columns, {@link RowContainer#refreshRows(int, int)} needs to be called as
* appropriate.
*
* @param index
* the index of the column before which new columns are inserted,
* or {@link #getColumnCount()} to add new columns at the end
* @param numberOfColumns
* the number of columns to insert after the <code>index</code>
* @throws IndexOutOfBoundsException
* if <code>index</code> is not an integer in the range
* <code>[0..{@link #getColumnCount()}]</code>
* @throws IllegalArgumentException
* if {@code numberOfColumns} is less than 1.
*/
public void insertColumns(int index, int numberOfColumns)
throws IndexOutOfBoundsException, IllegalArgumentException;

/**
* Returns the number of columns in the escalator.
*
* @return the number of columns in the escalator
*/
public int getColumnCount();

/**
* Sets the number of leftmost columns that are not affected by horizontal
* scrolling.
*
* @param count
* the number of columns to freeze
*
* @throws IllegalArgumentException
* if the column count is &lt; 0 or &gt; the number of columns
*
*/
public void setFrozenColumnCount(int count) throws IllegalArgumentException;

/**
* Get the number of leftmost columns that are not affected by horizontal
* scrolling.
*
* @return the number of frozen columns
*/
public int getFrozenColumnCount();

/**
* Sets (or unsets) an explicit width for a column.
*
* @param index
* the index of the column for which to set a width
* @param px
* the number of pixels the indicated column should be, or a
* negative number to let the escalator decide
* @throws IllegalArgumentException
* if <code>index</code> is not a valid column index
*/
public void setColumnWidth(int index, int px)
throws IllegalArgumentException;

/**
* Returns the user-defined width of a column.
*
* @param index
* the index of the column for which to retrieve the width
* @return the column's width in pixels, or a negative number if the width
* is implicitly decided by the escalator
* @throws IllegalArgumentException
* if <code>index</code> is not a valid column index
*/
public int getColumnWidth(int index) throws IllegalArgumentException;

/**
* Returns the actual width of a column.
*
* @param index
* the index of the column for which to retrieve the width
* @return the column's actual width in pixels
* @throws IllegalArgumentException
* if <code>index</code> is not a valid column index
*/
public int getColumnWidthActual(int index) throws IllegalArgumentException;
}

+ 184
- 0
client/src/com/vaadin/client/ui/grid/ColumnGroup.java View File

@@ -0,0 +1,184 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import com.vaadin.client.ui.grid.renderers.TextRenderer;

/**
* Column groups are used to group columns together for adding common auxiliary
* headers and footers. Columns groups are added to {@link ColumnGroupRow
* ColumnGroupRows}.
*
* @param <T>
* The row type of the grid. The row type is the POJO type from where
* the data is retrieved into the column cells.
* @since 7.2
* @author Vaadin Ltd
*/
public class ColumnGroup<T> {

/**
* The text shown in the header
*/
private String header;

/**
* The text shown in the footer
*/
private String footer;

/**
* Renders the header cells for the column group
*/
private Renderer<String> headerRenderer = new TextRenderer();

/**
* Renders the footer cells for the column group
*/
private Renderer<String> footerRenderer = new TextRenderer();

/**
* The columns included in the group when also accounting for subgroup
* columns
*/
private final List<GridColumn<?, T>> columns;

/**
* The grid associated with the column group
*/
private final Grid<T> grid;

/**
* Constructs a new column group
*/
ColumnGroup(Grid<T> grid, Collection<GridColumn<?, T>> columns) {
if (columns == null) {
throw new IllegalArgumentException(
"columns cannot be null. Pass an empty list instead.");
}
this.grid = grid;
this.columns = Collections
.unmodifiableList(new ArrayList<GridColumn<?, T>>(columns));
}

/**
* Gets the header text.
*
* @return the header text
*/
public String getHeaderCaption() {
return header;
}

/**
* Sets the text shown in the header.
*
* @param header
* the header to set
*/
public void setHeaderCaption(String header) {
this.header = header;
grid.refreshHeader();
}

/**
* Gets the text shown in the footer.
*
* @return the text in the footer
*/
public String getFooterCaption() {
return footer;
}

/**
* Sets the text displayed in the footer.
*
* @param footer
* the footer to set
*/
public void setFooterCaption(String footer) {
this.footer = footer;
grid.refreshFooter();
}

/**
* Returns all column in this group. It includes the subgroups columns as
* well.
*
* @return unmodifiable list of columns
*/
public List<GridColumn<?, T>> getColumns() {
return columns;
}

/**
* Returns the renderer used for rendering the header cells
*
* @return a renderer that renders header cells
*/
public Renderer<String> getHeaderRenderer() {
return headerRenderer;
}

/**
* Sets the renderer that renders header cells.
*
* @param renderer
* The renderer to use for rendering header cells. Must not be
* null.
* @throws IllegalArgumentException
* thrown when renderer is null
*/
public void setHeaderRenderer(Renderer<String> renderer) {
if (renderer == null) {
throw new IllegalArgumentException("Renderer cannot be null.");
}
this.headerRenderer = renderer;
grid.refreshHeader();
}

/**
* Returns the renderer used for rendering the footer cells
*
* @return a renderer that renders footer cells
*/
public Renderer<String> getFooterRenderer() {
return footerRenderer;
}

/**
* Sets the renderer that renders footer cells.
*
* @param renderer
* The renderer to use for rendering footer cells. Must not be
* null.
* @throws IllegalArgumentException
* thrown when renderer is null
*/
public void setFooterRenderer(Renderer<String> renderer) {
if (renderer == null) {
throw new IllegalArgumentException("Renderer cannot be null.");
}
this.footerRenderer = renderer;
grid.refreshFooter();
}
}

+ 243
- 0
client/src/com/vaadin/client/ui/grid/ColumnGroupRow.java View File

@@ -0,0 +1,243 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* A column group row represents an auxiliary header or footer row added to the
* grid. A column group row includes column groups that group columns together.
*
* @param <T>
* Row type
* @since 7.2
* @author Vaadin Ltd
*/
public class ColumnGroupRow<T> {

/**
* The column groups in this row
*/
private List<ColumnGroup<T>> groups = new ArrayList<ColumnGroup<T>>();

/**
* The grid associated with the column row
*/
private final Grid<T> grid;

/**
* Is the header shown
*/
private boolean headerVisible = true;

/**
* Is the footer shown
*/
private boolean footerVisible = false;

/**
* Constructs a new column group row
*
* @param grid
* Grid associated with this column
*
*/
ColumnGroupRow(Grid<T> grid) {
this.grid = grid;
}

/**
* Add a new group to the row by using column instances.
*
* @param columns
* The columns that should belong to the group
* @return a column group representing the collection of columns added to
* the group.
*/
public ColumnGroup<T> addGroup(GridColumn<?, T>... columns)
throws IllegalArgumentException {

for (GridColumn<?, T> column : columns) {
if (isColumnGrouped(column)) {
throw new IllegalArgumentException("Column "
+ String.valueOf(column.getHeaderCaption())
+ " already belongs to another group.");
}
}

validateNewGroupProperties(Arrays.asList(columns));

ColumnGroup<T> group = new ColumnGroup<T>(grid, Arrays.asList(columns));
groups.add(group);
grid.refreshHeader();
grid.refreshFooter();
return group;
}

private void validateNewGroupProperties(Collection<GridColumn<?, T>> columns) {

int rowIndex = grid.getColumnGroupRows().indexOf(this);
int parentRowIndex = rowIndex - 1;

// Get the parent row of this row.
ColumnGroupRow<T> parentRow = null;
if (parentRowIndex > -1) {
parentRow = grid.getColumnGroupRows().get(parentRowIndex);
}

if (parentRow == null) {
// A parentless row is always valid and is usually the first row
// added to the grid
return;
}

for (GridColumn<?, T> column : columns) {
if (parentRow.hasColumnBeenGrouped(column)) {
/*
* If a property has been grouped in the parent row then all of
* the properties in the parent group also needs to be included
* in the child group for the groups to be valid
*/
ColumnGroup parentGroup = parentRow.getGroupForColumn(column);
if (!columns.containsAll(parentGroup.getColumns())) {
throw new IllegalArgumentException(
"Grouped properties overlaps previous grouping bounderies");
}
}
}
}

private boolean hasColumnBeenGrouped(GridColumn<?, T> column) {
return getGroupForColumn(column) != null;
}

private ColumnGroup<T> getGroupForColumn(GridColumn<?, T> column) {
for (ColumnGroup<T> group : groups) {
if (group.getColumns().contains(column)) {
return group;
}
}
return null;
}

/**
* Add a new group to the row by using other already greated groups
*
* @param groups
* The subgroups of the group.
* @return a column group representing the collection of columns added to
* the group.
*
*/
public ColumnGroup<T> addGroup(ColumnGroup<T>... groups)
throws IllegalArgumentException {
assert groups != null : "groups cannot be null";

Set<GridColumn<?, T>> columns = new HashSet<GridColumn<?, T>>();
for (ColumnGroup<T> group : groups) {
columns.addAll(group.getColumns());
}

validateNewGroupProperties(columns);

ColumnGroup<T> group = new ColumnGroup<T>(grid, columns);
this.groups.add(group);
grid.refreshHeader();
grid.refreshFooter();
return group;
}

/**
* Removes a group from the row.
*
* @param group
* The group to remove
*/
public void removeGroup(ColumnGroup<T> group) {
groups.remove(group);
grid.refreshHeader();
grid.refreshFooter();
}

/**
* Get the groups in the row
*
* @return unmodifiable list of groups in this row
*/
public List<ColumnGroup<T>> getGroups() {
return Collections.unmodifiableList(groups);
}

/**
* Is the header visible for the row.
*
* @return <code>true</code> if header is visible
*/
public boolean isHeaderVisible() {
return headerVisible;
}

/**
* Sets the header visible for the row.
*
* @param visible
* should the header be shown
*/
public void setHeaderVisible(boolean visible) {
headerVisible = visible;
grid.refreshHeader();
}

/**
* Is the footer visible for the row.
*
* @return <code>true</code> if footer is visible
*/
public boolean isFooterVisible() {
return footerVisible;
}

/**
* Sets the footer visible for the row.
*
* @param visible
* should the footer be shown
*/
public void setFooterVisible(boolean visible) {
footerVisible = visible;
grid.refreshFooter();
}

/**
* Iterates all the column groups and checks if the columns alread has been
* added to a group.
*/
private boolean isColumnGrouped(GridColumn<?, T> column) {
for (ColumnGroup<T> group : groups) {
if (group.getColumns().contains(column)) {
return true;
}
}
return false;
}
}

+ 4042
- 0
client/src/com/vaadin/client/ui/grid/Escalator.java
File diff suppressed because it is too large
View File


+ 66
- 0
client/src/com/vaadin/client/ui/grid/EscalatorUpdater.java View File

@@ -0,0 +1,66 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;


/**
* A functional interface that allows client code to define how a certain row in
* Escalator will be displayed. The contents of an escalator's header, body and
* footer are rendered by their respective updaters.
* <p>
* The updater is responsible for internally handling all remote communication,
* should the displayed data need to be fetched remotely.
*
* @since 7.2
* @author Vaadin Ltd
* @see RowContainer#setEscalatorUpdater(EscalatorUpdater)
* @see Escalator#getHeader()
* @see Escalator#getBody()
* @see Escalator#getFooter()
*/
public interface EscalatorUpdater {
/** An {@link EscalatorUpdater} that doesn't render anything. */
public static final EscalatorUpdater NULL = new EscalatorUpdater() {
@Override
public void updateCells(final Row row,
final Iterable<Cell> cellsToUpdate) {
// NOOP
}
};

/**
* Renders a row contained in a row container.
* <p>
* <em>Note:</em> If rendering of cells is deferred (e.g. because
* asynchronous data retrieval), this method is responsible for explicitly
* displaying some placeholder data (empty content is valid). Because the
* cells (and rows) in an escalator are recycled, failing to reset a cell
* will lead to invalid data being displayed in the escalator.
* <p>
* For performance reasons, the escalator will never autonomously clear any
* data in a cell.
*
* @param row
* information about the row to update. <em>Note:</em> You should
* not store nor reuse this reference
* @param cellsToUpdate
* a collection of cells which need to be updated. <em>Note:</em>
* You should neither store nor reuse the reference to the list,
* nor to the individual cells
*/
public void updateCells(Row row, Iterable<Cell> cellsToUpdate);
}

+ 205
- 0
client/src/com/vaadin/client/ui/grid/FlyweightCell.java View File

@@ -0,0 +1,205 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.ui.grid;

import java.util.List;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.ui.IsWidget;
import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ui.grid.FlyweightRow.CellIterator;

/**
* An internal implementation of the {@link Cell} interface.
* <p>
* These instances are populated into a {@link FlyweightRow} instance, and
* intended to be reused when rendering cells in an escalator.
*
* @since 7.2
* @author Vaadin Ltd
* @see FlyweightRow#getCells()
* @see FlyweightRow#addCells(int, int)
* @see FlyweightRow#removeCells(int, int)
*/
class FlyweightCell implements Cell {
static final String COLSPAN_ATTR = "colSpan";

private final int column;
private final FlyweightRow row;

private CellIterator currentIterator = null;

private final Escalator escalator;

public FlyweightCell(final FlyweightRow row, final int column,
Escalator escalator) {
this.row = row;
this.column = column;
this.escalator = escalator;
}

@Override
public int getRow() {
assertSetup();
return row.getRow();
}

@Override
public int getColumn() {
assertSetup();
return column;
}

@Override
public Element getElement() {
return (Element) row.getElement().getChild(column);
}

void setup(final CellIterator cellIterator) {
currentIterator = cellIterator;

final Element e = getElement();
e.setPropertyInt(COLSPAN_ATTR, 1);
e.getStyle().setWidth(row.getColumnWidth(column), Unit.PX);
e.getStyle().clearDisplay();
}

/**
* Tear down the state of the Cell.
* <p>
* This is an internal check method, to prevent retrieving uninitialized
* data by calling {@link #getRow()}, {@link #getColumn()} or
* {@link #getElement()} at an improper time.
* <p>
* This should only be used with asserts ("
* <code>assert flyweightCell.teardown()</code> ") so that the code is never
* run when asserts aren't enabled.
*
* @return always <code>true</code>
* @see FlyweightRow#teardown()
*/
boolean teardown() {
currentIterator = null;
return true;
}

/**
* Asserts that the flyweight cell has properly been set up before trying to
* access any of its data.
*/
private void assertSetup() {
assert currentIterator != null : "FlyweightCell was not properly "
+ "initialized. This is either a bug in Grid/Escalator "
+ "or a Cell reference has been stored and reused "
+ "inappropriately.";
}

@Override
public void setColSpan(final int numberOfCells) {
/*-
* This will default to 1 if unset, as per DOM specifications:
* http://www.w3.org/TR/html5/tabular-data.html#attributes-common-to-td-and-th-elements
*/
final int prevColSpan = getElement().getPropertyInt(COLSPAN_ATTR);
if (numberOfCells == 1 && prevColSpan == 1) {
return;
}

getElement().setPropertyInt(COLSPAN_ATTR, numberOfCells);
adjustCellWidthForSpan(numberOfCells);
hideOrRevealAdjacentCellElements(numberOfCells, prevColSpan);
currentIterator.setSkipNext(numberOfCells - 1);
}

private void adjustCellWidthForSpan(final int numberOfCells) {
final int cellsToTheRight = currentIterator.rawPeekNext(
numberOfCells - 1).size();

final int selfWidth = row.getColumnWidth(column);
int widthsOfColumnsToTheRight = 0;
for (int i = 0; i < cellsToTheRight; i++) {
widthsOfColumnsToTheRight += row.getColumnWidth(column + i + 1);
}
getElement().getStyle().setWidth(selfWidth + widthsOfColumnsToTheRight,
Unit.PX);
}

private void hideOrRevealAdjacentCellElements(final int numberOfCells,
final int prevColSpan) {
final int affectedCellsNumber = Math.max(prevColSpan, numberOfCells);
final List<FlyweightCell> affectedCells = currentIterator
.rawPeekNext(affectedCellsNumber - 1);
if (prevColSpan < numberOfCells) {
for (int i = 0; i < affectedCells.size(); i++) {
affectedCells.get(prevColSpan + i - 1).getElement().getStyle()
.setDisplay(Display.NONE);
}
} else if (prevColSpan > numberOfCells) {
for (int i = 0; i < affectedCells.size(); i++) {
affectedCells.get(numberOfCells + i - 1).getElement()
.getStyle().clearDisplay();
}
}
}

@Override
public Widget getWidget() {
return Escalator.getWidgetFromCell(getElement());
}

@Override
public void setWidget(Widget widget) {

Widget oldWidget = getWidget();

// Validate
if (oldWidget == widget) {
return;
}

// Detach old child.
if (oldWidget != null) {
// Orphan.
Escalator.setParent(oldWidget, null);

// Physical detach.
getElement().removeChild(oldWidget.getElement());
}

// Remove any previous text nodes from previous
// setInnerText/setInnerHTML
getElement().removeAllChildren();

// Attach new child.
if (widget != null) {
// Detach new child from old parent.
widget.removeFromParent();

// Physical attach.
getElement().appendChild(widget.getElement());

Escalator.setParent(widget, escalator);
}
}

@Override
public void setWidget(IsWidget w) {
setWidget(Widget.asWidgetOrNull(w));
}

}

+ 217
- 0
client/src/com/vaadin/client/ui/grid/FlyweightRow.java View File

@@ -0,0 +1,217 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.ui.grid;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Node;

/**
* An internal implementation of the {@link Row} interface.
* <p>
* There is only one instance per Escalator. This is designed to be re-used when
* rendering rows.
*
* @since 7.2
* @author Vaadin Ltd
* @see Escalator.AbstractRowContainer#refreshRow(Node, int)
*/
class FlyweightRow implements Row {

static class CellIterator implements Iterator<Cell> {
/** A defensive copy of the cells in the current row. */
private final ArrayList<FlyweightCell> cells;
private int cursor = 0;
private int skipNext = 0;

public CellIterator(final Collection<FlyweightCell> cells) {
this.cells = new ArrayList<FlyweightCell>(cells);
}

@Override
public boolean hasNext() {
return cursor + skipNext < cells.size();
}

@Override
public FlyweightCell next() {
// if we needed to skip some cells since the last invocation.
for (int i = 0; i < skipNext; i++) {
cells.remove(cursor);
}
skipNext = 0;

final FlyweightCell cell = cells.get(cursor++);
cell.setup(this);
return cell;
}

@Override
public void remove() {
throw new UnsupportedOperationException(
"Cannot remove cells via iterator");
}

/**
* Sets the number of cells to skip when {@link #next()} is called the
* next time. Cell hiding is also handled eagerly in this method.
*
* @param colspan
* the number of cells to skip on next invocation of
* {@link #next()}
*/
public void setSkipNext(final int colspan) {
assert colspan > 0 : "Number of cells didn't make sense: "
+ colspan;
skipNext = colspan;
}

/**
* Gets the next <code>n</code> cells in the iterator, ignoring any
* possibly spanned cells.
*
* @param n
* the number of next cells to retrieve
* @return A list of next <code>n</code> cells, or less if there aren't
* enough cells to retrieve
*/
public List<FlyweightCell> rawPeekNext(final int n) {
final int from = Math.min(cursor, cells.size());
final int to = Math.min(cursor + n, cells.size());
return cells.subList(from, to);
}
}

private static final int BLANK = Integer.MIN_VALUE;

private int row;
private Element element;
private int[] columnWidths = null;
private final Escalator escalator;
private final List<FlyweightCell> cells = new ArrayList<FlyweightCell>();

public FlyweightRow(final Escalator escalator) {
this.escalator = escalator;
}

@Override
public Escalator getEscalator() {
return escalator;
}

void setup(final Element e, final int row, int[] columnWidths) {
element = e;
this.row = row;
this.columnWidths = columnWidths;
}

/**
* Tear down the state of the Row.
* <p>
* This is an internal check method, to prevent retrieving uninitialized
* data by calling {@link #getRow()}, {@link #getElement()} or
* {@link #getCells()} at an improper time.
* <p>
* This should only be used with asserts ("
* <code>assert flyweightRow.teardown()</code> ") so that the code is never
* run when asserts aren't enabled.
*
* @return always <code>true</code>
*/
boolean teardown() {
element = null;
row = BLANK;
columnWidths = null;
for (final FlyweightCell cell : cells) {
assert cell.teardown();
}
return true;
}

@Override
public int getRow() {
assertSetup();
return row;
}

@Override
public Element getElement() {
assertSetup();
return element;
}

void addCells(final int index, final int numberOfColumns) {
for (int i = 0; i < numberOfColumns; i++) {
final int col = index + i;
cells.add(col, new FlyweightCell(this, col, escalator));
}
updateRestOfCells(index + numberOfColumns);
}

void removeCells(final int index, final int numberOfColumns) {
for (int i = 0; i < numberOfColumns; i++) {
cells.remove(index);
}
updateRestOfCells(index);
}

private void updateRestOfCells(final int startPos) {
// update the column number for the cells to the right
for (int col = startPos; col < cells.size(); col++) {
cells.set(col, new FlyweightCell(this, col, escalator));
}
}

/**
* Get flyweight cells for the client code to render.
*
* @return a list of {@link FlyweightCell FlyweightCells}. They are
* generified into {@link Cell Cells}, because Java's generics
* system isn't expressive enough.
* @see #setup(Element, int)
* @see #teardown()
*/
Iterable<Cell> getCells() {
assertSetup();
return new Iterable<Cell>() {
@Override
public Iterator<Cell> iterator() {
return new CellIterator(cells);
}
};
}

/**
* Asserts that the flyweight row has properly been set up before trying to
* access any of its data.
*/
private void assertSetup() {
assert element != null && row != BLANK && columnWidths != null : "Flyweight row was not "
+ "properly initialized. Make sure the setup-method is "
+ "called before retrieving data. This is either a bug "
+ "in Escalator, or the instance of the flyweight row "
+ "has been stored and accessed.";
}

int getColumnWidth(int column) {
assertSetup();
return columnWidths[column];
}
}

+ 1318
- 0
client/src/com/vaadin/client/ui/grid/Grid.java
File diff suppressed because it is too large
View File


+ 54
- 0
client/src/com/vaadin/client/ui/grid/GridColumn.java View File

@@ -0,0 +1,54 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.ui.grid;

/**
* Represents a column in the {@link Grid}.
*
* @param <C>
* The column type
*
* @param <T>
* The row type
*
* @since 7.2
* @author Vaadin Ltd
*/
public abstract class GridColumn<C, T> extends Grid.AbstractGridColumn<C, T> {

/*
* This class is a convenience class so you do not have to reference
* Grid.AbstractGridColumn in your production code. The real implementation
* should be in the abstract class.
*/

/**
* Constructs a new column.
*/
public GridColumn() {
super();
}

/**
* Constructs a new column with a custom renderer.
*
* @param renderer
* The renderer to use for rendering the cells
*/
public GridColumn(Renderer<C> renderer) {
super(renderer);
}
}

+ 277
- 0
client/src/com/vaadin/client/ui/grid/GridConnector.java View File

@@ -0,0 +1,277 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.ui.AbstractComponentConnector;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.grid.ColumnGroupRowState;
import com.vaadin.shared.ui.grid.ColumnGroupState;
import com.vaadin.shared.ui.grid.GridClientRpc;
import com.vaadin.shared.ui.grid.GridColumnState;
import com.vaadin.shared.ui.grid.GridServerRpc;
import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.shared.ui.grid.ScrollDestination;

/**
* Connects the client side {@link Grid} widget with the server side
* {@link com.vaadin.ui.components.grid.Grid} component.
*
* @since 7.2
* @author Vaadin Ltd
*/
@Connect(com.vaadin.ui.components.grid.Grid.class)
public class GridConnector extends AbstractComponentConnector {

/**
* Custom implementation of the custom grid column using a String[] to
* represent the cell value and String as a column type.
*/
private class CustomGridColumn extends GridColumn<String, String[]> {

private final int columnIndex;

public CustomGridColumn(int columnIndex) {
this.columnIndex = columnIndex;
}

@Override
public String getValue(String[] obj) {
return obj[columnIndex];
}
}

/**
* Maps a generated column id to a grid column instance
*/
private Map<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>();

@Override
protected Grid<String[]> createWidget() {
// FIXME Shouldn't be needed after #12873 has been fixed.
return new Grid<String[]>();
}

@Override
@SuppressWarnings("unchecked")
public Grid<String[]> getWidget() {
return (Grid<String[]>) super.getWidget();
}

@Override
public GridState getState() {
return (GridState) super.getState();
}

@Override
protected void init() {
super.init();
getWidget().addRowVisibilityChangeHandler(
new RowVisibilityChangeHandler() {
@Override
public void onRowVisibilityChange(
RowVisibilityChangeEvent event) {
getRpcProxy(GridServerRpc.class).setVisibleRows(
event.getFirstVisibleRow(),
event.getVisibleRowCount());
}
});

registerRpc(GridClientRpc.class, new GridClientRpc() {
@Override
public void scrollToStart() {
getWidget().scrollToStart();
}

@Override
public void scrollToEnd() {
getWidget().scrollToEnd();
}

@Override
public void scrollToRow(int row, ScrollDestination destination) {
getWidget().scrollToRow(row, destination);
}
});
}

@Override
public void onStateChanged(StateChangeEvent stateChangeEvent) {
super.onStateChanged(stateChangeEvent);

// Column updates
if (stateChangeEvent.hasPropertyChanged("columns")) {

int totalColumns = getState().columns.size();

// Remove old columns
purgeRemovedColumns();

int currentColumns = getWidget().getColumnCount();

// Add new columns
for (int columnIndex = currentColumns; columnIndex < totalColumns; columnIndex++) {
addColumnFromStateChangeEvent(columnIndex);
}

// Update old columns
for (int columnIndex = 0; columnIndex < currentColumns; columnIndex++) {
// FIXME Currently updating all column header / footers when a
// change in made in one column. When the framework supports
// quering a specific item in a list then it should do so here.
updateColumnFromStateChangeEvent(columnIndex);
}
}

// Header
if (stateChangeEvent.hasPropertyChanged("columnHeadersVisible")) {
getWidget()
.setColumnHeadersVisible(getState().columnHeadersVisible);
}

// Footer
if (stateChangeEvent.hasPropertyChanged("columnFootersVisible")) {
getWidget()
.setColumnFootersVisible(getState().columnFootersVisible);
}

// Column row groups
if (stateChangeEvent.hasPropertyChanged("columnGroupRows")) {
updateColumnGroupsFromStateChangeEvent();
}

if (stateChangeEvent.hasPropertyChanged("lastFrozenColumnId")) {
String frozenColId = getState().lastFrozenColumnId;
if (frozenColId != null) {
CustomGridColumn column = columnIdToColumn.get(frozenColId);
assert column != null : "Column to be frozen could not be found (id:"
+ frozenColId + ")";
getWidget().setLastFrozenColumn(column);
} else {
getWidget().setLastFrozenColumn(null);
}
}
}

/**
* Updates a column from a state change event.
*
* @param columnIndex
* The index of the column to update
*/
private void updateColumnFromStateChangeEvent(int columnIndex) {
GridColumn<?, String[]> column = getWidget().getColumn(columnIndex);
GridColumnState columnState = getState().columns.get(columnIndex);
updateColumnFromState(column, columnState);
}

/**
* Adds a new column to the grid widget from a state change event
*
* @param columnIndex
* The index of the column, according to how it
*/
private void addColumnFromStateChangeEvent(int columnIndex) {
GridColumnState state = getState().columns.get(columnIndex);
CustomGridColumn column = new CustomGridColumn(columnIndex);
updateColumnFromState(column, state);

columnIdToColumn.put(state.id, column);

getWidget().addColumn(column, columnIndex);
}

/**
* Updates the column values from a state
*
* @param column
* The column to update
* @param state
* The state to get the data from
*/
private static void updateColumnFromState(GridColumn<?, String[]> column,
GridColumnState state) {
column.setVisible(state.visible);
column.setHeaderCaption(state.header);
column.setFooterCaption(state.footer);
column.setWidth(state.width);
}

/**
* Removes any orphan columns that has been removed from the state from the
* grid
*/
private void purgeRemovedColumns() {

// Get columns still registered in the state
Set<String> columnsInState = new HashSet<String>();
for (GridColumnState columnState : getState().columns) {
columnsInState.add(columnState.id);
}

// Remove column no longer in state
Iterator<String> columnIdIterator = columnIdToColumn.keySet()
.iterator();
while (columnIdIterator.hasNext()) {
String id = columnIdIterator.next();
if (!columnsInState.contains(id)) {
CustomGridColumn column = columnIdToColumn.get(id);
columnIdIterator.remove();
getWidget().removeColumn(column);
}
}
}

/**
* Updates the column groups from a state change
*/
private void updateColumnGroupsFromStateChangeEvent() {

// FIXME When something changes the header/footer rows will be
// re-created. At some point we should optimize this so partial updates
// can be made on the header/footer.
for (ColumnGroupRow<String[]> row : getWidget().getColumnGroupRows()) {
getWidget().removeColumnGroupRow(row);
}

for (ColumnGroupRowState rowState : getState().columnGroupRows) {
ColumnGroupRow<String[]> row = getWidget().addColumnGroupRow();
row.setFooterVisible(rowState.footerVisible);
row.setHeaderVisible(rowState.headerVisible);

for (ColumnGroupState groupState : rowState.groups) {
List<GridColumn<String, String[]>> columns = new ArrayList<GridColumn<String, String[]>>();
for (String columnId : groupState.columns) {
CustomGridColumn column = columnIdToColumn.get(columnId);
columns.add(column);
}
ColumnGroup<String[]> group = row.addGroup(columns
.toArray(new GridColumn[columns.size()]));
group.setFooterCaption(groupState.footer);
group.setHeaderCaption(groupState.header);
}
}
}
}

+ 118
- 0
client/src/com/vaadin/client/ui/grid/PositionFunction.java View File

@@ -0,0 +1,118 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;

/**
* A functional interface that can be used for positioning elements in the DOM.
*
* @since 7.2
* @author Vaadin Ltd
*/
interface PositionFunction {
/**
* A position function using "transform: translate3d(x,y,z)" to position
* elements in the DOM.
*/
public static class Translate3DPosition implements PositionFunction {
@Override
public void set(Element e, double x, double y) {
e.getStyle().setProperty("transform",
"translate3d(" + x + "px, " + y + "px, 0)");
}

@Override
public void reset(Element e) {
e.getStyle().clearProperty("transform");
}
}

/**
* A position function using "transform: translate(x,y)" to position
* elements in the DOM.
*/
public static class TranslatePosition implements PositionFunction {
@Override
public void set(Element e, double x, double y) {
e.getStyle().setProperty("transform",
"translate(" + x + "px," + y + "px)");
}

@Override
public void reset(Element e) {
e.getStyle().clearProperty("transform");
}
}

/**
* A position function using "-webkit-transform: translate3d(x,y,z)" to
* position elements in the DOM.
*/
public static class WebkitTranslate3DPosition implements PositionFunction {
@Override
public void set(Element e, double x, double y) {
e.getStyle().setProperty("webkitTransform",
"translate3d(" + x + "px," + y + "px,0)");
}

@Override
public void reset(Element e) {
e.getStyle().clearProperty("webkitTransform");
}
}

/**
* A position function using "left: x" and "top: y" to position elements in
* the DOM.
*/
public static class AbsolutePosition implements PositionFunction {
@Override
public void set(Element e, double x, double y) {
e.getStyle().setLeft(x, Unit.PX);
e.getStyle().setTop(y, Unit.PX);
}

@Override
public void reset(Element e) {
e.getStyle().clearLeft();
e.getStyle().clearTop();
}
}

/**
* Position an element in an (x,y) coordinate system in the DOM.
*
* @param e
* the element to position. Never <code>null</code>.
* @param x
* the x coordinate, in pixels
* @param y
* the y coordinate, in pixels
*/
void set(Element e, double x, double y);

/**
* Resets any previously applied positioning, clearing the used style
* attributes.
*
* @param e
* the element for which to reset the positioning
*/
void reset(Element e);
}

+ 43
- 0
client/src/com/vaadin/client/ui/grid/Renderer.java View File

@@ -0,0 +1,43 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.ui.grid;

/**
* Renderer for rending a value &lt;T&gt; into cell.
* <p>
* You can add a renderer to any column by overring the
* {@link GridColumn#getRenderer()} method and returning your own renderer. You
* can retrieve the cell element using {@link Cell#getElement()}.
*
* @param <T>
* The column type
*
* @since 7.2
* @author Vaadin Ltd
*/
public interface Renderer<T> {

/**
* Called whenever the {@link Grid} updates a cell
*
* @param cell
* The cell that gets updated
*
* @param data
* The column data object
*/
public void renderCell(Cell cell, T data);
}

+ 55
- 0
client/src/com/vaadin/client/ui/grid/Row.java View File

@@ -0,0 +1,55 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

import com.google.gwt.dom.client.Element;

/**
* A representation of a row in an {@link Escalator}.
*
* @since 7.2
* @author Vaadin Ltd
*/
public interface Row {
/**
* Gets the escalator containing the row.
*
* @return the escalator containing the row
*/
public Escalator getEscalator();

/**
* Gets the row index.
*
* @return the row index
*/
public int getRow();

/**
* Gets the root element for this row.
* <p>
* The {@link EscalatorUpdater} may update the class names of the element
* and add inline styles, but may not modify the contained DOM structure.
* <p>
* If you wish to modify the cells within this row element, access them via
* the <code>List&lt;{@link Cell}&gt;</code> objects passed in to
* {@code EscalatorUpdater.updateCells(Row, List)}
*
* @return the root element of the row
*/
public Element getElement();
}

+ 156
- 0
client/src/com/vaadin/client/ui/grid/RowContainer.java View File

@@ -0,0 +1,156 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

/**
* A representation of the rows in each of the sections (header, body and
* footer) in an {@link Escalator}.
*
* @since 7.2
* @author Vaadin Ltd
* @see Escalator#getHeader()
* @see Escalator#getBody()
* @see Escalator#getFooter()
*/
public interface RowContainer {

/**
* 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();
}

+ 90
- 0
client/src/com/vaadin/client/ui/grid/RowVisibilityChangeEvent.java View File

@@ -0,0 +1,90 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

import com.google.gwt.event.shared.GwtEvent;

/**
* Event fired when the range of visible rows changes e.g. because of scrolling.
*
* @since 7.2
* @author Vaadin Ltd
*/
public class RowVisibilityChangeEvent extends
GwtEvent<RowVisibilityChangeHandler> {
/**
* The type of this event.
*/
public static final Type<RowVisibilityChangeHandler> TYPE = new Type<RowVisibilityChangeHandler>();

private final int firstVisibleRow;
private final int visibleRowCount;

/**
* Creates a new row visibility change event
*
* @param firstVisibleRow
* the index of the first visible row
* @param visibleRowCount
* the number of visible rows
*/
public RowVisibilityChangeEvent(int firstVisibleRow, int visibleRowCount) {
this.firstVisibleRow = firstVisibleRow;
this.visibleRowCount = visibleRowCount;
}

/**
* Gets the index of the first row that is at least partially visible.
*
* @return the index of the first visible row
*/
public int getFirstVisibleRow() {
return firstVisibleRow;
}

/**
* Gets the number of at least partially visible rows.
*
* @return the number of visible rows
*/
public int getVisibleRowCount() {
return visibleRowCount;
}

/*
* (non-Javadoc)
*
* @see com.google.gwt.event.shared.GwtEvent#getAssociatedType()
*/
@Override
public Type<RowVisibilityChangeHandler> getAssociatedType() {
return TYPE;
}

/*
* (non-Javadoc)
*
* @see
* com.google.gwt.event.shared.GwtEvent#dispatch(com.google.gwt.event.shared
* .EventHandler)
*/
@Override
protected void dispatch(RowVisibilityChangeHandler handler) {
handler.onRowVisibilityChange(this);
}

}

+ 38
- 0
client/src/com/vaadin/client/ui/grid/RowVisibilityChangeHandler.java View File

@@ -0,0 +1,38 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

import com.google.gwt.event.shared.EventHandler;

/**
* Event handler that gets notified when the range of visible rows changes e.g.
* because of scrolling.
*
* @since 7.2
* @author Vaadin Ltd
*/
public interface RowVisibilityChangeHandler extends EventHandler {

/**
* Called when the range of visible rows changes e.g. because of scrolling.
*
* @param event
* the row visibility change event describing the change
*/
void onRowVisibilityChange(RowVisibilityChangeEvent event);

}

+ 403
- 0
client/src/com/vaadin/client/ui/grid/ScrollbarBundle.java View File

@@ -0,0 +1,403 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.client.ui.grid;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.DOM;

/**
* An element-like bundle representing a configurable and visual scrollbar in
* one axis.
*
* @since 7.2
* @author Vaadin Ltd
* @see VerticalScrollbarBundle
* @see HorizontalScrollbarBundle
*/
abstract class ScrollbarBundle {

/**
* The pixel size for OSX's invisible scrollbars.
* <p>
* Touch devices don't show a scrollbar at all, so the scrollbar size is
* irrelevant in their case. There doesn't seem to be any other popular
* platforms that has scrollbars similar to OSX. Thus, this behavior is
* tailored for OSX only, until additional platforms start behaving this
* way.
*/
private static final int OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX = 13;

/**
* A representation of a single vertical scrollbar.
*
* @see VerticalScrollbarBundle#getElement()
*/
final static class VerticalScrollbarBundle extends ScrollbarBundle {

@Override
public void setStylePrimaryName(String primaryStyleName) {
super.setStylePrimaryName(primaryStyleName);
root.addClassName(primaryStyleName + "-scroller-vertical");
}

@Override
protected void internalSetScrollPos(int px) {
root.setScrollTop(px);
}

@Override
protected int internalGetScrollPos() {
return root.getScrollTop();
}

@Override
protected void internalSetScrollSize(int px) {
scrollSizeElement.getStyle().setHeight(px, Unit.PX);
}

@Override
public int getScrollSize() {
return scrollSizeElement.getOffsetHeight();
}

@Override
protected void internalSetOffsetSize(int px) {
root.getStyle().setHeight(px, Unit.PX);
}

@Override
public int getOffsetSize() {
return root.getOffsetHeight();
}

@Override
protected void internalSetScrollbarThickness(int px) {
root.getStyle().setWidth(px, Unit.PX);
scrollSizeElement.getStyle().setWidth(px, Unit.PX);
}

@Override
protected int internalGetScrollbarThickness() {
return root.getOffsetWidth();
}

@Override
protected void forceScrollbar(boolean enable) {
if (enable) {
root.getStyle().setOverflowY(Overflow.SCROLL);
} else {
root.getStyle().clearOverflowY();
}
}
}

/**
* A representation of a single horizontal scrollbar.
*
* @see HorizontalScrollbarBundle#getElement()
*/
final static class HorizontalScrollbarBundle extends ScrollbarBundle {

@Override
public void setStylePrimaryName(String primaryStyleName) {
super.setStylePrimaryName(primaryStyleName);
root.addClassName(primaryStyleName + "-scroller-horizontal");
}

@Override
protected void internalSetScrollPos(int px) {
root.setScrollLeft(px);
}

@Override
protected int internalGetScrollPos() {
return root.getScrollLeft();
}

@Override
protected void internalSetScrollSize(int px) {
scrollSizeElement.getStyle().setWidth(px, Unit.PX);
}

@Override
public int getScrollSize() {
return scrollSizeElement.getOffsetWidth();
}

@Override
protected void internalSetOffsetSize(int px) {
root.getStyle().setWidth(px, Unit.PX);
}

@Override
public int getOffsetSize() {
return root.getOffsetWidth();
}

@Override
protected void internalSetScrollbarThickness(int px) {
root.getStyle().setHeight(px, Unit.PX);
scrollSizeElement.getStyle().setHeight(px, Unit.PX);
}

@Override
protected int internalGetScrollbarThickness() {
return root.getOffsetHeight();
}

@Override
protected void forceScrollbar(boolean enable) {
if (enable) {
root.getStyle().setOverflowX(Overflow.SCROLL);
} else {
root.getStyle().clearOverflowX();
}
}
}

protected final Element root = DOM.createDiv();
protected final Element scrollSizeElement = DOM.createDiv();
protected boolean isInvisibleScrollbar = false;

private int scrollPos = 0;
private int maxScrollPos = 0;

private ScrollbarBundle() {
root.appendChild(scrollSizeElement);
}

/**
* Sets the primary style name
*
* @param primaryStyleName
* The primary style name to use
*/
public void setStylePrimaryName(String primaryStyleName) {
root.setClassName(primaryStyleName + "-scroller");
}

/**
* Gets the root element of this scrollbar-composition.
*
* @return the root element
*/
public final Element getElement() {
return root;
}

/**
* Modifies the scroll position of this scrollbar by a number of pixels
*
* @param delta
* the delta in pixels to change the scroll position by
*/
public final void setScrollPosByDelta(int delta) {
if (delta != 0) {
setScrollPos(getScrollPos() + delta);
}
}

/**
* Modifies {@link #root root's} dimensions in the axis the scrollbar is
* representing.
*
* @param px
* the new size of {@link #root} in the dimension this scrollbar
* is representing
*/
protected abstract void internalSetOffsetSize(int px);

/**
* Sets the length of the scrollbar.
*
* @param px
* the length of the scrollbar in pixels
*/
public final void setOffsetSize(int px) {
internalSetOffsetSize(px);
forceScrollbar(showsScrollHandle());
recalculateMaxScrollPos();
}

/**
* Force the scrollbar to be visible with CSS. In practice, this means to
* set either <code>overflow-x</code> or <code>overflow-y</code> to "
* <code>scroll</code>" in the scrollbar's direction.
* <p>
* This is an IE8 workaround, since it doesn't always show scrollbars with
* <code>overflow: auto</code> enabled.
*/
protected abstract void forceScrollbar(boolean enable);

/**
* Gets the length of the scrollbar
*
* @return the length of the scrollbar in pixels
*/
public abstract int getOffsetSize();

/**
* Sets the scroll position of the scrollbar in the axis the scrollbar is
* representing.
*
* @param px
* the new scroll position in pixels
*/
public final void setScrollPos(int px) {
int oldScrollPos = scrollPos;
scrollPos = Math.max(0, Math.min(maxScrollPos, px));

if (oldScrollPos != scrollPos) {
internalSetScrollPos(px);
}
}

protected abstract void internalSetScrollPos(int px);

/**
* Gets the scroll position of the scrollbar in the axis the scrollbar is
* representing.
*
* @return the new scroll position in pixels
*/
public final int getScrollPos() {
assert internalGetScrollPos() == scrollPos : "calculated scroll position ("
+ scrollPos
+ ") did not match the DOM element scroll position ("
+ internalGetScrollPos() + ")";
return scrollPos;
}

protected abstract int internalGetScrollPos();

/**
* Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in
* such a way that the scrollbar is able to scroll a certain number of
* pixels in the axis it is representing.
*
* @param px
* the new size of {@link #scrollSizeElement} in the dimension
* this scrollbar is representing
*/
protected abstract void internalSetScrollSize(int px);

/**
* Sets the amount of pixels the scrollbar needs to be able to scroll
* through.
*
* @param px
* the number of pixels the scrollbar should be able to scroll
* through
*/
public final void setScrollSize(int px) {
internalSetScrollSize(px);
forceScrollbar(showsScrollHandle());
recalculateMaxScrollPos();
}

/**
* Gets the amount of pixels the scrollbar needs to be able to scroll
* through.
*
* @return the number of pixels the scrollbar should be able to scroll
* through
*/
public abstract int getScrollSize();

/**
* Modifies {@link #scrollSizeElement scrollSizeElement's} dimensions in the
* opposite axis to what the scrollbar is representing.
*
* @param px
* the dimension that {@link #scrollSizeElement} should take in
* the opposite axis to what the scrollbar is representing
*/
protected abstract void internalSetScrollbarThickness(int px);

/**
* Sets the scrollbar's thickness.
* <p>
* If the thickness is set to 0, the scrollbar will be treated as an
* "invisible" scrollbar. This means, the DOM structure will be given a
* non-zero size, but {@link #getScrollbarThickness()} will still return the
* value 0.
*
* @param px
* the scrollbar's thickness in pixels
*/
public final void setScrollbarThickness(int px) {
isInvisibleScrollbar = (px == 0);
internalSetScrollbarThickness(px != 0 ? px
: OSX_INVISIBLE_SCROLLBAR_FAKE_SIZE_PX);
}

/**
* Gets the scrollbar's thickness as defined in the DOM.
*
* @return the scrollbar's thickness as defined in the DOM, in pixels
*/
protected abstract int internalGetScrollbarThickness();

/**
* Gets the scrollbar's thickness.
* <p>
* This value will differ from the value in the DOM, if the thickness was
* set to 0 with {@link #setScrollbarThickness(int)}, as the scrollbar is
* then treated as "invisible."
*
* @return the scrollbar's thickness in pixels
*/
public final int getScrollbarThickness() {
if (!isInvisibleScrollbar) {
return internalGetScrollbarThickness();
} else {
return 0;
}
}

/**
* Checks whether the scrollbar's handle is visible.
* <p>
* In other words, this method checks whether the contents is larger than
* can visually fit in the element.
*
* @return <code>true</code> iff the scrollbar's handle is visible
*/
public boolean showsScrollHandle() {
return getOffsetSize() < getScrollSize();
}

public void recalculateMaxScrollPos() {
int scrollSize = getScrollSize();
int offsetSize = getOffsetSize();
maxScrollPos = Math.max(0, scrollSize - offsetSize);

// make sure that the correct max scroll position is maintained.
setScrollPos(scrollPos);
}

/**
* This is a method that JSNI can call to synchronize the object state from
* the DOM.
*/
@SuppressWarnings("unused")
private final void updateScrollPosFromDom() {
scrollPos = internalGetScrollPos();
}
}

+ 357
- 0
client/src/com/vaadin/client/ui/grid/datasources/ListDataSource.java View File

@@ -0,0 +1,357 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.ui.grid.datasources;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;

import com.vaadin.client.data.DataChangeHandler;
import com.vaadin.client.data.DataSource;

/**
* A simple list based on an in-memory data source for simply adding a list of
* row pojos to the grid. Based on a wrapped list instance which supports adding
* and removing of items.
*
* <p>
* Usage:
*
* <pre>
* ListDataSource&lt;Integer&gt; ds = new ListDataSource&lt;Integer&gt;(1, 2, 3, 4);
*
* // Add item to the data source
* ds.asList().add(5);
*
* // Remove item from the data source
* ds.asList().remove(3);
*
* // Add multiple items
* ds.asList().addAll(Arrays.asList(5, 6, 7));
* </pre>
*
* @since 7.2
* @author Vaadin Ltd
*/
public class ListDataSource<T> implements DataSource<T> {

/**
* Wraps the datasource list and notifies the change handler of changing to
* the list
*/
private class ListWrapper implements List<T> {

@Override
public int size() {
return ds.size();
}

@Override
public boolean isEmpty() {
return ds.isEmpty();
}

@Override
public boolean contains(Object o) {
return contains(o);
}

@Override
public Iterator<T> iterator() {
return new ListWrapperIterator(ds.iterator());
}

@Override
public Object[] toArray() {
return ds.toArray();
}

@Override
public <T> T[] toArray(T[] a) {
return toArray(a);
}

@Override
public boolean add(T e) {
if (ds.add(e)) {
if (changeHandler != null) {
changeHandler.dataAdded(ds.size() - 1, 1);
}
return true;
}
return false;
}

@Override
public boolean remove(Object o) {
int index = ds.indexOf(o);
if (ds.remove(o)) {
if (changeHandler != null) {
changeHandler.dataRemoved(index, 1);
}
return true;
}
return false;
}

@Override
public boolean containsAll(Collection<?> c) {
return ds.containsAll(c);
}

@Override
public boolean addAll(Collection<? extends T> c) {
int idx = ds.size();
if (ds.addAll(c)) {
if (changeHandler != null) {
changeHandler.dataAdded(idx, c.size());
}
return true;
}
return false;
}

@Override
public boolean addAll(int index, Collection<? extends T> c) {
if (ds.addAll(index, c)) {
if (changeHandler != null) {
changeHandler.dataAdded(index, c.size());
}
return true;
}
return false;
}

@Override
public boolean removeAll(Collection<?> c) {
if (ds.removeAll(c)) {
if (changeHandler != null) {
// Have to update the whole list as the removal does not
// have to be a continuous range
changeHandler.dataUpdated(0, ds.size());
}
return true;
}
return false;
}

@Override
public boolean retainAll(Collection<?> c) {
if (ds.retainAll(c)) {
if (changeHandler != null) {
// Have to update the whole list as the retain does not
// have to be a continuous range
changeHandler.dataUpdated(0, ds.size());
}
return true;
}
return false;
}

@Override
public void clear() {
int size = ds.size();
ds.clear();
if (changeHandler != null) {
changeHandler.dataRemoved(0, size);
}
}

@Override
public T get(int index) {
return ds.get(index);
}

@Override
public T set(int index, T element) {
T prev = ds.set(index, element);
if (changeHandler != null) {
changeHandler.dataUpdated(index, 1);
}
return prev;
}

@Override
public void add(int index, T element) {
ds.add(index, element);
if (changeHandler != null) {
changeHandler.dataAdded(index, 1);
}
}

@Override
public T remove(int index) {
T removed = ds.remove(index);
if (changeHandler != null) {
changeHandler.dataRemoved(index, 1);
}
return removed;
}

@Override
public int indexOf(Object o) {
return ds.indexOf(o);
}

@Override
public int lastIndexOf(Object o) {
return ds.lastIndexOf(o);
}

@Override
public ListIterator<T> listIterator() {
// TODO could be implemented by a custom iterator.
throw new UnsupportedOperationException(
"List iterators not supported at this time.");
}

@Override
public ListIterator<T> listIterator(int index) {
// TODO could be implemented by a custom iterator.
throw new UnsupportedOperationException(
"List iterators not supported at this time.");
}

@Override
public List<T> subList(int fromIndex, int toIndex) {
throw new UnsupportedOperationException("Sub lists not supported.");
}
}

/**
* Iterator returned by {@link ListWrapper}
*/
private class ListWrapperIterator implements Iterator<T> {

private final Iterator<T> iterator;

/**
* Constructs a new iterator
*/
public ListWrapperIterator(Iterator<T> iterator) {
this.iterator = iterator;
}

@Override
public boolean hasNext() {
return iterator.hasNext();
}

@Override
public T next() {
return iterator.next();
}

@Override
public void remove() {
throw new UnsupportedOperationException(
"Iterator.remove() is not supported by this iterator.");
}
}

/**
* Datasource for providing row pojo's
*/
private final List<T> ds;

/**
* Wrapper that wraps the data source
*/
private final ListWrapper wrapper;

/**
* Handler for listening to changes in the underlying list.
*/
private DataChangeHandler changeHandler;

/**
* Constructs a new list data source.
* <p>
* Note: Modifications to the original list will not be reflected in the
* data source after the data source has been constructed. To add or remove
* items to the data source after it has been constructed use
* {@link ListDataSource#asList()}.
*
*
* @param datasource
* The list to use for providing the data to the grid
*/
public ListDataSource(List<T> datasource) {
if (datasource == null) {
throw new IllegalArgumentException("datasource cannot be null");
}
ds = new ArrayList<T>(datasource);
wrapper = new ListWrapper();
}

/**
* Constructs a data source with a set of rows. You can dynamically add and
* remove rows from the data source via the list you get from
* {@link ListDataSource#asList()}
*
* @param rows
* The rows to initially add to the data source
*/
public ListDataSource(T... rows) {
if (rows == null) {
ds = new ArrayList<T>();
} else {
ds = new ArrayList<T>(Arrays.asList(rows));
}
wrapper = new ListWrapper();
}

@Override
public void ensureAvailability(int firstRowIndex, int numberOfRows) {
if (firstRowIndex >= ds.size()) {
throw new IllegalStateException(
"Trying to fetch rows outside of array");
}
}

@Override
public T getRow(int rowIndex) {
return ds.get(rowIndex);
}

@Override
public int getEstimatedSize() {
return ds.size();
}

@Override
public void setDataChangeHandler(DataChangeHandler dataChangeHandler) {
this.changeHandler = dataChangeHandler;
}

/**
* Gets the list that backs this datasource. Any changes made to this list
* will be reflected in the datasource.
* <p>
* Note: The list is not the same list as passed into the data source via
* the constructor.
*
* @return Returns a list implementation that wraps the real list that backs
* the data source and provides events for the data source
* listeners.
*/
public List<T> asList() {
return wrapper;
}
}

+ 94
- 0
client/src/com/vaadin/client/ui/grid/renderers/DateRenderer.java View File

@@ -0,0 +1,94 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.ui.grid.renderers;

import java.util.Date;

import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.i18n.client.TimeZone;
import com.vaadin.client.ui.grid.Cell;
import com.vaadin.client.ui.grid.Renderer;

/**
* A renderer for rendering dates into cells
*
* @since 7.2
* @author Vaadin Ltd
*/
public class DateRenderer implements Renderer<Date> {

private DateTimeFormat format = DateTimeFormat.getShortDateTimeFormat();

private TimeZone timeZone = TimeZone.createTimeZone(new Date()
.getTimezoneOffset());

@Override
public void renderCell(Cell cell, Date date) {
String dateStr = format.format(date, timeZone);
cell.getElement().setInnerText(dateStr);
}

/**
* Gets the format of how the date is formatted.
*
* @return the format
* @see <a
* href="http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/DateTimeFormat.html">GWT
* documentation on DateTimeFormat</a>
*/
public DateTimeFormat getFormat() {
return format;
}

/**
* Sets the format used for formatting the dates.
*
* @param format
* the format to set
* @see <a
* href="http://www.gwtproject.org/javadoc/latest/com/google/gwt/i18n/client/DateTimeFormat.html">GWT
* documentation on DateTimeFormat</a>
*/
public void setFormat(DateTimeFormat format) {
if (format == null) {
throw new IllegalArgumentException("Format should not be null");
}
this.format = format;
}

/**
* Returns the time zone of the date.
*
* @return the time zone
*/
public TimeZone getTimeZone() {
return timeZone;
}

/**
* Sets the time zone of the the date. By default uses the time zone of the
* browser.
*
* @param timeZone
* the timeZone to set
*/
public void setTimeZone(TimeZone timeZone) {
if (timeZone == null) {
throw new IllegalArgumentException("Timezone should not be null");
}
this.timeZone = timeZone;
}
}

+ 42
- 0
client/src/com/vaadin/client/ui/grid/renderers/HtmlRenderer.java View File

@@ -0,0 +1,42 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.ui.grid.renderers;

import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.vaadin.client.ui.grid.Cell;
import com.vaadin.client.ui.grid.Renderer;

/**
* Renders a string as HTML into a cell.
* <p>
* The html string is rendered as is without any escaping. It is up to the
* developer to ensure that the html string honors the {@link SafeHtml}
* contract. For more information see
* {@link SafeHtmlUtils#fromSafeConstant(String)}.
*
* @since 7.2
* @author Vaadin Ltd
* @see SafeHtmlUtils#fromSafeConstant(String)
*/
public class HtmlRenderer implements Renderer<String> {

@Override
public void renderCell(Cell cell, String htmlString) {
cell.getElement().setInnerSafeHtml(
SafeHtmlUtils.fromSafeConstant(htmlString));
}
}

+ 64
- 0
client/src/com/vaadin/client/ui/grid/renderers/NumberRenderer.java View File

@@ -0,0 +1,64 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.ui.grid.renderers;

import com.google.gwt.i18n.client.NumberFormat;
import com.vaadin.client.ui.grid.Cell;
import com.vaadin.client.ui.grid.Renderer;

/**
* Renders a number into a cell using a specific {@link NumberFormat}. By
* default uses the default number format returned by
* {@link NumberFormat#getDecimalFormat()}.
*
* @since 7.2
* @author Vaadin Ltd
* @param <T>
* The number type to render.
*/
public class NumberRenderer<T extends Number> implements Renderer<T> {

private NumberFormat format = NumberFormat.getDecimalFormat();

/**
* Gets the number format that the number should be formatted in.
*
* @return the number format used to render the number
*/
public NumberFormat getFormat() {
return format;
}

/**
* Sets the number format to use for formatting the number.
*
* @param format
* the format to use
* @throws IllegalArgumentException
* when the format is null
*/
public void setFormat(NumberFormat format) throws IllegalArgumentException {
if (format == null) {
throw new IllegalArgumentException("Format cannot be null");
}
this.format = format;
}

@Override
public void renderCell(Cell cell, Number number) {
cell.getElement().setInnerText(format.format(number));
}
}

+ 33
- 0
client/src/com/vaadin/client/ui/grid/renderers/TextRenderer.java View File

@@ -0,0 +1,33 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.ui.grid.renderers;

import com.vaadin.client.ui.grid.Cell;
import com.vaadin.client.ui.grid.Renderer;

/**
* Renderer that renders text into a cell.
*
* @since 7.2
* @author Vaadin Ltd
*/
public class TextRenderer implements Renderer<String> {

@Override
public void renderCell(Cell cell, String text) {
cell.getElement().setInnerText(text);
}
}

+ 178
- 0
client/tests/src/com/vaadin/client/ui/grid/ListDataSourceTest.java View File

@@ -0,0 +1,178 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.ui.grid;

import static org.junit.Assert.assertEquals;

import java.util.Arrays;

import org.easymock.EasyMock;
import org.junit.Test;

import com.vaadin.client.data.DataChangeHandler;
import com.vaadin.client.ui.grid.datasources.ListDataSource;

/**
*
* @since 7.2
* @author Vaadin Ltd
*/
public class ListDataSourceTest {

@Test
public void testDataSourceConstruction() throws Exception {

ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);

assertEquals(4, ds.getEstimatedSize());
assertEquals(0, (int) ds.getRow(0));
assertEquals(1, (int) ds.getRow(1));
assertEquals(2, (int) ds.getRow(2));
assertEquals(3, (int) ds.getRow(3));

ds = new ListDataSource<Integer>(Arrays.asList(0, 1, 2, 3));

assertEquals(4, ds.getEstimatedSize());
assertEquals(0, (int) ds.getRow(0));
assertEquals(1, (int) ds.getRow(1));
assertEquals(2, (int) ds.getRow(2));
assertEquals(3, (int) ds.getRow(3));
}

@Test
public void testListAddOperation() throws Exception {

ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);

DataChangeHandler handler = EasyMock
.createNiceMock(DataChangeHandler.class);
ds.setDataChangeHandler(handler);

handler.dataAdded(4, 1);
EasyMock.expectLastCall();

EasyMock.replay(handler);

ds.asList().add(4);

assertEquals(5, ds.getEstimatedSize());
assertEquals(0, (int) ds.getRow(0));
assertEquals(1, (int) ds.getRow(1));
assertEquals(2, (int) ds.getRow(2));
assertEquals(3, (int) ds.getRow(3));
assertEquals(4, (int) ds.getRow(4));
}

@Test
public void testListAddAllOperation() throws Exception {

ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);

DataChangeHandler handler = EasyMock
.createNiceMock(DataChangeHandler.class);
ds.setDataChangeHandler(handler);

handler.dataAdded(4, 3);
EasyMock.expectLastCall();

EasyMock.replay(handler);

ds.asList().addAll(Arrays.asList(4, 5, 6));

assertEquals(7, ds.getEstimatedSize());
assertEquals(0, (int) ds.getRow(0));
assertEquals(1, (int) ds.getRow(1));
assertEquals(2, (int) ds.getRow(2));
assertEquals(3, (int) ds.getRow(3));
assertEquals(4, (int) ds.getRow(4));
assertEquals(5, (int) ds.getRow(5));
assertEquals(6, (int) ds.getRow(6));
}

@Test
public void testListRemoveOperation() throws Exception {

ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);

DataChangeHandler handler = EasyMock
.createNiceMock(DataChangeHandler.class);
ds.setDataChangeHandler(handler);

handler.dataRemoved(3, 1);
EasyMock.expectLastCall();

EasyMock.replay(handler);

ds.asList().remove(2);

assertEquals(3, ds.getEstimatedSize());
assertEquals(0, (int) ds.getRow(0));
assertEquals(1, (int) ds.getRow(1));
assertEquals(3, (int) ds.getRow(2));
}

@Test
public void testListRemoveAllOperation() throws Exception {

ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);

DataChangeHandler handler = EasyMock
.createNiceMock(DataChangeHandler.class);
ds.setDataChangeHandler(handler);

handler.dataRemoved(0, 3);
EasyMock.expectLastCall();

EasyMock.replay(handler);

ds.asList().removeAll(Arrays.asList(0, 2, 3));

assertEquals(1, ds.getEstimatedSize());
assertEquals(1, (int) ds.getRow(0));
}

@Test
public void testListClearOperation() throws Exception {

ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);

DataChangeHandler handler = EasyMock
.createNiceMock(DataChangeHandler.class);
ds.setDataChangeHandler(handler);

handler.dataRemoved(0, 4);
EasyMock.expectLastCall();

EasyMock.replay(handler);

ds.asList().clear();

assertEquals(0, ds.getEstimatedSize());
}

@Test(expected = IllegalStateException.class)
public void testFetchingNonExistantItem() {
ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
ds.ensureAvailability(5, 1);
}

@Test(expected = UnsupportedOperationException.class)
public void testUnsupportedIteratorRemove() {
ListDataSource<Integer> ds = new ListDataSource<Integer>(0, 1, 2, 3);
ds.asList().iterator().remove();
}

}

+ 104
- 0
client/tests/src/com/vaadin/client/ui/grid/PartitioningTest.java View File

@@ -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]);
}
}

+ 148
- 0
server/src/com/vaadin/data/RpcDataProviderExtension.java View File

@@ -0,0 +1,148 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.data;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import com.vaadin.data.Container.Indexed;
import com.vaadin.server.AbstractExtension;
import com.vaadin.shared.data.DataProviderRpc;
import com.vaadin.shared.data.DataProviderState;
import com.vaadin.shared.data.DataRequestRpc;
import com.vaadin.ui.components.grid.Grid;

/**
* Provides Vaadin server-side container data source to a
* {@link com.vaadin.client.ui.grid.GridConnector}. This is currently
* implemented as an Extension hardcoded to support a specific connector type.
* This will be changed once framework support for something more flexible has
* been implemented.
*
* @since 7.2
* @author Vaadin Ltd
*/
public class RpcDataProviderExtension extends AbstractExtension {

private final Indexed container;

/**
* Creates a new data provider using the given container.
*
* @param container
* the container to make available
*/
public RpcDataProviderExtension(Indexed container) {
this.container = container;

// TODO support for reacting to events from the container added later

registerRpc(new DataRequestRpc() {
@Override
public void requestRows(int firstRow, int numberOfRows) {
pushRows(firstRow, numberOfRows);
}
});

getState().containerSize = container.size();
}

private void pushRows(int firstRow, int numberOfRows) {
List<?> itemIds = container.getItemIds(firstRow, numberOfRows);
Collection<?> propertyIds = container.getContainerPropertyIds();
List<String[]> rows = new ArrayList<String[]>(itemIds.size());
for (Object itemId : itemIds) {
rows.add(getRowData(propertyIds, itemId));
}
getRpcProxy(DataProviderRpc.class).setRowData(firstRow, rows);
}

private String[] getRowData(Collection<?> propertyIds, Object itemId) {
Item item = container.getItem(itemId);
String[] row = new String[propertyIds.size()];

int i = 0;
for (Object propertyId : propertyIds) {
Object value = item.getItemProperty(propertyId).getValue();
String stringValue = String.valueOf(value);
row[i++] = stringValue;
}
return row;
}

@Override
protected DataProviderState getState() {
return (DataProviderState) super.getState();
}

/**
* Makes the data source available to the given {@link Grid} component.
*
* @param component
* the remote data grid component to extend
*/
public void extend(Grid component) {
super.extend(component);
}

/**
* Informs the client side that new rows have been inserted into the data
* source.
*
* @param index
* the index at which new rows have been inserted
* @param count
* the number of rows inserted at <code>index</code>
*/
public void insertRowData(int index, int count) {
getState().containerSize += count;
getRpcProxy(DataProviderRpc.class).insertRowData(index, count);
}

/**
* Informs the client side that rows have been removed from the data source.
*
* @param firstIndex
* the index of the first row removed
* @param count
* the number of rows removed
*/
public void removeRowData(int firstIndex, int count) {
getState().containerSize -= count;
getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count);
}

/**
* Informs the client side that data of a row has been modified in the data
* source.
*
* @param index
* the index of the row that was updated
*/
public void updateRowData(int index) {
/*
* TODO: ignore duplicate requests for the same index during the same
* roundtrip.
*/
Object itemId = container.getIdByIndex(index);
String[] row = getRowData(container.getContainerPropertyIds(), itemId);
getRpcProxy(DataProviderRpc.class).setRowData(index,
Collections.singletonList(row));
}
}

+ 165
- 0
server/src/com/vaadin/ui/components/grid/ColumnGroup.java View File

@@ -0,0 +1,165 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.ui.components.grid;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.vaadin.shared.ui.grid.ColumnGroupState;

/**
* Column groups are used to group columns together for adding common auxiliary
* headers and footers. Columns groups are added to {@link ColumnGroupRow}'s.
*
* @since 7.2
* @author Vaadin Ltd
*/
public class ColumnGroup implements Serializable {

/**
* List of property ids belonging to this group
*/
private List<Object> columns;

/**
* The grid the column group is associated with
*/
private final Grid grid;

/**
* The column group row the column group is attached to
*/
private final ColumnGroupRow row;

/**
* The common state between the server and the client
*/
private final ColumnGroupState state;

/**
* Constructs a new column group
*
* @param grid
* the grid the column group is associated with
* @param state
* the state representing the data of the grid. Sent to the
* client
* @param propertyIds
* the property ids of the columns that belongs to the group
* @param groups
* the sub groups who should be included in this group
*
*/
ColumnGroup(Grid grid, ColumnGroupRow row, ColumnGroupState state,
List<Object> propertyIds) {
if (propertyIds == null) {
throw new IllegalArgumentException(
"propertyIds cannot be null. Use empty list instead.");
}

this.state = state;
this.row = row;
columns = Collections.unmodifiableList(new ArrayList<Object>(
propertyIds));
this.grid = grid;
}

/**
* Sets the text displayed in the header of the column group.
*
* @param header
* the text displayed in the header of the column
*/
public void setHeaderCaption(String header) {
checkGroupIsAttached();
state.header = header;
grid.markAsDirty();
}

/**
* Sets the text displayed in the header of the column group.
*
* @return the text displayed in the header of the column
*/
public String getHeaderCaption() {
checkGroupIsAttached();
return state.header;
}

/**
* Sets the text displayed in the footer of the column group.
*
* @param footer
* the text displayed in the footer of the column
*/
public void setFooterCaption(String footer) {
checkGroupIsAttached();
state.footer = footer;
grid.markAsDirty();
}

/**
* The text displayed in the footer of the column group.
*
* @return the text displayed in the footer of the column
*/
public String getFooterCaption() {
checkGroupIsAttached();
return state.footer;
}

/**
* Is a property id in this group or in some sub group of this group.
*
* @param propertyId
* the property id to check for
* @return <code>true</code> if the property id is included in this group.
*/
public boolean isColumnInGroup(Object propertyId) {
if (columns.contains(propertyId)) {
return true;
}
return false;
}

/**
* Returns a list of property ids where all also the child groups property
* ids are included.
*
* @return a unmodifiable list with all the columns in the group. Includes
* any subgroup columns as well.
*/
public List<Object> getColumns() {
return columns;
}

/**
* Checks if column group is attached to a row and throws an
* {@link IllegalStateException} if it is not.
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
protected void checkGroupIsAttached() throws IllegalStateException {
if (!row.getState().groups.contains(state)) {
throw new IllegalStateException(
"Column Group has been removed from the row.");
}
}
}

+ 303
- 0
server/src/com/vaadin/ui/components/grid/ColumnGroupRow.java View File

@@ -0,0 +1,303 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.ui.components.grid;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import com.vaadin.server.KeyMapper;
import com.vaadin.shared.ui.grid.ColumnGroupRowState;
import com.vaadin.shared.ui.grid.ColumnGroupState;

/**
* A column group row represents an auxiliary header or footer row added to the
* grid. A column group row includes column groups that group columns together.
*
* @since 7.2
* @author Vaadin Ltd
*/
public class ColumnGroupRow implements Serializable {

/**
* The common state shared between the client and server
*/
private final ColumnGroupRowState state;

/**
* The column groups in this row
*/
private List<ColumnGroup> groups = new ArrayList<ColumnGroup>();

/**
* Grid that the group row belongs to
*/
private final Grid grid;

/**
* The column keys used to identify the column on the client side
*/
private final KeyMapper<Object> columnKeys;

/**
* Constructs a new column group
*
* @param grid
* The grid that the column group is associated to
* @param state
* The shared state which contains the data shared between server
* and client
* @param columnKeys
* The column key mapper for converting property ids to client
* side column identifiers
*/
ColumnGroupRow(Grid grid, ColumnGroupRowState state,
KeyMapper<Object> columnKeys) {
this.grid = grid;
this.columnKeys = columnKeys;
this.state = state;
}

/**
* Gets the shared state for the column group row. Used internally to send
* the group row to the client.
*
* @return The current state of the row
*/
ColumnGroupRowState getState() {
return state;
}

/**
* Add a new group to the row by using property ids for the columns.
*
* @param propertyIds
* The property ids of the columns that should be included in the
* group. A column can only belong in group on a row at a time.
* @return a column group representing the collection of columns added to
* the group
*/
public ColumnGroup addGroup(Object... propertyIds)
throws IllegalArgumentException {
assert propertyIds != null : "propertyIds cannot be null.";

for (Object propertyId : propertyIds) {
if (hasColumnBeenGrouped(propertyId)) {
throw new IllegalArgumentException("Column "
+ String.valueOf(propertyId)
+ " already belongs to another group.");
}
}

validateNewGroupProperties(Arrays.asList(propertyIds));

ColumnGroupState state = new ColumnGroupState();
for (Object propertyId : propertyIds) {
assert propertyId != null : "null items in columns array not supported.";
state.columns.add(columnKeys.key(propertyId));
}
this.state.groups.add(state);

ColumnGroup group = new ColumnGroup(grid, this, state,
Arrays.asList(propertyIds));
groups.add(group);

grid.markAsDirty();
return group;
}

private void validateNewGroupProperties(List<Object> propertyIds)
throws IllegalArgumentException {

/*
* Validate parent grouping
*/
int rowIndex = grid.getColumnGroupRows().indexOf(this);
int parentRowIndex = rowIndex - 1;

// Get the parent row of this row.
ColumnGroupRow parentRow = null;
if (parentRowIndex > -1) {
parentRow = grid.getColumnGroupRows().get(parentRowIndex);
}

if (parentRow == null) {
// A parentless row is always valid and is usually the first row
// added to the grid
return;
}

for (Object id : propertyIds) {
if (parentRow.hasColumnBeenGrouped(id)) {
/*
* If a property has been grouped in the parent row then all of
* the properties in the parent group also needs to be included
* in the child group for the groups to be valid
*/
ColumnGroup parentGroup = parentRow.getGroupForProperty(id);
if (!propertyIds.containsAll(parentGroup.getColumns())) {
throw new IllegalArgumentException(
"Grouped properties overlaps previous grouping bounderies");
}
}
}
}

/**
* Add a new group to the row by using column instances.
*
* @param columns
* the columns that should belong to the group
* @return a column group representing the collection of columns added to
* the group
*/
public ColumnGroup addGroup(GridColumn... columns)
throws IllegalArgumentException {
assert columns != null : "columns cannot be null";

List<Object> propertyIds = new ArrayList<Object>();
for (GridColumn column : columns) {
assert column != null : "null items in columns array not supported.";

String columnId = column.getState().id;
Object propertyId = grid.getPropertyIdByColumnId(columnId);
propertyIds.add(propertyId);
}
return addGroup(propertyIds.toArray());
}

/**
* Add a new group to the row by using other already greated groups
*
* @param groups
* the subgroups of the group
* @return a column group representing the collection of columns added to
* the group
*
*/
public ColumnGroup addGroup(ColumnGroup... groups)
throws IllegalArgumentException {
assert groups != null : "groups cannot be null";

// Gather all groups columns into one list
List<Object> propertyIds = new ArrayList<Object>();
for (ColumnGroup group : groups) {
propertyIds.addAll(group.getColumns());
}

validateNewGroupProperties(propertyIds);

ColumnGroupState state = new ColumnGroupState();
ColumnGroup group = new ColumnGroup(grid, this, state, propertyIds);
this.groups.add(group);

// Update state
for (Object propertyId : group.getColumns()) {
state.columns.add(columnKeys.key(propertyId));
}
this.state.groups.add(state);

grid.markAsDirty();
return group;
}

/**
* Removes a group from the row. Does not remove the group from subgroups,
* to remove it from the subgroup invoke removeGroup on the subgroup.
*
* @param group
* the group to remove
*/
public void removeGroup(ColumnGroup group) {
int index = groups.indexOf(group);
groups.remove(index);
state.groups.remove(index);
grid.markAsDirty();
}

/**
* Get the groups in the row.
*
* @return unmodifiable list of groups in this row
*/
public List<ColumnGroup> getGroups() {
return Collections.unmodifiableList(groups);
}

/**
* Checks if a property id has been added to a group in this row.
*
* @param propertyId
* the property id to check for
* @return <code>true</code> if the column is included in a group
*/
private boolean hasColumnBeenGrouped(Object propertyId) {
return getGroupForProperty(propertyId) != null;
}

private ColumnGroup getGroupForProperty(Object propertyId) {
for (ColumnGroup group : groups) {
if (group.isColumnInGroup(propertyId)) {
return group;
}
}
return null;
}

/**
* Is the header visible for the row.
*
* @return <code>true</code> if header is visible
*/
public boolean isHeaderVisible() {
return state.headerVisible;
}

/**
* Sets the header visible for the row.
*
* @param visible
* should the header be shown
*/
public void setHeaderVisible(boolean visible) {
state.headerVisible = visible;
grid.markAsDirty();
}

/**
* Is the footer visible for the row.
*
* @return <code>true</code> if footer is visible
*/
public boolean isFooterVisible() {
return state.footerVisible;
}

/**
* Sets the footer visible for the row.
*
* @param visible
* should the footer be shown
*/
public void setFooterVisible(boolean visible) {
state.footerVisible = visible;
grid.markAsDirty();
}

}

+ 861
- 0
server/src/com/vaadin/ui/components/grid/Grid.java View File

@@ -0,0 +1,861 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.ui.components.grid;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import com.vaadin.data.Container;
import com.vaadin.data.Container.Indexed.ItemAddEvent;
import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
import com.vaadin.data.Container.ItemSetChangeEvent;
import com.vaadin.data.Container.ItemSetChangeListener;
import com.vaadin.data.Container.ItemSetChangeNotifier;
import com.vaadin.data.Container.PropertySetChangeEvent;
import com.vaadin.data.Container.PropertySetChangeListener;
import com.vaadin.data.Container.PropertySetChangeNotifier;
import com.vaadin.data.Item;
import com.vaadin.data.Property;
import com.vaadin.data.Property.ValueChangeEvent;
import com.vaadin.data.Property.ValueChangeListener;
import com.vaadin.data.Property.ValueChangeNotifier;
import com.vaadin.data.RpcDataProviderExtension;
import com.vaadin.server.KeyMapper;
import com.vaadin.shared.ui.grid.ColumnGroupRowState;
import com.vaadin.shared.ui.grid.GridClientRpc;
import com.vaadin.shared.ui.grid.GridColumnState;
import com.vaadin.shared.ui.grid.GridServerRpc;
import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.shared.ui.grid.Range;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.Component;

/**
* Data grid component
*
* <h3>Lazy loading</h3> TODO To be revised when the data data source
* implementation has been don.
*
* <h3>Columns</h3> The grid columns are based on the property ids of the
* underlying data source. Each property id represents one column in the grid.
* To retrive a column in the grid you can use {@link Grid#getColumn(Object)}
* with the property id of the column. A grid column contains properties like
* the width, the footer and header captions of the column.
*
* <h3>Auxiliary headers and footers</h3> TODO To be revised when column
* grouping is implemented.
*
* @since 7.2
* @author Vaadin Ltd
*/
public class Grid extends AbstractComponent {

/**
* A helper class that handles the client-side Escalator logic relating to
* making sure that whatever is currently visible to the user, is properly
* initialized and otherwise handled on the server side (as far as
* requried).
* <p>
* This bookeeping includes, but is not limited to:
* <ul>
* <li>listening to the currently visible {@link Property Properties'} value
* changes on the server side and sending those back to the client; and
* <li>attaching and detaching {@link Component Components} from the Vaadin
* Component hierarchy.
* </ul>
*/
private final class ActiveRowHandler {
/**
* A map from itemId to the value change listener used for all of its
* properties
*/
private final Map<Object, GridValueChangeListener> valueChangeListeners = new HashMap<Object, GridValueChangeListener>();

/**
* The currently active range. Practically, it's the range of row
* indices being displayed currently.
*/
private Range activeRange = Range.withLength(0, 0);

/**
* A hook for making sure that appropriate data is "active". All other
* rows should be "inactive".
* <p>
* "Active" can mean different things in different contexts. For
* example, only the Properties in the active range need
* ValueChangeListeners. Also, whenever a row with a Component becomes
* active, it needs to be attached (and conversely, when inactive, it
* needs to be detached).
*
* @param firstActiveRow
* the first active row
* @param activeRowCount
* the number of active rows
*/
public void setActiveRows(int firstActiveRow, int activeRowCount) {

final Range newActiveRange = Range.withLength(firstActiveRow,
activeRowCount);

// TODO [[Components]] attach and detach components

/*-
* Example
*
* New Range: [3, 4, 5, 6, 7]
* Old Range: [1, 2, 3, 4, 5]
* Result: [1, 2][3, 4, 5] []
*/
final Range[] depractionPartition = activeRange
.partitionWith(newActiveRange);
removeValueChangeListeners(depractionPartition[0]);
removeValueChangeListeners(depractionPartition[2]);

/*-
* Example
*
* Old Range: [1, 2, 3, 4, 5]
* New Range: [3, 4, 5, 6, 7]
* Result: [] [3, 4, 5][6, 7]
*/
final Range[] activationPartition = newActiveRange
.partitionWith(activeRange);
addValueChangeListeners(activationPartition[0]);
addValueChangeListeners(activationPartition[2]);

activeRange = newActiveRange;
}

private void addValueChangeListeners(Range range) {
for (int i = range.getStart(); i < range.getEnd(); i++) {

final Object itemId = datasource.getIdByIndex(i);
final Item item = datasource.getItem(itemId);

if (valueChangeListeners.containsKey(itemId)) {
/*
* This might occur when items are removed from above the
* viewport, the escalator scrolls up to compensate, but the
* same items remain in the view: It looks as if one row was
* scrolled, when in fact the whole viewport was shifted up.
*/
continue;
}

GridValueChangeListener listener = new GridValueChangeListener(
itemId);
valueChangeListeners.put(itemId, listener);

for (final Object propertyId : item.getItemPropertyIds()) {
final Property<?> property = item
.getItemProperty(propertyId);
if (property instanceof ValueChangeNotifier) {
((ValueChangeNotifier) property)
.addValueChangeListener(listener);
}
}
}
}

private void removeValueChangeListeners(Range range) {
for (int i = range.getStart(); i < range.getEnd(); i++) {
final Object itemId = datasource.getIdByIndex(i);
final Item item = datasource.getItem(itemId);
final GridValueChangeListener listener = valueChangeListeners
.remove(itemId);

if (listener != null) {
for (final Object propertyId : item.getItemPropertyIds()) {
final Property<?> property = item
.getItemProperty(propertyId);

/*
* Because listener != null, we can be certain that this
* property is a ValueChangeNotifier: It wouldn't be
* inserted in addValueChangeListeners if the property
* wasn't a suitable type. I.e. No need for "instanceof"
* check.
*/
((ValueChangeNotifier) property)
.removeValueChangeListener(listener);
}
}
}
}

public void clear() {
removeValueChangeListeners(activeRange);
/*
* we're doing an assert for emptiness there (instead of a
* carte-blanche ".clear()"), to be absolutely sure that everything
* is cleaned up properly, and that we have no dangling listeners.
*/
assert valueChangeListeners.isEmpty() : "GridValueChangeListeners are leaking";

activeRange = Range.withLength(0, 0);
}

/**
* Manages removed properties in active rows.
*
* @param removedPropertyIds
* the property ids that have been removed from the container
*/
public void propertiesRemoved(Collection<Object> removedPropertyIds) {
/*
* no-op, for now.
*
* The Container should be responsible for cleaning out any
* ValueChangeListeners from removed Properties. Components will
* benefit from this, however.
*/
}

/**
* Manages added properties in active rows.
*
* @param addedPropertyIds
* the property ids that have been added to the container
*/
public void propertiesAdded(Collection<Object> addedPropertyIds) {
for (int i = activeRange.getStart(); i < activeRange.getEnd(); i++) {
final Object itemId = datasource.getIdByIndex(i);
final Item item = datasource.getItem(itemId);
final GridValueChangeListener listener = valueChangeListeners
.get(itemId);
assert (listener != null) : "a listener should've been pre-made by addValueChangeListeners";

for (final Object propertyId : addedPropertyIds) {
final Property<?> property = item
.getItemProperty(propertyId);
if (property instanceof ValueChangeNotifier) {
((ValueChangeNotifier) property)
.addValueChangeListener(listener);
}
}
}
}

/**
* Handles the insertion of rows.
* <p>
* This method's responsibilities are to:
* <ul>
* <li>shift the internal bookkeeping by <code>count</code> if the
* insertion happens above currently active range
* <li>ignore rows inserted below the currently active range
* <li>shift (and deactivate) rows pushed out of view
* <li>activate rows that are inserted in the current viewport
* </ul>
*
* @param firstIndex
* the index of the first inserted rows
* @param count
* the number of rows inserted at <code>firstIndex</code>
*/
public void insertRows(int firstIndex, int count) {
if (firstIndex < activeRange.getStart()) {
activeRange = activeRange.offsetBy(count);
} else if (firstIndex < activeRange.getEnd()) {
final Range deprecatedRange = Range.withLength(
activeRange.getEnd(), count);
removeValueChangeListeners(deprecatedRange);

final Range freshRange = Range.between(firstIndex, count);
addValueChangeListeners(freshRange);
} else {
// out of view, noop
}
}

/**
* Removes a single item by its id.
*
* @param itemId
* the id of the removed id. <em>Note:</em> this item does
* not exist anymore in the datasource
*/
public void removeItemId(Object itemId) {
final GridValueChangeListener removedListener = valueChangeListeners
.remove(itemId);
if (removedListener != null) {
/*
* We removed an item from somewhere in the visible range, so we
* make the active range shorter. The empty hole will be filled
* by the client-side code when it asks for more information.
*/
activeRange = Range.withLength(activeRange.getStart(),
activeRange.length() - 1);
}
}
}

/**
* A class to listen to changes in property values in the Container added
* with {@link Grid#setContainerDatasource(Container.Indexed)}, and notifies
* the data source to update the client-side representation of the modified
* item.
* <p>
* One instance of this class can (and should) be reused for all the
* properties in an item, since this class will inform that the entire row
* needs to be re-evaluated (in contrast to a property-based change
* management)
* <p>
* Since there's no Container-wide possibility to listen to any kind of
* value changes, an instance of this class needs to be attached to each and
* every Item's Property in the container.
*
* @see Grid#addValueChangeListener(Container, Object, Object)
* @see Grid#valueChangeListeners
*/
private class GridValueChangeListener implements ValueChangeListener {
private final Object itemId;

public GridValueChangeListener(Object itemId) {
/*
* Using an assert instead of an exception throw, just to optimize
* prematurely
*/
assert itemId != null : "null itemId not accepted";
this.itemId = itemId;
}

@Override
public void valueChange(ValueChangeEvent event) {
datasourceExtension.updateRowData(datasource.indexOfId(itemId));
}
}

/**
* The data source attached to the grid
*/
private Container.Indexed datasource;

/**
* Property id to column instance mapping
*/
private final Map<Object, GridColumn> columns = new HashMap<Object, GridColumn>();

/**
* Key generator for column server-to-client communication
*/
private final KeyMapper<Object> columnKeys = new KeyMapper<Object>();

/**
* The column groups added to the grid
*/
private final List<ColumnGroupRow> columnGroupRows = new ArrayList<ColumnGroupRow>();

/**
* Property listener for listening to changes in data source properties.
*/
private final PropertySetChangeListener propertyListener = new PropertySetChangeListener() {

@Override
public void containerPropertySetChange(PropertySetChangeEvent event) {
Collection<?> properties = new HashSet<Object>(event.getContainer()
.getContainerPropertyIds());

// Cleanup columns that are no longer in grid
List<Object> removedColumns = new LinkedList<Object>();
for (Object columnId : columns.keySet()) {
if (!properties.contains(columnId)) {
removedColumns.add(columnId);
}
}
for (Object columnId : removedColumns) {
GridColumn column = columns.remove(columnId);
columnKeys.remove(columnId);
getState().columns.remove(column.getState());
}
activeRowHandler.propertiesRemoved(removedColumns);

// Add new columns
HashSet<Object> addedPropertyIds = new HashSet<Object>();
for (Object propertyId : properties) {
if (!columns.containsKey(propertyId)) {
appendColumn(propertyId);
addedPropertyIds.add(propertyId);
}
}
activeRowHandler.propertiesAdded(addedPropertyIds);

Object frozenPropertyId = columnKeys
.get(getState(false).lastFrozenColumnId);
if (!columns.containsKey(frozenPropertyId)) {
setLastFrozenPropertyId(null);
}
}
};

private ItemSetChangeListener itemListener = new ItemSetChangeListener() {
@Override
public void containerItemSetChange(ItemSetChangeEvent event) {

if (event instanceof ItemAddEvent) {
ItemAddEvent addEvent = (ItemAddEvent) event;
int firstIndex = addEvent.getFirstIndex();
int count = addEvent.getAddedItemsCount();
datasourceExtension.insertRowData(firstIndex, count);
activeRowHandler.insertRows(firstIndex, count);
}

else if (event instanceof ItemRemoveEvent) {
ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
int firstIndex = removeEvent.getFirstIndex();
int count = removeEvent.getRemovedItemsCount();
datasourceExtension.removeRowData(firstIndex, count);

/*
* Unfortunately, there's no sane way of getting the rest of the
* removed itemIds.
*
* Fortunately, the only time _currently_ an event with more
* than one removed item seems to be when calling
* AbstractInMemoryContainer.removeAllElements(). Otherwise,
* it's only removing one item at a time.
*
* We _could_ have a backup of all the itemIds, and compare to
* that one, but we really really don't want to go there.
*/
activeRowHandler.removeItemId(removeEvent.getFirstItemId());
}

else {
// TODO no diff info available, redraw everything
throw new UnsupportedOperationException("bare "
+ "ItemSetChangeEvents are currently "
+ "not supported, use a container that "
+ "uses AddItemEvents and RemoveItemEvents.");
}
}
};

private RpcDataProviderExtension datasourceExtension;

private final ActiveRowHandler activeRowHandler = new ActiveRowHandler();

/**
* Creates a new Grid using the given datasource.
*
* @param datasource
* the data source for the grid
*/
public Grid(Container.Indexed datasource) {
setContainerDatasource(datasource);

registerRpc(new GridServerRpc() {
@Override
public void setVisibleRows(int firstVisibleRow, int visibleRowCount) {
activeRowHandler
.setActiveRows(firstVisibleRow, visibleRowCount);
}
});
}

/**
* Sets the grid data source.
*
* @param container
* The container data source. Cannot be null.
* @throws IllegalArgumentException
* if the data source is null
*/
public void setContainerDatasource(Container.Indexed container) {
if (container == null) {
throw new IllegalArgumentException(
"Cannot set the datasource to null");
}
if (datasource == container) {
return;
}

// Remove old listeners
if (datasource instanceof PropertySetChangeNotifier) {
((PropertySetChangeNotifier) datasource)
.removePropertySetChangeListener(propertyListener);
}
if (datasource instanceof ItemSetChangeNotifier) {
((ItemSetChangeNotifier) datasource)
.removeItemSetChangeListener(itemListener);
}
activeRowHandler.clear();

if (datasourceExtension != null) {
removeExtension(datasourceExtension);
}

datasource = container;
datasourceExtension = new RpcDataProviderExtension(container);
datasourceExtension.extend(this);

// Listen to changes in properties and remove columns if needed
if (datasource instanceof PropertySetChangeNotifier) {
((PropertySetChangeNotifier) datasource)
.addPropertySetChangeListener(propertyListener);
}
if (datasource instanceof ItemSetChangeNotifier) {
((ItemSetChangeNotifier) datasource)
.addItemSetChangeListener(itemListener);
}
/*
* activeRowHandler will be updated by the client-side request that
* occurs on container change - no need to actively re-insert any
* ValueChangeListeners at this point.
*/

getState().columns.clear();
setLastFrozenPropertyId(null);

// Add columns
for (Object propertyId : datasource.getContainerPropertyIds()) {
if (!columns.containsKey(propertyId)) {
GridColumn column = appendColumn(propertyId);

// Add by default property id as column header
column.setHeaderCaption(String.valueOf(propertyId));
}
}

}

/**
* Returns the grid data source.
*
* @return the container data source of the grid
*/
public Container.Indexed getContainerDatasource() {
return datasource;
}

/**
* Returns a column based on the property id
*
* @param propertyId
* the property id of the column
* @return the column or <code>null</code> if not found
*/
public GridColumn getColumn(Object propertyId) {
return columns.get(propertyId);
}

/**
* Sets the header rows visible.
*
* @param visible
* <code>true</code> if the header rows should be visible
*/
public void setColumnHeadersVisible(boolean visible) {
getState().columnHeadersVisible = visible;
}

/**
* Are the header rows visible?
*
* @return <code>true</code> if the headers of the columns are visible
*/
public boolean isColumnHeadersVisible() {
return getState(false).columnHeadersVisible;
}

/**
* Sets the footer rows visible.
*
* @param visible
* <code>true</code> if the footer rows should be visible
*/
public void setColumnFootersVisible(boolean visible) {
getState().columnFootersVisible = visible;
}

/**
* Are the footer rows visible.
*
* @return <code>true</code> if the footer rows should be visible
*/
public boolean isColumnFootersVisible() {
return getState(false).columnFootersVisible;
}

/**
* <p>
* Adds a new column group to the grid.
*
* <p>
* Column group rows are rendered in the header and footer of the grid.
* Column group rows are made up of column groups which groups together
* columns for adding a common auxiliary header or footer for the columns.
* </p>
* </p>
*
* <p>
* Example usage:
*
* <pre>
* // Add a new column group row to the grid
* ColumnGroupRow row = grid.addColumnGroupRow();
*
* // Group &quot;Column1&quot; and &quot;Column2&quot; together to form a header in the row
* ColumnGroup column12 = row.addGroup(&quot;Column1&quot;, &quot;Column2&quot;);
*
* // Set a common header for &quot;Column1&quot; and &quot;Column2&quot;
* column12.setHeader(&quot;Column 1&amp;2&quot;);
* </pre>
*
* </p>
*
* @return a column group instance you can use to add column groups
*/
public ColumnGroupRow addColumnGroupRow() {
ColumnGroupRowState state = new ColumnGroupRowState();
ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys);
columnGroupRows.add(row);
getState().columnGroupRows.add(state);
return row;
}

/**
* Adds a new column group to the grid at a specific index
*
* @param rowIndex
* the index of the row
* @return a column group instance you can use to add column groups
*/
public ColumnGroupRow addColumnGroupRow(int rowIndex) {
ColumnGroupRowState state = new ColumnGroupRowState();
ColumnGroupRow row = new ColumnGroupRow(this, state, columnKeys);
columnGroupRows.add(rowIndex, row);
getState().columnGroupRows.add(rowIndex, state);
return row;
}

/**
* Removes a column group.
*
* @param row
* the row to remove
*/
public void removeColumnGroupRow(ColumnGroupRow row) {
columnGroupRows.remove(row);
getState().columnGroupRows.remove(row.getState());
}

/**
* Gets the column group rows.
*
* @return an unmodifiable list of column group rows
*/
public List<ColumnGroupRow> getColumnGroupRows() {
return Collections.unmodifiableList(new ArrayList<ColumnGroupRow>(
columnGroupRows));
}

/**
* Used internally by the {@link Grid} to get a {@link GridColumn} by
* referencing its generated state id. Also used by {@link GridColumn} to
* verify if it has been detached from the {@link Grid}.
*
* @param columnId
* the client id generated for the column when the column is
* added to the grid
* @return the column with the id or <code>null</code> if not found
*/
GridColumn getColumnByColumnId(String columnId) {
Object propertyId = getPropertyIdByColumnId(columnId);
return getColumn(propertyId);
}

/**
* Used internally by the {@link Grid} to get a property id by referencing
* the columns generated state id.
*
* @param columnId
* The state id of the column
* @return The column instance or null if not found
*/
Object getPropertyIdByColumnId(String columnId) {
return columnKeys.get(columnId);
}

@Override
protected GridState getState() {
return (GridState) super.getState();
}

@Override
protected GridState getState(boolean markAsDirty) {
return (GridState) super.getState(markAsDirty);
}

/**
* Creates a new column based on a property id and appends it as the last
* column.
*
* @param datasourcePropertyId
* The property id of a property in the datasource
*/
private GridColumn appendColumn(Object datasourcePropertyId) {
if (datasourcePropertyId == null) {
throw new IllegalArgumentException("Property id cannot be null");
}
assert datasource.getContainerPropertyIds().contains(
datasourcePropertyId) : "Datasource should contain the property id";

GridColumnState columnState = new GridColumnState();
columnState.id = columnKeys.key(datasourcePropertyId);
getState().columns.add(columnState);

GridColumn column = new GridColumn(this, columnState);
columns.put(datasourcePropertyId, column);

return column;
}

/**
* Sets (or unsets) the rightmost frozen column in the grid.
* <p>
* All columns up to and including the given column will be frozen in place
* when the grid is scrolled sideways.
*
* @param lastFrozenColumn
* the rightmost column to freeze, or <code>null</code> to not
* have any columns frozen
* @throws IllegalArgumentException
* if {@code lastFrozenColumn} is not a column from this grid
*/
void setLastFrozenColumn(GridColumn lastFrozenColumn) {
/*
* TODO: If and when Grid supports column reordering or insertion of
* columns before other columns, make sure to mention that adding
* columns before lastFrozenColumn will change the frozen column count
*/

if (lastFrozenColumn == null) {
getState().lastFrozenColumnId = null;
} else if (columns.containsValue(lastFrozenColumn)) {
getState().lastFrozenColumnId = lastFrozenColumn.getState().id;
} else {
throw new IllegalArgumentException(
"The given column isn't attached to this grid");
}
}

/**
* Sets (or unsets) the rightmost frozen column in the grid.
* <p>
* All columns up to and including the indicated property will be frozen in
* place when the grid is scrolled sideways.
* <p>
* <em>Note:</em> If the container used by this grid supports a propertyId
* <code>null</code>, it can never be defined as the last frozen column, as
* a <code>null</code> parameter will always reset the frozen columns in
* Grid.
*
* @param propertyId
* the property id corresponding to the column that should be the
* last frozen column, or <code>null</code> to not have any
* columns frozen.
* @throws IllegalArgumentException
* if {@code lastFrozenColumn} is not a column from this grid
*/
public void setLastFrozenPropertyId(Object propertyId) {
final GridColumn column;
if (propertyId == null) {
column = null;
} else {
column = getColumn(propertyId);
if (column == null) {
throw new IllegalArgumentException(
"property id does not exist.");
}
}
setLastFrozenColumn(column);
}

/**
* Gets the rightmost frozen column in the grid.
* <p>
* <em>Note:</em> Most often, this method returns the very value set with
* {@link #setLastFrozenPropertyId(Object)}. This value, however, can be
* reset to <code>null</code> if the column is detached from this grid.
*
* @return the rightmost frozen column in the grid, or <code>null</code> if
* no columns are frozen.
*/
public Object getLastFrozenPropertyId() {
return columnKeys.get(getState().lastFrozenColumnId);
}

/**
* Scrolls to a certain item, using {@link ScrollDestination#ANY}.
*
* @param itemId
* id of item to scroll to.
* @throws IllegalArgumentException
* if the provided id is not recognized by the data source.
*/
public void scrollToItem(Object itemId) throws IllegalArgumentException {
scrollToItem(itemId, ScrollDestination.ANY);
}

/**
* Scrolls to a certain item, using user-specified scroll destination.
*
* @param itemId
* id of item to scroll to.
* @param destination
* value specifying desired position of scrolled-to row.
* @throws IllegalArgumentException
* if the provided id is not recognized by the data source.
*/
public void scrollToItem(Object itemId, ScrollDestination destination)
throws IllegalArgumentException {

int row = datasource.indexOfId(itemId);

if (row == -1) {
throw new IllegalArgumentException(
"Item with specified ID does not exist in data source");
}

GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
clientRPC.scrollToRow(row, destination);
}

/**
* Scrolls to the beginning of the first data row.
*/
public void scrollToStart() {
GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
clientRPC.scrollToStart();
}

/**
* Scrolls to the end of the last data row.
*/
public void scrollToEnd() {
GridClientRpc clientRPC = getRpcProxy(GridClientRpc.class);
clientRPC.scrollToEnd();
}
}

+ 216
- 0
server/src/com/vaadin/ui/components/grid/GridColumn.java View File

@@ -0,0 +1,216 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.ui.components.grid;

import java.io.Serializable;

import com.vaadin.shared.ui.grid.GridColumnState;

/**
* A column in the grid. Can be obtained by calling
* {@link Grid#getColumn(Object propertyId)}.
*
* @since 7.2
* @author Vaadin Ltd
*/
public class GridColumn implements Serializable {

/**
* The state of the column shared to the client
*/
private final GridColumnState state;

/**
* The grid this column is associated with
*/
private final Grid grid;

/**
* Internally used constructor.
*
* @param grid
* The grid this column belongs to. Should not be null.
* @param state
* the shared state of this column
*/
GridColumn(Grid grid, GridColumnState state) {
this.grid = grid;
this.state = state;
}

/**
* Returns the serializable state of this column that is sent to the client
* side connector.
*
* @return the internal state of the column
*/
GridColumnState getState() {
return state;
}

/**
* Returns the caption of the header. By default the header caption is the
* property id of the column.
*
* @return the text in the header
*
* @throws IllegalStateException
* if the column no longer is attached to the grid
*/
public String getHeaderCaption() throws IllegalStateException {
checkColumnIsAttached();
return state.header;
}

/**
* Sets the caption of the header.
*
* @param caption
* the text to show in the caption
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public void setHeaderCaption(String caption) throws IllegalStateException {
checkColumnIsAttached();
state.header = caption;
grid.markAsDirty();
}

/**
* Returns the caption of the footer. By default the captions are
* <code>null</code>.
*
* @return the text in the footer
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public String getFooterCaption() throws IllegalStateException {
checkColumnIsAttached();
return state.footer;
}

/**
* Sets the caption of the footer.
*
* @param caption
* the text to show in the caption
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public void setFooterCaption(String caption) throws IllegalStateException {
checkColumnIsAttached();
state.footer = caption;
grid.markAsDirty();
}

/**
* Returns the width (in pixels). By default a column is 100px wide.
*
* @return the width in pixels of the column
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public int getWidth() throws IllegalStateException {
checkColumnIsAttached();
return state.width;
}

/**
* Sets the width (in pixels).
*
* @param pixelWidth
* the new pixel width of the column
* @throws IllegalStateException
* if the column is no longer attached to any grid
* @throws IllegalArgumentException
* thrown if pixel width is less than zero
*/
public void setWidth(int pixelWidth) throws IllegalStateException,
IllegalArgumentException {
checkColumnIsAttached();
if (pixelWidth < 0) {
throw new IllegalArgumentException(
"Pixel width should be greated than 0");
}
state.width = pixelWidth;
grid.markAsDirty();
}

/**
* Marks the column width as undefined meaning that the grid is free to
* resize the column based on the cell contents and available space in the
* grid.
*/
public void setWidthUndefined() {
checkColumnIsAttached();
state.width = -1;
grid.markAsDirty();
}

/**
* Is this column visible in the grid. By default all columns are visible.
*
* @return <code>true</code> if the column is visible
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public boolean isVisible() throws IllegalStateException {
checkColumnIsAttached();
return state.visible;
}

/**
* Set the visibility of this column
*
* @param visible
* is the column visible
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
public void setVisible(boolean visible) throws IllegalStateException {
checkColumnIsAttached();
state.visible = visible;
grid.markAsDirty();
}

/**
* Checks if column is attached and throws an {@link IllegalStateException}
* if it is not
*
* @throws IllegalStateException
* if the column is no longer attached to any grid
*/
protected void checkColumnIsAttached() throws IllegalStateException {
if (grid.getColumnByColumnId(state.id) == null) {
throw new IllegalStateException("Column no longer exists.");
}
}

/**
* Sets this column as the last frozen column in its grid.
*
* @throws IllegalArgumentException
* if the column is no longer attached to any grid
* @see Grid#setLastFrozenColumn(GridColumn)
*/
public void setLastFrozenColumn() {
checkColumnIsAttached();
grid.setLastFrozenColumn(this);
}
}

+ 265
- 0
server/tests/src/com/vaadin/tests/server/component/grid/GridColumnGroups.java View File

@@ -0,0 +1,265 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.tests.server.component.grid;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;

import org.junit.Before;
import org.junit.Test;

import com.vaadin.data.util.IndexedContainer;
import com.vaadin.server.KeyMapper;
import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.ui.components.grid.ColumnGroup;
import com.vaadin.ui.components.grid.ColumnGroupRow;
import com.vaadin.ui.components.grid.Grid;

/**
*
* @since
* @author Vaadin Ltd
*/
public class GridColumnGroups {

private Grid grid;

private GridState state;

private Method getStateMethod;

private Field columnIdGeneratorField;

private KeyMapper<Object> columnIdMapper;

@Before
public void setup() throws Exception {
IndexedContainer ds = new IndexedContainer();
for (int c = 0; c < 10; c++) {
ds.addContainerProperty("column" + c, String.class, "");
}
grid = new Grid(ds);

getStateMethod = Grid.class.getDeclaredMethod("getState");
getStateMethod.setAccessible(true);

state = (GridState) getStateMethod.invoke(grid);

columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys");
columnIdGeneratorField.setAccessible(true);

columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid);
}

@Test
public void testColumnGroupRows() throws Exception {

// No column group rows by default
List<ColumnGroupRow> rows = grid.getColumnGroupRows();
assertEquals(0, rows.size());

// Add some rows
ColumnGroupRow row1 = grid.addColumnGroupRow();
ColumnGroupRow row3 = grid.addColumnGroupRow();
ColumnGroupRow row2 = grid.addColumnGroupRow(1);

rows = grid.getColumnGroupRows();
assertEquals(3, rows.size());
assertEquals(row1, rows.get(0));
assertEquals(row2, rows.get(1));
assertEquals(row3, rows.get(2));

// Header should be visible by default, footer should not
assertTrue(row1.isHeaderVisible());
assertFalse(row1.isFooterVisible());

row1.setHeaderVisible(false);
assertFalse(row1.isHeaderVisible());
row1.setHeaderVisible(true);
assertTrue(row1.isHeaderVisible());

row1.setFooterVisible(true);
assertTrue(row1.isFooterVisible());
row1.setFooterVisible(false);
assertFalse(row1.isFooterVisible());

row1.setHeaderVisible(true);
row1.setFooterVisible(true);
assertTrue(row1.isHeaderVisible());
assertTrue(row1.isFooterVisible());

row1.setHeaderVisible(false);
row1.setFooterVisible(false);
assertFalse(row1.isHeaderVisible());
assertFalse(row1.isFooterVisible());
}

@Test
public void testColumnGroupsInState() throws Exception {

// Add a new row
ColumnGroupRow row = grid.addColumnGroupRow();
assertTrue(state.columnGroupRows.size() == 1);

// Add a group by property id
ColumnGroup columns12 = row.addGroup("column1", "column2");
assertTrue(state.columnGroupRows.get(0).groups.size() == 1);

// Set header of column
columns12.setHeaderCaption("Column12");
assertEquals("Column12",
state.columnGroupRows.get(0).groups.get(0).header);

// Set footer of column
columns12.setFooterCaption("Footer12");
assertEquals("Footer12",
state.columnGroupRows.get(0).groups.get(0).footer);

// Add another group by column instance
ColumnGroup columns34 = row.addGroup(grid.getColumn("column3"),
grid.getColumn("column4"));
assertTrue(state.columnGroupRows.get(0).groups.size() == 2);

// add another group row
ColumnGroupRow row2 = grid.addColumnGroupRow();
assertTrue(state.columnGroupRows.size() == 2);

// add a group by combining the two previous groups
ColumnGroup columns1234 = row2.addGroup(columns12, columns34);
assertTrue(columns1234.getColumns().size() == 4);

// Insert a group as the second group
ColumnGroupRow newRow2 = grid.addColumnGroupRow(1);
assertTrue(state.columnGroupRows.size() == 3);
}

@Test
public void testAddingColumnGroups() throws Exception {

ColumnGroupRow row = grid.addColumnGroupRow();

// By property id
ColumnGroup columns01 = row.addGroup("column0", "column1");
assertEquals(2, columns01.getColumns().size());
assertEquals("column0", columns01.getColumns().get(0));
assertTrue(columns01.isColumnInGroup("column0"));
assertEquals("column1", columns01.getColumns().get(1));
assertTrue(columns01.isColumnInGroup("column1"));

// By grid column
ColumnGroup columns23 = row.addGroup(grid.getColumn("column2"),
grid.getColumn("column3"));
assertEquals(2, columns23.getColumns().size());
assertEquals("column2", columns23.getColumns().get(0));
assertTrue(columns23.isColumnInGroup("column2"));
assertEquals("column3", columns23.getColumns().get(1));
assertTrue(columns23.isColumnInGroup("column3"));

// Combine groups
ColumnGroupRow row2 = grid.addColumnGroupRow();
ColumnGroup columns0123 = row2.addGroup(columns01, columns23);
assertEquals(4, columns0123.getColumns().size());
assertEquals("column0", columns0123.getColumns().get(0));
assertTrue(columns0123.isColumnInGroup("column0"));
assertEquals("column1", columns0123.getColumns().get(1));
assertTrue(columns0123.isColumnInGroup("column1"));
assertEquals("column2", columns0123.getColumns().get(2));
assertTrue(columns0123.isColumnInGroup("column2"));
assertEquals("column3", columns0123.getColumns().get(3));
assertTrue(columns0123.isColumnInGroup("column3"));
}

@Test
public void testColumnGroupHeadersAndFooters() throws Exception {

ColumnGroupRow row = grid.addColumnGroupRow();
ColumnGroup group = row.addGroup("column1", "column2");

// Header
assertNull(group.getHeaderCaption());
group.setHeaderCaption("My header");
assertEquals("My header", group.getHeaderCaption());
group.setHeaderCaption(null);
assertNull(group.getHeaderCaption());

// Footer
assertNull(group.getFooterCaption());
group.setFooterCaption("My footer");
assertEquals("My footer", group.getFooterCaption());
group.setFooterCaption(null);
assertNull(group.getFooterCaption());
}

@Test
public void testColumnGroupDetachment() throws Exception {

ColumnGroupRow row = grid.addColumnGroupRow();
ColumnGroup group = row.addGroup("column1", "column2");

// Remove group
row.removeGroup(group);

try {
group.setHeaderCaption("Header");
fail("Should throw exception for setting header caption on detached group");
} catch (IllegalStateException ise) {

}

try {
group.setFooterCaption("Footer");
fail("Should throw exception for setting footer caption on detached group");
} catch (IllegalStateException ise) {

}
}

@Test
public void testColumnGroupLimits() throws Exception {

ColumnGroupRow row = grid.addColumnGroupRow();
row.addGroup("column1", "column2");
row.addGroup("column3", "column4");

try {
row.addGroup("column2", "column3");
fail("Adding a group with already grouped properties should throw exception");
} catch (IllegalArgumentException iae) {

}

ColumnGroupRow row2 = grid.addColumnGroupRow();

try {
row2.addGroup("column2", "column3");
fail("Adding a group that breaks previous grouping boundaries should throw exception");
} catch (IllegalArgumentException iae) {

}

// This however should not throw an exception as it spans completely
// over the parent rows groups
row2.addGroup("column1", "column2", "column3", "column4");

}
}

+ 228
- 0
server/tests/src/com/vaadin/tests/server/component/grid/GridColumns.java View File

@@ -0,0 +1,228 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.tests.server.component.grid;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.junit.Before;
import org.junit.Test;

import com.vaadin.data.util.IndexedContainer;
import com.vaadin.server.KeyMapper;
import com.vaadin.shared.ui.grid.GridColumnState;
import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.ui.components.grid.Grid;
import com.vaadin.ui.components.grid.GridColumn;

public class GridColumns {

private Grid grid;

private GridState state;

private Method getStateMethod;

private Field columnIdGeneratorField;

private KeyMapper<Object> columnIdMapper;

@Before
public void setup() throws Exception {
IndexedContainer ds = new IndexedContainer();
for (int c = 0; c < 10; c++) {
ds.addContainerProperty("column" + c, String.class, "");
}
grid = new Grid(ds);

getStateMethod = Grid.class.getDeclaredMethod("getState");
getStateMethod.setAccessible(true);

state = (GridState) getStateMethod.invoke(grid);

columnIdGeneratorField = Grid.class.getDeclaredField("columnKeys");
columnIdGeneratorField.setAccessible(true);

columnIdMapper = (KeyMapper<Object>) columnIdGeneratorField.get(grid);
}

@Test
public void testColumnGeneration() throws Exception {

for (Object propertyId : grid.getContainerDatasource()
.getContainerPropertyIds()) {

// All property ids should get a column
GridColumn column = grid.getColumn(propertyId);
assertNotNull(column);

// Property id should be the column header by default
assertEquals(propertyId.toString(), column.getHeaderCaption());
}
}

@Test
public void testModifyingColumnProperties() throws Exception {

// Modify first column
GridColumn column = grid.getColumn("column1");
assertNotNull(column);

column.setFooterCaption("CustomFooter");
assertEquals("CustomFooter", column.getFooterCaption());
assertEquals(column.getFooterCaption(),
getColumnState("column1").footer);

column.setHeaderCaption("CustomHeader");
assertEquals("CustomHeader", column.getHeaderCaption());
assertEquals(column.getHeaderCaption(),
getColumnState("column1").header);

column.setVisible(false);
assertFalse(column.isVisible());
assertFalse(getColumnState("column1").visible);

column.setVisible(true);
assertTrue(column.isVisible());
assertTrue(getColumnState("column1").visible);

column.setWidth(100);
assertEquals(100, column.getWidth());
assertEquals(column.getWidth(), getColumnState("column1").width);

try {
column.setWidth(-1);
fail("Setting width to -1 should throw exception");
} catch (IllegalArgumentException iae) {

}

assertEquals(100, column.getWidth());
assertEquals(100, getColumnState("column1").width);
}

@Test
public void testRemovingColumn() throws Exception {

GridColumn column = grid.getColumn("column1");
assertNotNull(column);

// Remove column
grid.getContainerDatasource().removeContainerProperty("column1");

try {
column.setHeaderCaption("asd");

fail("Succeeded in modifying a detached column");
} catch (IllegalStateException ise) {
// Detached state should throw exception
}

try {
column.setFooterCaption("asd");
fail("Succeeded in modifying a detached column");
} catch (IllegalStateException ise) {
// Detached state should throw exception
}

try {
column.setVisible(false);
fail("Succeeded in modifying a detached column");
} catch (IllegalStateException ise) {
// Detached state should throw exception
}

try {
column.setWidth(123);
fail("Succeeded in modifying a detached column");
} catch (IllegalStateException ise) {
// Detached state should throw exception
}

assertNull(grid.getColumn("column1"));
assertNull(getColumnState("column1"));
}

@Test
public void testAddingColumn() throws Exception {
grid.getContainerDatasource().addContainerProperty("columnX",
String.class, "");
GridColumn column = grid.getColumn("columnX");
assertNotNull(column);
}

@Test
public void testHeaderVisiblility() throws Exception {

assertTrue(grid.isColumnHeadersVisible());
assertTrue(state.columnHeadersVisible);

grid.setColumnHeadersVisible(false);
assertFalse(grid.isColumnHeadersVisible());
assertFalse(state.columnHeadersVisible);

grid.setColumnHeadersVisible(true);
assertTrue(grid.isColumnHeadersVisible());
assertTrue(state.columnHeadersVisible);
}

@Test
public void testFooterVisibility() throws Exception {

assertFalse(grid.isColumnFootersVisible());
assertFalse(state.columnFootersVisible);

grid.setColumnFootersVisible(false);
assertFalse(grid.isColumnFootersVisible());
assertFalse(state.columnFootersVisible);

grid.setColumnFootersVisible(true);
assertTrue(grid.isColumnFootersVisible());
assertTrue(state.columnFootersVisible);
}

@Test
public void testFrozenColumnByPropertyId() {
assertNull("Grid should not start with a frozen column",
grid.getLastFrozenPropertyId());

Object propertyId = grid.getContainerDatasource()
.getContainerPropertyIds().iterator().next();
grid.setLastFrozenPropertyId(propertyId);
assertEquals(propertyId, grid.getLastFrozenPropertyId());

grid.getContainerDatasource().removeContainerProperty(propertyId);
assertNull(grid.getLastFrozenPropertyId());
}

private GridColumnState getColumnState(Object propertyId) {
String columnId = columnIdMapper.key(propertyId);
for (GridColumnState columnState : state.columns) {
if (columnState.id.equals(columnId)) {
return columnState;
}
}
return null;
}

}

+ 61
- 0
shared/src/com/vaadin/shared/data/DataProviderRpc.java View File

@@ -0,0 +1,61 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.shared.data;

import java.util.List;

import com.vaadin.shared.communication.ClientRpc;

/**
* RPC interface used for pushing container data to the client.
*
* @since 7.2
* @author Vaadin Ltd
*/
public interface DataProviderRpc extends ClientRpc {

/**
* Sends updated row data to a client.
*
* @param firstRowIndex
* the index of the first updated row
* @param rowData
* the updated row data
*/
public void setRowData(int firstRowIndex, List<String[]> rowData);

/**
* Informs the client to remove row data.
*
* @param firstRowIndex
* the index of the first removed row
* @param count
* the number of rows removed from <code>firstRowIndex</code> and
* onwards
*/
public void removeRowData(int firstRowIndex, int count);

/**
* Informs the client to insert new row data.
*
* @param firstRowIndex
* the index of the first new row
* @param count
* the number of rows inserted at <code>firstRowIndex</code>
*/
public void insertRowData(int firstRowIndex, int count);
}

+ 32
- 0
shared/src/com/vaadin/shared/data/DataProviderState.java View File

@@ -0,0 +1,32 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.shared.data;

import com.vaadin.shared.communication.SharedState;

/**
* Shared state used by client-side data sources.
*
* @since 7.2
* @author Vaadin Ltd
*/
public class DataProviderState extends SharedState {
/**
* The size of the container.
*/
public int containerSize;
}

+ 38
- 0
shared/src/com/vaadin/shared/data/DataRequestRpc.java View File

@@ -0,0 +1,38 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.shared.data;

import com.vaadin.shared.communication.ServerRpc;

/**
* RPC interface used for requesting container data to the client.
*
* @since 7.2
* @author Vaadin Ltd
*/
public interface DataRequestRpc extends ServerRpc {

/**
* Request rows from the server.
*
* @param firstRowIndex
* the index of the first requested row
* @param numberOfRows
* the number of requested rows
*/
public void requestRows(int firstRowIndex, int numberOfRows);
}

+ 46
- 0
shared/src/com/vaadin/shared/ui/grid/ColumnGroupRowState.java View File

@@ -0,0 +1,46 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.shared.ui.grid;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
* The column group row data shared between the server and client
*
* @since 7.2
* @author Vaadin Ltd
*/
public class ColumnGroupRowState implements Serializable {

/**
* The groups that has been added to the row
*/
public List<ColumnGroupState> groups = new ArrayList<ColumnGroupState>();

/**
* Is the header shown
*/
public boolean headerVisible = true;

/**
* Is the footer shown
*/
public boolean footerVisible = false;

}

+ 45
- 0
shared/src/com/vaadin/shared/ui/grid/ColumnGroupState.java View File

@@ -0,0 +1,45 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.shared.ui.grid;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
* The column group data shared between the server and the client
*
* @since 7.2
* @author Vaadin Ltd
*/
public class ColumnGroupState implements Serializable {

/**
* The columns that is included in the group
*/
public List<String> columns = new ArrayList<String>();

/**
* The header text of the group
*/
public String header;

/**
* The footer text of the group
*/
public String footer;
}

+ 53
- 0
shared/src/com/vaadin/shared/ui/grid/GridClientRpc.java View File

@@ -0,0 +1,53 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.shared.ui.grid;

import com.vaadin.shared.communication.ClientRpc;

/**
* Server-to-client RPC interface for the Grid component.
*
* @since 7.2
* @author Vaadin Ltd
*/
public interface GridClientRpc extends ClientRpc {

/**
* Command client Grid to scroll to a specific data row.
*
* @param row
* zero-based row index. If the row index is below zero or above
* the row count of the client-side data source, a client-side
* exception will be triggered. Since this exception has no
* handling by default, an out-of-bounds value will cause a
* client-side crash.
* @param destination
* desired placement of scrolled-to row. See the documentation
* for {@link ScrollDestination} for more information.
*/
public void scrollToRow(int row, ScrollDestination destination);

/**
* Command client Grid to scroll to the first row.
*/
public void scrollToStart();

/**
* Command client Grid to scroll to the last row.
*/
public void scrollToEnd();

}

+ 55
- 0
shared/src/com/vaadin/shared/ui/grid/GridColumnState.java View File

@@ -0,0 +1,55 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.shared.ui.grid;

import java.io.Serializable;

/**
* Column state DTO for transferring column properties from the server to the
* client
*
* @since 7.2
* @author Vaadin Ltd
*/
public class GridColumnState implements Serializable {

/**
* Id used by grid connector to map server side column with client side
* column
*/
public String id;

/**
* Header caption for the column
*/
public String header;

/**
* Footer caption for the column
*/
public String footer;

/**
* Has the column been hidden. By default the column is visible.
*/
public boolean visible = true;

/**
* Column width in pixels. Default column width is 100px.
*/
public int width = 100;

}

+ 33
- 0
shared/src/com/vaadin/shared/ui/grid/GridConstants.java View File

@@ -0,0 +1,33 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.shared.ui.grid;

/**
* Container class for common constants and default values used by the Grid
* component.
*
* @since 7.2
* @author Vaadin Ltd
*/
public final class GridConstants {

/**
* Default padding in pixels when scrolling programmatically, without an
* explicitly defined padding value.
*/
public static final int DEFAULT_PADDING = 0;

}

+ 39
- 0
shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java View File

@@ -0,0 +1,39 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.shared.ui.grid;

import com.vaadin.shared.communication.ServerRpc;

/**
* TODO
*
* @since 7.2
* @author Vaadin Ltd
*/
public interface GridServerRpc extends ServerRpc {

/**
* TODO
*
* @param firstVisibleRow
* the index of the first visible row
* @param visibleRowCount
* the number of rows visible, counted from
* <code>firstVisibleRow</code>
*/
void setVisibleRows(int firstVisibleRow, int visibleRowCount);

}

+ 64
- 0
shared/src/com/vaadin/shared/ui/grid/GridState.java View File

@@ -0,0 +1,64 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.shared.ui.grid;

import java.util.ArrayList;
import java.util.List;

import com.vaadin.shared.AbstractComponentState;

/**
* The shared state for the {@link com.vaadin.ui.components.grid.Grid} component
*
* @since 7.2
* @author Vaadin Ltd
*/
public class GridState extends AbstractComponentState {
{
// FIXME Grid currently does not support undefined size
width = "400px";
height = "400px";
}

/**
* Columns in grid. Column order implicitly deferred from list order.
*/
public List<GridColumnState> columns = new ArrayList<GridColumnState>();

/**
* Is the column header row visible
*/
public boolean columnHeadersVisible = true;

/**
* Is the column footer row visible
*/
public boolean columnFootersVisible = false;

/**
* The column groups added to the grid
*/
public List<ColumnGroupRowState> columnGroupRows = new ArrayList<ColumnGroupRowState>();

/**
* The id for the last frozen column.
*
* @see GridColumnState#id
*/
public String lastFrozenColumnId = null;

}

+ 378
- 0
shared/src/com/vaadin/shared/ui/grid/Range.java View File

@@ -0,0 +1,378 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.shared.ui.grid;

/**
* An immutable representation of a range, marked by start and end points.
* <p>
* The range is treated as inclusive at the start, and exclusive at the end.
* I.e. the range [0..1[ has the length 1, and represents one integer: 0.
* <p>
* The range is considered {@link #isEmpty() empty} if the start is the same as
* the end.
*
* @since 7.2
* @author Vaadin Ltd
*/
public final class Range {
private final int start;
private final int end;

/**
* Creates a range object representing a single integer.
*
* @param integer
* the number to represent as a range
* @return the range represented by <code>integer</code>
*/
public static Range withOnly(final int integer) {
return new Range(integer, integer + 1);
}

/**
* Creates a range between two integers.
* <p>
* The range start is <em>inclusive</em> and the end is <em>exclusive</em>.
* So, a range "between" 0 and 5 represents the numbers 0, 1, 2, 3 and 4,
* but not 5.
*
* @param start
* the start of the the range, inclusive
* @param end
* the end of the range, exclusive
* @return a range representing <code>[start..end[</code>
* @throws IllegalArgumentException
* if <code>start &gt; end</code>
*/
public static Range between(final int start, final int end)
throws IllegalArgumentException {
return new Range(start, end);
}

/**
* Creates a range from a start point, with a given length.
*
* @param start
* the first integer to include in the range
* @param length
* the length of the resulting range
* @return a range starting from <code>start</code>, with
* <code>length</code> number of integers following
* @throws IllegalArgumentException
* if length &lt; 0
*/
public static Range withLength(final int start, final int length)
throws IllegalArgumentException {
if (length < 0) {
/*
* The constructor of Range will throw an exception if start >
* start+length (i.e. if length is negative). We're throwing the
* same exception type, just with a more descriptive message.
*/
throw new IllegalArgumentException("length must not be negative");
}
return new Range(start, start + length);
}

/**
* Creates a new range between two numbers: <code>[start..end[</code>.
*
* @param start
* the start integer, inclusive
* @param end
* the end integer, exclusive
* @throws IllegalArgumentException
* if <code>start &gt; end</code>
*/
private Range(final int start, final int end)
throws IllegalArgumentException {
if (start > end) {
throw new IllegalArgumentException(
"start must not be greater than end");
}

this.start = start;
this.end = end;
}

/**
* Returns the <em>inclusive</em> start point of this range.
*
* @return the start point of this range
*/
public int getStart() {
return start;
}

/**
* Returns the <em>exclusive</em> end point of this range.
*
* @return the end point of this range
*/
public int getEnd() {
return end;
}

/**
* The number of integers contained in the range.
*
* @return the number of integers contained in the range
*/
public int length() {
return getEnd() - getStart();
}

/**
* Checks whether the range has no elements between the start and end.
*
* @return <code>true</code> iff the range contains no elements.
*/
public boolean isEmpty() {
return getStart() >= getEnd();
}

/**
* Checks whether this range and another range are at least partially
* covering the same values.
*
* @param other
* the other range to check against
* @return <code>true</code> if this and <code>other</code> intersect
*/
public boolean intersects(final Range other) {
return getStart() < other.getEnd() && other.getStart() < getEnd();
}

/**
* Checks whether an integer is found within this range.
*
* @param integer
* an integer to test for presence in this range
* @return <code>true</code> iff <code>integer</code> is in this range
*/
public boolean contains(final int integer) {
return getStart() <= integer && integer < getEnd();
}

/**
* Checks whether this range is a subset of another range.
*
* @return <code>true</code> iff <code>other</code> completely wraps this
* range
*/
public boolean isSubsetOf(final Range other) {
return other.getStart() <= getStart() && getEnd() <= other.getEnd();
}

/**
* Overlay this range with another one, and partition the ranges according
* to how they position relative to each other.
* <p>
* The three partitions are returned as a three-element Range array:
* <ul>
* <li>Elements in this range that occur before elements in
* <code>other</code>.
* <li>Elements that are shared between the two ranges.
* <li>Elements in this range that occur after elements in
* <code>other</code>.
* </ul>
*
* @param other
* the other range to act as delimiters.
* @return a three-element Range array of partitions depicting the elements
* before (index 0), shared/inside (index 1) and after (index 2).
*/
public Range[] partitionWith(final Range other) {
final Range[] splitBefore = splitAt(other.getStart());
final Range rangeBefore = splitBefore[0];
final Range[] splitAfter = splitBefore[1].splitAt(other.getEnd());
final Range rangeInside = splitAfter[0];
final Range rangeAfter = splitAfter[1];
return new Range[] { rangeBefore, rangeInside, rangeAfter };
}

/**
* Get a range that is based on this one, but offset by a number.
*
* @param offset
* the number to offset by
* @return a copy of this range, offset by <code>offset</code>
*/
public Range offsetBy(final int offset) {
if (offset == 0) {
return this;
} else {
return new Range(start + offset, end + offset);
}
}

@Override
public String toString() {
return getClass().getSimpleName() + " [" + getStart() + ".." + getEnd()
+ "[" + (isEmpty() ? " (empty)" : "");
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + end;
result = prime * result + start;
return result;
}

@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final Range other = (Range) obj;
if (end != other.end) {
return false;
}
if (start != other.start) {
return false;
}
return true;
}

/**
* Checks whether this range starts before the start of another range.
*
* @param other
* the other range to compare against
* @return <code>true</code> iff this range starts before the
* <code>other</code>
*/
public boolean startsBefore(final Range other) {
return getStart() < other.getStart();
}

/**
* Checks whether this range ends before the start of another range.
*
* @param other
* the other range to compare against
* @return <code>true</code> iff this range ends before the
* <code>other</code>
*/
public boolean endsBefore(final Range other) {
return getEnd() <= other.getStart();
}

/**
* Checks whether this range ends after the end of another range.
*
* @param other
* the other range to compare against
* @return <code>true</code> iff this range ends after the
* <code>other</code>
*/
public boolean endsAfter(final Range other) {
return getEnd() > other.getEnd();
}

/**
* Checks whether this range starts after the end of another range.
*
* @param other
* the other range to compare against
* @return <code>true</code> iff this range starts after the
* <code>other</code>
*/
public boolean startsAfter(final Range other) {
return getStart() >= other.getEnd();
}

/**
* Split the range into two at a certain integer.
* <p>
* <em>Example:</em> <code>[5..10[.splitAt(7) == [5..7[, [7..10[</code>
*
* @param integer
* the integer at which to split the range into two
* @return an array of two ranges, with <code>[start..integer[</code> in the
* first element, and <code>[integer..end[</code> in the second
* element.
* <p>
* If {@code integer} is less than {@code start}, [empty,
* {@code this} ] is returned. if <code>integer</code> is equal to
* or greater than {@code end}, [{@code this}, empty] is returned
* instead.
*/
public Range[] splitAt(final int integer) {
if (integer < start) {
return new Range[] { Range.withLength(start, 0), this };
} else if (integer >= end) {
return new Range[] { this, Range.withLength(end, 0) };
} else {
return new Range[] { new Range(start, integer),
new Range(integer, end) };
}
}

/**
* Split the range into two after a certain number of integers into the
* range.
* <p>
* Calling this method is equivalent to calling
* <code>{@link #splitAt(int) splitAt}({@link #getStart()}+length);</code>
* <p>
* <em>Example:</em>
* <code>[5..10[.splitAtFromStart(2) == [5..7[, [7..10[</code>
*
* @param length
* the length at which to split this range into two
* @return an array of two ranges, having the <code>length</code>-first
* elements of this range, and the second range having the rest. If
* <code>length</code> &leq; 0, the first element will be empty, and
* the second element will be this range. If <code>length</code>
* &geq; {@link #length()}, the first element will be this range,
* and the second element will be empty.
*/
public Range[] splitAtFromStart(final int length) {
return splitAt(getStart() + length);
}

/**
* Combines two ranges to create a range containing all values in both
* ranges, provided there are no gaps between the ranges.
*
* @param other
* the range to combine with this range
*
* @return the combined range
*
* @throws IllegalArgumentException
* if the two ranges aren't connected
*/
public Range combineWith(Range other) throws IllegalArgumentException {
if (getStart() > other.getEnd() || other.getStart() > getEnd()) {
throw new IllegalArgumentException("There is a gap between " + this
+ " and " + other);
}

return Range.between(Math.min(getStart(), other.getStart()),
Math.max(getEnd(), other.getEnd()));
}
}

+ 55
- 0
shared/src/com/vaadin/shared/ui/grid/ScrollDestination.java View File

@@ -0,0 +1,55 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.shared.ui.grid;

/**
* Enumeration, specifying the destinations that are supported when scrolling
* rows or columns into view.
*
* @since 7.2
* @author Vaadin Ltd
*/
public enum ScrollDestination {

/**
* Scroll as little as possible to show the target element. If the element
* fits into view, this works as START or END depending on the current
* scroll position. If the element does not fit into view, this works as
* START.
*/
ANY,

/**
* Scrolls so that the element is shown at the start of the viewport. The
* viewport will, however, not scroll beyond its contents.
*/
START,

/**
* Scrolls so that the element is shown in the middle of the viewport. The
* viewport will, however, not scroll beyond its contents, given more
* elements than what the viewport is able to show at once. Under no
* circumstances will the viewport scroll before its first element.
*/
MIDDLE,

/**
* Scrolls so that the element is shown at the end of the viewport. The
* viewport will, however, not scroll before its first element.
*/
END

}

+ 318
- 0
shared/tests/src/com/vaadin/shared/ui/grid/RangeTest.java View File

@@ -0,0 +1,318 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.shared.ui.grid;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import org.junit.Test;

@SuppressWarnings("static-method")
public class RangeTest {

@Test(expected = IllegalArgumentException.class)
public void startAfterEndTest() {
Range.between(10, 9);
}

@Test(expected = IllegalArgumentException.class)
public void negativeLengthTest() {
Range.withLength(10, -1);
}

@Test
public void constructorEquivalenceTest() {
assertEquals("10 == [10,11[", Range.withOnly(10), Range.between(10, 11));
assertEquals("[10,20[ == 10, length 10", Range.between(10, 20),
Range.withLength(10, 10));
assertEquals("10 == 10, length 1", Range.withOnly(10),
Range.withLength(10, 1));
}

@Test
public void boundsTest() {
{
final Range range = Range.between(0, 10);
assertEquals("between(0, 10) start", 0, range.getStart());
assertEquals("between(0, 10) end", 10, range.getEnd());
}

{
final Range single = Range.withOnly(10);
assertEquals("withOnly(10) start", 10, single.getStart());
assertEquals("withOnly(10) end", 11, single.getEnd());
}

{
final Range length = Range.withLength(10, 5);
assertEquals("withLength(10, 5) start", 10, length.getStart());
assertEquals("withLength(10, 5) end", 15, length.getEnd());
}
}

@Test
@SuppressWarnings("boxing")
public void equalsTest() {
final Range range1 = Range.between(0, 10);
final Range range2 = Range.withLength(0, 11);

assertTrue("null", !range1.equals(null));
assertTrue("reflexive", range1.equals(range1));
assertEquals("symmetric", range1.equals(range2), range2.equals(range1));
}

@Test
public void containsTest() {
final int start = 0;
final int end = 10;
final Range range = Range.between(start, end);

assertTrue("start should be contained", range.contains(start));
assertTrue("start-1 should not be contained",
!range.contains(start - 1));
assertTrue("end should not be contained", !range.contains(end));
assertTrue("end-1 should be contained", range.contains(end - 1));

assertTrue("[0..10[ contains 5", Range.between(0, 10).contains(5));
assertTrue("empty range does not contain 5", !Range.between(5, 5)
.contains(5));
}

@Test
public void emptyTest() {
assertTrue("[0..0[ should be empty", Range.between(0, 0).isEmpty());
assertTrue("Range of length 0 should be empty", Range.withLength(0, 0)
.isEmpty());

assertTrue("[0..1[ should not be empty", !Range.between(0, 1).isEmpty());
assertTrue("Range of length 1 should not be empty",
!Range.withLength(0, 1).isEmpty());
}

@Test
public void splitTest() {
final Range startRange = Range.between(0, 10);
final Range[] splitRanges = startRange.splitAt(5);
assertEquals("[0..10[ split at 5, lower", Range.between(0, 5),
splitRanges[0]);
assertEquals("[0..10[ split at 5, upper", Range.between(5, 10),
splitRanges[1]);
}

@Test
public void split_valueBefore() {
Range range = Range.between(10, 20);
Range[] splitRanges = range.splitAt(5);

assertEquals(Range.between(10, 10), splitRanges[0]);
assertEquals(range, splitRanges[1]);
}

@Test
public void split_valueAfter() {
Range range = Range.between(10, 20);
Range[] splitRanges = range.splitAt(25);

assertEquals(range, splitRanges[0]);
assertEquals(Range.between(20, 20), splitRanges[1]);
}

@Test
public void emptySplitTest() {
final Range range = Range.between(5, 10);
final Range[] split1 = range.splitAt(0);
assertTrue("split1, [0]", split1[0].isEmpty());
assertEquals("split1, [1]", range, split1[1]);

final Range[] split2 = range.splitAt(15);
assertEquals("split2, [0]", range, split2[0]);
assertTrue("split2, [1]", split2[1].isEmpty());
}

@Test
public void lengthTest() {
assertEquals("withLength length", 5, Range.withLength(10, 5).length());
assertEquals("between length", 5, Range.between(10, 15).length());
assertEquals("withOnly 10 length", 1, Range.withOnly(10).length());
}

@Test
public void intersectsTest() {
assertTrue("[0..10[ intersects [5..15[", Range.between(0, 10)
.intersects(Range.between(5, 15)));
assertTrue("[0..10[ does not intersect [10..20[", !Range.between(0, 10)
.intersects(Range.between(10, 20)));
}

@Test
public void intersects_emptyInside() {
assertTrue("[5..5[ does intersect with [0..10[", Range.between(5, 5)
.intersects(Range.between(0, 10)));
assertTrue("[0..10[ does intersect with [5..5[", Range.between(0, 10)
.intersects(Range.between(5, 5)));
}

@Test
public void intersects_emptyOutside() {
assertTrue("[15..15[ does not intersect with [0..10[",
!Range.between(15, 15).intersects(Range.between(0, 10)));
assertTrue("[0..10[ does not intersect with [15..15[",
!Range.between(0, 10).intersects(Range.between(15, 15)));
}

@Test
public void subsetTest() {
assertTrue("[5..10[ is subset of [0..20[", Range.between(5, 10)
.isSubsetOf(Range.between(0, 20)));

final Range range = Range.between(0, 10);
assertTrue("range is subset of self", range.isSubsetOf(range));

assertTrue("[0..10[ is not subset of [5..15[", !Range.between(0, 10)
.isSubsetOf(Range.between(5, 15)));
}

@Test
public void offsetTest() {
assertEquals(Range.between(5, 15), Range.between(0, 10).offsetBy(5));
}

@Test
public void rangeStartsBeforeTest() {
final Range former = Range.between(0, 5);
final Range latter = Range.between(1, 5);
assertTrue("former should starts before latter",
former.startsBefore(latter));
assertTrue("latter shouldn't start before latter",
!latter.startsBefore(former));

assertTrue("no overlap allowed",
!Range.between(0, 5).startsBefore(Range.between(0, 10)));
}

@Test
public void rangeStartsAfterTest() {
final Range former = Range.between(0, 5);
final Range latter = Range.between(5, 10);
assertTrue("latter should start after former",
latter.startsAfter(former));
assertTrue("former shouldn't start after latter",
!former.startsAfter(latter));

assertTrue("no overlap allowed",
!Range.between(5, 10).startsAfter(Range.between(0, 6)));
}

@Test
public void rangeEndsBeforeTest() {
final Range former = Range.between(0, 5);
final Range latter = Range.between(5, 10);
assertTrue("latter should end before former", former.endsBefore(latter));
assertTrue("former shouldn't end before latter",
!latter.endsBefore(former));

assertTrue("no overlap allowed",
!Range.between(5, 10).endsBefore(Range.between(9, 15)));
}

@Test
public void rangeEndsAfterTest() {
final Range former = Range.between(1, 5);
final Range latter = Range.between(1, 6);
assertTrue("latter should end after former", latter.endsAfter(former));
assertTrue("former shouldn't end after latter",
!former.endsAfter(latter));

assertTrue("no overlap allowed",
!Range.between(0, 10).endsAfter(Range.between(5, 10)));
}

@Test(expected = IllegalArgumentException.class)
public void combine_notOverlappingFirstSmaller() {
Range.between(0, 10).combineWith(Range.between(11, 20));
}

@Test(expected = IllegalArgumentException.class)
public void combine_notOverlappingSecondLarger() {
Range.between(11, 20).combineWith(Range.between(0, 10));
}

@Test(expected = IllegalArgumentException.class)
public void combine_firstEmptyNotOverlapping() {
Range.between(15, 15).combineWith(Range.between(0, 10));
}

@Test(expected = IllegalArgumentException.class)
public void combine_secondEmptyNotOverlapping() {
Range.between(0, 10).combineWith(Range.between(15, 15));
}

@Test
public void combine_barelyOverlapping() {
Range r1 = Range.between(0, 10);
Range r2 = Range.between(10, 20);

// Test both ways, should give the same result
Range combined1 = r1.combineWith(r2);
Range combined2 = r2.combineWith(r1);
assertEquals(combined1, combined2);

assertEquals(0, combined1.getStart());
assertEquals(20, combined1.getEnd());
}

@Test
public void combine_subRange() {
Range r1 = Range.between(0, 10);
Range r2 = Range.between(2, 8);

// Test both ways, should give the same result
Range combined1 = r1.combineWith(r2);
Range combined2 = r2.combineWith(r1);
assertEquals(combined1, combined2);

assertEquals(r1, combined1);
}

@Test
public void combine_intersecting() {
Range r1 = Range.between(0, 10);
Range r2 = Range.between(5, 15);

// Test both ways, should give the same result
Range combined1 = r1.combineWith(r2);
Range combined2 = r2.combineWith(r1);
assertEquals(combined1, combined2);

assertEquals(0, combined1.getStart());
assertEquals(15, combined1.getEnd());

}

@Test
public void combine_emptyInside() {
Range r1 = Range.between(0, 10);
Range r2 = Range.between(5, 5);

// Test both ways, should give the same result
Range combined1 = r1.combineWith(r2);
Range combined2 = r2.combineWith(r1);
assertEquals(combined1, combined2);

assertEquals(r1, combined1);
}

}

+ 176
- 0
uitest/src/com/vaadin/tests/components/grid/BasicEscalator.html View File

@@ -0,0 +1,176 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head profile="http://selenium-ide.openqa.org/profiles/test-case">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="selenium.base" href="http://localhost:8888/" />
<title>BasicEscalator</title>
</head>
<body>
<table cellpadding="1" cellspacing="1" border="1">
<thead>
<tr><td rowspan="1" colspan="3">BasicEscalator</td></tr>
</thead><tbody>
<tr>
<td>open</td>
<td>/run/com.vaadin.tests.components.grid.BasicEscalator?restartApplication</td>
<td></td>
</tr>
<tr>
<td>verifyText</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[0]/domChild[0]</td>
<td>Row 0: 0,0 (0)</td>
</tr>
<tr>
<td>verifyText</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[17]/domChild[9]</td>
<td>Cell: 9,17 (179)</td>
</tr>
<tr>
<td>verifyTextNotPresent</td>
<td>Cell: 0,100</td>
<td></td>
</tr>
<tr>
<td>verifyTextNotPresent</td>
<td>Cell: 0,101</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
<td>0</td>
</tr>
<tr>
<td>type</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
<td>1</td>
</tr>
<tr>
<td>click</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
<td></td>
</tr>
<tr>
<td>verifyText</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[18]/domChild[0]</td>
<td>Row 0: 0,100 (190)</td>
</tr>
<tr>
<td>type</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
<td>11</td>
</tr>
<tr>
<td>click</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
<td></td>
</tr>
<tr>
<td>verifyText</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[17]/domChild[0]</td>
<td>Row 11: 0,101 (200)</td>
</tr>
<tr>
<td>type</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
<td>0</td>
</tr>
<tr>
<td>type</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
<td>100</td>
</tr>
<tr>
<td>click</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[1]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
<td></td>
</tr>
<tr>
<td>verifyText</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[16]/domChild[0]</td>
<td>Row 0: 0,102 (210)</td>
</tr>
<tr>
<td>verifyText</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[1]/domChild[0]</td>
<td>Row 16: 0,118 (370)</td>
</tr>
<tr>
<td>scroll</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[0]</td>
<td>1109</td>
</tr>
<tr>
<td>verifyTextPresent</td>
<td>Row 56: 0,158</td>
<td></td>
</tr>
<tr>
<td>verifyTextPresent</td>
<td>Row 72: 0,174</td>
<td></td>
</tr>
<tr>
<td>scroll</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[0]</td>
<td>3690</td>
</tr>
<tr>
<td>verifyTextPresent</td>
<td>Row 201: 0,99</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
<td>201</td>
</tr>
<tr>
<td>type</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
<td>1</td>
</tr>
<tr>
<td>click</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[2]/VHorizontalLayout[0]/Slot[2]/VButton[0]/domChild[0]/domChild[0]</td>
<td></td>
</tr>
<tr>
<td>verifyText</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[1]/domChild[0]</td>
<td>Row 200: 0,98 (960)</td>
</tr>
<tr>
<td>verifyTextNotPresent</td>
<td>Row 201:</td>
<td></td>
</tr>
<tr>
<td>type</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[3]/VHorizontalLayout[0]/Slot[0]/VTextField[0]</td>
<td>0</td>
</tr>
<tr>
<td>type</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[3]/VHorizontalLayout[0]/Slot[1]/VTextField[0]</td>
<td>2</td>
</tr>
<tr>
<td>click</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[3]/VHorizontalLayout[0]/Slot[2]/VButton[0]</td>
<td></td>
</tr>
<tr>
<td>verifyText</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[16]/domChild[0]</td>
<td>Row 184: 10,82 (974)</td>
</tr>
<tr>
<td>verifyText</td>
<td>vaadin=runcomvaadintestscomponentsgridBasicEscalator::/VVerticalLayout[0]/Slot[1]/VVerticalLayout[0]/Slot[0]/VTestGrid[0]/domChild[1]/domChild[0]/domChild[1]/domChild[1]/domChild[0]</td>
<td>Row 200: 10,98 (1006)</td>
</tr>
</tbody></table>
</body>
</html>

+ 312
- 0
uitest/src/com/vaadin/tests/components/grid/BasicEscalator.java View File

@@ -0,0 +1,312 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

package com.vaadin.tests.components.grid;

import java.util.Random;

import com.vaadin.annotations.Widgetset;
import com.vaadin.server.VaadinRequest;
import com.vaadin.tests.components.AbstractTestUI;
import com.vaadin.tests.widgetset.TestingWidgetSet;
import com.vaadin.tests.widgetset.server.grid.TestGrid;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.Layout;
import com.vaadin.ui.NativeSelect;
import com.vaadin.ui.TextField;

/**
* @since 7.2
* @author Vaadin Ltd
*/
@Widgetset(TestingWidgetSet.NAME)
public class BasicEscalator extends AbstractTestUI {
public static final String ESCALATOR = "escalator";
public static final String INSERT_ROWS_OFFSET = "iro";
public static final String INSERT_ROWS_AMOUNT = "ira";
public static final String INSERT_ROWS_BUTTON = "irb";

private final Random random = new Random();

@Override
protected void setup(final VaadinRequest request) {
final TestGrid grid = new TestGrid();
grid.setId(ESCALATOR);
addComponent(grid);

final Layout insertRowsLayout = new HorizontalLayout();
final TextField insertRowsOffset = new TextField();
insertRowsOffset.setId(INSERT_ROWS_OFFSET);
insertRowsLayout.addComponent(insertRowsOffset);
final TextField insertRowsAmount = new TextField();
insertRowsAmount.setId(INSERT_ROWS_AMOUNT);
insertRowsLayout.addComponent(insertRowsAmount);
insertRowsLayout.addComponent(new Button("insert rows",
new Button.ClickListener() {
@Override
public void buttonClick(final ClickEvent event) {
final int offset = Integer.parseInt(insertRowsOffset
.getValue());
final int amount = Integer.parseInt(insertRowsAmount
.getValue());
grid.insertRows(offset, amount);
}
}) {
{
setId(INSERT_ROWS_BUTTON);
}
});
addComponent(insertRowsLayout);

final Layout removeRowsLayout = new HorizontalLayout();
final TextField removeRowsOffset = new TextField();
removeRowsLayout.addComponent(removeRowsOffset);
final TextField removeRowsAmount = new TextField();
removeRowsLayout.addComponent(removeRowsAmount);
removeRowsLayout.addComponent(new Button("remove rows",
new Button.ClickListener() {
@Override
public void buttonClick(final ClickEvent event) {
final int offset = Integer.parseInt(removeRowsOffset
.getValue());
final int amount = Integer.parseInt(removeRowsAmount
.getValue());
grid.removeRows(offset, amount);
}
}));
addComponent(removeRowsLayout);

final Layout insertColumnsLayout = new HorizontalLayout();
final TextField insertColumnsOffset = new TextField();
insertColumnsLayout.addComponent(insertColumnsOffset);
final TextField insertColumnsAmount = new TextField();
insertColumnsLayout.addComponent(insertColumnsAmount);
insertColumnsLayout.addComponent(new Button("insert columns",
new Button.ClickListener() {
@Override
public void buttonClick(final ClickEvent event) {
final int offset = Integer.parseInt(insertColumnsOffset
.getValue());
final int amount = Integer.parseInt(insertColumnsAmount
.getValue());
grid.insertColumns(offset, amount);
}
}));
addComponent(insertColumnsLayout);

final Layout removeColumnsLayout = new HorizontalLayout();
final TextField removeColumnsOffset = new TextField();
removeColumnsLayout.addComponent(removeColumnsOffset);
final TextField removeColumnsAmount = new TextField();
removeColumnsLayout.addComponent(removeColumnsAmount);
removeColumnsLayout.addComponent(new Button("remove columns",
new Button.ClickListener() {
@Override
public void buttonClick(final ClickEvent event) {
final int offset = Integer.parseInt(removeColumnsOffset
.getValue());
final int amount = Integer.parseInt(removeColumnsAmount
.getValue());
grid.removeColumns(offset, amount);
}
}));
addComponent(removeColumnsLayout);

final HorizontalLayout rowScroll = new HorizontalLayout();
final NativeSelect destination = new NativeSelect();
destination.setNullSelectionAllowed(false);
destination.addItem("any");
destination.setValue("any");
destination.addItem("start");
destination.addItem("end");
destination.addItem("middle");
rowScroll.addComponent(destination);
final TextField rowIndex = new TextField();
rowScroll.addComponent(rowIndex);
final TextField rowPadding = new TextField();
rowScroll.addComponent(rowPadding);
rowScroll.addComponent(new Button("scroll to row",
new Button.ClickListener() {
@Override
public void buttonClick(final ClickEvent event) {
int index;
try {
index = Integer.parseInt(rowIndex.getValue());
} catch (NumberFormatException e) {
index = 0;
}

int padding;
try {
padding = Integer.parseInt(rowPadding.getValue());
} catch (NumberFormatException e) {
padding = 0;
}

grid.scrollToRow(index,
(String) destination.getValue(), padding);
}
}));
addComponent(rowScroll);

final HorizontalLayout colScroll = new HorizontalLayout();
final NativeSelect colDestination = new NativeSelect();
colDestination.setNullSelectionAllowed(false);
colDestination.addItem("any");
colDestination.setValue("any");
colDestination.addItem("start");
colDestination.addItem("end");
colDestination.addItem("middle");
colScroll.addComponent(colDestination);
final TextField colIndex = new TextField();
colScroll.addComponent(colIndex);
final TextField colPadding = new TextField();
colScroll.addComponent(colPadding);
colScroll.addComponent(new Button("scroll to column",
new Button.ClickListener() {
@Override
public void buttonClick(final ClickEvent event) {
int index;
try {
index = Integer.parseInt(colIndex.getValue());
} catch (NumberFormatException e) {
index = 0;
}

int padding;
try {
padding = Integer.parseInt(colPadding.getValue());
} catch (NumberFormatException e) {
padding = 0;
}

grid.scrollToColumn(index,
(String) colDestination.getValue(), padding);
}
}));
addComponent(colScroll);

final TextField freezeCount = new TextField();
freezeCount.setConverter(Integer.class);
freezeCount.setNullRepresentation("");
addComponent(new HorizontalLayout(freezeCount, new Button(
"set frozen columns", new Button.ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
grid.setFrozenColumns(((Integer) freezeCount
.getConvertedValue()).intValue());
freezeCount.setValue(null);
}
})));

addComponent(new Button("Resize randomly", new Button.ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
int width = random.nextInt(300) + 500;
int height = random.nextInt(300) + 200;
grid.setWidth(width + "px");
grid.setHeight(height + "px");
}
}));

addComponent(new Button("Random headers count",
new Button.ClickListener() {
private int headers = 1;

@Override
public void buttonClick(ClickEvent event) {
int diff = 0;
while (diff == 0) {
final int nextHeaders = random.nextInt(4);
diff = nextHeaders - headers;
headers = nextHeaders;
}
if (diff > 0) {
grid.insertHeaders(0, diff);
} else if (diff < 0) {
grid.removeHeaders(0, -diff);
}
}
}));

addComponent(new Button("Random footers count",
new Button.ClickListener() {
private int footers = 1;

@Override
public void buttonClick(ClickEvent event) {
int diff = 0;
while (diff == 0) {
final int nextFooters = random.nextInt(4);
diff = nextFooters - footers;
footers = nextFooters;
}
if (diff > 0) {
grid.insertFooters(0, diff);
} else if (diff < 0) {
grid.removeFooters(0, -diff);
}
}
}));

final Layout resizeColumnsLayout = new HorizontalLayout();
final TextField resizeColumnIndex = new TextField();
resizeColumnsLayout.addComponent(resizeColumnIndex);
final TextField resizeColumnPx = new TextField();
resizeColumnsLayout.addComponent(resizeColumnPx);
resizeColumnsLayout.addComponent(new Button("resize column",
new Button.ClickListener() {
@Override
public void buttonClick(final ClickEvent event) {
final int index = Integer.parseInt(resizeColumnIndex
.getValue());
final int px = Integer.parseInt(resizeColumnPx
.getValue());
grid.setColumnWidth(index, px);
}
}));
addComponent(resizeColumnsLayout);

addComponent(new Button("Autoresize columns",
new Button.ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
grid.calculateColumnWidths();
}
}));

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;
}

}

+ 49
- 0
uitest/src/com/vaadin/tests/components/grid/BasicEscalatorTest.java View File

@@ -0,0 +1,49 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.tests.components.grid;

import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebElement;

import com.vaadin.tests.tb3.MultiBrowserTest;

public class BasicEscalatorTest extends MultiBrowserTest {

@Test
public void testNormalHeight() throws Exception {
openTestURL();
compareScreen("normalHeight");
}

@Test
public void testModifiedHeight() throws Exception {
openTestURLWithTheme("reindeer-tests");
compareScreen("modifiedHeight");
}

private WebElement getFirstBodyRowCell() {
return getDriver().findElement(
By.xpath("//tbody/tr[@class='v-escalator-row'][1]/td[1]"));
}

private void openTestURLWithTheme(String themeName) {
String testUrl = getTestUrl();
testUrl += (testUrl.contains("?")) ? "&" : "?";
testUrl += "theme=" + themeName;
getDriver().get(testUrl);
}
}

+ 343
- 0
uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java View File

@@ -0,0 +1,343 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.tests.components.grid;

import java.util.ArrayList;
import java.util.LinkedHashMap;

import com.vaadin.data.Item;
import com.vaadin.data.util.IndexedContainer;
import com.vaadin.tests.components.AbstractComponentTest;
import com.vaadin.ui.components.grid.ColumnGroup;
import com.vaadin.ui.components.grid.ColumnGroupRow;
import com.vaadin.ui.components.grid.Grid;
import com.vaadin.ui.components.grid.GridColumn;

/**
* Tests the basic features like columns, footers and headers
*
* @since 7.2
* @author Vaadin Ltd
*/
public class GridBasicFeatures extends AbstractComponentTest<Grid> {

private final int COLUMNS = 10;

private int columnGroupRows = 0;

private final int ROWS = 1000;

private IndexedContainer ds;

@Override
protected Grid constructComponent() {

// Build data source
ds = new IndexedContainer();

for (int col = 0; col < COLUMNS; col++) {
ds.addContainerProperty(getColumnProperty(col), String.class, "");
}

for (int row = 0; row < ROWS; row++) {
Item item = ds.addItem(Integer.valueOf(row));
for (int col = 0; col < COLUMNS; col++) {
item.getItemProperty(getColumnProperty(col)).setValue(
"(" + row + ", " + col + ")");
}
}

// Create grid
Grid grid = new Grid(ds);

// Add footer values (header values are automatically created)
for (int col = 0; col < COLUMNS; col++) {
grid.getColumn(getColumnProperty(col)).setFooterCaption(
"Footer " + col);
}

// Set varying column widths
for (int col = 0; col < COLUMNS; col++) {
grid.getColumn("Column" + col).setWidth(100 + col * 50);
}

createGridActions();

createColumnActions();

createHeaderActions();

createFooterActions();

createColumnGroupActions();

createRowActions();

return grid;
}

protected void createGridActions() {
LinkedHashMap<String, String> primaryStyleNames = new LinkedHashMap<String, String>();
primaryStyleNames.put("v-grid", "v-grid");
primaryStyleNames.put("v-escalator", "v-escalator");
primaryStyleNames.put("my-grid", "my-grid");

createMultiClickAction("Primary style name", "State",
primaryStyleNames, new Command<Grid, String>() {

@Override
public void execute(Grid grid, String value, Object data) {
grid.setPrimaryStyleName(value);

}
}, primaryStyleNames.get("v-grid"));
}

protected void createHeaderActions() {
createCategory("Headers", null);

createBooleanAction("Visible", "Headers", true,
new Command<Grid, Boolean>() {

@Override
public void execute(Grid grid, Boolean value, Object data) {
grid.setColumnHeadersVisible(value);
}
});
}

protected void createFooterActions() {
createCategory("Footers", null);

createBooleanAction("Visible", "Footers", false,
new Command<Grid, Boolean>() {

@Override
public void execute(Grid grid, Boolean value, Object data) {
grid.setColumnFootersVisible(value);
}
});
}

protected void createColumnActions() {
createCategory("Columns", null);

for (int c = 0; c < COLUMNS; c++) {
createCategory(getColumnProperty(c), "Columns");

createBooleanAction("Visible", getColumnProperty(c), true,
new Command<Grid, Boolean>() {

@Override
public void execute(Grid grid, Boolean value,
Object columnIndex) {
Object propertyId = (new ArrayList(grid
.getContainerDatasource()
.getContainerPropertyIds())
.get((Integer) columnIndex));
GridColumn column = grid.getColumn(propertyId);
column.setVisible(!column.isVisible());
}
}, c);

createClickAction("Remove", getColumnProperty(c),
new Command<Grid, String>() {

@Override
public void execute(Grid grid, String value, Object data) {
grid.getContainerDatasource()
.removeContainerProperty("Column" + data);
}
}, null, c);

createClickAction("Freeze", getColumnProperty(c),
new Command<Grid, String>() {

@Override
public void execute(Grid grid, String value, Object data) {
grid.setLastFrozenPropertyId("Column" + data);
}
}, null, c);

createCategory("Column" + c + " Width", getColumnProperty(c));

createClickAction("Auto", "Column" + c + " Width",
new Command<Grid, Integer>() {

@Override
public void execute(Grid grid, Integer value,
Object columnIndex) {
Object propertyId = (new ArrayList(grid
.getContainerDatasource()
.getContainerPropertyIds())
.get((Integer) columnIndex));
GridColumn column = grid.getColumn(propertyId);
column.setWidthUndefined();
}
}, -1, c);

for (int w = 50; w < 300; w += 50) {
createClickAction(w + "px", "Column" + c + " Width",
new Command<Grid, Integer>() {

@Override
public void execute(Grid grid, Integer value,
Object columnIndex) {
Object propertyId = (new ArrayList(grid
.getContainerDatasource()
.getContainerPropertyIds())
.get((Integer) columnIndex));
GridColumn column = grid.getColumn(propertyId);
column.setWidth(value);
}
}, w, c);
}
}
}

private static String getColumnProperty(int c) {
return "Column" + c;
}

protected void createColumnGroupActions() {
createCategory("Column groups", null);

createClickAction("Add group row", "Column groups",
new Command<Grid, String>() {

@Override
public void execute(Grid grid, String value, Object data) {
final ColumnGroupRow row = grid.addColumnGroupRow();
columnGroupRows++;
createCategory("Column group row " + columnGroupRows,
"Column groups");

createBooleanAction("Header Visible",
"Column group row " + columnGroupRows, true,
new Command<Grid, Boolean>() {

@Override
public void execute(Grid grid,
Boolean value, Object columnIndex) {
row.setHeaderVisible(value);
}
}, row);

createBooleanAction("Footer Visible",
"Column group row " + columnGroupRows, false,
new Command<Grid, Boolean>() {

@Override
public void execute(Grid grid,
Boolean value, Object columnIndex) {
row.setFooterVisible(value);
}
}, row);

for (int i = 0; i < COLUMNS; i++) {
final int columnIndex = i;
createClickAction("Group Column " + columnIndex
+ " & " + (columnIndex + 1),
"Column group row " + columnGroupRows,
new Command<Grid, Integer>() {

@Override
public void execute(Grid c,
Integer value, Object data) {
final ColumnGroup group = row
.addGroup(
"Column" + value,
"Column"
+ (value + 1));

group.setHeaderCaption("Column "
+ value + " & "
+ (value + 1));

group.setFooterCaption("Column "
+ value + " & "
+ (value + 1));
}
}, i, row);
}
}
}, null, null);

}

protected void createRowActions() {
createCategory("Body rows", null);

createClickAction("Add first row", "Body rows",
new Command<Grid, String>() {
@Override
public void execute(Grid c, String value, Object data) {
Item item = ds.addItemAt(0, new Object());
for (int i = 0; i < COLUMNS; i++) {
item.getItemProperty(getColumnProperty(i))
.setValue("newcell: " + i);
}
}
}, null);

createClickAction("Remove first row", "Body rows",
new Command<Grid, String>() {
@Override
public void execute(Grid c, String value, Object data) {
Object firstItemId = ds.getIdByIndex(0);
ds.removeItem(firstItemId);
}
}, null);

createClickAction("Modify first row (getItemProperty)", "Body rows",
new Command<Grid, String>() {
@Override
public void execute(Grid c, String value, Object data) {
Object firstItemId = ds.getIdByIndex(0);
Item item = ds.getItem(firstItemId);
for (int i = 0; i < COLUMNS; i++) {
item.getItemProperty(getColumnProperty(i))
.setValue("modified: " + i);
}
}
}, null);

createClickAction("Modify first row (getContainerProperty)",
"Body rows", new Command<Grid, String>() {
@Override
public void execute(Grid c, String value, Object data) {
Object firstItemId = ds.getIdByIndex(0);
for (Object containerPropertyId : ds
.getContainerPropertyIds()) {
ds.getContainerProperty(firstItemId,
containerPropertyId).setValue(
"modified: " + containerPropertyId);
}
}
}, null);
}

@Override
protected Integer getTicketNumber() {
return 12829;
}

@Override
protected Class<Grid> getTestClass() {
return Grid.class;
}

}

+ 373
- 0
uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java View File

@@ -0,0 +1,373 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.tests.components.grid;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.util.List;

import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;

import com.vaadin.tests.tb3.MultiBrowserTest;

/**
*
* @since
* @author Vaadin Ltd
*/
public class GridBasicFeaturesTest extends MultiBrowserTest {

@Test
public void testColumnHeaderCaptions() throws Exception {
openTestURL();

// Column headers should be visible
List<WebElement> cells = getGridHeaderRowCells();
assertEquals(10, cells.size());
assertEquals("Column0", cells.get(0).getText());
assertEquals("Column1", cells.get(1).getText());
assertEquals("Column2", cells.get(2).getText());
}

@Test
public void testColumnFooterCaptions() throws Exception {
openTestURL();

// footer row should by default be hidden
List<WebElement> cells = getGridFooterRowCells();
assertEquals(0, cells.size());

// Open footer row
selectMenuPath("Component", "Footers", "Visible");

// Footers should now be visible
cells = getGridFooterRowCells();
assertEquals(10, cells.size());
assertEquals("Footer 0", cells.get(0).getText());
assertEquals("Footer 1", cells.get(1).getText());
assertEquals("Footer 2", cells.get(2).getText());
}

@Test
public void testColumnGroupHeaders() throws Exception {
openTestURL();

// Hide column headers for this test
selectMenuPath("Component", "Headers", "Visible");

List<WebElement> cells = getGridHeaderRowCells();

// header row should be empty
assertEquals(0, cells.size());

// add a group row
selectMenuPath("Component", "Column groups", "Add group row");

// Empty group row cells should be present
cells = getGridHeaderRowCells();
assertEquals(10, cells.size());

// Group columns 0 & 1
selectMenuPath("Component", "Column groups", "Column group row 1",
"Group Column 0 & 1");

cells = getGridHeaderRowCells();
assertEquals("Column 0 & 1", cells.get(0).getText());
}

@Test
public void testColumnGroupFooters() throws Exception {
openTestURL();

// add a group row
selectMenuPath("Component", "Column groups", "Add group row");

// Set footer visible
selectMenuPath("Component", "Column groups", "Column group row 1",
"Footer Visible");

// Group columns 0 & 1
selectMenuPath("Component", "Column groups", "Column group row 1",
"Group Column 0 & 1");

List<WebElement> cells = getGridFooterRowCells();
assertEquals("Column 0 & 1", cells.get(0).getText());
}

@Test
public void testGroupingSameColumnsOnRowThrowsException() throws Exception {
openTestURL();

// add a group row
selectMenuPath("Component", "Column groups", "Add group row");

// Group columns 0 & 1
selectMenuPath("Component", "Column groups", "Column group row 1",
"Group Column 0 & 1");

// Group columns 1 & 2 shoud fail
selectMenuPath("Component", "Column groups", "Column group row 1",
"Group Column 1 & 2");

assertTrue(getLogRow(0)
.contains(
"Exception occured, java.lang.IllegalArgumentExceptionColumn Column1 already belongs to another group."));
}

@Test
public void testHidingColumn() throws Exception {
openTestURL();

// Column 0 should be visible
List<WebElement> cells = getGridHeaderRowCells();
assertEquals("Column0", cells.get(0).getText());

// Hide column 0
selectMenuPath("Component", "Columns", "Column0", "Visible");

// Column 1 should now be the first cell
cells = getGridHeaderRowCells();
assertEquals("Column1", cells.get(0).getText());
}

@Test
public void testRemovingColumn() throws Exception {
openTestURL();

// Column 0 should be visible
List<WebElement> cells = getGridHeaderRowCells();
assertEquals("Column0", cells.get(0).getText());

// Hide column 0
selectMenuPath("Component", "Columns", "Column0", "Remove");

// Column 1 should now be the first cell
cells = getGridHeaderRowCells();
assertEquals("Column1", cells.get(0).getText());
}

@Test
public void testFreezingColumn() throws Exception {
openTestURL();

// Freeze column 2
selectMenuPath("Component", "Columns", "Column2", "Freeze");

WebElement cell = getBodyCellByRowAndColumn(1, 1);
assertTrue(cell.getAttribute("class").contains("frozen"));

cell = getBodyCellByRowAndColumn(1, 2);
assertTrue(cell.getAttribute("class").contains("frozen"));
}

@Test
public void testInitialColumnWidths() throws Exception {
openTestURL();

// Default borders and margins implemented by escalator
int cellBorder = 1 + 1;
int cellMargin = 2 + 2;

WebElement cell = getBodyCellByRowAndColumn(1, 1);
assertEquals((100 - cellBorder - cellMargin) + "px",
cell.getCssValue("width"));

cell = getBodyCellByRowAndColumn(1, 2);
assertEquals((150 - cellBorder - cellMargin) + "px",
cell.getCssValue("width"));

cell = getBodyCellByRowAndColumn(1, 3);
assertEquals((200 - cellBorder - cellMargin) + "px",
cell.getCssValue("width"));
}

@Test
public void testColumnWidths() throws Exception {
openTestURL();

// Default borders and margins implemented by escalator
int cellBorder = 1 + 1;
int cellMargin = 2 + 2;

// Default column width is 100px
WebElement cell = getBodyCellByRowAndColumn(1, 1);
assertEquals((100 - cellBorder - cellMargin) + "px",
cell.getCssValue("width"));

// Set first column to be 200px wide
selectMenuPath("Component", "Columns", "Column0", "Column0 Width",
"200px");

cell = getBodyCellByRowAndColumn(1, 1);
assertEquals((200 - cellBorder - cellMargin) + "px",
cell.getCssValue("width"));

// Set second column to be 150px wide
selectMenuPath("Component", "Columns", "Column1", "Column1 Width",
"150px");
cell = getBodyCellByRowAndColumn(1, 2);
assertEquals((150 - cellBorder - cellMargin) + "px",
cell.getCssValue("width"));

// Set first column to be auto sized (defaults to 100px currently)
selectMenuPath("Component", "Columns", "Column0", "Column0 Width",
"Auto");

cell = getBodyCellByRowAndColumn(1, 1);
assertEquals((100 - cellBorder - cellMargin) + "px",
cell.getCssValue("width"));
}

@Test
public void testPrimaryStyleNames() throws Exception {
openTestURL();

// v-grid is default primary style namea
assertPrimaryStylename("v-grid");

selectMenuPath("Component", "State", "Primary style name",
"v-escalator");
assertPrimaryStylename("v-escalator");

selectMenuPath("Component", "State", "Primary style name", "my-grid");
assertPrimaryStylename("my-grid");

selectMenuPath("Component", "State", "Primary style name", "v-grid");
assertPrimaryStylename("v-grid");
}

/**
* Test that the current view is updated when a server-side container change
* occurs (without scrolling back and forth)
*/
@Test
public void testItemSetChangeEvent() throws Exception {
openTestURL();

final By newRow = By.xpath("//td[text()='newcell: 0']");

assertTrue("Unexpected initial state", !elementIsFound(newRow));

selectMenuPath("Component", "Body rows", "Add first row");
assertTrue("Add row failed", elementIsFound(newRow));

selectMenuPath("Component", "Body rows", "Remove first row");
assertTrue("Remove row failed", !elementIsFound(newRow));
}

/**
* Test that the current view is updated when a property's value is reflect
* to the client, when the value is modified server-side.
*/
@Test
public void testPropertyValueChangeEvent() throws Exception {
openTestURL();

assertEquals("Unexpected cell initial state", "(0, 0)",
getBodyCellByRowAndColumn(1, 1).getText());

selectMenuPath("Component", "Body rows",
"Modify first row (getItemProperty)");
assertEquals("(First) modification with getItemProperty failed",
"modified: 0", getBodyCellByRowAndColumn(1, 1).getText());

selectMenuPath("Component", "Body rows",
"Modify first row (getContainerProperty)");
assertEquals("(Second) modification with getItemProperty failed",
"modified: Column0", getBodyCellByRowAndColumn(1, 1).getText());
}

private boolean elementIsFound(By locator) {
try {
return driver.findElement(locator) != null;
} catch (NoSuchElementException e) {
return false;
}
}

private void assertPrimaryStylename(String stylename) {
assertTrue(getGridElement().getAttribute("class").contains(stylename));

String tableWrapperStyleName = getTableWrapper().getAttribute("class");
assertTrue(tableWrapperStyleName.contains(stylename + "-tablewrapper"));

String hscrollStyleName = getHorizontalScroller().getAttribute("class");
assertTrue(hscrollStyleName.contains(stylename + "-scroller"));
assertTrue(hscrollStyleName
.contains(stylename + "-scroller-horizontal"));

String vscrollStyleName = getVerticalScroller().getAttribute("class");
assertTrue(vscrollStyleName.contains(stylename + "-scroller"));
assertTrue(vscrollStyleName.contains(stylename + "-scroller-vertical"));
}

private WebElement getBodyCellByRowAndColumn(int row, int column) {
return getDriver().findElement(
By.xpath("//div[@id='testComponent']//tbody/tr[" + row
+ "]/td[" + column + "]"));
}

private void selectSubMenu(String menuCaption) {
selectMenu(menuCaption);
new Actions(getDriver()).moveByOffset(100, 0).build().perform();
}

private void selectMenu(String menuCaption) {
getDriver().findElement(
By.xpath("//span[text() = '" + menuCaption + "']")).click();
}

private void selectMenuPath(String... menuCaptions) {
selectMenu(menuCaptions[0]);
for (int i = 1; i < menuCaptions.length; i++) {
selectSubMenu(menuCaptions[i]);
}
}

private WebElement getVerticalScroller() {
return getDriver().findElement(
By.xpath("//div[@id='testComponent']/div[1]"));
}

private WebElement getHorizontalScroller() {
return getDriver().findElement(
By.xpath("//div[@id='testComponent']/div[2]"));
}

private WebElement getTableWrapper() {
return getDriver().findElement(
By.xpath("//div[@id='testComponent']/div[3]"));
}

private WebElement getGridElement() {
return getDriver().findElement(By.id("testComponent"));
}

private List<WebElement> getGridHeaderRowCells() {
return getDriver().findElements(
By.xpath("//div[@id='testComponent']//thead//th"));
}

private List<WebElement> getGridFooterRowCells() {
return getDriver().findElements(
By.xpath("//div[@id='testComponent']//tfoot//td"));
}
}

+ 111
- 0
uitest/src/com/vaadin/tests/components/grid/GridColumnGroups.java View File

@@ -0,0 +1,111 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

/**
*
*/
package com.vaadin.tests.components.grid;

import com.vaadin.data.util.IndexedContainer;
import com.vaadin.server.VaadinRequest;
import com.vaadin.tests.components.AbstractTestUI;
import com.vaadin.ui.components.grid.ColumnGroup;
import com.vaadin.ui.components.grid.ColumnGroupRow;
import com.vaadin.ui.components.grid.Grid;
import com.vaadin.ui.components.grid.GridColumn;

/**
*
* @since
* @author Vaadin Ltd
*/
public class GridColumnGroups extends AbstractTestUI {

private final int COLUMNS = 4;

@Override
protected void setup(VaadinRequest request) {

// Setup grid
IndexedContainer ds = new IndexedContainer();
for (int col = 0; col < COLUMNS; col++) {
ds.addContainerProperty("Column" + col, String.class, "");
}
Grid grid = new Grid(ds);
addComponent(grid);

/*-
* ---------------------------------------------
* | Header 1 | <- Auxiliary row 2
* |-------------------------------------------|
* | Header 2 | Header 3 | <- Auxiliary row 1
* |-------------------------------------------|
* | Column 1 | Column 2 | Column 3 | Column 4 | <- Column headers
* --------------------------------------------|
* | ... | ... | ... | ... |
* | ... | ... | ... | ... |
* --------------------------------------------|
* | Column 1 | Column 2 | Column 3 | Column 4 | <- Column footers
* --------------------------------------------|
* | Footer 2 | Footer 3 | <- Auxiliary row 1
* --------------------------------------------|
* | Footer 1 | <- Auxiliary row 2
* ---------------------------------------------
-*/

// Set column footers (headers are generated automatically)
grid.setColumnFootersVisible(true);
for (Object propertyId : ds.getContainerPropertyIds()) {
GridColumn column = grid.getColumn(propertyId);
column.setFooterCaption(String.valueOf(propertyId));
}

// First auxiliary row
ColumnGroupRow auxRow1 = grid.addColumnGroupRow();

// Using property id to create a column group
ColumnGroup columns12 = auxRow1.addGroup("Column0", "Column1");
columns12.setHeaderCaption("Header 2");
columns12.setFooterCaption("Footer 2");

// Using grid columns to create a column group
GridColumn column3 = grid.getColumn("Column2");
GridColumn column4 = grid.getColumn("Column3");
ColumnGroup columns34 = auxRow1.addGroup(column3, column4);
columns34.setHeaderCaption("Header 3");
columns34.setFooterCaption("Footer 3");

// Second auxiliary row
ColumnGroupRow auxRow2 = grid.addColumnGroupRow();

// Using previous groups to create a column group
ColumnGroup columns1234 = auxRow2.addGroup(columns12, columns34);
columns1234.setHeaderCaption("Header 1");
columns1234.setFooterCaption("Footer 1");

}

@Override
protected String getTestDescription() {
return "Grid should support headers and footer groups";
}

@Override
protected Integer getTicketNumber() {
return 12894;
}

}

+ 115
- 0
uitest/src/com/vaadin/tests/components/grid/GridScrolling.java View File

@@ -0,0 +1,115 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.tests.components.grid;

import com.vaadin.data.Item;
import com.vaadin.data.util.IndexedContainer;
import com.vaadin.server.VaadinRequest;
import com.vaadin.shared.ui.grid.ScrollDestination;
import com.vaadin.tests.components.AbstractTestUI;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
import com.vaadin.ui.Button.ClickListener;
import com.vaadin.ui.HorizontalLayout;
import com.vaadin.ui.VerticalLayout;
import com.vaadin.ui.components.grid.Grid;

/**
*
*/
@SuppressWarnings("serial")
public class GridScrolling extends AbstractTestUI {

private Grid grid;

private IndexedContainer ds;

@Override
@SuppressWarnings("unchecked")
protected void setup(VaadinRequest request) {
// Build data source
ds = new IndexedContainer();

for (int col = 0; col < 5; col++) {
ds.addContainerProperty("col" + col, String.class, "");
}

for (int row = 0; row < 65536; row++) {
Item item = ds.addItem(Integer.valueOf(row));
for (int col = 0; col < 5; col++) {
item.getItemProperty("col" + col).setValue(
"(" + row + ", " + col + ")");
}
}

grid = new Grid(ds);

HorizontalLayout hl = new HorizontalLayout();
hl.addComponent(grid);
hl.setMargin(true);
hl.setSpacing(true);

VerticalLayout vl = new VerticalLayout();
vl.setSpacing(true);

// Add scroll buttons
Button scrollUpButton = new Button("Top", new ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
grid.scrollToStart();
}
});
scrollUpButton.setSizeFull();
vl.addComponent(scrollUpButton);

for (int i = 1; i < 7; ++i) {
final int row = (ds.size() / 7) * i;
Button scrollButton = new Button("Scroll to row " + row,
new ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
grid.scrollToItem(Integer.valueOf(row),
ScrollDestination.MIDDLE);
}
});
scrollButton.setSizeFull();
vl.addComponent(scrollButton);
}

Button scrollDownButton = new Button("Bottom", new ClickListener() {
@Override
public void buttonClick(ClickEvent event) {
grid.scrollToEnd();
}
});
scrollDownButton.setSizeFull();
vl.addComponent(scrollDownButton);

hl.addComponent(vl);
addComponent(hl);
}

@Override
protected String getTestDescription() {
return "Test Grid programmatic scrolling features";
}

@Override
protected Integer getTicketNumber() {
return 13327;
}

}

+ 48
- 0
uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridClientRpc.java View File

@@ -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();
}

+ 138
- 0
uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridConnector.java View File

@@ -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 7.2
* @author Vaadin Ltd
*/
@Connect(TestGrid.class)
public class TestGridConnector extends AbstractComponentConnector {
@Override
protected void init() {
super.init();
registerRpc(TestGridClientRpc.class, new TestGridClientRpc() {
@Override
public void insertRows(int offset, int amount) {
getWidget().insertRows(offset, amount);
}

@Override
public void removeRows(int offset, int amount) {
getWidget().removeRows(offset, amount);
}

@Override
public void removeColumns(int offset, int amount) {
getWidget().removeColumns(offset, amount);
}

@Override
public void insertColumns(int offset, int amount) {
getWidget().insertColumns(offset, amount);
}

@Override
public void scrollToRow(int index, String destination, int padding) {
getWidget().scrollToRow(index, getDestination(destination),
padding);
}

@Override
public void scrollToColumn(int index, String destination,
int padding) {
getWidget().scrollToColumn(index, getDestination(destination),
padding);
}

private ScrollDestination getDestination(String destination) {
final ScrollDestination d;
if (destination.equals("start")) {
d = ScrollDestination.START;
} else if (destination.equals("middle")) {
d = ScrollDestination.MIDDLE;
} else if (destination.equals("end")) {
d = ScrollDestination.END;
} else {
d = ScrollDestination.ANY;
}
return d;
}

@Override
public void setFrozenColumns(int frozenColumns) {
getWidget().getColumnConfiguration().setFrozenColumnCount(
frozenColumns);
}

@Override
public void insertHeaders(int index, int amount) {
getWidget().getHeader().insertRows(index, amount);
}

@Override
public void removeHeaders(int index, int amount) {
getWidget().getHeader().removeRows(index, amount);
}

@Override
public void insertFooters(int index, int amount) {
getWidget().getFooter().insertRows(index, amount);
}

@Override
public void removeFooters(int index, int amount) {
getWidget().getFooter().removeRows(index, amount);
}

@Override
public void setColumnWidth(int index, int px) {
getWidget().getColumnConfiguration().setColumnWidth(index, px);
}

@Override
public void calculateColumnWidths() {
getWidget().calculateColumnWidths();
}

@Override
public 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();
}
}

+ 29
- 0
uitest/src/com/vaadin/tests/widgetset/client/grid/TestGridState.java View File

@@ -0,0 +1,29 @@
/*
* Copyright 2000-2013 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.tests.widgetset.client.grid;

import com.vaadin.shared.AbstractComponentState;

/**
* @since 7.2
* @author Vaadin Ltd
*/
public class TestGridState extends AbstractComponentState {
public static final String DEFAULT_HEIGHT = "400.0px";

/* TODO: this should be "100%" before setting final. */
public static final String DEFAULT_WIDTH = "800.0px";
}

+ 220
- 0
uitest/src/com/vaadin/tests/widgetset/client/grid/VTestGrid.java View File

@@ -0,0 +1,220 @@
package com.vaadin.tests.widgetset.client.grid;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.user.client.ui.Composite;
import com.vaadin.client.ui.grid.Cell;
import com.vaadin.client.ui.grid.ColumnConfiguration;
import com.vaadin.client.ui.grid.Escalator;
import com.vaadin.client.ui.grid.EscalatorUpdater;
import com.vaadin.client.ui.grid.Row;
import com.vaadin.client.ui.grid.RowContainer;
import com.vaadin.shared.ui.grid.ScrollDestination;

public class VTestGrid extends Composite {

private static class Data {
private int columnCounter = 0;
private int rowCounter = 0;
private final List<Integer> columns = new ArrayList<Integer>();
private final List<Integer> rows = new ArrayList<Integer>();

@SuppressWarnings("boxing")
public void insertRows(final int offset, final int amount) {
final List<Integer> newRows = new ArrayList<Integer>();
for (int i = 0; i < amount; i++) {
newRows.add(rowCounter++);
}
rows.addAll(offset, newRows);
}

@SuppressWarnings("boxing")
public void insertColumns(final int offset, final int amount) {
final List<Integer> newColumns = new ArrayList<Integer>();
for (int i = 0; i < amount; i++) {
newColumns.add(columnCounter++);
}
columns.addAll(offset, newColumns);
}

public EscalatorUpdater createHeaderUpdater() {
return new EscalatorUpdater() {
@Override
public void updateCells(final Row row,
final Iterable<Cell> cellsToUpdate) {
for (final Cell cell : cellsToUpdate) {
if (cell.getColumn() % 3 == 0) {
cell.setColSpan(2);
}

final Integer columnName = columns
.get(cell.getColumn());
cell.getElement().setInnerText("Header " + columnName);
}
}
};
}

public EscalatorUpdater createFooterUpdater() {
return new EscalatorUpdater() {
@Override
public void updateCells(final Row row,
final Iterable<Cell> cellsToUpdate) {
for (final Cell cell : cellsToUpdate) {
if (cell.getColumn() % 3 == 1) {
cell.setColSpan(2);
}

final Integer columnName = columns
.get(cell.getColumn());
cell.getElement().setInnerText("Footer " + columnName);
}
}
};
}

public EscalatorUpdater createBodyUpdater() {
return new EscalatorUpdater() {
private int i = 0;

public void renderCell(final Cell cell) {
final Integer columnName = columns.get(cell.getColumn());
final Integer rowName = rows.get(cell.getRow());
final String cellInfo = columnName + "," + rowName + " ("
+ i + ")";

if (cell.getColumn() > 0) {
cell.getElement().setInnerText("Cell: " + cellInfo);
} else {
cell.getElement().setInnerText(
"Row " + cell.getRow() + ": " + cellInfo);
}

if (cell.getColumn() % 3 == cell.getRow() % 3) {
cell.setColSpan(3);
}

final double c = i * .1;
final int r = (int) ((Math.cos(c) + 1) * 128);
final int g = (int) ((Math.cos(c / Math.PI) + 1) * 128);
final int b = (int) ((Math.cos(c / (Math.PI * 2)) + 1) * 128);
cell.getElement()
.getStyle()
.setBackgroundColor(
"rgb(" + r + "," + g + "," + b + ")");
if ((r * .8 + g * 1.3 + b * .9) / 3 < 127) {
cell.getElement().getStyle().setColor("white");
} else {
cell.getElement().getStyle().clearColor();
}

i++;
}

@Override
public void updateCells(final Row row,
final Iterable<Cell> cellsToUpdate) {
for (final Cell cell : cellsToUpdate) {
renderCell(cell);
}
}
};
}

public void removeRows(final int offset, final int amount) {
for (int i = 0; i < amount; i++) {
rows.remove(offset);
}
}

public void removeColumns(final int offset, final int amount) {
for (int i = 0; i < amount; i++) {
columns.remove(offset);
}
}
}

private final Escalator escalator = new Escalator();
private final Data data = new Data();

public VTestGrid() {
initWidget(escalator);
final RowContainer header = escalator.getHeader();
header.setEscalatorUpdater(data.createHeaderUpdater());
header.insertRows(0, 1);

final RowContainer footer = escalator.getFooter();
footer.setEscalatorUpdater(data.createFooterUpdater());
footer.insertRows(0, 1);

escalator.getBody().setEscalatorUpdater(data.createBodyUpdater());

insertRows(0, 100);
insertColumns(0, 10);

setWidth(TestGridState.DEFAULT_WIDTH);
setHeight(TestGridState.DEFAULT_HEIGHT);

}

public void insertRows(final int offset, final int number) {
data.insertRows(offset, number);
escalator.getBody().insertRows(offset, number);
}

public void insertColumns(final int offset, final int number) {
data.insertColumns(offset, number);
escalator.getColumnConfiguration().insertColumns(offset, number);
}

public ColumnConfiguration getColumnConfiguration() {
return escalator.getColumnConfiguration();
}

public void scrollToRow(final int index,
final ScrollDestination destination, final int padding) {
escalator.scrollToRow(index, destination, padding);
}

public void scrollToColumn(final int index,
final ScrollDestination destination, final int padding) {
escalator.scrollToColumn(index, destination, padding);
}

public void removeRows(final int offset, final int amount) {
data.removeRows(offset, amount);
escalator.getBody().removeRows(offset, amount);
}

public void removeColumns(final int offset, final int amount) {
data.removeColumns(offset, amount);
escalator.getColumnConfiguration().removeColumns(offset, amount);
}

@Override
public void setWidth(String width) {
escalator.setWidth(width);
}

@Override
public void setHeight(String height) {
escalator.setHeight(height);
}

public RowContainer getHeader() {
return escalator.getHeader();
}

public RowContainer getBody() {
return escalator.getBody();
}

public RowContainer getFooter() {
return escalator.getFooter();
}

public void calculateColumnWidths() {
escalator.calculateColumnWidths();
}
}

+ 96
- 0
uitest/src/com/vaadin/tests/widgetset/server/grid/TestGrid.java View File

@@ -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 7.2
* @author Vaadin Ltd
*/
public class TestGrid extends AbstractComponent {
public TestGrid() {
setWidth(TestGridState.DEFAULT_WIDTH);
setHeight(TestGridState.DEFAULT_HEIGHT);
}

@Override
protected TestGridState getState() {
return (TestGridState) super.getState();
}

public void insertRows(int offset, int amount) {
rpc().insertRows(offset, amount);
}

public void removeRows(int offset, int amount) {
rpc().removeRows(offset, amount);
}

public void insertColumns(int offset, int amount) {
rpc().insertColumns(offset, amount);
}

public void removeColumns(int offset, int amount) {
rpc().removeColumns(offset, amount);
}

private TestGridClientRpc rpc() {
return getRpcProxy(TestGridClientRpc.class);
}

public void scrollToRow(int index, String destination, int padding) {
rpc().scrollToRow(index, destination, padding);
}

public void scrollToColumn(int index, String destination, int padding) {
rpc().scrollToColumn(index, destination, padding);
}

public void setFrozenColumns(int frozenColumns) {
rpc().setFrozenColumns(frozenColumns);
}

public void insertHeaders(int index, int amount) {
rpc().insertHeaders(index, amount);
}

public void removeHeaders(int index, int amount) {
rpc().removeHeaders(index, amount);
}

public void insertFooters(int index, int amount) {
rpc().insertFooters(index, amount);
}

public void removeFooters(int index, int amount) {
rpc().removeFooters(index, amount);
}

public void setColumnWidth(int index, int px) {
rpc().setColumnWidth(index, px);
}

public void calculateColumnWidths() {
rpc().calculateColumnWidths();
}

public void randomizeDefaultRowHeight() {
rpc().randomRowHeight();
}
}

Loading…
Cancel
Save