]> source.dussan.org Git - vaadin-framework.git/commitdiff
Send selection between server and client (#13334)
authorHenrik Paul <henrik@vaadin.com>
Tue, 10 Jun 2014 18:50:51 +0000 (21:50 +0300)
committerHenrik Paul <henrik@vaadin.com>
Fri, 27 Jun 2014 09:39:42 +0000 (12:39 +0300)
Change-Id: I75174af63092fca72d9aa63ccf3c06a77f42c4f6

21 files changed:
WebContent/VAADIN/themes/base/grid/grid.scss
client/src/com/vaadin/client/data/AbstractRemoteDataSource.java
client/src/com/vaadin/client/data/RpcDataSourceConnector.java
client/src/com/vaadin/client/ui/grid/Escalator.java
client/src/com/vaadin/client/ui/grid/Grid.java
client/src/com/vaadin/client/ui/grid/GridConnector.java
client/src/com/vaadin/client/ui/grid/selection/MultiSelectionRenderer.java
client/src/com/vaadin/client/ui/grid/selection/SelectionModel.java
client/src/com/vaadin/client/ui/grid/selection/SelectionModelMulti.java
client/src/com/vaadin/client/ui/grid/selection/SelectionModelNone.java
client/src/com/vaadin/client/ui/grid/selection/SelectionModelSingle.java
server/src/com/vaadin/data/RpcDataProviderExtension.java
server/src/com/vaadin/ui/components/grid/Grid.java
server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java
server/tests/src/com/vaadin/tests/server/component/grid/DataProviderExtension.java [new file with mode: 0644]
shared/src/com/vaadin/shared/data/DataProviderRpc.java
shared/src/com/vaadin/shared/ui/grid/GridServerRpc.java [new file with mode: 0644]
shared/src/com/vaadin/shared/ui/grid/GridState.java
uitest/src/com/vaadin/tests/components/grid/GridBasicFeatures.java
uitest/src/com/vaadin/tests/components/grid/GridBasicFeaturesTest.java
uitest/src/com/vaadin/tests/components/grid/GridClientRenderers.java

index 6a050405cbc252edf5eaf6e3c8bd31ac6b76765f..d1875a7ab3751ae972d8cfbd7e3c35812d83f782 100644 (file)
@@ -14,4 +14,8 @@
                }
 
        }
-}
\ No newline at end of file
+       
+       .#{$primaryStyleName}-row-selected > td {
+               background: lightblue;
+       }
+}
index 2395dc848ca056894d6a1a8bb8cca1acc8e29c34..d6a609a3c8be140a9c858c9153f2fb9c419472d2 100644 (file)
@@ -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;
 
index 2b9bf5c90efc9d27cd7443871f38553e7f11d8fb..3761ea92dfa4e8b1a9dc0cc14813742ce9202d63 100644 (file)
@@ -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
index 8a1f6f584281d37984f941fa59617cf50eea58a3..c8feb6d18e98e15bfd052256417a3355d4e86720 100644 (file)
@@ -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()),
index b5461e4a3b2489a495df99919de62b0e1e3f655d..9a75b37c42adf645b069c157f8d251afcce75baa 100644 (file)
@@ -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
@@ -1340,6 +1349,13 @@ public class Grid<T> extends Composite implements
                 true);
     }
 
+    /**
+     * Refreshes all body rows
+     */
+    private void refreshBody() {
+        escalator.getBody().refreshRows(0, escalator.getBody().getRowCount());
+    }
+
     /**
      * Refreshes all footer rows
      */
@@ -1796,6 +1812,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>
@@ -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;
-    }
 }
index 0bfcf8ffcdb01a261dc7760dc1b7a15c2c474d33..3b1ecb44d80cdeb3e8d63080f70abb80f5bead80 100644 (file)
 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);
+    }
 }
index 52bb6c0f60e14e8bbd603fee0d04401b59724f9b..53b0d064ab3bb915f6af04185f93e29d0f36006b 100644 (file)
@@ -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);
         }
     }
 }
index d11b7764d00a7c39e3ac3b1ac4d797ac1f49772b..989a8946c765823fc7fbb2db97407c5be100ba91 100644 (file)
@@ -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
      */
index 8afb5927716c6e11912b739be2028daa50076089..de62dc9cbc9a3b80d7e16662ecb42ddbec31b3d8 100644 (file)
@@ -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();
     }
-
 }
index bcb03570892d9fdf41945d27eaf5f86e07a18087..93dfb49df2d3a173e4526648f6310d3c11b3bdec 100644 (file)
@@ -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;
     }
 
index 6b5f645e23c22004cc6930682baaa05352e82216..775e1878c5241f041796d284ded6fc5406ff12c4 100644 (file)
@@ -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);
         }
     }
 
index 0046b256bb112c8df7c6b28ebf1b9e3ebe82fa8f..1834822d9913ad550b74c0d629b5110a27e2c4b5 100644 (file)
@@ -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;
@@ -62,6 +66,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
@@ -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();
     }
index 1ebf227330328c61630ad9928388bc59b88ebd75..bc6a69e85058a2b8d7a6cf6723f4f9c4bdd452c9 100644 (file)
@@ -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,11 +1048,21 @@ 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.
      * 
index cecdca80df6f72f0b8bbd34348ac298ed7a203f7..f0e25405ccd8c27314e385e18ac246124b430c31 100644 (file)
@@ -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 (file)
index 0000000..9ecf131
--- /dev/null
@@ -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);
+    }
+}
index a92ffe0421ac413de3432ae5593fdb0a34954c06..43469914e5389b9631c41080bfc13d723f1d99f2 100644 (file)
@@ -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 (file)
index 0000000..b763174
--- /dev/null
@@ -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);
+}
index eceaedd1fcbb3c1768607a83dbcc361fff8a38e5..0b23e2c11dc91dbef385ae923cbfd846c697656e 100644 (file)
@@ -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>();
+
 }
index 06fe088deed18e435c64d4e312bfcfe855293fb6..c6597ef23bfda1efa7d82ff277f270bf5b0ea113 100644 (file)
@@ -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")
index a11b0f1be9e656729e53e830d1b1a6feb03254d8..3dc8ac814f905af3615bbd20fbf880c8dc91cb54 100644 (file)
@@ -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));
 
index 91a4e19886113544d565c1f14ea27f5a6abaae62..8ea652cc74929ad5f27ae1d6a9429fe2a48e0a4d 100644 (file)
@@ -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;
@@ -42,6 +43,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;