summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHenrik Paul <henrik@vaadin.com>2014-06-10 21:50:51 +0300
committerHenrik Paul <henrik@vaadin.com>2014-06-27 12:39:42 +0300
commitc4a1ee8a4fbc3fafaabea695d8aaf40aecbeba48 (patch)
tree9bbb9bef83f0e70309067e6d69adde7a7ee5a32a
parent51718c646883f6a9ca26a315d04de6d49119d492 (diff)
downloadvaadin-framework-c4a1ee8a4fbc3fafaabea695d8aaf40aecbeba48.tar.gz
vaadin-framework-c4a1ee8a4fbc3fafaabea695d8aaf40aecbeba48.zip
Send selection between server and client (#13334)
Change-Id: I75174af63092fca72d9aa63ccf3c06a77f42c4f6
-rw-r--r--WebContent/VAADIN/themes/base/grid/grid.scss6
-rw-r--r--client/src/com/vaadin/client/data/AbstractRemoteDataSource.java2
-rw-r--r--client/src/com/vaadin/client/data/RpcDataSourceConnector.java26
-rw-r--r--client/src/com/vaadin/client/ui/grid/Escalator.java3
-rw-r--r--client/src/com/vaadin/client/ui/grid/Grid.java44
-rw-r--r--client/src/com/vaadin/client/ui/grid/GridConnector.java91
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java26
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java34
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java50
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java4
-rw-r--r--client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java39
-rw-r--r--server/src/com/vaadin/data/RpcDataProviderExtension.java278
-rw-r--r--server/src/com/vaadin/ui/components/grid/Grid.java127
-rw-r--r--server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java10
-rw-r--r--server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java88
-rw-r--r--shared/src/com/vaadin/shared/data/DataProviderRpc.java3
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java30
-rw-r--r--shared/src/com/vaadin/shared/ui/grid/GridState.java14
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java14
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java78
-rw-r--r--uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java9
21 files changed, 850 insertions, 126 deletions
diff --git a/WebContent/VAADIN/themes/base/grid/grid.scss b/WebContent/VAADIN/themes/base/grid/grid.scss
index 6a050405cb..d1875a7ab3 100644
--- a/WebContent/VAADIN/themes/base/grid/grid.scss
+++ b/WebContent/VAADIN/themes/base/grid/grid.scss
@@ -14,4 +14,8 @@
}
}
-} \ No newline at end of file
+
+ .#{$primaryStyleName}-row-selected > td {
+ background: lightblue;
+ }
+}
diff --git a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
index 2395dc848c..d6a609a3c8 100644
--- a/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
+++ b/client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
@@ -42,7 +42,7 @@ import com.vaadin.shared.ui.grid.Range;
*/
public abstract class AbstractRemoteDataSource<T> implements DataSource<T> {
- private class RowHandleImpl extends RowHandle<T> {
+ protected class RowHandleImpl extends RowHandle<T> {
private T row;
private final Object key;
diff --git a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
index 2b9bf5c90e..3761ea92df 100644
--- a/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
+++ b/client/src/com/vaadin/client/data/RpcDataSourceConnector.java
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.vaadin.client.ServerConnector;
import com.vaadin.client.extensions.AbstractExtensionConnector;
@@ -29,6 +30,7 @@ import com.vaadin.shared.data.DataProviderRpc;
import com.vaadin.shared.data.DataProviderState;
import com.vaadin.shared.data.DataRequestRpc;
import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.grid.GridState;
import com.vaadin.shared.ui.grid.Range;
/**
@@ -43,7 +45,8 @@ import com.vaadin.shared.ui.grid.Range;
@Connect(com.vaadin.data.RpcDataProviderExtension.class)
public class RpcDataSourceConnector extends AbstractExtensionConnector {
- private final AbstractRemoteDataSource<JSONObject> dataSource = new AbstractRemoteDataSource<JSONObject>() {
+ public class RpcDataSource extends AbstractRemoteDataSource<JSONObject> {
+
@Override
protected void requestRows(int firstRowIndex, int numberOfRows) {
Range cached = getCachedRange();
@@ -54,18 +57,25 @@ public class RpcDataSourceConnector extends AbstractExtensionConnector {
@Override
public Object getRowKey(JSONObject row) {
- /*
- * FIXME will be properly implemented by another patch (Henrik Paul:
- * 16.6.2014)
- */
- return row;
+ JSONString string = row.get(GridState.JSONKEY_ROWKEY).isString();
+ if (string != null) {
+ return string.stringValue();
+ } else {
+ return null;
+ }
+ }
+
+ public RowHandle<JSONObject> getHandleByKey(Object key) {
+ return new RowHandleImpl(null, key);
}
- };
+ }
+
+ private final RpcDataSource dataSource = new RpcDataSource();
@Override
protected void extend(ServerConnector target) {
dataSource.setEstimatedSize(getState().containerSize);
- ((GridConnector) target).getWidget().setDataSource(dataSource);
+ ((GridConnector) target).setDataSource(dataSource);
registerRpc(DataProviderRpc.class, new DataProviderRpc() {
@Override
diff --git a/client/src/com/vaadin/client/ui/grid/Escalator.java b/client/src/com/vaadin/client/ui/grid/Escalator.java
index 8a1f6f5842..c8feb6d18e 100644
--- a/client/src/com/vaadin/client/ui/grid/Escalator.java
+++ b/client/src/com/vaadin/client/ui/grid/Escalator.java
@@ -2678,6 +2678,9 @@ public class Escalator extends Widget {
@Override
protected void paintRemoveRows(final int index, final int numberOfRows) {
+ if (numberOfRows == 0) {
+ return;
+ }
final Range viewportRange = Range.withLength(
getLogicalRowIndex(visualRowOrder.getFirst()),
diff --git a/client/src/com/vaadin/client/ui/grid/Grid.java b/client/src/com/vaadin/client/ui/grid/Grid.java
index b5461e4a3b..9a75b37c42 100644
--- a/client/src/com/vaadin/client/ui/grid/Grid.java
+++ b/client/src/com/vaadin/client/ui/grid/Grid.java
@@ -48,7 +48,6 @@ import com.vaadin.client.ui.grid.renderers.ComplexRenderer;
import com.vaadin.client.ui.grid.renderers.TextRenderer;
import com.vaadin.client.ui.grid.renderers.WidgetRenderer;
import com.vaadin.client.ui.grid.selection.HasSelectionChangeHandlers;
-import com.vaadin.client.ui.grid.selection.MultiSelectionRenderer;
import com.vaadin.client.ui.grid.selection.SelectionChangeEvent;
import com.vaadin.client.ui.grid.selection.SelectionChangeHandler;
import com.vaadin.client.ui.grid.selection.SelectionModel;
@@ -1061,7 +1060,7 @@ public class Grid<T> extends Composite implements
refreshHeader();
refreshFooter();
- selectionModel = SelectionMode.SINGLE.createModel();
+ setSelectionMode(SelectionMode.SINGLE);
escalator
.addRowVisibilityChangeHandler(new RowVisibilityChangeHandler() {
@@ -1075,6 +1074,16 @@ public class Grid<T> extends Composite implements
}
}
});
+
+ // Default action on SelectionChangeEvents. Refresh the body so changed
+ // become visible.
+ addSelectionChangeHandler(new SelectionChangeHandler() {
+
+ @Override
+ public void onSelectionChange(SelectionChangeEvent<?> event) {
+ refreshBody();
+ }
+ });
}
@Override
@@ -1341,6 +1350,13 @@ public class Grid<T> extends Composite implements
}
/**
+ * Refreshes all body rows
+ */
+ private void refreshBody() {
+ escalator.getBody().refreshRows(0, escalator.getBody().getRowCount());
+ }
+
+ /**
* Refreshes all footer rows
*/
void refreshFooter() {
@@ -1797,6 +1813,15 @@ public class Grid<T> extends Composite implements
}
/**
+ * Gets the {@Link DataSource} for this Grid.
+ *
+ * @return the data source used by this grid
+ */
+ public DataSource<T> getDataSource() {
+ return dataSource;
+ }
+
+ /**
* Sets the rightmost frozen column in the grid.
* <p>
* All columns up to and including the given column will be frozen in place
@@ -2177,7 +2202,7 @@ public class Grid<T> extends Composite implements
/* TODO remove before final */
public void setSelectionCheckboxes(boolean set) {
if (set) {
- setSelectColumnRenderer(new MultiSelectionRenderer(this));
+ setSelectColumnRenderer(selectionModel.getSelectionColumnRenderer());
} else {
setSelectColumnRenderer(null);
}
@@ -2198,6 +2223,8 @@ public class Grid<T> extends Composite implements
/**
* Sets the current selection model.
+ * <p>
+ * This function will call {@link SelectionModel#setGrid(Grid)}.
*
* @param selectionModel
* a selection model implementation.
@@ -2211,6 +2238,7 @@ public class Grid<T> extends Composite implements
}
this.selectionModel = selectionModel;
+ selectionModel.setGrid(this);
}
@@ -2412,14 +2440,4 @@ public class Grid<T> extends Composite implements
fireEvent(new SortEvent<T>(this,
Collections.unmodifiableList(sortOrder)));
}
-
- /**
- * Missing getDataSource method. TODO: remove this and other duplicates
- * after The Merge
- *
- * @return a DataSource reference
- */
- public DataSource<T> getDataSource() {
- return dataSource;
- }
}
diff --git a/client/src/com/vaadin/client/ui/grid/GridConnector.java b/client/src/com/vaadin/client/ui/grid/GridConnector.java
index 0bfcf8ffcd..3b1ecb44d8 100644
--- a/client/src/com/vaadin/client/ui/grid/GridConnector.java
+++ b/client/src/com/vaadin/client/ui/grid/GridConnector.java
@@ -17,9 +17,11 @@
package com.vaadin.client.ui.grid;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -28,13 +30,18 @@ import com.google.gwt.json.client.JSONArray;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONValue;
import com.vaadin.client.communication.StateChangeEvent;
+import com.vaadin.client.data.RpcDataSourceConnector.RpcDataSource;
import com.vaadin.client.ui.AbstractComponentConnector;
import com.vaadin.client.ui.grid.renderers.AbstractRendererConnector;
+import com.vaadin.client.ui.grid.selection.SelectionChangeEvent;
+import com.vaadin.client.ui.grid.selection.SelectionChangeHandler;
+import com.vaadin.client.ui.grid.selection.SelectionModelMulti;
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;
@@ -53,7 +60,66 @@ import com.vaadin.shared.ui.grid.ScrollDestination;
public class GridConnector extends AbstractComponentConnector {
/**
- * Custom implementation of the custom grid column using a String[] to
+ * Hacked SelectionModelMulti to make selection communication work for now.
+ */
+ private class RowKeyBasedMultiSelection extends
+ SelectionModelMulti<JSONObject> {
+
+ private final LinkedHashSet<String> selectedKeys = new LinkedHashSet<String>();
+
+ public List<String> getSelectedKeys() {
+ List<String> keys = new ArrayList<String>();
+ keys.addAll(selectedKeys);
+ return keys;
+ }
+
+ public void updateFromState() {
+ boolean changed = false;
+ Set<String> stateKeys = new LinkedHashSet<String>();
+ stateKeys.addAll(getState().selectedKeys);
+ for (String key : stateKeys) {
+ if (!selectedKeys.contains(key)) {
+ changed = true;
+ selectByHandle(dataSource.getHandleByKey(key));
+ }
+ }
+ for (String key : selectedKeys) {
+ changed = true;
+ if (!stateKeys.contains(key)) {
+ deselectByHandle(dataSource.getHandleByKey(key));
+ }
+ }
+ selectedKeys.clear();
+ selectedKeys.addAll(stateKeys);
+
+ if (changed) {
+ // At least for now there's no way to send the selected and/or
+ // deselected row data. Some data is only stored as keys
+ getWidget().fireEvent(
+ new SelectionChangeEvent<JSONObject>(getWidget(),
+ (List<JSONObject>) null, null));
+ }
+ }
+
+ @Override
+ public boolean select(Collection<JSONObject> rows) {
+ for (JSONObject row : rows) {
+ selectedKeys.add((String) dataSource.getRowKey(row));
+ }
+ return super.select(rows);
+ }
+
+ @Override
+ public boolean deselect(Collection<JSONObject> rows) {
+ for (JSONObject row : rows) {
+ selectedKeys.remove(dataSource.getRowKey(row));
+ }
+ return super.deselect(rows);
+ }
+ }
+
+ /**
+ * Custom implementation of the custom grid column using a JSONObject to
* represent the cell value and String as a column type.
*/
private class CustomGridColumn extends GridColumn<Object, JSONObject> {
@@ -107,6 +173,8 @@ public class GridConnector extends AbstractComponentConnector {
* Maps a generated column id to a grid column instance
*/
private Map<String, CustomGridColumn> columnIdToColumn = new HashMap<String, CustomGridColumn>();
+ private final RowKeyBasedMultiSelection selectionModel = new RowKeyBasedMultiSelection();
+ private RpcDataSource dataSource;
@Override
@SuppressWarnings("unchecked")
@@ -139,6 +207,18 @@ public class GridConnector extends AbstractComponentConnector {
getWidget().scrollToRow(row, destination);
}
});
+
+ getWidget().setSelectionModel(selectionModel);
+
+ getWidget().addSelectionChangeHandler(new SelectionChangeHandler() {
+ @Override
+ public void onSelectionChange(SelectionChangeEvent<?> event) {
+ // TODO change this to diff based. (henrik paul 24.6.2014)
+ getRpcProxy(GridServerRpc.class).selectionChange(
+ selectionModel.getSelectedKeys());
+ }
+ });
+
}
@Override
@@ -211,6 +291,10 @@ public class GridConnector extends AbstractComponentConnector {
if (stateChangeEvent.hasPropertyChanged("heightMode")) {
getWidget().setHeightMode(getState().heightMode);
}
+
+ if (stateChangeEvent.hasPropertyChanged("selectedKeys")) {
+ selectionModel.updateFromState();
+ }
}
/**
@@ -332,4 +416,9 @@ public class GridConnector extends AbstractComponentConnector {
}
}
}
+
+ public void setDataSource(RpcDataSource dataSource) {
+ this.dataSource = dataSource;
+ getWidget().setDataSource(this.dataSource);
+ }
}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java b/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java
index 52bb6c0f60..53b0d064ab 100644
--- a/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java
+++ b/client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java
@@ -17,7 +17,6 @@ package com.vaadin.client.ui.grid.selection;
import java.util.Collection;
import java.util.HashSet;
-import java.util.logging.Logger;
import com.google.gwt.dom.client.BrowserEvents;
import com.google.gwt.dom.client.Element;
@@ -37,7 +36,7 @@ import com.vaadin.client.ui.grid.Grid;
import com.vaadin.client.ui.grid.renderers.ComplexRenderer;
/* This class will probably not survive the final merge of all selection functionality. */
-public class MultiSelectionRenderer extends ComplexRenderer<Boolean> {
+public class MultiSelectionRenderer<T> extends ComplexRenderer<Boolean> {
private class TouchEventHandler implements NativePreviewHandler {
@Override
@@ -168,12 +167,12 @@ public class MultiSelectionRenderer extends ComplexRenderer<Boolean> {
private static final String LOGICAL_ROW_PROPERTY_INT = "vEscalatorLogicalRow";
- private final Grid<?> grid;
+ private final Grid<T> grid;
private HandlerRegistration nativePreviewHandlerRegistration;
private final SelectionHandler selectionHandler = new SelectionHandler();
- public MultiSelectionRenderer(final Grid<?> grid) {
+ public MultiSelectionRenderer(final Grid<T> grid) {
this.grid = grid;
}
@@ -276,23 +275,16 @@ public class MultiSelectionRenderer extends ComplexRenderer<Boolean> {
}
}
- private boolean isSelected(final int logicalRow) {
- // TODO
- // return grid.getSelectionModel().isSelected(logicalRow);
- return false;
+ protected boolean isSelected(final int logicalRow) {
+ return grid.isSelected(grid.getDataSource().getRow(logicalRow));
}
- private void setSelected(final int logicalRow, final boolean select) {
+ protected void setSelected(final int logicalRow, final boolean select) {
+ T row = grid.getDataSource().getRow(logicalRow);
if (select) {
- // TODO
- // grid.getSelectionModel().select(logicalRow);
- Logger.getLogger(getClass().getName()).warning(
- "Selecting " + logicalRow);
+ grid.select(row);
} else {
- // TODO
- // grid.getSelectionModel().deselect(logicalRow);
- Logger.getLogger(getClass().getName()).warning(
- "Deselecting " + logicalRow);
+ grid.deselect(row);
}
}
}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java
index d11b7764d0..989a8946c7 100644
--- a/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java
+++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java
@@ -25,7 +25,7 @@ import com.vaadin.client.ui.grid.Renderer;
* <p>
* Selection models perform tracking of selected rows in the Grid, as well as
* dispatching events when the selection state changes.
- *
+ *
* @author Vaadin Ltd
* @since 7.4
* @param <T>
@@ -36,7 +36,7 @@ public interface SelectionModel<T> {
/**
* Return true if the provided row is considered selected under the
* implementing selection model.
- *
+ *
* @param row
* row object instance
* @return <code>true</code>, if the row given as argument is considered
@@ -47,18 +47,18 @@ public interface SelectionModel<T> {
/**
* Return the {@link Renderer} responsible for rendering the selection
* column.
- *
+ *
* @return a renderer instance. If null is returned, a selection column will
* not be drawn.
*/
- public Renderer<T> getSelectionColumnRenderer();
+ public Renderer<Boolean> getSelectionColumnRenderer();
/**
* Tells this SelectionModel which Grid it belongs to.
* <p>
* Implementations are free to have this be a no-op. This method is called
* internally by Grid.
- *
+ *
* @param grid
* a {@link Grid} instance
*/
@@ -74,7 +74,7 @@ public interface SelectionModel<T> {
/**
* Returns a Collection containing all selected rows.
- *
+ *
* @return a non-null collection.
*/
public Collection<T> getSelectedRows();
@@ -82,7 +82,7 @@ public interface SelectionModel<T> {
/**
* Selection model that allows a maximum of one row to be selected at any
* one time.
- *
+ *
* @param <T>
* type parameter corresponding with Grid row type
*/
@@ -90,7 +90,7 @@ public interface SelectionModel<T> {
/**
* Selects a row.
- *
+ *
* @param row
* a {@link Grid} row object
* @return true, if this row as not previously selected.
@@ -101,7 +101,7 @@ public interface SelectionModel<T> {
* Deselects a row.
* <p>
* This is a no-op unless {@link row} is the currently selected row.
- *
+ *
* @param row
* a {@link Grid} row object
* @return true, if the currently selected row was deselected.
@@ -110,7 +110,7 @@ public interface SelectionModel<T> {
/**
* Returns the currently selected row.
- *
+ *
* @return a {@link Grid} row object or null, if nothing is selected.
*/
public T getSelectedRow();
@@ -119,7 +119,7 @@ public interface SelectionModel<T> {
/**
* Selection model that allows for several rows to be selected at once.
- *
+ *
* @param <T>
* type parameter corresponding with Grid row type
*/
@@ -127,7 +127,7 @@ public interface SelectionModel<T> {
/**
* Selects one or more rows.
- *
+ *
* @param rows
* {@link Grid} row objects
* @return true, if the set of selected rows was changed.
@@ -136,7 +136,7 @@ public interface SelectionModel<T> {
/**
* Deselects one or more rows.
- *
+ *
* @param rows
* Grid row objects
* @return true, if the set of selected rows was changed.
@@ -145,14 +145,14 @@ public interface SelectionModel<T> {
/**
* De-selects all rows.
- *
+ *
* @return true, if any row was previously selected.
*/
public boolean deselectAll();
/**
* Select all rows in a {@link Collection}.
- *
+ *
* @param rows
* a collection of Grid row objects
* @return true, if the set of selected rows was changed.
@@ -161,7 +161,7 @@ public interface SelectionModel<T> {
/**
* Deselect all rows in a {@link Collection}.
- *
+ *
* @param rows
* a collection of Grid row objects
* @return true, if the set of selected rows was changed.
@@ -173,7 +173,7 @@ public interface SelectionModel<T> {
/**
* Interface for a selection model that does not allow anything to be
* selected.
- *
+ *
* @param <T>
* type parameter corresponding with Grid row type
*/
diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java
index 8afb592771..de62dc9cbc 100644
--- a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java
+++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java
@@ -17,38 +17,38 @@ package com.vaadin.client.ui.grid.selection;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
+import com.vaadin.client.data.DataSource.RowHandle;
import com.vaadin.client.ui.grid.Grid;
import com.vaadin.client.ui.grid.Renderer;
/**
* Multi-row selection model.
- *
+ *
* @author Vaadin Ltd
* @since 7.4
*/
public class SelectionModelMulti<T> implements SelectionModel.Multi<T> {
- private final Renderer<T> renderer;
- private final Set<T> selectedRows;
+ private final Set<RowHandle<T>> selectedRows;
+ private Renderer<Boolean> renderer;
private Grid<T> grid;
public SelectionModelMulti() {
grid = null;
renderer = null;
- selectedRows = new LinkedHashSet<T>();
+ selectedRows = new LinkedHashSet<RowHandle<T>>();
}
@Override
public boolean isSelected(T row) {
- return selectedRows.contains(row);
+ return isSelectedByHandle(grid.getDataSource().getHandle(row));
}
@Override
- public Renderer<T> getSelectionColumnRenderer() {
+ public Renderer<Boolean> getSelectionColumnRenderer() {
return renderer;
}
@@ -64,6 +64,8 @@ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> {
throw new IllegalStateException(
"Grid reference cannot be reassigned");
}
+
+ this.renderer = new MultiSelectionRenderer<T>(grid);
}
@Override
@@ -87,7 +89,7 @@ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> {
if (selectedRows.size() > 0) {
SelectionChangeEvent<T> event = new SelectionChangeEvent<T>(grid,
- null, selectedRows);
+ null, getSelectedRows());
selectedRows.clear();
grid.fireEvent(event);
@@ -105,7 +107,8 @@ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> {
Set<T> added = new LinkedHashSet<T>();
for (T row : rows) {
- if (selectedRows.add(row)) {
+ RowHandle<T> handle = grid.getDataSource().getHandle(row);
+ if (selectByHandle(handle)) {
added.add(row);
}
}
@@ -127,7 +130,7 @@ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> {
Set<T> removed = new LinkedHashSet<T>();
for (T row : rows) {
- if (selectedRows.remove(row)) {
+ if (deselectByHandle(grid.getDataSource().getHandle(row))) {
removed.add(row);
}
}
@@ -140,14 +143,37 @@ public class SelectionModelMulti<T> implements SelectionModel.Multi<T> {
return false;
}
+ protected boolean isSelectedByHandle(RowHandle<T> handle) {
+ return selectedRows.contains(handle);
+ }
+
+ protected boolean selectByHandle(RowHandle<T> handle) {
+ if (selectedRows.add(handle)) {
+ handle.pin();
+ return true;
+ }
+ return false;
+ }
+
+ protected boolean deselectByHandle(RowHandle<T> handle) {
+ if (selectedRows.remove(handle)) {
+ handle.unpin();
+ return true;
+ }
+ return false;
+ }
+
@Override
public Collection<T> getSelectedRows() {
- return Collections.unmodifiableSet(selectedRows);
+ Set<T> selected = new LinkedHashSet<T>();
+ for (RowHandle<T> handle : selectedRows) {
+ selected.add(handle.getRow());
+ }
+ return selected;
}
@Override
public void reset() {
deselectAll();
}
-
}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java
index bcb0357089..93dfb49df2 100644
--- a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java
+++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java
@@ -23,7 +23,7 @@ import com.vaadin.client.ui.grid.Renderer;
/**
* No-row selection model.
- *
+ *
* @author Vaadin Ltd
* @since 7.4
*/
@@ -35,7 +35,7 @@ public class SelectionModelNone<T> implements SelectionModel.None<T> {
}
@Override
- public Renderer<T> getSelectionColumnRenderer() {
+ public Renderer<Boolean> getSelectionColumnRenderer() {
return null;
}
diff --git a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java
index 6b5f645e23..775e1878c5 100644
--- a/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java
+++ b/client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java
@@ -18,30 +18,31 @@ package com.vaadin.client.ui.grid.selection;
import java.util.Collection;
import java.util.Collections;
+import com.vaadin.client.data.DataSource.RowHandle;
import com.vaadin.client.ui.grid.Grid;
import com.vaadin.client.ui.grid.Renderer;
/**
* Single-row selection model.
- *
+ *
* @author Vaadin Ltd
* @since 7.4
*/
public class SelectionModelSingle<T> implements SelectionModel.Single<T> {
private Grid<T> grid;
- private T selectedRow;
+ private RowHandle<T> selectedRow;
+ private Renderer<Boolean> renderer;
@Override
public boolean isSelected(T row) {
- return row == null ? null : row.equals(getSelectedRow());
+ return selectedRow != null
+ && selectedRow.equals(grid.getDataSource().getHandle(row));
}
@Override
- public Renderer<T> getSelectionColumnRenderer() {
- // TODO: Add implementation of SelectionColumnRenderer; currently none
- // exists
- return null;
+ public Renderer<Boolean> getSelectionColumnRenderer() {
+ return renderer;
}
@Override
@@ -56,6 +57,7 @@ public class SelectionModelSingle<T> implements SelectionModel.Single<T> {
throw new IllegalStateException(
"Grid reference cannot be reassigned");
}
+ renderer = new MultiSelectionRenderer<T>(grid);
}
@Override
@@ -65,12 +67,17 @@ public class SelectionModelSingle<T> implements SelectionModel.Single<T> {
throw new IllegalArgumentException("Row cannot be null");
}
- if (row.equals(getSelectedRow())) {
+ if (isSelected(row)) {
return false;
}
- T removed = selectedRow;
- selectedRow = row;
+ T removed = getSelectedRow();
+ if (selectedRow != null) {
+ selectedRow.unpin();
+ }
+ selectedRow = grid.getDataSource().getHandle(row);
+ selectedRow.pin();
+
grid.fireEvent(new SelectionChangeEvent<T>(grid, row, removed));
return true;
@@ -83,8 +90,9 @@ public class SelectionModelSingle<T> implements SelectionModel.Single<T> {
throw new IllegalArgumentException("Row cannot be null");
}
- if (row.equals(selectedRow)) {
- T removed = selectedRow;
+ if (isSelected(row)) {
+ T removed = selectedRow.getRow();
+ selectedRow.unpin();
selectedRow = null;
grid.fireEvent(new SelectionChangeEvent<T>(grid, null, removed));
return true;
@@ -95,16 +103,15 @@ public class SelectionModelSingle<T> implements SelectionModel.Single<T> {
@Override
public T getSelectedRow() {
- return selectedRow;
+ return (selectedRow != null ? selectedRow.getRow() : null);
}
@Override
public void reset() {
- T removed = selectedRow;
- selectedRow = null;
+ T removed = getSelectedRow();
if (removed != null) {
- grid.fireEvent(new SelectionChangeEvent<T>(grid, null, removed));
+ deselect(removed);
}
}
diff --git a/server/src/com/vaadin/data/RpcDataProviderExtension.java b/server/src/com/vaadin/data/RpcDataProviderExtension.java
index 0046b256bb..1834822d99 100644
--- a/server/src/com/vaadin/data/RpcDataProviderExtension.java
+++ b/server/src/com/vaadin/data/RpcDataProviderExtension.java
@@ -17,6 +17,7 @@
package com.vaadin.data;
import java.io.Serializable;
+import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
@@ -24,11 +25,14 @@ import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import com.google.gwt.thirdparty.guava.common.collect.BiMap;
+import com.google.gwt.thirdparty.guava.common.collect.HashBiMap;
import com.vaadin.data.Container.Indexed;
import com.vaadin.data.Container.Indexed.ItemAddEvent;
import com.vaadin.data.Container.Indexed.ItemRemoveEvent;
@@ -63,6 +67,227 @@ import com.vaadin.ui.components.grid.Renderer;
public class RpcDataProviderExtension extends AbstractExtension {
/**
+ * ItemId to Key to ItemId mapper.
+ * <p>
+ * This class is used when transmitting information about items in container
+ * related to Grid. It introduces a consistent way of mapping ItemIds and
+ * its container to a String that can be mapped back to ItemId.
+ * <p>
+ * <em>Technical note:</em> This class also keeps tabs on which indices are
+ * being shown/selected, and is able to clean up after itself once the
+ * itemId &lrarr; key mapping is not needed anymore. In other words, this
+ * doesn't leak memory.
+ */
+ public class DataProviderKeyMapper {
+ private final BiMap<Integer, Object> indexToItemId = HashBiMap.create();
+ private final BiMap<Object, String> itemIdToKey = HashBiMap.create();
+ private Set<Object> pinnedItemIds = new HashSet<Object>();
+ private Range activeRange = Range.withLength(0, 0);
+ private long rollingIndex = 0;
+
+ private DataProviderKeyMapper() {
+ // private implementation
+ }
+
+ void preActiveRowsChange(Range newActiveRange, int firstNewIndex,
+ List<?> itemIds) {
+ final Range[] removed = activeRange.partitionWith(newActiveRange);
+ final Range[] added = newActiveRange.partitionWith(activeRange);
+
+ removeActiveRows(removed[0]);
+ removeActiveRows(removed[2]);
+ addActiveRows(added[0], firstNewIndex, itemIds);
+ addActiveRows(added[2], firstNewIndex, itemIds);
+
+ activeRange = newActiveRange;
+ }
+
+ private void removeActiveRows(final Range deprecated) {
+ for (int i = deprecated.getStart(); i < deprecated.getEnd(); i++) {
+ final Integer ii = Integer.valueOf(i);
+ final Object itemId = indexToItemId.get(ii);
+
+ if (!pinnedItemIds.contains(itemId)) {
+ itemIdToKey.remove(itemId);
+ }
+ indexToItemId.remove(ii);
+ }
+ }
+
+ private void addActiveRows(final Range added, int firstNewIndex,
+ List<?> newItemIds) {
+
+ for (int i = added.getStart(); i < added.getEnd(); i++) {
+
+ /*
+ * We might be in a situation we have an index <-> itemId entry
+ * already. This happens when something was selected, scrolled
+ * out of view and now we're scrolling it back into view. It's
+ * unnecessary to overwrite it in that case.
+ *
+ * Fun thought: considering branch prediction, it _might_ even
+ * be a bit faster to simply always run the code beyond this
+ * if-state. But it sounds too stupid (and most often too
+ * insignificant) to try out.
+ */
+ final Integer ii = Integer.valueOf(i);
+ if (indexToItemId.containsKey(ii)) {
+ continue;
+ }
+
+ /*
+ * We might be in a situation where we have an itemId <-> key
+ * entry already, but no index for it. This happens when
+ * something that is out of view is selected programmatically.
+ * In that case, we only want to add an index for that entry,
+ * and not overwrite the key.
+ */
+ final Object itemId = newItemIds.get(i - firstNewIndex);
+ if (!itemIdToKey.containsKey(itemId)) {
+ itemIdToKey.put(itemId, nextKey());
+ }
+ indexToItemId.put(ii, itemId);
+ }
+ }
+
+ private String nextKey() {
+ return String.valueOf(rollingIndex++);
+ }
+
+ String getKey(Object itemId) {
+ String key = itemIdToKey.get(itemId);
+ if (key == null) {
+ key = nextKey();
+ itemIdToKey.put(itemId, key);
+ }
+ return key;
+ }
+
+ /**
+ * Gets keys for a collection of item ids.
+ * <p>
+ * If the itemIds are currently cached, the existing keys will be used.
+ * Otherwise new ones will be created.
+ *
+ * @param itemIds
+ * the item ids for which to get keys
+ * @return keys for the {@code itemIds}
+ */
+ public List<String> getKeys(Collection<Object> itemIds) {
+ if (itemIds == null) {
+ throw new IllegalArgumentException("itemIds can't be null");
+ }
+
+ ArrayList<String> keys = new ArrayList<String>(itemIds.size());
+ for (Object itemId : itemIds) {
+ keys.add(getKey(itemId));
+ }
+ return keys;
+ }
+
+ Object getItemId(String key) throws IllegalStateException {
+ Object itemId = itemIdToKey.inverse().get(key);
+ if (itemId != null) {
+ return itemId;
+ } else {
+ throw new IllegalStateException("No item id for key " + key
+ + " found.");
+ }
+ }
+
+ /**
+ * Gets corresponding item ids for each of the keys in a collection.
+ *
+ * @param keys
+ * the keys for which to retrieve item ids
+ * @return a collection of item ids for the {@code keys}
+ * @throws IllegalStateException
+ * if one or more of keys don't have a corresponding item id
+ * in the cache
+ */
+ public Collection<Object> getItemIds(Collection<String> keys)
+ throws IllegalStateException {
+ if (keys == null) {
+ throw new IllegalArgumentException("keys may not be null");
+ }
+
+ ArrayList<Object> itemIds = new ArrayList<Object>(keys.size());
+ for (String key : keys) {
+ itemIds.add(getItemId(key));
+ }
+ return itemIds;
+ }
+
+ /**
+ * Pin an item id to be cached indefinitely.
+ * <p>
+ * Normally when an itemId is not an active row, it is discarded from
+ * the cache. Pinning an item id will make sure that it is kept in the
+ * cache.
+ * <p>
+ * In effect, while an item id is pinned, it always has the same key.
+ *
+ * @param itemId
+ * the item id to pin
+ * @throws IllegalStateException
+ * if {@code itemId} was already pinned
+ * @see #unpin(Object)
+ * @see #isPinned(Object)
+ * @see #getItemIds(Collection)
+ */
+ public void pin(Object itemId) throws IllegalStateException {
+ if (isPinned(itemId)) {
+ throw new IllegalStateException("Item id " + itemId
+ + " was pinned already");
+ }
+ pinnedItemIds.add(itemId);
+ }
+
+ /**
+ * Unpin an item id.
+ * <p>
+ * This cancels the effect of pinning an item id. If the item id is
+ * currently inactive, it will be immediately removed from the cache.
+ *
+ * @param itemId
+ * the item id to unpin
+ * @throws IllegalStateException
+ * if {@code itemId} was not pinned
+ * @see #pin(Object)
+ * @see #isPinned(Object)
+ * @see #getItemIds(Collection)
+ */
+ public void unpin(Object itemId) throws IllegalStateException {
+ if (!isPinned(itemId)) {
+ throw new IllegalStateException("Item id " + itemId
+ + " was not pinned");
+ }
+
+ pinnedItemIds.remove(itemId);
+ final Integer removedIndex = indexToItemId.inverse().remove(itemId);
+ if (removedIndex == null
+ || !activeRange.contains(removedIndex.intValue())) {
+ itemIdToKey.remove(itemId);
+ }
+ }
+
+ /**
+ * Checks whether an item id is pinned or not.
+ *
+ * @param itemId
+ * the item id to check for pin status
+ * @return {@code true} iff the item id is currently pinned
+ */
+ public boolean isPinned(Object itemId) {
+ return pinnedItemIds.contains(itemId);
+ }
+
+ Object itemIdAtIndex(int index) {
+ return indexToItemId.inverse().get(Integer.valueOf(index));
+ }
+ }
+
+ /**
* A helper class that handles the client-side Escalator logic relating to
* making sure that whatever is currently visible to the user, is properly
* initialized and otherwise handled on the server side (as far as
@@ -70,8 +295,9 @@ public class RpcDataProviderExtension extends AbstractExtension {
* <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>listening to the currently visible {@link com.vaadin.data.Property
+ * Properties'} value changes on the server side and sending those back to
+ * the client; and
* <li>attaching and detaching {@link com.vaadin.ui.Component Components}
* from the Vaadin Component hierarchy.
* </ul>
@@ -340,7 +566,7 @@ public class RpcDataProviderExtension extends AbstractExtension {
ItemRemoveEvent removeEvent = (ItemRemoveEvent) event;
int firstIndex = removeEvent.getFirstIndex();
int count = removeEvent.getRemovedItemsCount();
- removeRowData(firstIndex, count, removeEvent.getFirstItemId());
+ removeRowData(firstIndex, count);
}
else {
@@ -353,6 +579,8 @@ public class RpcDataProviderExtension extends AbstractExtension {
}
};
+ private final DataProviderKeyMapper keyMapper = new DataProviderKeyMapper();
+
/**
* Creates a new data provider using the given container.
*
@@ -366,8 +594,6 @@ public class RpcDataProviderExtension extends AbstractExtension {
@Override
public void requestRows(int firstRow, int numberOfRows,
int firstCachedRowIndex, int cacheSize) {
- pushRows(firstRow, numberOfRows);
-
Range active = Range.withLength(firstRow, numberOfRows);
if (cacheSize != 0) {
Range cached = Range.withLength(firstCachedRowIndex,
@@ -375,6 +601,11 @@ public class RpcDataProviderExtension extends AbstractExtension {
active = active.combineWith(cached);
}
+ List<?> itemIds = RpcDataProviderExtension.this.container
+ .getItemIds(firstRow, numberOfRows);
+ keyMapper.preActiveRowsChange(active, firstRow, itemIds);
+ pushRows(firstRow, itemIds);
+
activeRowHandler.setActiveRows(active.getStart(),
active.length());
}
@@ -389,8 +620,7 @@ public class RpcDataProviderExtension extends AbstractExtension {
}
- private void pushRows(int firstRow, int numberOfRows) {
- List<?> itemIds = container.getItemIds(firstRow, numberOfRows);
+ private void pushRows(int firstRow, List<?> itemIds) {
Collection<?> propertyIds = container.getContainerPropertyIds();
JSONArray rows = new JSONArray();
for (Object itemId : itemIds) {
@@ -402,6 +632,7 @@ public class RpcDataProviderExtension extends AbstractExtension {
private JSONObject getRowData(Collection<?> propertyIds, Object itemId) {
Item item = container.getItem(itemId);
+ String[] row = new String[propertyIds.size()];
JSONArray rowData = new JSONArray();
@@ -421,13 +652,7 @@ public class RpcDataProviderExtension extends AbstractExtension {
final JSONObject rowObject = new JSONObject();
rowObject.put(GridState.JSONKEY_DATA, rowData);
- /*
- * TODO: selection wants to put here something in the lines of:
- *
- * rowObject.put(GridState.JSONKEY_ROWKEY, getKey(itemId))
- *
- * Henrik Paul: 18.6.2014
- */
+ rowObject.put(GridState.JSONKEY_ROWKEY, keyMapper.getKey(itemId));
return rowObject;
} catch (final JSONException e) {
throw new RuntimeException("Grid was unable to serialize "
@@ -477,23 +702,16 @@ public class RpcDataProviderExtension extends AbstractExtension {
* @param firstItemId
* the item id of the first removed item
*/
- private void removeRowData(int firstIndex, int count, Object firstItemId) {
+ private void removeRowData(int firstIndex, int count) {
getState().containerSize -= count;
getRpcProxy(DataProviderRpc.class).removeRowData(firstIndex, count);
- /*
- * Unfortunately, there's no sane way of getting the rest of the removed
- * itemIds unless we cache a mapping between index and itemId.
- *
- * 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(firstItemId);
+ for (int i = 0; i < count; i++) {
+ Object itemId = keyMapper.itemIdAtIndex(firstIndex + i);
+ if (itemId != null) {
+ activeRowHandler.removeItemId(itemId);
+ }
+ }
}
/**
@@ -566,6 +784,10 @@ public class RpcDataProviderExtension extends AbstractExtension {
activeRowHandler.propertiesAdded(addedPropertyIds);
}
+ public DataProviderKeyMapper getKeyMapper() {
+ return keyMapper;
+ }
+
protected Grid getGrid() {
return (Grid) getParent();
}
diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java
index 1ebf227330..bc6a69e850 100644
--- a/server/src/com/vaadin/ui/components/grid/Grid.java
+++ b/server/src/com/vaadin/ui/components/grid/Grid.java
@@ -26,16 +26,23 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+import com.google.gwt.thirdparty.guava.common.collect.Sets.SetView;
import com.vaadin.data.Container;
import com.vaadin.data.Container.PropertySetChangeEvent;
import com.vaadin.data.Container.PropertySetChangeListener;
import com.vaadin.data.Container.PropertySetChangeNotifier;
import com.vaadin.data.Container.Sortable;
import com.vaadin.data.RpcDataProviderExtension;
+import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper;
import com.vaadin.server.KeyMapper;
import com.vaadin.shared.ui.grid.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.HeightMode;
import com.vaadin.shared.ui.grid.ScrollDestination;
@@ -181,6 +188,15 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier {
*/
private SelectionModel selectionModel;
+ /**
+ * The number of times to ignore selection state sync to the client.
+ * <p>
+ * This usually means that the client side has modified the selection. We
+ * still want to inform the listeners that the selection has changed, but we
+ * don't want to send those changes "back to the client".
+ */
+ private int ignoreSelectionClientSync = 0;
+
private static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(SelectionChangeListener.class, "selectionChange",
SelectionChangeEvent.class);
@@ -191,9 +207,105 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier {
* @param datasource
* the data source for the grid
*/
- public Grid(Container.Indexed datasource) {
+ public Grid(final Container.Indexed datasource) {
setContainerDataSource(datasource);
setSelectionMode(SelectionMode.MULTI);
+ addSelectionChangeListener(new SelectionChangeListener() {
+ @Override
+ public void selectionChange(SelectionChangeEvent event) {
+ for (Object removedItemId : event.getRemoved()) {
+ keyMapper().unpin(removedItemId);
+ }
+
+ for (Object addedItemId : event.getAdded()) {
+ keyMapper().pin(addedItemId);
+ }
+
+ List<String> keys = keyMapper().getKeys(getSelectedRows());
+
+ boolean markAsDirty = true;
+
+ /*
+ * If this clause is true, it means that the selection event
+ * originated from the client. This means that we don't want to
+ * send the changes back to the client (markAsDirty => false).
+ */
+ if (ignoreSelectionClientSync > 0) {
+ ignoreSelectionClientSync--;
+ markAsDirty = false;
+
+ try {
+
+ /*
+ * Make sure that the diffstate is aware of the
+ * "undirty" modification, so that the diffs are
+ * calculated correctly the next time we actually want
+ * to send the selection state to the client.
+ */
+ getUI().getConnectorTracker().getDiffState(Grid.this)
+ .put("selectedKeys", new JSONArray(keys));
+ } catch (JSONException e) {
+ throw new RuntimeException("Internal error", e);
+ }
+ }
+
+ getState(markAsDirty).selectedKeys = keys;
+ }
+ });
+
+ registerRpc(new GridServerRpc() {
+
+ @Override
+ public void selectionChange(List<String> selection) {
+ final HashSet<Object> newSelection = new HashSet<Object>(
+ keyMapper().getItemIds(selection));
+ final HashSet<Object> oldSelection = new HashSet<Object>(
+ getSelectedRows());
+
+ SetView<Object> addedItemIds = Sets.difference(newSelection,
+ oldSelection);
+ SetView<Object> removedItemIds = Sets.difference(oldSelection,
+ newSelection);
+
+ if (!addedItemIds.isEmpty()) {
+ /*
+ * Since these changes come from the client, we want to
+ * modify the selection model and get that event fired to
+ * all the listeners. One of the listeners is our internal
+ * selection listener, and this tells it not to send the
+ * selection event back to the client.
+ */
+ ignoreSelectionClientSync++;
+
+ if (addedItemIds.size() == 1) {
+ select(addedItemIds.iterator().next());
+ } else {
+ assert getSelectionModel() instanceof SelectionModel.Multi : "Got multiple selections, but the selection model is not a SelectionModel.Multi";
+ ((SelectionModel.Multi) getSelectionModel())
+ .select(addedItemIds);
+ }
+ }
+
+ if (!removedItemIds.isEmpty()) {
+ /*
+ * Since these changes come from the client, we want to
+ * modify the selection model and get that event fired to
+ * all the listeners. One of the listeners is our internal
+ * selection listener, and this tells it not to send the
+ * selection event back to the client.
+ */
+ ignoreSelectionClientSync++;
+
+ if (removedItemIds.size() == 1) {
+ deselect(removedItemIds.iterator().next());
+ } else {
+ assert getSelectionModel() instanceof SelectionModel.Multi : "Got multiple deselections, but the selection model is not a SelectionModel.Multi";
+ ((SelectionModel.Multi) getSelectionModel())
+ .deselect(removedItemIds);
+ }
+ }
+ }
+ });
}
/**
@@ -205,6 +317,7 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier {
* if the data source is null
*/
public void setContainerDataSource(Container.Indexed container) {
+
if (container == null) {
throw new IllegalArgumentException(
"Cannot set the datasource to null");
@@ -935,12 +1048,22 @@ public class Grid extends AbstractComponent implements SelectionChangeNotifier {
SELECTION_CHANGE_METHOD);
}
- /** FIXME remove once selection mode communcation is done. only for testing. */
+ /**
+ * FIXME remove once selection mode communication is done. only for testing.
+ */
public void setSelectionCheckboxes(boolean value) {
getState().selectionCheckboxes = value;
}
/**
+ * A shortcut for
+ * <code>{@link #datasourceExtension}.{@link com.vaadin.data.RpcDataProviderExtension#getKeyMapper() getKeyMapper()}</code>
+ */
+ private DataProviderKeyMapper keyMapper() {
+ return datasourceExtension.getKeyMapper();
+ }
+
+ /**
* Adds a renderer to this grid's connector hierarchy.
*
* @param renderer
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java
index cecdca80df..f0e25405cc 100644
--- a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java
+++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java
@@ -17,7 +17,7 @@ package com.vaadin.ui.components.grid.selection;
import java.util.Collection;
import java.util.EventObject;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.Set;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
@@ -32,14 +32,14 @@ import com.vaadin.ui.components.grid.Grid;
*/
public class SelectionChangeEvent extends EventObject {
- private Set<Object> oldSelection;
- private Set<Object> newSelection;
+ private LinkedHashSet<Object> oldSelection;
+ private LinkedHashSet<Object> newSelection;
public SelectionChangeEvent(Grid source, Collection<Object> oldSelection,
Collection<Object> newSelection) {
super(source);
- this.oldSelection = new HashSet<Object>(oldSelection);
- this.newSelection = new HashSet<Object>(newSelection);
+ this.oldSelection = new LinkedHashSet<Object>(oldSelection);
+ this.newSelection = new LinkedHashSet<Object>(newSelection);
}
/**
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
new file mode 100644
index 0000000000..9ecf131c5b
--- /dev/null
+++ b/server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.Container;
+import com.vaadin.data.Container.Indexed;
+import com.vaadin.data.Item;
+import com.vaadin.data.Property;
+import com.vaadin.data.RpcDataProviderExtension;
+import com.vaadin.data.RpcDataProviderExtension.DataProviderKeyMapper;
+import com.vaadin.data.util.IndexedContainer;
+
+public class DataProviderExtension {
+ private RpcDataProviderExtension dataProvider;
+ private DataProviderKeyMapper keyMapper;
+ private Container.Indexed container;
+
+ private static final Object ITEM_ID1 = "itemid1";
+ private static final Object ITEM_ID2 = "itemid2";
+ private static final Object ITEM_ID3 = "itemid3";
+
+ private static final Object PROPERTY_ID1_STRING = "property1";
+
+ @Before
+ public void setup() {
+ container = new IndexedContainer();
+ populate(container);
+
+ dataProvider = new RpcDataProviderExtension(container);
+ keyMapper = dataProvider.getKeyMapper();
+ }
+
+ private static void populate(Indexed container) {
+ container.addContainerProperty(PROPERTY_ID1_STRING, String.class, "");
+ for (Object itemId : Arrays.asList(ITEM_ID1, ITEM_ID2, ITEM_ID3)) {
+ final Item item = container.addItem(itemId);
+ @SuppressWarnings("unchecked")
+ final Property<String> stringProperty = item
+ .getItemProperty(PROPERTY_ID1_STRING);
+ stringProperty.setValue(itemId.toString());
+ }
+ }
+
+ @Test
+ public void pinBasics() {
+ assertFalse("itemId1 should not start as pinned",
+ keyMapper.isPinned(ITEM_ID2));
+
+ keyMapper.pin(ITEM_ID1);
+ assertTrue("itemId1 should now be pinned", keyMapper.isPinned(ITEM_ID1));
+
+ keyMapper.unpin(ITEM_ID1);
+ assertFalse("itemId1 should not be pinned anymore",
+ keyMapper.isPinned(ITEM_ID2));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void doublePinning() {
+ keyMapper.pin(ITEM_ID1);
+ keyMapper.pin(ITEM_ID1);
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void nonexistentUnpin() {
+ keyMapper.unpin(ITEM_ID1);
+ }
+}
diff --git a/shared/src/com/vaadin/shared/data/DataProviderRpc.java b/shared/src/com/vaadin/shared/data/DataProviderRpc.java
index a92ffe0421..43469914e5 100644
--- a/shared/src/com/vaadin/shared/data/DataProviderRpc.java
+++ b/shared/src/com/vaadin/shared/data/DataProviderRpc.java
@@ -34,7 +34,8 @@ public interface DataProviderRpc extends ClientRpc {
*
* <pre>
* [{
- * "d": [COL_1_JSON, COL_2_json, ...]
+ * "d": [COL_1_JSON, COL_2_json, ...],
+ * "k": "1"
* },
* ...
* ]
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
new file mode 100644
index 0000000000..b763174e53
--- /dev/null
+++ b/shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.shared.ui.grid;
+
+import java.util.List;
+
+import com.vaadin.shared.communication.ServerRpc;
+
+/**
+ * Client-to-server RPC interface for the Grid component
+ *
+ * @since 7.4
+ * @author Vaadin Ltd
+ */
+public interface GridServerRpc extends ServerRpc {
+ void selectionChange(List<String> newSelection);
+}
diff --git a/shared/src/com/vaadin/shared/ui/grid/GridState.java b/shared/src/com/vaadin/shared/ui/grid/GridState.java
index eceaedd1fc..0b23e2c11d 100644
--- a/shared/src/com/vaadin/shared/ui/grid/GridState.java
+++ b/shared/src/com/vaadin/shared/ui/grid/GridState.java
@@ -39,11 +39,18 @@ public class GridState extends AbstractComponentState {
/**
* The key in which a row's data can be found
- * {@link com.vaadin.shared.data.DataProviderRpc#setRowData(int, List)
- * DataProviderRpc.setRowData(int, List)}
+ *
+ * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String)
*/
public static final String JSONKEY_DATA = "d";
+ /**
+ * The key in which a row's own key can be found
+ *
+ * @see com.vaadin.shared.data.DataProviderRpc#setRowData(int, String)
+ */
+ public static final String JSONKEY_ROWKEY = "k";
+
{
// FIXME Grid currently does not support undefined size
width = "400px";
@@ -97,4 +104,7 @@ public class GridState extends AbstractComponentState {
@DelegateToWidget
public boolean selectionCheckboxes;
+ // instantiated just to avoid NPEs
+ public List<String> selectedKeys = new ArrayList<String>();
+
}
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
index 06fe088dee..c6597ef23b 100644
--- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
+++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
@@ -363,6 +363,20 @@ public class GridBasicFeatures extends AbstractComponentTest<Grid> {
}
}
}, null);
+
+ createBooleanAction("Select first row", "Body rows", false,
+ new Command<Grid, Boolean>() {
+ @Override
+ public void execute(Grid grid, Boolean select, Object data) {
+ final Object firstItemId = grid
+ .getContainerDatasource().firstItemId();
+ if (select.booleanValue()) {
+ grid.select(firstItemId);
+ } else {
+ grid.deselect(firstItemId);
+ }
+ }
+ });
}
@SuppressWarnings("boxing")
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java
index a11b0f1be9..3dc8ac814f 100644
--- a/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java
+++ b/uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java
@@ -18,6 +18,7 @@ package com.vaadin.tests.components.grid;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsNot.not;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.util.ArrayList;
@@ -301,6 +302,83 @@ public class GridBasicFeaturesTest extends MultiBrowserTest {
"modified: Column0", getBodyCellByRowAndColumn(0, 0).getText());
}
+ @Test
+ public void testSelectOnOff() throws Exception {
+ openTestURL();
+
+ assertFalse("row shouldn't start out as selected",
+ isSelected(getRow(0)));
+ toggleFirstRowSelection();
+ assertTrue("row should become selected", isSelected(getRow(0)));
+ toggleFirstRowSelection();
+ assertFalse("row shouldn't remain selected", isSelected(getRow(0)));
+ }
+
+ @Test
+ public void testSelectOnScrollOffScroll() throws Exception {
+ openTestURL();
+ assertFalse("row shouldn't start out as selected",
+ isSelected(getRow(0)));
+ toggleFirstRowSelection();
+ assertTrue("row should become selected", isSelected(getRow(0)));
+
+ scrollGridVerticallyTo(10000); // make sure the row is out of cache
+ scrollGridVerticallyTo(0); // scroll it back into view
+
+ assertTrue("row should still be selected when scrolling "
+ + "back into view", isSelected(getRow(0)));
+ }
+
+ @Test
+ public void testSelectScrollOnScrollOff() throws Exception {
+ openTestURL();
+ assertFalse("row shouldn't start out as selected",
+ isSelected(getRow(0)));
+
+ scrollGridVerticallyTo(10000); // make sure the row is out of cache
+ toggleFirstRowSelection();
+
+ scrollGridVerticallyTo(0); // scroll it back into view
+ assertTrue("row should still be selected when scrolling "
+ + "back into view", isSelected(getRow(0)));
+
+ toggleFirstRowSelection();
+ assertFalse("row shouldn't remain selected", isSelected(getRow(0)));
+ }
+
+ @Test
+ public void testSelectScrollOnOffScroll() throws Exception {
+ openTestURL();
+ assertFalse("row shouldn't start out as selected",
+ isSelected(getRow(0)));
+
+ scrollGridVerticallyTo(10000); // make sure the row is out of cache
+ toggleFirstRowSelection();
+ toggleFirstRowSelection();
+
+ scrollGridVerticallyTo(0); // make sure the row is out of cache
+ assertFalse("row shouldn't be selected when scrolling "
+ + "back into view", isSelected(getRow(0)));
+ }
+
+ private void toggleFirstRowSelection() {
+ selectMenuPath("Component", "Body rows", "Select first row");
+ }
+
+ @SuppressWarnings("static-method")
+ private boolean isSelected(TestBenchElement row) {
+ /*
+ * FIXME We probably should get a GridRow instead of a plain
+ * TestBenchElement, that has an "isSelected" thing integrated. (henrik
+ * paul 26.6.2014)
+ */
+ return row.getAttribute("class").contains("-row-selected");
+ }
+
+ private TestBenchElement getRow(int i) {
+ return getGridElement().getRow(i);
+ }
+
private void assertPrimaryStylename(String stylename) {
assertTrue(getGridElement().getAttribute("class").contains(stylename));
diff --git a/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java b/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java
index 91a4e19886..8ea652cc74 100644
--- a/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java
+++ b/uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue;
import org.junit.Test;
import org.openqa.selenium.WebElement;
+import org.openqa.selenium.remote.DesiredCapabilities;
import com.vaadin.testbench.By;
import com.vaadin.testbench.TestBenchElement;
@@ -43,6 +44,14 @@ public class GridClientRenderers extends MultiBrowserTest {
private int latency = 0;
@Override
+ protected DesiredCapabilities getDesiredCapabilities() {
+ DesiredCapabilities c = new DesiredCapabilities(
+ super.getDesiredCapabilities());
+ c.setCapability("handlesAlerts", true);
+ return c;
+ }
+
+ @Override
protected Class<?> getUIClass() {
return GridClientColumnRenderers.class;
}