aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java340
-rw-r--r--server/src/main/java/com/vaadin/ui/CheckBoxGroup.java220
-rw-r--r--server/src/main/java/com/vaadin/ui/RadioButtonGroup.java10
-rw-r--r--server/src/main/java/com/vaadin/ui/TwinColSelect.java158
-rw-r--r--server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java71
-rw-r--r--server/src/test/java/com/vaadin/ui/AbstractMultiSelectTest.java235
-rw-r--r--server/src/test/java/com/vaadin/ui/CheckBoxGroupBoVTest.java11
-rw-r--r--server/src/test/java/com/vaadin/ui/CheckBoxGroupTest.java105
8 files changed, 820 insertions, 330 deletions
diff --git a/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java b/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java
index 1d17841812..3d2cb151b8 100644
--- a/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java
+++ b/server/src/main/java/com/vaadin/ui/AbstractMultiSelect.java
@@ -16,16 +16,33 @@
package com.vaadin.ui;
import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
import com.vaadin.event.selection.MultiSelectionEvent;
import com.vaadin.event.selection.MultiSelectionListener;
+import com.vaadin.server.Resource;
+import com.vaadin.server.ResourceReference;
+import com.vaadin.server.data.DataGenerator;
import com.vaadin.shared.Registration;
+import com.vaadin.shared.data.selection.MultiSelectServerRpc;
+import com.vaadin.shared.data.selection.SelectionModel;
import com.vaadin.shared.data.selection.SelectionModel.Multi;
+import com.vaadin.shared.ui.ListingJsonConstants;
import com.vaadin.util.ReflectTools;
+import elemental.json.JsonObject;
+
/**
* Base class for listing components that allow selecting multiple items.
+ * <p>
+ * Sends selection information individually for each item.
*
* @param <T>
* item type
@@ -35,16 +52,240 @@ import com.vaadin.util.ReflectTools;
public abstract class AbstractMultiSelect<T>
extends AbstractListing<T, Multi<T>> {
+ /**
+ * Simple implementation of multiselectmodel.
+ */
+ protected class SimpleMultiSelectModel implements SelectionModel.Multi<T> {
+
+ private Set<T> selection = new LinkedHashSet<>();
+
+ @Override
+ public void select(T item) {
+ // Not user originated
+ select(item, false);
+ }
+
+ /**
+ * Selects the given item. Depending on the implementation, may cause
+ * other items to be deselected. If the item is already selected, does
+ * nothing.
+ *
+ * @param item
+ * the item to select, not null
+ * @param userOriginated
+ * {@code true} if this was used originated, {@code false} if
+ * not
+ */
+ protected void select(T item, boolean userOriginated) {
+ if (selection.contains(item)) {
+ return;
+ }
+
+ updateSelection(set -> set.add(item), userOriginated);
+ }
+
+ @Override
+ public void updateSelection(Set<T> addedItems, Set<T> removedItems) {
+ updateSelection(addedItems, removedItems, false);
+ }
+
+ /**
+ * Updates the selection by adding and removing the given items.
+ *
+ * @param addedItems
+ * the items added to selection, not {@code} null
+ * @param removedItems
+ * the items removed from selection, not {@code} null
+ * @param userOriginated
+ * {@code true} if this was used originated, {@code false} if
+ * not
+ */
+ protected void updateSelection(Set<T> addedItems, Set<T> removedItems,
+ boolean userOriginated) {
+ Objects.requireNonNull(addedItems);
+ Objects.requireNonNull(removedItems);
+
+ // if there are duplicates, some item is both added & removed, just
+ // discard that and leave things as was before
+ addedItems.removeIf(item -> removedItems.remove(item));
+
+ if (selection.containsAll(addedItems)
+ && Collections.disjoint(selection, removedItems)) {
+ return;
+ }
+
+ updateSelection(set -> {
+ // order of add / remove does not matter since no duplicates
+ set.removeAll(removedItems);
+ set.addAll(addedItems);
+ }, userOriginated);
+ }
+
+ @Override
+ public Set<T> getSelectedItems() {
+ return Collections.unmodifiableSet(new LinkedHashSet<>(selection));
+ }
+
+ @Override
+ public void deselect(T item) {
+ // Not user originated
+ deselect(item, false);
+ }
+
+ /**
+ * Deselects the given item. If the item is not currently selected, does
+ * nothing.
+ *
+ * @param item
+ * the item to deselect, not null
+ * @param userOriginated
+ * {@code true} if this was used originated, {@code false} if
+ * not
+ */
+ protected void deselect(T item, boolean userOriginated) {
+ if (!selection.contains(item)) {
+ return;
+ }
+
+ updateSelection(set -> set.remove(item), userOriginated);
+ }
+
+ /**
+ * Removes the given items. Any item that is not currently selected, is
+ * ignored. If none of the items are selected, does nothing.
+ *
+ * @param items
+ * the items to deselect, not {@code null}
+ * @param userOriginated
+ * {@code true} if this was used originated, {@code false} if
+ * not
+ */
+ protected void deselect(Set<T> items, boolean userOriginated) {
+ Objects.requireNonNull(items);
+ if (items.stream().noneMatch(i -> isSelected(i))) {
+ return;
+ }
+
+ updateSelection(set -> set.removeAll(items), userOriginated);
+ }
+
+ @Override
+ public void deselectAll() {
+ if (selection.isEmpty()) {
+ return;
+ }
+
+ updateSelection(Set::clear, false);
+ }
+
+ private void updateSelection(Consumer<Set<T>> handler,
+ boolean userOriginated) {
+ LinkedHashSet<T> oldSelection = new LinkedHashSet<>(selection);
+ handler.accept(selection);
+ LinkedHashSet<T> newSelection = new LinkedHashSet<>(selection);
+
+ fireEvent(new MultiSelectionEvent<>(AbstractMultiSelect.this,
+ oldSelection, newSelection, userOriginated));
+
+ getDataCommunicator().reset();
+ }
+
+ @Override
+ public boolean isSelected(T item) {
+ return selection.contains(item);
+ }
+ }
+
+ private class MultiSelectServerRpcImpl implements MultiSelectServerRpc {
+ @Override
+ public void updateSelection(Set<String> selectedItemKeys,
+ Set<String> deselectedItemKeys) {
+ getSelectionModel().updateSelection(
+ getItemsForSelectionChange(selectedItemKeys),
+ getItemsForSelectionChange(deselectedItemKeys), true);
+ }
+
+ private Set<T> getItemsForSelectionChange(Set<String> keys) {
+ return keys.stream().map(key -> getItemForSelectionChange(key))
+ .filter(Optional::isPresent).map(Optional::get)
+ .collect(Collectors.toSet());
+ }
+
+ private Optional<T> getItemForSelectionChange(String key) {
+ T item = getDataCommunicator().getKeyMapper().get(key);
+ if (item == null || !getItemEnabledProvider().test(item)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(item);
+ }
+
+ private SimpleMultiSelectModel getSelectionModel() {
+ return (SimpleMultiSelectModel) AbstractMultiSelect.this
+ .getSelectionModel();
+ }
+ }
+
+ private class MultiSelectDataGenerator implements DataGenerator<T> {
+ @Override
+ public void generateData(T data, JsonObject jsonObject) {
+ jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_VALUE,
+ getItemCaptionGenerator().apply(data));
+ Resource icon = getItemIconGenerator().apply(data);
+ if (icon != null) {
+ String iconUrl = ResourceReference
+ .create(icon, AbstractMultiSelect.this, null).getURL();
+ jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_ICON, iconUrl);
+ }
+ if (!getItemEnabledProvider().test(data)) {
+ jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_DISABLED,
+ true);
+ }
+
+ if (getSelectionModel().isSelected(data)) {
+ jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_SELECTED,
+ true);
+ }
+ }
+
+ @Override
+ public void destroyData(T data) {
+ }
+ }
+
@Deprecated
private static final Method SELECTION_CHANGE_METHOD = ReflectTools
.findMethod(MultiSelectionListener.class, "accept",
MultiSelectionEvent.class);
/**
+ * The item icon caption provider.
+ */
+ private ItemCaptionGenerator<T> itemCaptionGenerator = String::valueOf;
+
+ /**
+ * The item icon provider. It is up to the implementing class to support
+ * this or not.
+ */
+ private IconGenerator<T> itemIconGenerator = item -> null;
+
+ /**
+ * The item enabled status provider. It is up to the implementing class to
+ * support this or not.
+ */
+ private Predicate<T> itemEnabledProvider = item -> true;
+
+ /**
* Creates a new multi select with an empty data source.
*/
protected AbstractMultiSelect() {
- super();
+ setSelectionModel(new SimpleMultiSelectModel());
+
+ registerRpc(new MultiSelectServerRpcImpl());
+
+ // #FIXME it should be the responsibility of the SelectionModel
+ // (AbstractSelectionModel) to add selection data for item
+ addDataGenerator(new MultiSelectDataGenerator());
}
/**
@@ -52,7 +293,7 @@ public abstract class AbstractMultiSelect<T>
* changed either by the user or programmatically.
*
* @param listener
- * the value change listener, not <code>null</code>
+ * the value change listener, not {@code null}
* @return a registration for the listener
*/
public Registration addSelectionListener(
@@ -63,4 +304,99 @@ public abstract class AbstractMultiSelect<T>
return () -> removeListener(MultiSelectionEvent.class, listener);
}
+ /**
+ * Gets the item caption generator that is used to produce the strings shown
+ * in the select for each item.
+ *
+ * @return the item caption generator used, not {@code null}
+ * @see #setItemCaptionGenerator(ItemCaptionGenerator)
+ */
+ public ItemCaptionGenerator<T> getItemCaptionGenerator() {
+ return itemCaptionGenerator;
+ }
+
+ /**
+ * Sets the item caption generator that is used to produce the strings shown
+ * in the select for each item. By default, {@link String#valueOf(Object)}
+ * is used.
+ *
+ * @param itemCaptionGenerator
+ * the item caption generator to use, not {@code null}
+ */
+ public void setItemCaptionGenerator(
+ ItemCaptionGenerator<T> itemCaptionGenerator) {
+ Objects.requireNonNull(itemCaptionGenerator);
+ this.itemCaptionGenerator = itemCaptionGenerator;
+ getDataCommunicator().reset();
+ }
+
+ /**
+ * Returns the item icon generator for this multiselect.
+ * <p>
+ * <em>Implementation note:</em> Override this method and
+ * {@link #setItemIconGenerator(IconGenerator)} as {@code public} and invoke
+ * {@code super} methods to support this feature in the multiselect
+ * component.
+ *
+ * @return the item icon generator, not {@code null}
+ * @see #setItemIconGenerator(IconGenerator)
+ */
+ protected IconGenerator<T> getItemIconGenerator() {
+ return itemIconGenerator;
+ }
+
+ /**
+ * Sets the item icon generator for this multiselect. The icon generator is
+ * queried for each item to optionally display an icon next to the item
+ * caption. If the generator returns null for an item, no icon is displayed.
+ * The default provider always returns null (no icons).
+ * <p>
+ * <em>Implementation note:</em> Override this method and
+ * {@link #getItemIconGenerator()} as {@code public} and invoke
+ * {@code super} methods to support this feature in the multiselect
+ * component.
+ *
+ * @param itemIconGenerator
+ * the item icon generator to set, not {@code null}
+ */
+ protected void setItemIconGenerator(IconGenerator<T> itemIconGenerator) {
+ Objects.requireNonNull(itemIconGenerator);
+ this.itemIconGenerator = itemIconGenerator;
+ }
+
+ /**
+ * Returns the item enabled provider for this multiselect.
+ * <p>
+ * <em>Implementation note:</em> Override this method and
+ * {@link #setItemEnabledProvider(Predicate)} as {@code public} and invoke
+ * {@code super} methods to support this feature in the multiselect
+ * component.
+ *
+ * @return the item enabled provider, not {@code null}
+ * @see #setItemEnabledProvider(Predicate)
+ */
+ protected Predicate<T> getItemEnabledProvider() {
+ return itemEnabledProvider;
+ }
+
+ /**
+ * Sets the item enabled predicate for this multiselect. The predicate is
+ * applied to each item to determine whether the item should be enabled
+ * ({@code true}) or disabled ({@code false}). Disabled items are displayed
+ * as grayed out and the user cannot select them. The default predicate
+ * always returns {@code true} (all the items are enabled).
+ * <p>
+ * <em>Implementation note:</em> Override this method and
+ * {@link #getItemEnabledProvider()} as {@code public} and invoke
+ * {@code super} methods to support this feature in the multiselect
+ * component.
+ *
+ * @param itemEnabledProvider
+ * the item enabled provider to set, not {@code null}
+ */
+ protected void setItemEnabledProvider(Predicate<T> itemEnabledProvider) {
+ Objects.requireNonNull(itemEnabledProvider);
+ this.itemEnabledProvider = itemEnabledProvider;
+ }
+
} \ No newline at end of file
diff --git a/server/src/main/java/com/vaadin/ui/CheckBoxGroup.java b/server/src/main/java/com/vaadin/ui/CheckBoxGroup.java
index d5b2c5a851..0a1e4c71af 100644
--- a/server/src/main/java/com/vaadin/ui/CheckBoxGroup.java
+++ b/server/src/main/java/com/vaadin/ui/CheckBoxGroup.java
@@ -17,28 +17,12 @@
package com.vaadin.ui;
import java.util.Collection;
-import java.util.Collections;
-import java.util.LinkedHashSet;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Consumer;
-import java.util.function.Function;
import java.util.function.Predicate;
import com.vaadin.data.Listing;
-import com.vaadin.event.selection.MultiSelectionEvent;
-import com.vaadin.server.Resource;
-import com.vaadin.server.ResourceReference;
-import com.vaadin.server.data.DataGenerator;
import com.vaadin.server.data.DataSource;
-import com.vaadin.shared.data.selection.SelectionModel;
-import com.vaadin.shared.data.selection.SelectionServerRpc;
-import com.vaadin.shared.ui.optiongroup.CheckBoxGroupConstants;
import com.vaadin.shared.ui.optiongroup.CheckBoxGroupState;
-import elemental.json.JsonObject;
-
/**
* A group of Checkboxes. Individual checkboxes are made from items supplied by
* a {@link DataSource}. Checkboxes may have captions and icons.
@@ -50,77 +34,6 @@ import elemental.json.JsonObject;
*/
public class CheckBoxGroup<T> extends AbstractMultiSelect<T> {
- private final class SimpleMultiSelectModel
- implements SelectionModel.Multi<T> {
-
- private Set<T> selection = new LinkedHashSet<>();
-
- @Override
- public void select(T item) {
- // Not user originated
- select(item, false);
- }
-
- private void select(T item, boolean userOriginated) {
- if (selection.contains(item)) {
- return;
- }
-
- updateSelection(set -> set.add(item), userOriginated);
- }
-
- @Override
- public Set<T> getSelectedItems() {
- return Collections.unmodifiableSet(new LinkedHashSet<>(selection));
- }
-
- @Override
- public void deselect(T item) {
- // Not user originated
- deselect(item, false);
- }
-
- private void deselect(T item, boolean userOriginated) {
- if (!selection.contains(item)) {
- return;
- }
-
- updateSelection(set -> set.remove(item), userOriginated);
- }
-
- @Override
- public void deselectAll() {
- if (selection.isEmpty()) {
- return;
- }
-
- updateSelection(Set::clear, false);
- }
-
- private void updateSelection(Consumer<Set<T>> handler,
- boolean userOriginated) {
- LinkedHashSet<T> oldSelection = new LinkedHashSet<>(selection);
- handler.accept(selection);
- LinkedHashSet<T> newSelection = new LinkedHashSet<>(selection);
-
- fireEvent(new MultiSelectionEvent<>(CheckBoxGroup.this,
- oldSelection, newSelection, userOriginated));
-
- getDataCommunicator().reset();
- }
-
- @Override
- public boolean isSelected(T item) {
- return selection.contains(item);
- }
- }
-
- private Function<T, Resource> itemIconProvider = item -> null;
-
- private Function<T, String> itemCaptionProvider = String::valueOf;
-
- private Predicate<T> itemEnabledProvider = item -> true;
-
/**
* Constructs a new CheckBoxGroup with caption.
*
@@ -167,65 +80,6 @@ public class CheckBoxGroup<T> extends AbstractMultiSelect<T> {
* @see Listing#setDataSource(DataSource)
*/
public CheckBoxGroup() {
- setSelectionModel(new SimpleMultiSelectModel());
-
- registerRpc(new SelectionServerRpc() {
-
- @Override
- public void select(String key) {
- getItemForSelectionChange(key).ifPresent(
- item -> getSelectionModel().select(item, true));
- }
-
- @Override
- public void deselect(String key) {
- getItemForSelectionChange(key).ifPresent(
- item -> getSelectionModel().deselect(item, true));
- }
-
- private Optional<T> getItemForSelectionChange(String key) {
- T item = getDataCommunicator().getKeyMapper().get(key);
- if (item == null || !itemEnabledProvider.test(item)) {
- return Optional.empty();
- }
-
- return Optional.of(item);
- }
-
- private SimpleMultiSelectModel getSelectionModel() {
- return (SimpleMultiSelectModel) CheckBoxGroup.this
- .getSelectionModel();
- }
- });
-
- addDataGenerator(new DataGenerator<T>() {
- @Override
- public void generateData(T data, JsonObject jsonObject) {
- jsonObject.put(CheckBoxGroupConstants.JSONKEY_ITEM_VALUE,
- itemCaptionProvider.apply(data));
- Resource icon = itemIconProvider.apply(data);
- if (icon != null) {
- String iconUrl = ResourceReference
- .create(icon, CheckBoxGroup.this, null).getURL();
- jsonObject.put(CheckBoxGroupConstants.JSONKEY_ITEM_ICON,
- iconUrl);
- }
- if (!itemEnabledProvider.test(data)) {
- jsonObject.put(CheckBoxGroupConstants.JSONKEY_ITEM_DISABLED,
- true);
- }
-
- if (getSelectionModel().isSelected(data)) {
- jsonObject.put(CheckBoxGroupConstants.JSONKEY_ITEM_SELECTED,
- true);
- }
- }
-
- @Override
- public void destroyData(T data) {
- }
- });
-
}
/**
@@ -263,77 +117,23 @@ public class CheckBoxGroup<T> extends AbstractMultiSelect<T> {
return (CheckBoxGroupState) super.getState(markAsDirty);
}
- /**
- * Returns the item icons provider.
- *
- * @return the icons provider for items
- * @see #setItemIconProvider
- */
- public Function<T, Resource> getItemIconProvider() {
- return itemIconProvider;
- }
-
- /**
- * Sets the item icon provider for this checkbox group. The icon provider is
- * queried for each item to optionally display an icon next to the item
- * caption. If the provider returns null for an item, no icon is displayed.
- * The default provider always returns null (no icons).
- *
- * @param itemIconProvider
- * icons provider, not null
- */
- public void setItemIconProvider(Function<T, Resource> itemIconProvider) {
- Objects.nonNull(itemIconProvider);
- this.itemIconProvider = itemIconProvider;
- }
-
- /**
- * Returns the item caption provider.
- *
- * @return the captions provider
- * @see #setItemCaptionProvider
- */
- public Function<T, String> getItemCaptionProvider() {
- return itemCaptionProvider;
+ @Override
+ public IconGenerator<T> getItemIconGenerator() {
+ return super.getItemIconGenerator();
}
- /**
- * Sets the item caption provider for this checkbox group. The caption
- * provider is queried for each item to optionally display an item textual
- * representation. The default provider returns
- * {@code String.valueOf(item)}.
- *
- * @param itemCaptionProvider
- * the item caption provider, not null
- */
- public void setItemCaptionProvider(
- Function<T, String> itemCaptionProvider) {
- Objects.nonNull(itemCaptionProvider);
- this.itemCaptionProvider = itemCaptionProvider;
+ @Override
+ public void setItemIconGenerator(IconGenerator<T> itemIconGenerator) {
+ super.setItemIconGenerator(itemIconGenerator);
}
- /**
- * Returns the item enabled predicate.
- *
- * @return the item enabled predicate
- * @see #setItemEnabledProvider
- */
+ @Override
public Predicate<T> getItemEnabledProvider() {
- return itemEnabledProvider;
+ return super.getItemEnabledProvider();
}
- /**
- * Sets the item enabled predicate for this checkbox group. The predicate is
- * applied to each item to determine whether the item should be enabled
- * (true) or disabled (false). Disabled items are displayed as grayed out
- * and the user cannot select them. The default predicate always returns
- * true (all the items are enabled).
- *
- * @param itemEnabledProvider
- * the item enable predicate, not null
- */
+ @Override
public void setItemEnabledProvider(Predicate<T> itemEnabledProvider) {
- Objects.nonNull(itemEnabledProvider);
- this.itemEnabledProvider = itemEnabledProvider;
+ super.setItemEnabledProvider(itemEnabledProvider);
}
}
diff --git a/server/src/main/java/com/vaadin/ui/RadioButtonGroup.java b/server/src/main/java/com/vaadin/ui/RadioButtonGroup.java
index e3cc1892b0..f22e4a9535 100644
--- a/server/src/main/java/com/vaadin/ui/RadioButtonGroup.java
+++ b/server/src/main/java/com/vaadin/ui/RadioButtonGroup.java
@@ -21,7 +21,7 @@ import com.vaadin.server.Resource;
import com.vaadin.server.ResourceReference;
import com.vaadin.server.data.DataGenerator;
import com.vaadin.server.data.DataSource;
-import com.vaadin.shared.ui.optiongroup.RadioButtonGroupConstants;
+import com.vaadin.shared.ui.ListingJsonConstants;
import com.vaadin.shared.ui.optiongroup.RadioButtonGroupState;
import elemental.json.JsonObject;
@@ -98,22 +98,22 @@ public class RadioButtonGroup<T> extends AbstractSingleSelect<T> {
addDataGenerator(new DataGenerator<T>() {
@Override
public void generateData(T data, JsonObject jsonObject) {
- jsonObject.put(RadioButtonGroupConstants.JSONKEY_ITEM_VALUE,
+ jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_VALUE,
itemCaptionProvider.apply(data));
Resource icon = itemIconProvider.apply(data);
if (icon != null) {
String iconUrl = ResourceReference
.create(icon, RadioButtonGroup.this, null).getURL();
- jsonObject.put(RadioButtonGroupConstants.JSONKEY_ITEM_ICON,
+ jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_ICON,
iconUrl);
}
if (!itemEnabledProvider.test(data)) {
- jsonObject.put(RadioButtonGroupConstants.JSONKEY_ITEM_DISABLED,
+ jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_DISABLED,
true);
}
if (getSelectionModel().isSelected(data)) {
- jsonObject.put(RadioButtonGroupConstants.JSONKEY_ITEM_SELECTED,
+ jsonObject.put(ListingJsonConstants.JSONKEY_ITEM_SELECTED,
true);
}
}
diff --git a/server/src/main/java/com/vaadin/ui/TwinColSelect.java b/server/src/main/java/com/vaadin/ui/TwinColSelect.java
new file mode 100644
index 0000000000..4b321559a3
--- /dev/null
+++ b/server/src/main/java/com/vaadin/ui/TwinColSelect.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2000-2016 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;
+
+import java.util.Collection;
+
+import com.vaadin.server.data.DataSource;
+import com.vaadin.shared.ui.twincolselect.TwinColSelectState;
+
+/**
+ * Multiselect component with two lists: left side for available items and right
+ * side for selected items.
+ *
+ * @author Vaadin Ltd
+ *
+ * @param <T>
+ * item type
+ */
+public class TwinColSelect<T> extends AbstractMultiSelect<T> {
+
+ /**
+ * Constructs a new TwinColSelect.
+ */
+ public TwinColSelect() {
+ }
+
+ /**
+ * Constructs a new TwinColSelect with the given caption.
+ *
+ * @param caption
+ * the caption to set, can be {@code null}
+ */
+ public TwinColSelect(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a new TwinColSelect with caption and data source for options.
+ *
+ * @param caption
+ * the caption to set, can be {@code null}
+ * @param dataSource
+ * the data source, not {@code null}
+ */
+ public TwinColSelect(String caption, DataSource<T> dataSource) {
+ this(caption);
+ setDataSource(dataSource);
+ }
+
+ /**
+ * Constructs a new TwinColSelect with caption and the given options.
+ *
+ * @param caption
+ * the caption to set, can be {@code null}
+ * @param options
+ * the options, cannot be {@code null}
+ */
+ public TwinColSelect(String caption, Collection<T> options) {
+ this(caption, DataSource.create(options));
+ }
+
+ /**
+ * Returns the number of rows in the selects.
+ *
+ * @return the number of rows visible
+ */
+ public int getRows() {
+ return getState(false).rows;
+ }
+
+ /**
+ * Sets the number of rows in the selects. If the number of rows is set to 0
+ * or less, the actual number of displayed rows is determined implicitly by
+ * the selects.
+ * <p>
+ * If a height if set (using {@link #setHeight(String)} or
+ * {@link #setHeight(float, int)}) it overrides the number of rows. Leave
+ * the height undefined to use this method.
+ *
+ * @param rows
+ * the number of rows to set.
+ */
+ public void setRows(int rows) {
+ if (rows < 0) {
+ rows = 0;
+ }
+ if (getState(false).rows != rows) {
+ getState().rows = rows;
+ }
+ }
+
+ /**
+ * Sets the text shown above the right column. {@code null} clears the
+ * caption.
+ *
+ * @param rightColumnCaption
+ * The text to show, {@code null} to clear
+ */
+ public void setRightColumnCaption(String rightColumnCaption) {
+ getState().rightColumnCaption = rightColumnCaption;
+ }
+
+ /**
+ * Returns the text shown above the right column.
+ *
+ * @return The text shown or {@code null} if not set.
+ */
+ public String getRightColumnCaption() {
+ return getState(false).rightColumnCaption;
+ }
+
+ /**
+ * Sets the text shown above the left column. {@code null} clears the
+ * caption.
+ *
+ * @param leftColumnCaption
+ * The text to show, {@code null} to clear
+ */
+ public void setLeftColumnCaption(String leftColumnCaption) {
+ getState().leftColumnCaption = leftColumnCaption;
+ markAsDirty();
+ }
+
+ /**
+ * Returns the text shown above the left column.
+ *
+ * @return The text shown or {@code null} if not set.
+ */
+ public String getLeftColumnCaption() {
+ return getState(false).leftColumnCaption;
+ }
+
+ @Override
+ protected TwinColSelectState getState() {
+ return (TwinColSelectState) super.getState();
+ }
+
+ @Override
+ protected TwinColSelectState getState(boolean markAsDirty) {
+ return (TwinColSelectState) super.getState(markAsDirty);
+ }
+
+}
diff --git a/server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java b/server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java
new file mode 100644
index 0000000000..6ec4f16f86
--- /dev/null
+++ b/server/src/test/java/com/vaadin/tests/server/component/twincolselect/TwinColSelectDeclarativeTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2000-2014 Vaadin Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.vaadin.tests.server.component.twincolselect;
+
+import org.junit.Test;
+
+import com.vaadin.tests.design.DeclarativeTestBase;
+import com.vaadin.ui.TwinColSelect;
+
+/**
+ * Test cases for reading the properties of selection components.
+ *
+ * @author Vaadin Ltd
+ */
+public class TwinColSelectDeclarativeTest
+ extends DeclarativeTestBase<TwinColSelect<String>> {
+
+ public String getBasicDesign() {
+ return "<vaadin-twin-col-select rows=5 right-column-caption='Selected values' left-column-caption='Unselected values'>\n"
+ + " <option>First item</option>\n"
+ + " <option selected>Second item</option>\n"
+ + " <option selected>Third item</option>\n"
+ + "</vaadin-twin-col-select>";
+
+ }
+
+ public TwinColSelect<String> getBasicExpected() {
+ TwinColSelect<String> s = new TwinColSelect<>();
+ s.setRightColumnCaption("Selected values");
+ s.setLeftColumnCaption("Unselected values");
+ s.setItems("First item", "Second item", "Third item");
+ s.getSelectionModel().select("Second item");
+ s.getSelectionModel().select("Third item");
+ s.setRows(5);
+ return s;
+ }
+
+ @Test
+ public void testReadBasic() {
+ testRead(getBasicDesign(), getBasicExpected());
+ }
+
+ @Test
+ public void testWriteBasic() {
+ testWrite(stripOptionTags(getBasicDesign()), getBasicExpected());
+ }
+
+ @Test
+ public void testReadEmpty() {
+ testRead("<vaadin-twin-col-select />", new TwinColSelect());
+ }
+
+ @Test
+ public void testWriteEmpty() {
+ testWrite("<vaadin-twin-col-select />", new TwinColSelect());
+ }
+
+} \ No newline at end of file
diff --git a/server/src/test/java/com/vaadin/ui/AbstractMultiSelectTest.java b/server/src/test/java/com/vaadin/ui/AbstractMultiSelectTest.java
new file mode 100644
index 0000000000..3505e4ec38
--- /dev/null
+++ b/server/src/test/java/com/vaadin/ui/AbstractMultiSelectTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2000-2016 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;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+import com.vaadin.server.data.DataSource;
+import com.vaadin.shared.Registration;
+import com.vaadin.shared.data.selection.MultiSelectServerRpc;
+import com.vaadin.shared.data.selection.SelectionModel.Multi;
+
+@RunWith(Parameterized.class)
+public class AbstractMultiSelectTest {
+
+ @Parameters(name = "{0}")
+ public static Iterable<AbstractMultiSelect<String>> multiSelects() {
+ return Arrays.asList(new CheckBoxGroup<>(), new TwinColSelect<>());
+ }
+
+ @Parameter
+ public AbstractMultiSelect<String> selectToTest;
+
+ private Multi<String> selectionModel;
+ private MultiSelectServerRpc rpc;
+
+ private Registration registration;
+
+ @Before
+ public void setUp() {
+ selectToTest.getSelectionModel().deselectAll();
+ // Intentional deviation from upcoming selection order
+ selectToTest.setDataSource(
+ DataSource.create("3", "2", "1", "5", "8", "7", "4", "6"));
+ selectionModel = selectToTest.getSelectionModel();
+ rpc = ComponentTest.getRpcProxy(selectToTest,
+ MultiSelectServerRpc.class);
+ }
+
+ @After
+ public void tearDown() {
+ if (registration != null) {
+ registration.remove();
+ registration = null;
+ }
+ }
+
+ @Test
+ public void stableSelectionOrder() {
+ selectionModel.select("1");
+ selectionModel.select("2");
+ selectionModel.select("3");
+
+ assertSelectionOrder(selectionModel, "1", "2", "3");
+
+ selectionModel.deselect("1");
+ assertSelectionOrder(selectionModel, "2", "3");
+
+ selectionModel.select("1");
+ assertSelectionOrder(selectionModel, "2", "3", "1");
+
+ selectionModel.selectItems("7", "8", "4");
+ assertSelectionOrder(selectionModel, "2", "3", "1", "7", "8", "4");
+
+ selectionModel.deselectItems("2", "1", "4", "5");
+ assertSelectionOrder(selectionModel, "3", "7", "8");
+ }
+
+ @Test
+ public void apiSelectionChange_notUserOriginated() {
+ AtomicInteger listenerCount = new AtomicInteger(0);
+ listenerCount.set(0);
+
+ registration = selectToTest.addSelectionListener(event -> {
+ listenerCount.incrementAndGet();
+ Assert.assertFalse(event.isUserOriginated());
+ });
+
+ selectToTest.select("1");
+ selectToTest.select("2");
+
+ selectToTest.deselect("2");
+ selectToTest.getSelectionModel().deselectAll();
+
+ selectToTest.getSelectionModel().selectItems("2", "3", "4");
+ selectToTest.getSelectionModel().deselectItems("1", "4");
+
+ Assert.assertEquals(6, listenerCount.get());
+
+ // select partly selected
+ selectToTest.getSelectionModel().selectItems("2", "3", "4");
+ Assert.assertEquals(7, listenerCount.get());
+
+ // select completely selected
+ selectToTest.getSelectionModel().selectItems("2", "3", "4");
+ Assert.assertEquals(7, listenerCount.get());
+
+ // deselect partly not selected
+ selectToTest.getSelectionModel().selectItems("1", "4");
+ Assert.assertEquals(8, listenerCount.get());
+
+ // deselect completely not selected
+ selectToTest.getSelectionModel().selectItems("1", "4");
+ Assert.assertEquals(8, listenerCount.get());
+ }
+
+ @Test
+ public void rpcSelectionChange_userOriginated() {
+ AtomicInteger listenerCount = new AtomicInteger(0);
+
+ registration = selectToTest.addSelectionListener(event -> {
+ listenerCount.incrementAndGet();
+ Assert.assertTrue(event.isUserOriginated());
+ });
+
+ rpcSelect("1");
+ assertSelectionOrder(selectionModel, "1");
+
+ rpcSelect("2");
+ assertSelectionOrder(selectionModel, "1", "2");
+ rpcDeselect("2");
+ assertSelectionOrder(selectionModel, "1");
+ rpcSelect("3", "6");
+ assertSelectionOrder(selectionModel, "1", "3", "6");
+ rpcDeselect("1", "3");
+ assertSelectionOrder(selectionModel, "6");
+
+ Assert.assertEquals(5, listenerCount.get());
+
+ // select partly selected
+ rpcSelect("2", "3", "4");
+ Assert.assertEquals(6, listenerCount.get());
+ assertSelectionOrder(selectionModel, "6", "2", "3", "4");
+
+ // select completely selected
+ rpcSelect("2", "3", "4");
+ Assert.assertEquals(6, listenerCount.get());
+ assertSelectionOrder(selectionModel, "6", "2", "3", "4");
+
+ // deselect partly not selected
+ rpcDeselect("1", "4");
+ Assert.assertEquals(7, listenerCount.get());
+ assertSelectionOrder(selectionModel, "6", "2", "3");
+
+ // deselect completely not selected
+ rpcDeselect("1", "4");
+ Assert.assertEquals(7, listenerCount.get());
+ assertSelectionOrder(selectionModel, "6", "2", "3");
+
+ // select completely selected and deselect completely not selected
+ rpcUpdateSelection(new String[] { "3" }, new String[] { "1", "4" });
+ Assert.assertEquals(7, listenerCount.get());
+ assertSelectionOrder(selectionModel, "6", "2", "3");
+
+ // select partly selected and deselect completely not selected
+ rpcUpdateSelection(new String[] { "4", "2" },
+ new String[] { "1", "8" });
+ Assert.assertEquals(8, listenerCount.get());
+ assertSelectionOrder(selectionModel, "6", "2", "3", "4");
+
+ // select completely selected and deselect partly not selected
+ rpcUpdateSelection(new String[] { "4", "3" },
+ new String[] { "1", "2" });
+ Assert.assertEquals(9, listenerCount.get());
+ assertSelectionOrder(selectionModel, "6", "3", "4");
+
+ // duplicate case - ignored
+ rpcUpdateSelection(new String[] { "2" }, new String[] { "2" });
+ Assert.assertEquals(9, listenerCount.get());
+ assertSelectionOrder(selectionModel, "6", "3", "4");
+
+ // duplicate case - duplicate removed
+ rpcUpdateSelection(new String[] { "2" }, new String[] { "2", "3" });
+ Assert.assertEquals(10, listenerCount.get());
+ assertSelectionOrder(selectionModel, "6", "4");
+
+ // duplicate case - duplicate removed
+ rpcUpdateSelection(new String[] { "6", "8" }, new String[] { "6" });
+ Assert.assertEquals(11, listenerCount.get());
+ assertSelectionOrder(selectionModel, "6", "4", "8");
+ }
+
+ private void rpcSelect(String... keysToSelect) {
+ rpcUpdateSelection(keysToSelect, new String[] {});
+ }
+
+ private void rpcDeselect(String... keysToDeselect) {
+ rpcUpdateSelection(new String[] {}, keysToDeselect);
+ }
+
+ private void rpcUpdateSelection(String[] added, String[] removed) {
+ rpc.updateSelection(
+ new LinkedHashSet<>(Stream.of(added).map(this::getItemKey)
+ .collect(Collectors.toList())),
+ new LinkedHashSet<>(Stream.of(removed).map(this::getItemKey)
+ .collect(Collectors.toList())));
+ }
+
+ private String getItemKey(String dataObject) {
+ return selectToTest.getDataCommunicator().getKeyMapper()
+ .key(dataObject);
+ }
+
+ private static void assertSelectionOrder(Multi<String> selectionModel,
+ String... selectionOrder) {
+ Assert.assertEquals(Arrays.asList(selectionOrder),
+ new ArrayList<>(selectionModel.getSelectedItems()));
+ }
+}
diff --git a/server/src/test/java/com/vaadin/ui/CheckBoxGroupBoVTest.java b/server/src/test/java/com/vaadin/ui/CheckBoxGroupBoVTest.java
index e8594757d0..c89d95a4b2 100644
--- a/server/src/test/java/com/vaadin/ui/CheckBoxGroupBoVTest.java
+++ b/server/src/test/java/com/vaadin/ui/CheckBoxGroupBoVTest.java
@@ -23,24 +23,19 @@ import java.util.EnumSet;
* @author Vaadin Ltd
* @since 8.0
*/
-public class CheckBoxGroupBoVTest
-{
+public class CheckBoxGroupBoVTest {
public enum Status {
- STATE_A,
- STATE_B,
- STATE_C,
- STATE_D;
+ STATE_A, STATE_B, STATE_C, STATE_D;
public String getCaption() {
return "** " + toString();
}
}
-
public void createOptionGroup() {
CheckBoxGroup<Status> s = new CheckBoxGroup<>();
s.setItems(EnumSet.allOf(Status.class));
- s.setItemCaptionProvider(Status::getCaption);
+ s.setItemCaptionGenerator(Status::getCaption);
}
}
diff --git a/server/src/test/java/com/vaadin/ui/CheckBoxGroupTest.java b/server/src/test/java/com/vaadin/ui/CheckBoxGroupTest.java
deleted file mode 100644
index 192dcb3d52..0000000000
--- a/server/src/test/java/com/vaadin/ui/CheckBoxGroupTest.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2000-2016 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;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.concurrent.atomic.AtomicInteger;
-
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Test;
-
-import com.vaadin.server.data.DataSource;
-import com.vaadin.shared.data.selection.SelectionModel.Multi;
-import com.vaadin.shared.data.selection.SelectionServerRpc;
-
-public class CheckBoxGroupTest {
- private CheckBoxGroup<String> checkBoxGroup;
- private Multi<String> selectionModel;
-
- @Before
- public void setUp() {
- checkBoxGroup = new CheckBoxGroup<>();
- // Intentional deviation from upcoming selection order
- checkBoxGroup
- .setDataSource(DataSource.create("Third", "Second", "First"));
- selectionModel = checkBoxGroup.getSelectionModel();
- }
-
- @Test
- public void stableSelectionOrder() {
- selectionModel.select("First");
- selectionModel.select("Second");
- selectionModel.select("Third");
-
- assertSelectionOrder(selectionModel, "First", "Second", "Third");
-
- selectionModel.deselect("First");
- assertSelectionOrder(selectionModel, "Second", "Third");
-
- selectionModel.select("First");
- assertSelectionOrder(selectionModel, "Second", "Third", "First");
- }
-
- @Test
- public void apiSelectionChange_notUserOriginated() {
- AtomicInteger listenerCount = new AtomicInteger(0);
-
- checkBoxGroup.addSelectionListener(event -> {
- listenerCount.incrementAndGet();
- Assert.assertFalse(event.isUserOriginated());
- });
-
- checkBoxGroup.select("First");
- checkBoxGroup.select("Second");
-
- checkBoxGroup.deselect("Second");
- checkBoxGroup.getSelectionModel().deselectAll();
-
- Assert.assertEquals(4, listenerCount.get());
- }
-
- @Test
- public void rpcSelectionChange_userOriginated() {
- AtomicInteger listenerCount = new AtomicInteger(0);
-
- checkBoxGroup.addSelectionListener(event -> {
- listenerCount.incrementAndGet();
- Assert.assertTrue(event.isUserOriginated());
- });
-
- SelectionServerRpc rpc = ComponentTest.getRpcProxy(checkBoxGroup,
- SelectionServerRpc.class);
-
- rpc.select(getItemKey("First"));
- rpc.select(getItemKey("Second"));
- rpc.deselect(getItemKey("Second"));
-
- Assert.assertEquals(3, listenerCount.get());
- }
-
- private String getItemKey(String dataObject) {
- return checkBoxGroup.getDataCommunicator().getKeyMapper()
- .key(dataObject);
- }
-
- private static void assertSelectionOrder(Multi<String> selectionModel,
- String... selectionOrder) {
- Assert.assertEquals(Arrays.asList(selectionOrder),
- new ArrayList<>(selectionModel.getSelectedItems()));
- }
-}