]> source.dussan.org Git - vaadin-framework.git/commitdiff
Grid server-side selection (#13334)
authorHenrik Paul <henrik@vaadin.com>
Mon, 19 May 2014 15:26:24 +0000 (18:26 +0300)
committerHenrik Paul <henrik@vaadin.com>
Wed, 28 May 2014 11:03:38 +0000 (11:03 +0000)
Change-Id: I62c5a2486360fe11de8a90efabb7775ef47124cb

server/src/com/vaadin/ui/components/grid/Grid.java
server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java [new file with mode: 0644]
server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java [new file with mode: 0644]
server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java [new file with mode: 0644]

index da5be35d44b6df9cd67d93c64d4a0bf210265a06..69beb260f45d882eaf50ac6659eb83191340335b 100644 (file)
@@ -17,6 +17,7 @@
 package com.vaadin.ui.components.grid;
 
 import java.io.Serializable;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -51,7 +52,14 @@ import com.vaadin.shared.ui.grid.HeightMode;
 import com.vaadin.shared.ui.grid.Range;
 import com.vaadin.shared.ui.grid.ScrollDestination;
 import com.vaadin.ui.AbstractComponent;
-import com.vaadin.ui.Component;
+import com.vaadin.ui.components.grid.selection.MultiSelectionModel;
+import com.vaadin.ui.components.grid.selection.NoSelectionModel;
+import com.vaadin.ui.components.grid.selection.SelectionChangeEvent;
+import com.vaadin.ui.components.grid.selection.SelectionChangeListener;
+import com.vaadin.ui.components.grid.selection.SelectionChangeNotifier;
+import com.vaadin.ui.components.grid.selection.SelectionModel;
+import com.vaadin.ui.components.grid.selection.SingleSelectionModel;
+import com.vaadin.util.ReflectTools;
 
 /**
  * Data grid component
@@ -71,7 +79,7 @@ import com.vaadin.ui.Component;
  * @since 7.4
  * @author Vaadin Ltd
  */
-public class Grid extends AbstractComponent {
+public class Grid extends AbstractComponent implements SelectionChangeNotifier {
 
     /**
      * A helper class that handles the client-side Escalator logic relating to
@@ -349,6 +357,47 @@ public class Grid extends AbstractComponent {
         }
     }
 
+    /**
+     * Selection modes representing built-in {@link SelectionModel
+     * SelectionModels} that come bundled with {@link Grid}.
+     * <p>
+     * Passing one of these enums into
+     * {@link Grid#setSelectionMode(SelectionMode)} is equivalent to calling
+     * {@link Grid#setSelectionModel(SelectionModel)} with one of the built-in
+     * implementations of {@link SelectionModel}.
+     * 
+     * @see Grid#setSelectionMode(SelectionMode)
+     * @see Grid#setSelectionModel(SelectionModel)
+     */
+    public enum SelectionMode {
+        /** A SelectionMode that maps to {@link SingleSelectionModel} */
+        SINGLE {
+            @Override
+            protected SelectionModel createModel() {
+                return new SingleSelectionModel();
+            }
+
+        },
+
+        /** A SelectionMode that maps to {@link MultiSelectionModel} */
+        MULTI {
+            @Override
+            protected SelectionModel createModel() {
+                return new MultiSelectionModel();
+            }
+        },
+
+        /** A SelectionMode that maps to {@link NoSelectionModel} */
+        NONE {
+            @Override
+            protected SelectionModel createModel() {
+                return new NoSelectionModel();
+            }
+        };
+
+        protected abstract SelectionModel createModel();
+    }
+
     /**
      * The data source attached to the grid
      */
@@ -458,6 +507,16 @@ public class Grid extends AbstractComponent {
 
     private final ActiveRowHandler activeRowHandler = new ActiveRowHandler();
 
+    /**
+     * The selection model that is currently in use. Never <code>null</code>
+     * after the constructor has been run.
+     */
+    private SelectionModel selectionModel;
+
+    private static final Method SELECTION_CHANGE_METHOD = ReflectTools
+            .findMethod(SelectionChangeListener.class, "selectionChange",
+                    SelectionChangeEvent.class);
+
     /**
      * Creates a new Grid using the given datasource.
      * 
@@ -465,7 +524,8 @@ public class Grid extends AbstractComponent {
      *            the data source for the grid
      */
     public Grid(Container.Indexed datasource) {
-        setContainerDatasource(datasource);
+        setContainerDataSource(datasource);
+        setSelectionMode(SelectionMode.MULTI);
 
         registerRpc(new GridServerRpc() {
             @Override
@@ -484,7 +544,7 @@ public class Grid extends AbstractComponent {
      * @throws IllegalArgumentException
      *             if the data source is null
      */
-    public void setContainerDatasource(Container.Indexed container) {
+    public void setContainerDataSource(Container.Indexed container) {
         if (container == null) {
             throw new IllegalArgumentException(
                     "Cannot set the datasource to null");
@@ -512,6 +572,14 @@ public class Grid extends AbstractComponent {
         datasourceExtension = new RpcDataProviderExtension(container);
         datasourceExtension.extend(this);
 
+        /*
+         * selectionModel == null when the invocation comes from the
+         * constructor.
+         */
+        if (selectionModel != null) {
+            selectionModel.reset();
+        }
+
         // Listen to changes in properties and remove columns if needed
         if (datasource instanceof PropertySetChangeNotifier) {
             ((PropertySetChangeNotifier) datasource)
@@ -958,4 +1026,254 @@ public class Grid extends AbstractComponent {
     public HeightMode getHeightMode() {
         return getState(false).heightMode;
     }
+
+    /* Selection related methods: */
+
+    /**
+     * Takes a new {@link SelectionModel} into use.
+     * <p>
+     * The SelectionModel that is previously in use will have all its items
+     * deselected.
+     * <p>
+     * If the given SelectionModel is already in use, this method does nothing.
+     * 
+     * @param selectionModel
+     *            the new SelectionModel to use
+     * @throws IllegalArgumentException
+     *             if {@code selectionModel} is <code>null</code>
+     */
+    public void setSelectionModel(SelectionModel selectionModel)
+            throws IllegalArgumentException {
+        if (selectionModel == null) {
+            throw new IllegalArgumentException(
+                    "Selection model may not be null");
+        }
+
+        if (this.selectionModel != selectionModel) {
+            // this.selectionModel is null on init
+            if (this.selectionModel != null) {
+                this.selectionModel.reset();
+                this.selectionModel.setGrid(null);
+            }
+
+            this.selectionModel = selectionModel;
+            this.selectionModel.setGrid(this);
+            this.selectionModel.reset();
+        }
+    }
+
+    /**
+     * Returns the currently used {@link SelectionModel}.
+     * 
+     * @return the currently used SelectionModel
+     */
+    public SelectionModel getSelectionModel() {
+        return selectionModel;
+    }
+
+    /**
+     * Changes the Grid's selection mode.
+     * <p>
+     * Grid supports three selection modes: multiselect, single select and no
+     * selection, and this is a conveniency method for choosing between one of
+     * them.
+     * <P>
+     * Technically, this method is a shortcut that can be used instead of
+     * calling {@code setSelectionModel} with a specific SelectionModel
+     * instance. Grid comes with three built-in SelectionModel classes, and the
+     * {@link SelectionMode} enum represents each of them.
+     * <p>
+     * Essentially, the two following method calls are equivalent:
+     * <p>
+     * <code><pre>
+     * grid.setSelectionMode(SelectionMode.MULTI);
+     * grid.setSelectionModel(new MultiSelectionMode());
+     * </pre></code>
+     * 
+     * 
+     * @param selectionMode
+     *            the selection mode to switch to
+     * @return The {@link SelectionModel} instance that was taken into use
+     * @throws IllegalArgumentException
+     *             if {@code selectionMode} is <code>null</code>
+     * @see SelectionModel
+     */
+    public SelectionModel setSelectionMode(final SelectionMode selectionMode)
+            throws IllegalArgumentException {
+        if (selectionMode == null) {
+            throw new IllegalArgumentException("selection mode may not be null");
+        }
+        final SelectionModel newSelectionModel = selectionMode.createModel();
+        setSelectionModel(newSelectionModel);
+        return newSelectionModel;
+    }
+
+    /**
+     * Checks whether an item is selected or not.
+     * 
+     * @param itemId
+     *            the item id to check for
+     * @return <code>true</code> iff the item is selected
+     */
+    // keep this javadoc in sync with SelectionModel.isSelected
+    public boolean isSelected(Object itemId) {
+        return selectionModel.isSelected(itemId);
+    }
+
+    /**
+     * Returns a collection of all the currently selected itemIds.
+     * <p>
+     * This method is a shorthand that is forwarded to the object that is
+     * returned by {@link #getSelectionModel()}.
+     * 
+     * @return a collection of all the currently selected itemIds
+     */
+    // keep this javadoc in sync with SelectionModel.getSelectedRows
+    public Collection<Object> getSelectedRows() {
+        return getSelectionModel().getSelectedRows();
+    }
+
+    /**
+     * Gets the item id of the currently selected item.
+     * <p>
+     * This method is a shorthand that is forwarded to the object that is
+     * returned by {@link #getSelectionModel()}. Only
+     * {@link SelectionModel.Single} is supported.
+     * 
+     * @return the item id of the currently selected item, or <code>null</code>
+     *         if nothing is selected
+     * @throws IllegalStateException
+     *             if the object that is returned by
+     *             {@link #getSelectionModel()} is not an instance of
+     *             {@link SelectionModel.Single}
+     */
+    // keep this javadoc in sync with SelectionModel.Single.getSelectedRow
+    public Object getSelectedRow() throws IllegalStateException {
+        if (selectionModel instanceof SelectionModel.Single) {
+            return ((SelectionModel.Single) selectionModel).getSelectedRow();
+        } else {
+            throw new IllegalStateException(Grid.class.getSimpleName()
+                    + " does not support the 'getSelectedRow' shortcut method "
+                    + "unless the selection model implements "
+                    + SelectionModel.Single.class.getName()
+                    + ". The current one does not ("
+                    + selectionModel.getClass().getName() + ")");
+        }
+    }
+
+    /**
+     * Marks an item as selected.
+     * <p>
+     * This method is a shorthand that is forwarded to the object that is
+     * returned by {@link #getSelectionModel()}. Only
+     * {@link SelectionModel.Single} or {@link SelectionModel.Multi} are
+     * supported.
+     * 
+     * 
+     * @param itemIds
+     *            the itemId to mark as selected
+     * @return <code>true</code> if the selection state changed.
+     *         <code>false</code> if the itemId already was selected
+     * @throws IllegalArgumentException
+     *             if the {@code itemId} doesn't exist in the currently active
+     *             Container
+     * @throws IllegalStateException
+     *             if the selection was illegal. One such reason might be that
+     *             the implementation already had an item selected, and that
+     *             needs to be explicitly deselected before re-selecting
+     *             something
+     * @throws IllegalStateException
+     *             if the object that is returned by
+     *             {@link #getSelectionModel()} does not implement
+     *             {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+     */
+    // keep this javadoc in sync with SelectionModel.Single.select
+    public boolean select(Object itemId) throws IllegalArgumentException,
+            IllegalStateException {
+        if (selectionModel instanceof SelectionModel.Single) {
+            return ((SelectionModel.Single) selectionModel).select(itemId);
+        } else if (selectionModel instanceof SelectionModel.Multi) {
+            return ((SelectionModel.Multi) selectionModel).select(itemId);
+        } else {
+            throw new IllegalStateException(Grid.class.getSimpleName()
+                    + " does not support the 'select' shortcut method "
+                    + "unless the selection model implements "
+                    + SelectionModel.Single.class.getName() + " or "
+                    + SelectionModel.Multi.class.getName()
+                    + ". The current one does not ("
+                    + selectionModel.getClass().getName() + ").");
+        }
+    }
+
+    /**
+     * Marks an item as deselected.
+     * <p>
+     * This method is a shorthand that is forwarded to the object that is
+     * returned by {@link #getSelectionModel()}. Only
+     * {@link SelectionModel.Single} and {@link SelectionModel.Multi} are
+     * supported.
+     * 
+     * @param itemId
+     *            the itemId to remove from being selected
+     * @return <code>true</code> if the selection state changed.
+     *         <code>false</code> if the itemId already was selected
+     * @throws IllegalArgumentException
+     *             if the {@code itemId} doesn't exist in the currently active
+     *             Container
+     * @throws IllegalStateException
+     *             if the deselection was illegal. One such reason might be that
+     *             the implementation already had an item selected, and that
+     *             needs to be explicitly deselected before re-selecting
+     *             something
+     * @throws IllegalStateException
+     *             if the object that is returned by
+     *             {@link #getSelectionModel()} does not implement
+     *             {@link SelectionModel.Single} or {@link SelectionModel.Multi}
+     */
+    // keep this javadoc in sync with SelectionModel.Single.deselect
+    public boolean deselect(Object itemId) throws IllegalStateException {
+        if (selectionModel instanceof SelectionModel.Single) {
+            return ((SelectionModel.Single) selectionModel).deselect(itemId);
+        } else if (selectionModel instanceof SelectionModel.Multi) {
+            return ((SelectionModel.Multi) selectionModel).deselect(itemId);
+        } else {
+            throw new IllegalStateException(Grid.class.getSimpleName()
+                    + " does not support the 'deselect' shortcut method "
+                    + "unless the selection model implements "
+                    + SelectionModel.Single.class.getName() + " or "
+                    + SelectionModel.Multi.class.getName()
+                    + ". The current one does not ("
+                    + selectionModel.getClass().getName() + ").");
+        }
+    }
+
+    /**
+     * Fires a selection change event.
+     * <p>
+     * <strong>Note:</strong> This is not a method that should be called by
+     * application logic. This method is publicly accessible only so that
+     * {@link SelectionModel SelectionModels} would be able to inform Grid of
+     * these events.
+     * 
+     * @param addedSelections
+     *            the selections that were added by this event
+     * @param removedSelections
+     *            the selections that were removed by this event
+     */
+    public void fireSelectionChangeEvent(Collection<Object> oldSelection,
+            Collection<Object> newSelection) {
+        fireEvent(new SelectionChangeEvent(this, oldSelection, newSelection));
+    }
+
+    @Override
+    public void addSelectionChangeListener(SelectionChangeListener listener) {
+        addListener(SelectionChangeEvent.class, listener,
+                SELECTION_CHANGE_METHOD);
+    }
+
+    @Override
+    public void removeSelectionChangeListener(SelectionChangeListener listener) {
+        removeListener(SelectionChangeEvent.class, listener,
+                SELECTION_CHANGE_METHOD);
+    }
 }
diff --git a/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java
new file mode 100644 (file)
index 0000000..3eb16d1
--- /dev/null
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * A base class for SelectionModels that contains some of the logic that is
+ * reusable.
+ * 
+ * @since 7.4.0
+ * @author Vaadin Ltd
+ */
+public abstract class AbstractSelectionModel implements SelectionModel {
+    protected final LinkedHashSet<Object> selection = new LinkedHashSet<Object>();
+    protected Grid grid = null;
+
+    @Override
+    public boolean isSelected(final Object itemId) {
+        return selection.contains(itemId);
+    }
+
+    @Override
+    public Collection<Object> getSelectedRows() {
+        return new ArrayList<Object>(selection);
+    }
+
+    @Override
+    public void setGrid(final Grid grid) {
+        this.grid = grid;
+    }
+
+    /**
+     * Fires a {@link SelectionChangeEvent} to all the
+     * {@link SelectionChangeListener SelectionChangeListeners} currently added
+     * to the Grid in which this SelectionModel is.
+     * <p>
+     * Note that this is only a helper method, and routes the call all the way
+     * to Grid. A {@link SelectionModel} is not a
+     * {@link SelectionChangeNotifier}
+     * 
+     * @param oldSelection
+     *            the complete {@link Collection} of the itemIds that were
+     *            selected <em>before</em> this event happened
+     * @param newSelection
+     *            the complete {@link Collection} of the itemIds that are
+     *            selected <em>after</em> this event happened
+     */
+    protected void fireSelectionChangeEvent(
+            final Collection<Object> oldSelection,
+            final Collection<Object> newSelection) {
+        grid.fireSelectionChangeEvent(oldSelection, newSelection);
+    }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java
new file mode 100644 (file)
index 0000000..ca5f344
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+
+import com.vaadin.data.Container.Indexed;
+
+/**
+ * A default implementation of a {@link SelectionModel.Multi}
+ * 
+ * @since 7.4.0
+ * @author Vaadin Ltd
+ */
+public class MultiSelectionModel extends AbstractSelectionModel implements
+        SelectionModel.Multi {
+
+    @Override
+    public boolean select(final Object... itemIds)
+            throws IllegalArgumentException {
+        if (itemIds != null) {
+            // select will fire the event
+            return select(Arrays.asList(itemIds));
+        } else {
+            throw new IllegalArgumentException(
+                    "Vararg array of itemIds may not be null");
+        }
+    }
+
+    @Override
+    public boolean select(final Collection<?> itemIds)
+            throws IllegalArgumentException {
+        if (itemIds == null) {
+            throw new IllegalArgumentException("itemIds may not be null");
+        }
+
+        final boolean hasSomeDifferingElements = !selection
+                .containsAll(itemIds);
+        if (hasSomeDifferingElements) {
+            final HashSet<Object> oldSelection = new HashSet<Object>(selection);
+            selection.addAll(itemIds);
+            fireSelectionChangeEvent(oldSelection, selection);
+        }
+        return hasSomeDifferingElements;
+    }
+
+    @Override
+    public boolean deselect(final Object... itemIds)
+            throws IllegalArgumentException {
+        if (itemIds != null) {
+            // deselect will fire the event
+            return deselect(Arrays.asList(itemIds));
+        } else {
+            throw new IllegalArgumentException(
+                    "Vararg array of itemIds may not be null");
+        }
+    }
+
+    @Override
+    public boolean deselect(final Collection<?> itemIds)
+            throws IllegalArgumentException {
+        if (itemIds == null) {
+            throw new IllegalArgumentException("itemIds may not be null");
+        }
+
+        final boolean hasCommonElements = !Collections.disjoint(itemIds,
+                selection);
+        if (hasCommonElements) {
+            final HashSet<Object> oldSelection = new HashSet<Object>(selection);
+            selection.removeAll(itemIds);
+            fireSelectionChangeEvent(oldSelection, selection);
+        }
+        return hasCommonElements;
+    }
+
+    @Override
+    public boolean selectAll() {
+        // select will fire the event
+        final Indexed container = grid.getContainerDatasource();
+        if (container != null) {
+            return select(container.getItemIds());
+        } else if (selection.isEmpty()) {
+            return false;
+        } else {
+            /*
+             * this should never happen (no container but has a selection), but
+             * I guess the only theoretically correct course of action...
+             */
+            return deselectAll();
+        }
+    }
+
+    @Override
+    public boolean deselectAll() {
+        // deselect will fire the event
+        return deselect(getSelectedRows());
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * The returned Collection is in <strong>order of selection</strong> &ndash;
+     * the item that was first selected will be first in the collection, and so
+     * on. Should an item have been selected twice without being deselected in
+     * between, it will have remained in its original position.
+     */
+    @Override
+    public Collection<Object> getSelectedRows() {
+        // overridden only for JavaDoc
+        return super.getSelectedRows();
+    }
+
+    /**
+     * Resets the selection model.
+     * <p>
+     * Equivalent to calling {@link #deselectAll()}
+     */
+    @Override
+    public void reset() {
+        deselectAll();
+    }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java
new file mode 100644 (file)
index 0000000..6d3213a
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * A default implementation for a {@link SelectionModel.None}
+ * 
+ * @since 7.4.0
+ * @author Vaadin Ltd
+ */
+public class NoSelectionModel implements SelectionModel.None {
+    @Override
+    public void setGrid(final Grid grid) {
+        // NOOP, not needed for anything
+    }
+
+    @Override
+    public boolean isSelected(final Object itemId) {
+        return false;
+    }
+
+    @Override
+    public Collection<Object> getSelectedRows() {
+        return Collections.emptyList();
+    }
+
+    /**
+     * Semantically resets the selection model.
+     * <p>
+     * Effectively a no-op.
+     */
+    @Override
+    public void reset() {
+        // NOOP
+    }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java
new file mode 100644 (file)
index 0000000..b1097c8
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.util.Collection;
+import java.util.EventObject;
+import java.util.HashSet;
+import java.util.Set;
+
+import com.google.gwt.thirdparty.guava.common.collect.Sets;
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * An event that specifies what in a selection has changed, and where the
+ * selection took place.
+ * 
+ * @since 7.4.0
+ * @author Vaadin Ltd
+ */
+public class SelectionChangeEvent extends EventObject {
+
+    private Set<Object> oldSelection;
+    private Set<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);
+    }
+
+    /**
+     * A {@link Collection} of all the itemIds that became selected.
+     * <p>
+     * <em>Note:</em> this excludes all itemIds that might have been previously
+     * selected.
+     * 
+     * @return a Collection of the itemIds that became selected
+     */
+    public Set<Object> getAdded() {
+        return Sets.difference(newSelection, oldSelection);
+    }
+
+    /**
+     * A {@link Collection} of all the itemIds that became deselected.
+     * <p>
+     * <em>Note:</em> this excludes all itemIds that might have been previously
+     * deselected.
+     * 
+     * @return a Collection of the itemIds that became deselected
+     */
+    public Set<Object> getRemoved() {
+        return Sets.difference(oldSelection, newSelection);
+    }
+
+    @Override
+    public Grid getSource() {
+        return (Grid) super.getSource();
+    }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java
new file mode 100644 (file)
index 0000000..764fee8
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.io.Serializable;
+
+/**
+ * The listener interface for receiving {@link SelectionChangeEvent
+ * SelectionChangeEvents}.
+ * 
+ * @since 7.4.0
+ * @author Vaadin Ltd
+ */
+public interface SelectionChangeListener extends Serializable {
+    /**
+     * Notifies the listener that the selection state has changed.
+     * 
+     * @param event
+     *            the selection change event
+     */
+    void selectionChange(SelectionChangeEvent event);
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java
new file mode 100644 (file)
index 0000000..f61dc13
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.io.Serializable;
+
+/**
+ * The interface for adding and removing listeners for
+ * {@link SelectionChangeEvent SelectionChangeEvents}.
+ * 
+ * @since 7.4.0
+ * @author Vaadin Ltd
+ */
+public interface SelectionChangeNotifier extends Serializable {
+    /**
+     * Registers a new selection change listener
+     * 
+     * @param listener
+     *            the listener to register
+     */
+    void addSelectionChangeListener(SelectionChangeListener listener);
+
+    /**
+     * Removes a previously registered selection change listener
+     * 
+     * @param listener
+     *            the listener to remove
+     */
+    void removeSelectionChangeListener(SelectionChangeListener listener);
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java
new file mode 100644 (file)
index 0000000..d48e72e
--- /dev/null
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.io.Serializable;
+import java.util.Collection;
+
+import com.vaadin.ui.components.grid.Grid;
+
+/**
+ * The server-side interface that controls Grid's selection state.
+ * 
+ * @since 7.4.0
+ * @author Vaadin Ltd
+ */
+public interface SelectionModel extends Serializable {
+    /**
+     * Checks whether an item is selected or not.
+     * 
+     * @param itemId
+     *            the item id to check for
+     * @return <code>true</code> iff the item is selected
+     */
+    boolean isSelected(Object itemId);
+
+    /**
+     * Returns a collection of all the currently selected itemIds.
+     * 
+     * @return a collection of all the currently selected itemIds
+     */
+    Collection<Object> getSelectedRows();
+
+    /**
+     * Injects the current {@link Grid} instance into the SelectionModel.
+     * <p>
+     * <em>Note:</em> This method should not be called manually.
+     * 
+     * @param grid
+     *            the Grid in which the SelectionModel currently is, or
+     *            <code>null</code> when a selection model is being detached
+     *            from a Grid.
+     */
+    void setGrid(Grid grid);
+
+    /**
+     * Resets the SelectiomModel to an initial state.
+     * <p>
+     * Most often this means that the selection state is cleared, but
+     * implementations are free to interpret the "initial state" as they wish.
+     * Some, for example, may want to keep the first selected item as selected.
+     */
+    void reset();
+
+    /**
+     * A SelectionModel that supports multiple selections to be made.
+     * <p>
+     * This interface has a contract of having the same behavior, no matter how
+     * the selection model is interacted with. In other words, if something is
+     * forbidden to do in e.g. the user interface, it must also be forbidden to
+     * do in the server-side and client-side APIs.
+     */
+    public interface Multi extends SelectionModel {
+
+        /**
+         * Marks items as selected.
+         * <p>
+         * This method does not clear any previous selection state, only adds to
+         * it.
+         * 
+         * @param itemIds
+         *            the itemId(s) to mark as selected
+         * @return <code>true</code> if the selection state changed.
+         *         <code>false</code> if all the given itemIds already were
+         *         selected
+         * @throws IllegalArgumentException
+         *             if the <code>itemIds</code> varargs array is
+         *             <code>null</code>
+         * @see #deselect(Object...)
+         */
+        boolean select(Object... itemIds) throws IllegalArgumentException;
+
+        /**
+         * Marks items as selected.
+         * <p>
+         * This method does not clear any previous selection state, only adds to
+         * it.
+         * 
+         * @param itemIds
+         *            the itemIds to mark as selected
+         * @return <code>true</code> if the selection state changed.
+         *         <code>false</code> if all the given itemIds already were
+         *         selected
+         * @throws IllegalArgumentException
+         *             if <code>itemIds</code> is <code>null</code>
+         * @see #deselect(Collection)
+         */
+        boolean select(Collection<?> itemIds) throws IllegalArgumentException;
+
+        /**
+         * Marks items as deselected.
+         * 
+         * @param itemIds
+         *            the itemId(s) to remove from being selected
+         * @return <code>true</code> if the selection state changed.
+         *         <code>false</code> if none the given itemIds were selected
+         *         previously
+         * @throws IllegalArgumentException
+         *             if the <code>itemIds</code> varargs array is
+         *             <code>null</code>
+         * @see #select(Object...)
+         */
+        boolean deselect(Object... itemIds) throws IllegalArgumentException;
+
+        /**
+         * Marks items as deselected.
+         * 
+         * @param itemIds
+         *            the itemId(s) to remove from being selected
+         * @return <code>true</code> if the selection state changed.
+         *         <code>false</code> if none the given itemIds were selected
+         *         previously
+         * @throws IllegalArgumentException
+         *             if <code>itemIds</code> is <code>null</code>
+         * @see #select(Collection)
+         */
+        boolean deselect(Collection<?> itemIds) throws IllegalArgumentException;
+
+        /**
+         * Marks all the items in the current Container as selected
+         * 
+         * @return <code>true</code> iff some items were previously not selected
+         * @see #deselectAll()
+         */
+        boolean selectAll();
+
+        /**
+         * Marks all the items in the current Container as deselected
+         * 
+         * @return <code>true</code> iff some items were previously selected
+         * @see #selectAll()
+         */
+        boolean deselectAll();
+    }
+
+    /**
+     * A SelectionModel that supports for only single rows to be selected at a
+     * time.
+     * <p>
+     * This interface has a contract of having the same behavior, no matter how
+     * the selection model is interacted with. In other words, if something is
+     * forbidden to do in e.g. the user interface, it must also be forbidden to
+     * do in the server-side and client-side APIs.
+     */
+    public interface Single extends SelectionModel {
+        /**
+         * Marks an item as selected.
+         * 
+         * @param itemIds
+         *            the itemId to mark as selected
+         * @return <code>true</code> if the selection state changed.
+         *         <code>false</code> if the itemId already was selected
+         * @throws IllegalStateException
+         *             if the selection was illegal. One such reason might be
+         *             that the implementation already had an item selected, and
+         *             that needs to be explicitly deselected before
+         *             re-selecting something
+         * @see #deselect(Object)
+         */
+        boolean select(Object itemId) throws IllegalStateException;
+
+        /**
+         * Marks an item as deselected.
+         * 
+         * @param itemId
+         *            the itemId to remove from being selected
+         * @return <code>true</code> if the selection state changed.
+         *         <code>false</code> if the itemId already was selected
+         * @throws IllegalStateException
+         *             if the deselection was illegal. One such reason might be
+         *             that the implementation enforces that an item is always
+         *             selected
+         * @see #select(Object)
+         */
+        boolean deselect(Object itemId) throws IllegalStateException;
+
+        /**
+         * Gets the item id of the currently selected item.
+         * 
+         * @return the item id of the currently selected item, or
+         *         <code>null</code> if nothing is selected
+         */
+        Object getSelectedRow();
+    }
+
+    /**
+     * A SelectionModel that does not allow for rows to be selected.
+     * <p>
+     * This interface has a contract of having the same behavior, no matter how
+     * the selection model is interacted with. In other words, if the developer
+     * is unable to select something programmatically, it is not allowed for the
+     * end-user to select anything, either.
+     */
+    public interface None extends SelectionModel {
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @return always <code>false</code>.
+         */
+        @Override
+        public boolean isSelected(Object itemId);
+
+        /**
+         * {@inheritDoc}
+         * 
+         * @return always an empty collection.
+         */
+        @Override
+        public Collection<Object> getSelectedRows();
+    }
+}
diff --git a/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java
new file mode 100644 (file)
index 0000000..dfac1d7
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.ui.components.grid.selection;
+
+import java.util.Collection;
+import java.util.Collections;
+
+/**
+ * A default implementation of a {@link SelectionModel.Single}
+ * 
+ * @since 7.4.0
+ * @author Vaadin Ltd
+ */
+public class SingleSelectionModel extends AbstractSelectionModel implements
+        SelectionModel.Single {
+    @Override
+    public boolean select(final Object itemId) {
+        final Object selectedRow = getSelectedRow();
+        final boolean modified = selection.add(itemId);
+        if (modified) {
+            final Collection<Object> deselected;
+            if (selectedRow != null) {
+                deselectInternal(selectedRow, false);
+                deselected = Collections.singleton(selectedRow);
+            } else {
+                deselected = Collections.emptySet();
+            }
+
+            fireSelectionChangeEvent(deselected, selection);
+        }
+
+        return modified;
+    }
+
+    @Override
+    public boolean deselect(final Object itemId) {
+        return deselectInternal(itemId, true);
+    }
+
+    private boolean deselectInternal(final Object itemId,
+            boolean fireEventIfNeeded) {
+        final boolean modified = selection.remove(itemId);
+        if (fireEventIfNeeded && modified) {
+            fireSelectionChangeEvent(Collections.singleton(itemId),
+                    Collections.emptySet());
+        }
+        return modified;
+    }
+
+    @Override
+    public Object getSelectedRow() {
+        if (selection.isEmpty()) {
+            return null;
+        } else {
+            return selection.iterator().next();
+        }
+    }
+
+    /**
+     * Resets the selection state.
+     * <p>
+     * If an item is selected, it will become deselected.
+     */
+    @Override
+    public void reset() {
+        deselect(getSelectedRow());
+    }
+}
diff --git a/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java b/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java
new file mode 100644 (file)
index 0000000..7993d31
--- /dev/null
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2000-2013 Vaadin Ltd.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.grid;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Collection;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.vaadin.data.util.IndexedContainer;
+import com.vaadin.ui.components.grid.Grid;
+import com.vaadin.ui.components.grid.Grid.SelectionMode;
+import com.vaadin.ui.components.grid.selection.SelectionChangeEvent;
+import com.vaadin.ui.components.grid.selection.SelectionChangeListener;
+import com.vaadin.ui.components.grid.selection.SelectionModel;
+
+/**
+ * 
+ * @since
+ * @author Vaadin Ltd
+ */
+public class GridSelection {
+
+    private static class MockSelectionChangeListener implements
+            SelectionChangeListener {
+        private SelectionChangeEvent event;
+
+        @Override
+        public void selectionChange(final SelectionChangeEvent event) {
+            this.event = event;
+        }
+
+        public Collection<?> getAdded() {
+            return event.getAdded();
+        }
+
+        public Collection<?> getRemoved() {
+            return event.getRemoved();
+        }
+
+        public void clearEvent() {
+            /*
+             * This method is not strictly needed as the event will simply be
+             * overridden, but it's good practice, and makes the code more
+             * obvious.
+             */
+            event = null;
+        }
+
+        public boolean eventHasHappened() {
+            return event != null;
+        }
+    }
+
+    private Grid grid;
+    private MockSelectionChangeListener mockListener;
+
+    private final Object itemId1Present = "itemId1Present";
+    private final Object itemId2Present = "itemId2Present";
+
+    private final Object itemId1NotPresent = "itemId1NotPresent";
+    private final Object itemId2NotPresent = "itemId2NotPresent";
+
+    @Before
+    public void setup() {
+        final IndexedContainer container = new IndexedContainer();
+        container.addItem(itemId1Present);
+        container.addItem(itemId2Present);
+        for (int i = 2; i < 10; i++) {
+            container.addItem(new Object());
+        }
+
+        assertEquals("init size", 10, container.size());
+        assertTrue("itemId1Present", container.containsId(itemId1Present));
+        assertTrue("itemId2Present", container.containsId(itemId2Present));
+        assertFalse("itemId1NotPresent",
+                container.containsId(itemId1NotPresent));
+        assertFalse("itemId2NotPresent",
+                container.containsId(itemId2NotPresent));
+
+        grid = new Grid(container);
+
+        mockListener = new MockSelectionChangeListener();
+        grid.addSelectionChangeListener(mockListener);
+
+        assertFalse("eventHasHappened", mockListener.eventHasHappened());
+    }
+
+    @Test
+    public void defaultSelectionModeIsMulti() {
+        assertTrue(grid.getSelectionModel() instanceof SelectionModel.Multi);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void getSelectedRowThrowsExceptionMulti() {
+        grid.setSelectionMode(SelectionMode.MULTI);
+        grid.getSelectedRow();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void getSelectedRowThrowsExceptionNone() {
+        grid.setSelectionMode(SelectionMode.NONE);
+        grid.getSelectedRow();
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void selectThrowsExceptionNone() {
+        grid.setSelectionMode(SelectionMode.NONE);
+        grid.select(itemId1Present);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void deselectRowThrowsExceptionNone() {
+        grid.setSelectionMode(SelectionMode.NONE);
+        grid.deselect(itemId1Present);
+    }
+
+    @Test
+    public void selectionModeMapsToMulti() {
+        assertTrue(grid.setSelectionMode(SelectionMode.MULTI) instanceof SelectionModel.Multi);
+    }
+
+    @Test
+    public void selectionModeMapsToSingle() {
+        assertTrue(grid.setSelectionMode(SelectionMode.SINGLE) instanceof SelectionModel.Single);
+    }
+
+    @Test
+    public void selectionModeMapsToNone() {
+        assertTrue(grid.setSelectionMode(SelectionMode.NONE) instanceof SelectionModel.None);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void selectionModeNullThrowsException() {
+        grid.setSelectionMode(null);
+    }
+
+    @Test
+    public void noSelectModel_isSelected() {
+        grid.setSelectionMode(SelectionMode.NONE);
+        assertFalse("itemId1Present", grid.isSelected(itemId1Present));
+        assertFalse("itemId1NotPresent", grid.isSelected(itemId1NotPresent));
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void noSelectModel_getSelectedRow() {
+        grid.setSelectionMode(SelectionMode.NONE);
+        grid.getSelectedRow();
+    }
+
+    @Test
+    public void noSelectModel_getSelectedRows() {
+        grid.setSelectionMode(SelectionMode.NONE);
+        assertTrue(grid.getSelectedRows().isEmpty());
+    }
+
+    @Test
+    public void selectionCallsListenerMulti() {
+        grid.setSelectionMode(SelectionMode.MULTI);
+        selectionCallsListener();
+    }
+
+    @Test
+    public void selectionCallsListenerSingle() {
+        grid.setSelectionMode(SelectionMode.SINGLE);
+        selectionCallsListener();
+    }
+
+    private void selectionCallsListener() {
+        grid.select(itemId1Present);
+        assertEquals("added size", 1, mockListener.getAdded().size());
+        assertEquals("added item", itemId1Present, mockListener.getAdded()
+                .iterator().next());
+        assertEquals("removed size", 0, mockListener.getRemoved().size());
+    }
+
+    @Test
+    public void deselectionCallsListenerMulti() {
+        grid.setSelectionMode(SelectionMode.MULTI);
+        deselectionCallsListener();
+    }
+
+    @Test
+    public void deselectionCallsListenerSingle() {
+        grid.setSelectionMode(SelectionMode.SINGLE);
+        deselectionCallsListener();
+    }
+
+    private void deselectionCallsListener() {
+        grid.select(itemId1Present);
+        mockListener.clearEvent();
+
+        grid.deselect(itemId1Present);
+        assertEquals("removed size", 1, mockListener.getRemoved().size());
+        assertEquals("removed item", itemId1Present, mockListener.getRemoved()
+                .iterator().next());
+        assertEquals("removed size", 0, mockListener.getAdded().size());
+    }
+
+    @Test
+    public void deselectPresentButNotSelectedItemIdShouldntFireListenerMulti() {
+        grid.setSelectionMode(SelectionMode.MULTI);
+        deselectPresentButNotSelectedItemIdShouldntFireListener();
+    }
+
+    @Test
+    public void deselectPresentButNotSelectedItemIdShouldntFireListenerSingle() {
+        grid.setSelectionMode(SelectionMode.SINGLE);
+        deselectPresentButNotSelectedItemIdShouldntFireListener();
+    }
+
+    private void deselectPresentButNotSelectedItemIdShouldntFireListener() {
+        grid.deselect(itemId1Present);
+        assertFalse(mockListener.eventHasHappened());
+    }
+
+    @Test
+    public void deselectNotPresentItemIdShouldNotThrowExceptionMulti() {
+        grid.setSelectionMode(SelectionMode.MULTI);
+        grid.deselect(itemId1NotPresent);
+    }
+
+    @Test
+    public void deselectNotPresentItemIdShouldNotThrowExceptionSingle() {
+        grid.setSelectionMode(SelectionMode.SINGLE);
+        grid.deselect(itemId1NotPresent);
+    }
+
+    @Test
+    public void selectNotPresentItemIdShouldNotThrowExceptionMulti() {
+        grid.setSelectionMode(SelectionMode.MULTI);
+        grid.select(itemId1NotPresent);
+    }
+
+    @Test
+    public void selectNotPresentItemIdShouldNotThrowExceptionSingle() {
+        grid.setSelectionMode(SelectionMode.SINGLE);
+        grid.select(itemId1NotPresent);
+    }
+
+    @Test
+    public void selectAllMulti() {
+        grid.setSelectionMode(SelectionMode.MULTI);
+        final SelectionModel.Multi select = (SelectionModel.Multi) grid
+                .getSelectionModel();
+        select.selectAll();
+        assertEquals("added size", 10, mockListener.getAdded().size());
+        assertEquals("removed size", 0, mockListener.getRemoved().size());
+        assertTrue("itemId1Present",
+                mockListener.getAdded().contains(itemId1Present));
+        assertTrue("itemId2Present",
+                mockListener.getAdded().contains(itemId2Present));
+    }
+
+    @Test
+    public void deselectAllMulti() {
+        grid.setSelectionMode(SelectionMode.MULTI);
+        final SelectionModel.Multi select = (SelectionModel.Multi) grid
+                .getSelectionModel();
+        select.selectAll();
+        mockListener.clearEvent();
+
+        select.deselectAll();
+        assertEquals("removed size", 10, mockListener.getRemoved().size());
+        assertEquals("added size", 0, mockListener.getAdded().size());
+        assertTrue("itemId1Present",
+                mockListener.getRemoved().contains(itemId1Present));
+        assertTrue("itemId2Present",
+                mockListener.getRemoved().contains(itemId2Present));
+        assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty());
+    }
+
+    @Test
+    public void reselectionDeselectsPreviousSingle() {
+        grid.setSelectionMode(SelectionMode.SINGLE);
+        grid.select(itemId1Present);
+        mockListener.clearEvent();
+
+        grid.select(itemId2Present);
+        assertEquals("added size", 1, mockListener.getAdded().size());
+        assertEquals("removed size", 1, mockListener.getRemoved().size());
+        assertEquals("added item", itemId2Present, mockListener.getAdded()
+                .iterator().next());
+        assertEquals("removed item", itemId1Present, mockListener.getRemoved()
+                .iterator().next());
+        assertEquals("selectedRows is correct", itemId2Present,
+                grid.getSelectedRow());
+    }
+}