From 2eff69356b37e5c90e210e0e388d7220d18e834b Mon Sep 17 00:00:00 2001 From: Henrik Paul Date: Mon, 19 May 2014 18:26:24 +0300 Subject: [PATCH] Grid server-side selection (#13334) Change-Id: I62c5a2486360fe11de8a90efabb7775ef47124cb --- .../com/vaadin/ui/components/grid/Grid.java | 326 +++++++++++++++++- .../selection/AbstractSelectionModel.java | 71 ++++ .../grid/selection/MultiSelectionModel.java | 138 ++++++++ .../grid/selection/NoSelectionModel.java | 54 +++ .../grid/selection/SelectionChangeEvent.java | 73 ++++ .../selection/SelectionChangeListener.java | 35 ++ .../selection/SelectionChangeNotifier.java | 43 +++ .../grid/selection/SelectionModel.java | 234 +++++++++++++ .../grid/selection/SingleSelectionModel.java | 81 +++++ .../server/component/grid/GridSelection.java | 306 ++++++++++++++++ 10 files changed, 1357 insertions(+), 4 deletions(-) create mode 100644 server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java create mode 100644 server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java create mode 100644 server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java create mode 100644 server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java create mode 100644 server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java create mode 100644 server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java create mode 100644 server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java create mode 100644 server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java create mode 100644 server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java diff --git a/server/src/com/vaadin/ui/components/grid/Grid.java b/server/src/com/vaadin/ui/components/grid/Grid.java index da5be35d44..69beb260f4 100644 --- a/server/src/com/vaadin/ui/components/grid/Grid.java +++ b/server/src/com/vaadin/ui/components/grid/Grid.java @@ -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}. + *

+ * 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 null + * 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. + *

+ * The SelectionModel that is previously in use will have all its items + * deselected. + *

+ * 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 null + */ + 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. + *

+ * Grid supports three selection modes: multiselect, single select and no + * selection, and this is a conveniency method for choosing between one of + * them. + *

+ * 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. + *

+ * Essentially, the two following method calls are equivalent: + *

+ *

+     * grid.setSelectionMode(SelectionMode.MULTI);
+     * grid.setSelectionModel(new MultiSelectionMode());
+     * 
+ * + * + * @param selectionMode + * the selection mode to switch to + * @return The {@link SelectionModel} instance that was taken into use + * @throws IllegalArgumentException + * if {@code selectionMode} is null + * @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 true 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. + *

+ * 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 getSelectedRows() { + return getSelectionModel().getSelectedRows(); + } + + /** + * Gets the item id of the currently selected item. + *

+ * 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 null + * 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. + *

+ * 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 true if the selection state changed. + * false 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. + *

+ * 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 true if the selection state changed. + * false 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. + *

+ * Note: 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 oldSelection, + Collection 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 index 0000000000..3eb16d11fd --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/AbstractSelectionModel.java @@ -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 selection = new LinkedHashSet(); + protected Grid grid = null; + + @Override + public boolean isSelected(final Object itemId) { + return selection.contains(itemId); + } + + @Override + public Collection getSelectedRows() { + return new ArrayList(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. + *

+ * 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 before this event happened + * @param newSelection + * the complete {@link Collection} of the itemIds that are + * selected after this event happened + */ + protected void fireSelectionChangeEvent( + final Collection oldSelection, + final Collection newSelection) { + grid.fireSelectionChangeEvent(oldSelection, newSelection); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java new file mode 100644 index 0000000000..ca5f34484e --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/MultiSelectionModel.java @@ -0,0 +1,138 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.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 oldSelection = new HashSet(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 oldSelection = new HashSet(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} + *

+ * The returned Collection is in order of selection – + * 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 getSelectedRows() { + // overridden only for JavaDoc + return super.getSelectedRows(); + } + + /** + * Resets the selection model. + *

+ * Equivalent to calling {@link #deselectAll()} + */ + @Override + public void reset() { + deselectAll(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java new file mode 100644 index 0000000000..6d3213a82c --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/NoSelectionModel.java @@ -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 getSelectedRows() { + return Collections.emptyList(); + } + + /** + * Semantically resets the selection model. + *

+ * Effectively a no-op. + */ + @Override + public void reset() { + // NOOP + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java new file mode 100644 index 0000000000..b1097c88a6 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeEvent.java @@ -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 oldSelection; + private Set newSelection; + + public SelectionChangeEvent(Grid source, Collection oldSelection, + Collection newSelection) { + super(source); + this.oldSelection = new HashSet(oldSelection); + this.newSelection = new HashSet(newSelection); + } + + /** + * A {@link Collection} of all the itemIds that became selected. + *

+ * Note: this excludes all itemIds that might have been previously + * selected. + * + * @return a Collection of the itemIds that became selected + */ + public Set getAdded() { + return Sets.difference(newSelection, oldSelection); + } + + /** + * A {@link Collection} of all the itemIds that became deselected. + *

+ * Note: this excludes all itemIds that might have been previously + * deselected. + * + * @return a Collection of the itemIds that became deselected + */ + public Set getRemoved() { + return Sets.difference(oldSelection, newSelection); + } + + @Override + public Grid getSource() { + return (Grid) super.getSource(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java new file mode 100644 index 0000000000..764fee894f --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeListener.java @@ -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 index 0000000000..f61dc138c0 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionChangeNotifier.java @@ -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 index 0000000000..d48e72e1d3 --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SelectionModel.java @@ -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 true 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 getSelectedRows(); + + /** + * Injects the current {@link Grid} instance into the SelectionModel. + *

+ * Note: This method should not be called manually. + * + * @param grid + * the Grid in which the SelectionModel currently is, or + * null when a selection model is being detached + * from a Grid. + */ + void setGrid(Grid grid); + + /** + * Resets the SelectiomModel to an initial state. + *

+ * 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. + *

+ * 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. + *

+ * This method does not clear any previous selection state, only adds to + * it. + * + * @param itemIds + * the itemId(s) to mark as selected + * @return true if the selection state changed. + * false if all the given itemIds already were + * selected + * @throws IllegalArgumentException + * if the itemIds varargs array is + * null + * @see #deselect(Object...) + */ + boolean select(Object... itemIds) throws IllegalArgumentException; + + /** + * Marks items as selected. + *

+ * This method does not clear any previous selection state, only adds to + * it. + * + * @param itemIds + * the itemIds to mark as selected + * @return true if the selection state changed. + * false if all the given itemIds already were + * selected + * @throws IllegalArgumentException + * if itemIds is null + * @see #deselect(Collection) + */ + boolean select(Collection itemIds) throws IllegalArgumentException; + + /** + * Marks items as deselected. + * + * @param itemIds + * the itemId(s) to remove from being selected + * @return true if the selection state changed. + * false if none the given itemIds were selected + * previously + * @throws IllegalArgumentException + * if the itemIds varargs array is + * null + * @see #select(Object...) + */ + boolean deselect(Object... itemIds) throws IllegalArgumentException; + + /** + * Marks items as deselected. + * + * @param itemIds + * the itemId(s) to remove from being selected + * @return true if the selection state changed. + * false if none the given itemIds were selected + * previously + * @throws IllegalArgumentException + * if itemIds is null + * @see #select(Collection) + */ + boolean deselect(Collection itemIds) throws IllegalArgumentException; + + /** + * Marks all the items in the current Container as selected + * + * @return true iff some items were previously not selected + * @see #deselectAll() + */ + boolean selectAll(); + + /** + * Marks all the items in the current Container as deselected + * + * @return true iff some items were previously selected + * @see #selectAll() + */ + boolean deselectAll(); + } + + /** + * A SelectionModel that supports for only single rows to be selected at a + * time. + *

+ * 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 true if the selection state changed. + * false 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 true if the selection state changed. + * false 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 + * null if nothing is selected + */ + Object getSelectedRow(); + } + + /** + * A SelectionModel that does not allow for rows to be selected. + *

+ * 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 false. + */ + @Override + public boolean isSelected(Object itemId); + + /** + * {@inheritDoc} + * + * @return always an empty collection. + */ + @Override + public Collection getSelectedRows(); + } +} diff --git a/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java new file mode 100644 index 0000000000..dfac1d777c --- /dev/null +++ b/server/src/com/vaadin/ui/components/grid/selection/SingleSelectionModel.java @@ -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 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. + *

+ * 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 index 0000000000..7993d31295 --- /dev/null +++ b/server/tests/src/com/vaadin/tests/server/component/grid/GridSelection.java @@ -0,0 +1,306 @@ +/* + * Copyright 2000-2013 Vaadin Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.vaadin.tests.server.component.grid; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.util.Collection; + +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.data.util.IndexedContainer; +import com.vaadin.ui.components.grid.Grid; +import com.vaadin.ui.components.grid.Grid.SelectionMode; +import com.vaadin.ui.components.grid.selection.SelectionChangeEvent; +import com.vaadin.ui.components.grid.selection.SelectionChangeListener; +import com.vaadin.ui.components.grid.selection.SelectionModel; + +/** + * + * @since + * @author Vaadin Ltd + */ +public class GridSelection { + + private static class MockSelectionChangeListener implements + SelectionChangeListener { + private SelectionChangeEvent event; + + @Override + public void selectionChange(final SelectionChangeEvent event) { + this.event = event; + } + + public Collection getAdded() { + return event.getAdded(); + } + + public Collection getRemoved() { + return event.getRemoved(); + } + + public void clearEvent() { + /* + * This method is not strictly needed as the event will simply be + * overridden, but it's good practice, and makes the code more + * obvious. + */ + event = null; + } + + public boolean eventHasHappened() { + return event != null; + } + } + + private Grid grid; + private MockSelectionChangeListener mockListener; + + private final Object itemId1Present = "itemId1Present"; + private final Object itemId2Present = "itemId2Present"; + + private final Object itemId1NotPresent = "itemId1NotPresent"; + private final Object itemId2NotPresent = "itemId2NotPresent"; + + @Before + public void setup() { + final IndexedContainer container = new IndexedContainer(); + container.addItem(itemId1Present); + container.addItem(itemId2Present); + for (int i = 2; i < 10; i++) { + container.addItem(new Object()); + } + + assertEquals("init size", 10, container.size()); + assertTrue("itemId1Present", container.containsId(itemId1Present)); + assertTrue("itemId2Present", container.containsId(itemId2Present)); + assertFalse("itemId1NotPresent", + container.containsId(itemId1NotPresent)); + assertFalse("itemId2NotPresent", + container.containsId(itemId2NotPresent)); + + grid = new Grid(container); + + mockListener = new MockSelectionChangeListener(); + grid.addSelectionChangeListener(mockListener); + + assertFalse("eventHasHappened", mockListener.eventHasHappened()); + } + + @Test + public void defaultSelectionModeIsMulti() { + assertTrue(grid.getSelectionModel() instanceof SelectionModel.Multi); + } + + @Test(expected = IllegalStateException.class) + public void getSelectedRowThrowsExceptionMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + grid.getSelectedRow(); + } + + @Test(expected = IllegalStateException.class) + public void getSelectedRowThrowsExceptionNone() { + grid.setSelectionMode(SelectionMode.NONE); + grid.getSelectedRow(); + } + + @Test(expected = IllegalStateException.class) + public void selectThrowsExceptionNone() { + grid.setSelectionMode(SelectionMode.NONE); + grid.select(itemId1Present); + } + + @Test(expected = IllegalStateException.class) + public void deselectRowThrowsExceptionNone() { + grid.setSelectionMode(SelectionMode.NONE); + grid.deselect(itemId1Present); + } + + @Test + public void selectionModeMapsToMulti() { + assertTrue(grid.setSelectionMode(SelectionMode.MULTI) instanceof SelectionModel.Multi); + } + + @Test + public void selectionModeMapsToSingle() { + assertTrue(grid.setSelectionMode(SelectionMode.SINGLE) instanceof SelectionModel.Single); + } + + @Test + public void selectionModeMapsToNone() { + assertTrue(grid.setSelectionMode(SelectionMode.NONE) instanceof SelectionModel.None); + } + + @Test(expected = IllegalArgumentException.class) + public void selectionModeNullThrowsException() { + grid.setSelectionMode(null); + } + + @Test + public void noSelectModel_isSelected() { + grid.setSelectionMode(SelectionMode.NONE); + assertFalse("itemId1Present", grid.isSelected(itemId1Present)); + assertFalse("itemId1NotPresent", grid.isSelected(itemId1NotPresent)); + } + + @Test(expected = IllegalStateException.class) + public void noSelectModel_getSelectedRow() { + grid.setSelectionMode(SelectionMode.NONE); + grid.getSelectedRow(); + } + + @Test + public void noSelectModel_getSelectedRows() { + grid.setSelectionMode(SelectionMode.NONE); + assertTrue(grid.getSelectedRows().isEmpty()); + } + + @Test + public void selectionCallsListenerMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + selectionCallsListener(); + } + + @Test + public void selectionCallsListenerSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + selectionCallsListener(); + } + + private void selectionCallsListener() { + grid.select(itemId1Present); + assertEquals("added size", 1, mockListener.getAdded().size()); + assertEquals("added item", itemId1Present, mockListener.getAdded() + .iterator().next()); + assertEquals("removed size", 0, mockListener.getRemoved().size()); + } + + @Test + public void deselectionCallsListenerMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + deselectionCallsListener(); + } + + @Test + public void deselectionCallsListenerSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + deselectionCallsListener(); + } + + private void deselectionCallsListener() { + grid.select(itemId1Present); + mockListener.clearEvent(); + + grid.deselect(itemId1Present); + assertEquals("removed size", 1, mockListener.getRemoved().size()); + assertEquals("removed item", itemId1Present, mockListener.getRemoved() + .iterator().next()); + assertEquals("removed size", 0, mockListener.getAdded().size()); + } + + @Test + public void deselectPresentButNotSelectedItemIdShouldntFireListenerMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + deselectPresentButNotSelectedItemIdShouldntFireListener(); + } + + @Test + public void deselectPresentButNotSelectedItemIdShouldntFireListenerSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + deselectPresentButNotSelectedItemIdShouldntFireListener(); + } + + private void deselectPresentButNotSelectedItemIdShouldntFireListener() { + grid.deselect(itemId1Present); + assertFalse(mockListener.eventHasHappened()); + } + + @Test + public void deselectNotPresentItemIdShouldNotThrowExceptionMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + grid.deselect(itemId1NotPresent); + } + + @Test + public void deselectNotPresentItemIdShouldNotThrowExceptionSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + grid.deselect(itemId1NotPresent); + } + + @Test + public void selectNotPresentItemIdShouldNotThrowExceptionMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + grid.select(itemId1NotPresent); + } + + @Test + public void selectNotPresentItemIdShouldNotThrowExceptionSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + grid.select(itemId1NotPresent); + } + + @Test + public void selectAllMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + final SelectionModel.Multi select = (SelectionModel.Multi) grid + .getSelectionModel(); + select.selectAll(); + assertEquals("added size", 10, mockListener.getAdded().size()); + assertEquals("removed size", 0, mockListener.getRemoved().size()); + assertTrue("itemId1Present", + mockListener.getAdded().contains(itemId1Present)); + assertTrue("itemId2Present", + mockListener.getAdded().contains(itemId2Present)); + } + + @Test + public void deselectAllMulti() { + grid.setSelectionMode(SelectionMode.MULTI); + final SelectionModel.Multi select = (SelectionModel.Multi) grid + .getSelectionModel(); + select.selectAll(); + mockListener.clearEvent(); + + select.deselectAll(); + assertEquals("removed size", 10, mockListener.getRemoved().size()); + assertEquals("added size", 0, mockListener.getAdded().size()); + assertTrue("itemId1Present", + mockListener.getRemoved().contains(itemId1Present)); + assertTrue("itemId2Present", + mockListener.getRemoved().contains(itemId2Present)); + assertTrue("selectedRows is empty", grid.getSelectedRows().isEmpty()); + } + + @Test + public void reselectionDeselectsPreviousSingle() { + grid.setSelectionMode(SelectionMode.SINGLE); + grid.select(itemId1Present); + mockListener.clearEvent(); + + grid.select(itemId2Present); + assertEquals("added size", 1, mockListener.getAdded().size()); + assertEquals("removed size", 1, mockListener.getRemoved().size()); + assertEquals("added item", itemId2Present, mockListener.getAdded() + .iterator().next()); + assertEquals("removed item", itemId1Present, mockListener.getRemoved() + .iterator().next()); + assertEquals("selectedRows is correct", itemId2Present, + grid.getSelectedRow()); + } +} -- 2.39.5