summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--client/src/main/java/com/vaadin/client/connectors/AbstractMultiSelectConnector.java182
-rw-r--r--client/src/main/java/com/vaadin/client/ui/VCheckBoxGroup.java46
-rw-r--r--client/src/main/java/com/vaadin/client/ui/VRadioButtonGroup.java34
-rw-r--r--client/src/main/java/com/vaadin/client/ui/VTwinColSelect.java716
-rw-r--r--client/src/main/java/com/vaadin/client/ui/optiongroup/CheckBoxGroupConnector.java13
-rw-r--r--client/src/main/java/com/vaadin/client/ui/twincolselect/TwinColSelectConnector.java95
-rw-r--r--pom.xml2
-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
-rw-r--r--shared/src/main/java/com/vaadin/shared/data/selection/MultiSelectServerRpc.java41
-rw-r--r--shared/src/main/java/com/vaadin/shared/data/selection/SelectionModel.java81
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/ListingJsonConstants.java (renamed from shared/src/main/java/com/vaadin/shared/ui/optiongroup/CheckBoxGroupConstants.java)6
-rw-r--r--shared/src/main/java/com/vaadin/shared/ui/twincolselect/TwinColSelectState.java (renamed from shared/src/main/java/com/vaadin/shared/ui/optiongroup/RadioButtonGroupConstants.java)27
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/abstractlisting/AbstractMultiSelectTestUI.java83
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/checkbox/CheckBoxGroupTestUI.java71
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/select/TwinColSelectCaptionStyles.java5
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/select/TwinColSelects.java18
-rw-r--r--uitest/src/main/java/com/vaadin/tests/components/twincolselect/TwinColSelectTestUI.java35
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/checkboxgroup/CheckBoxGroupTest.java13
-rw-r--r--uitest/src/test/java/com/vaadin/tests/components/twincolselect/TwinColSelectTest.java259
26 files changed, 2403 insertions, 474 deletions
diff --git a/client/src/main/java/com/vaadin/client/connectors/AbstractMultiSelectConnector.java b/client/src/main/java/com/vaadin/client/connectors/AbstractMultiSelectConnector.java
new file mode 100644
index 0000000000..86dbe672e0
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/connectors/AbstractMultiSelectConnector.java
@@ -0,0 +1,182 @@
+/*
+ * 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.client.connectors;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.IsWidget;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.data.DataSource;
+import com.vaadin.shared.Range;
+import com.vaadin.shared.Registration;
+import com.vaadin.shared.data.selection.MultiSelectServerRpc;
+import com.vaadin.shared.data.selection.SelectionModel;
+import com.vaadin.shared.ui.ListingJsonConstants;
+
+import elemental.json.JsonObject;
+
+/**
+ * A base connector class for multiselects.
+ * <p>
+ * Does not care about the framework provided selection model for now, instead
+ * just passes selection information per item.
+ *
+ * @author Vaadin Ltd.
+ *
+ * @since 8.0
+ */
+public abstract class AbstractMultiSelectConnector
+ extends AbstractListingConnector<SelectionModel.Multi<?>> {
+
+ /**
+ * Abstraction layer to help populate different multiselect widgets based on
+ * same JSON data.
+ */
+ public interface MultiSelectWidget {
+
+ /**
+ * Sets the given items to the select.
+ *
+ * @param items
+ * the items for the select
+ */
+ void setItems(List<JsonObject> items);
+
+ /**
+ * Adds a selection change listener the select.
+ *
+ * @param selectionChangeListener
+ * the listener to add, not {@code null}
+ * @return a registration handle to remove the listener
+ */
+ Registration addSelectionChangeListener(
+ BiConsumer<Set<String>, Set<String>> selectionChangeListener);
+
+ /**
+ * Returns the caption for the given item.
+ *
+ * @param item
+ * the item, not {@code null}
+ * @return caption of the item
+ */
+ static String getCaption(JsonObject item) {
+ return item.getString(ListingJsonConstants.JSONKEY_ITEM_VALUE);
+ }
+
+ /**
+ * Returns the key for the given item.
+ *
+ * @param item
+ * the item, not {@code null}
+ * @return key of the item
+ */
+ static String getKey(JsonObject item) {
+ return getRowKey(item);
+ }
+
+ /**
+ * Returns whether the given item is enabled or not.
+ * <p>
+ * Disabling items is not supported by all multiselects.
+ *
+ * @param item
+ * the item, not {@code null}
+ * @return {@code true} enabled, {@code false} if not
+ */
+ static boolean isEnabled(JsonObject item) {
+ return !(item.hasKey(ListingJsonConstants.JSONKEY_ITEM_DISABLED)
+ && item.getBoolean(
+ ListingJsonConstants.JSONKEY_ITEM_DISABLED));
+ }
+
+ /**
+ * Returns whether this item is selected or not.
+ *
+ * @param item
+ * the item, not {@code null}
+ * @return {@code true} is selected, {@code false} if not
+ */
+ static boolean isSelected(JsonObject item) {
+ return item.getBoolean(ListingJsonConstants.JSONKEY_ITEM_SELECTED);
+ }
+
+ /**
+ * Returns the optional icon URL for the given item.
+ * <p>
+ * Item icons are not supported by all multiselects.
+ *
+ * @param item
+ * the item
+ * @return the optional icon URL, or an empty optional if none specified
+ */
+ static Optional<String> getIconUrl(JsonObject item) {
+ return Optional.ofNullable(
+ item.getString(ListingJsonConstants.JSONKEY_ITEM_ICON));
+ }
+ }
+
+ /**
+ * Returns the multiselect widget for this connector.
+ * <p>
+ * This is used because {@link #getWidget()} returns a class
+ * ({@link Widget}) instead of an interface ({@link IsWidget}), and most
+ * multiselects extends {@link Composite}.
+ *
+ * @return the multiselect widget
+ */
+ public abstract MultiSelectWidget getMultiSelectWidget();
+
+ @Override
+ protected void init() {
+ super.init();
+
+ MultiSelectServerRpc rpcProxy = getRpcProxy(
+ MultiSelectServerRpc.class);
+ getMultiSelectWidget().addSelectionChangeListener(
+ (addedItems, removedItems) -> rpcProxy
+ .updateSelection(addedItems, removedItems));
+ }
+
+ @Override
+ public void setDataSource(DataSource<JsonObject> dataSource) {
+ dataSource.addDataChangeHandler(this::onDataChange);
+ super.setDataSource(dataSource);
+ }
+
+ /**
+ * This method handles the parsing of the new JSON data containing the items
+ * and the selection information.
+ *
+ * @param range
+ * the updated range, never {@code null}
+ */
+ protected void onDataChange(Range range) {
+ assert range.getStart() == 0
+ && range.getEnd() == getDataSource().size() : getClass()
+ .getSimpleName()
+ + " only supports full updates, but got range " + range;
+ List<JsonObject> items = new ArrayList<>(range.length());
+ for (int i = 0; i < range.getEnd(); i++) {
+ items.add(getDataSource().getRow(i));
+ }
+ getMultiSelectWidget().setItems(items);
+ }
+}
diff --git a/client/src/main/java/com/vaadin/client/ui/VCheckBoxGroup.java b/client/src/main/java/com/vaadin/client/ui/VCheckBoxGroup.java
index c51cb32a2f..e47598426b 100644
--- a/client/src/main/java/com/vaadin/client/ui/VCheckBoxGroup.java
+++ b/client/src/main/java/com/vaadin/client/ui/VCheckBoxGroup.java
@@ -16,8 +16,6 @@
package com.vaadin.client.ui;
-import static com.vaadin.shared.ui.optiongroup.CheckBoxGroupConstants.JSONKEY_ITEM_DISABLED;
-
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
@@ -38,7 +36,7 @@ import com.google.gwt.user.client.ui.Widget;
import com.vaadin.client.ApplicationConnection;
import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.Registration;
-import com.vaadin.shared.ui.optiongroup.CheckBoxGroupConstants;
+import com.vaadin.shared.ui.ListingJsonConstants;
import elemental.json.JsonObject;
@@ -77,7 +75,7 @@ public class VCheckBoxGroup extends Composite implements Field, ClickHandler,
public VCheckBoxGroup() {
optionsContainer = new FlowPanel();
- initWidget(this.optionsContainer);
+ initWidget(optionsContainer);
optionsContainer.setStyleName(CLASSNAME);
optionsToItems = new HashMap<>();
selectionChangeListeners = new ArrayList<>();
@@ -98,14 +96,14 @@ public class VCheckBoxGroup extends Composite implements Field, ClickHandler,
optionsContainer.clear();
for (JsonObject item : items) {
String itemHtml = item
- .getString(CheckBoxGroupConstants.JSONKEY_ITEM_VALUE);
+ .getString(ListingJsonConstants.JSONKEY_ITEM_VALUE);
if (!isHtmlContentAllowed()) {
itemHtml = WidgetUtil.escapeHTML(itemHtml);
}
VCheckBox checkBox = new VCheckBox();
String iconUrl = item
- .getString(CheckBoxGroupConstants.JSONKEY_ITEM_ICON);
+ .getString(ListingJsonConstants.JSONKEY_ITEM_ICON);
if (iconUrl != null && iconUrl.length() != 0) {
Icon icon = client.getIcon(iconUrl);
itemHtml = icon.getElement().getString() + itemHtml;
@@ -115,10 +113,8 @@ public class VCheckBoxGroup extends Composite implements Field, ClickHandler,
checkBox.addClickHandler(this);
checkBox.setHTML(itemHtml);
checkBox.setValue(item
- .getBoolean(CheckBoxGroupConstants.JSONKEY_ITEM_SELECTED));
- boolean optionEnabled = !item.getBoolean(JSONKEY_ITEM_DISABLED);
- boolean enabled = optionEnabled && !isReadonly() && isEnabled();
- checkBox.setEnabled(enabled);
+ .getBoolean(ListingJsonConstants.JSONKEY_ITEM_SELECTED));
+ setOptionEnabled(checkBox, item);
optionsContainer.add(checkBox);
optionsToItems.put(checkBox, item);
@@ -152,18 +148,20 @@ public class VCheckBoxGroup extends Composite implements Field, ClickHandler,
}
}
- protected void updateEnabledState() {
- boolean optionGroupEnabled = isEnabled() && !isReadonly();
- // sets options enabled according to the widget's enabled,
- // readonly and each options own enabled
- for (Map.Entry<VCheckBox, JsonObject> entry : optionsToItems
- .entrySet()) {
- VCheckBox checkBox = entry.getKey();
- JsonObject value = entry.getValue();
- Boolean isOptionEnabled = !value
- .getBoolean(CheckBoxGroupConstants.JSONKEY_ITEM_DISABLED);
- checkBox.setEnabled(optionGroupEnabled && isOptionEnabled);
- }
+ /**
+ * Updates the checkbox's enabled state according to the widget's enabled,
+ * read only and the item's enabled.
+ *
+ * @param checkBox
+ * the checkbox to update
+ * @param item
+ * the item for the checkbox
+ */
+ protected void setOptionEnabled(VCheckBox checkBox, JsonObject item) {
+ boolean optionEnabled = !item
+ .getBoolean(ListingJsonConstants.JSONKEY_ITEM_DISABLED);
+ boolean enabled = optionEnabled && !isReadonly() && isEnabled();
+ checkBox.setEnabled(enabled);
}
@Override
@@ -194,7 +192,7 @@ public class VCheckBoxGroup extends Composite implements Field, ClickHandler,
public void setReadonly(boolean readonly) {
if (this.readonly != readonly) {
this.readonly = readonly;
- updateEnabledState();
+ optionsToItems.forEach(this::setOptionEnabled);
}
}
@@ -202,7 +200,7 @@ public class VCheckBoxGroup extends Composite implements Field, ClickHandler,
public void setEnabled(boolean enabled) {
if (this.enabled != enabled) {
this.enabled = enabled;
- updateEnabledState();
+ optionsToItems.forEach(this::setOptionEnabled);
}
}
diff --git a/client/src/main/java/com/vaadin/client/ui/VRadioButtonGroup.java b/client/src/main/java/com/vaadin/client/ui/VRadioButtonGroup.java
index 893935d562..bb81b2872e 100644
--- a/client/src/main/java/com/vaadin/client/ui/VRadioButtonGroup.java
+++ b/client/src/main/java/com/vaadin/client/ui/VRadioButtonGroup.java
@@ -16,6 +16,13 @@
package com.vaadin.client.ui;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
import com.google.gwt.aria.client.Roles;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
@@ -33,17 +40,9 @@ import com.vaadin.client.BrowserInfo;
import com.vaadin.client.WidgetUtil;
import com.vaadin.shared.Registration;
import com.vaadin.shared.data.DataCommunicatorConstants;
-import com.vaadin.shared.ui.optiongroup.RadioButtonGroupConstants;
-import elemental.json.JsonObject;
+import com.vaadin.shared.ui.ListingJsonConstants;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-
-import static com.vaadin.shared.ui.optiongroup.RadioButtonGroupConstants.JSONKEY_ITEM_DISABLED;
+import elemental.json.JsonObject;
/**
* The client-side widget for the {@code RadioButtonGroup} component.
@@ -83,7 +82,7 @@ public class VRadioButtonGroup extends Composite implements Field, ClickHandler,
public VRadioButtonGroup() {
groupId = DOM.createUniqueId();
optionsContainer = new FlowPanel();
- initWidget(this.optionsContainer);
+ initWidget(optionsContainer);
optionsContainer.setStyleName(CLASSNAME);
optionsToItems = new HashMap<>();
keyToOptions = new HashMap<>();
@@ -107,14 +106,14 @@ public class VRadioButtonGroup extends Composite implements Field, ClickHandler,
keyToOptions.clear();
for (JsonObject item : items) {
String itemHtml = item
- .getString(RadioButtonGroupConstants.JSONKEY_ITEM_VALUE);
+ .getString(ListingJsonConstants.JSONKEY_ITEM_VALUE);
if (!isHtmlContentAllowed()) {
itemHtml = WidgetUtil.escapeHTML(itemHtml);
}
RadioButton radioButton = new RadioButton(groupId);
String iconUrl = item
- .getString(RadioButtonGroupConstants.JSONKEY_ITEM_ICON);
+ .getString(ListingJsonConstants.JSONKEY_ITEM_ICON);
if (iconUrl != null && iconUrl.length() != 0) {
Icon icon = client.getIcon(iconUrl);
itemHtml = icon.getElement().getString() + itemHtml;
@@ -124,8 +123,9 @@ public class VRadioButtonGroup extends Composite implements Field, ClickHandler,
radioButton.addClickHandler(this);
radioButton.setHTML(itemHtml);
radioButton.setValue(item
- .getBoolean(RadioButtonGroupConstants.JSONKEY_ITEM_SELECTED));
- boolean optionEnabled = !item.getBoolean(JSONKEY_ITEM_DISABLED);
+ .getBoolean(ListingJsonConstants.JSONKEY_ITEM_SELECTED));
+ boolean optionEnabled = !item
+ .getBoolean(ListingJsonConstants.JSONKEY_ITEM_DISABLED);
boolean enabled = optionEnabled && !isReadonly() && isEnabled();
radioButton.setEnabled(enabled);
String key = item.getString(DataCommunicatorConstants.KEY);
@@ -175,7 +175,7 @@ public class VRadioButtonGroup extends Composite implements Field, ClickHandler,
RadioButton radioButton = entry.getKey();
JsonObject value = entry.getValue();
Boolean isOptionEnabled = !value
- .getBoolean(RadioButtonGroupConstants.JSONKEY_ITEM_DISABLED);
+ .getBoolean(ListingJsonConstants.JSONKEY_ITEM_DISABLED);
radioButton.setEnabled(radioButtonEnabled && isOptionEnabled);
}
}
@@ -229,7 +229,7 @@ public class VRadioButtonGroup extends Composite implements Field, ClickHandler,
public void selectItemKey(String selectedItemKey) {
RadioButton radioButton = keyToOptions.get(selectedItemKey);
- assert radioButton!=null;
+ assert radioButton != null;
radioButton.setValue(true);
}
}
diff --git a/client/src/main/java/com/vaadin/client/ui/VTwinColSelect.java b/client/src/main/java/com/vaadin/client/ui/VTwinColSelect.java
new file mode 100644
index 0000000000..d744e289dd
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/ui/VTwinColSelect.java
@@ -0,0 +1,716 @@
+/*
+ * 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.client.ui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
+import com.google.gwt.event.dom.client.HasDoubleClickHandlers;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.MouseDownEvent;
+import com.google.gwt.event.dom.client.MouseDownHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HasEnabled;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.client.Focusable;
+import com.vaadin.client.StyleConstants;
+import com.vaadin.client.WidgetUtil;
+import com.vaadin.client.connectors.AbstractMultiSelectConnector.MultiSelectWidget;
+import com.vaadin.shared.Registration;
+
+import elemental.json.JsonObject;
+
+/**
+ * A list builder widget that has two selects; one for selectable options,
+ * another for selected options, and buttons for selecting and deselecting the
+ * items.
+ *
+ * @author Vaadin Ltd
+ */
+public class VTwinColSelect extends Composite implements MultiSelectWidget,
+ Field, ClickHandler, Focusable, HasEnabled, KeyDownHandler,
+ MouseDownHandler, DoubleClickHandler, SubPartAware {
+
+ private static final String SUBPART_OPTION_SELECT = "leftSelect";
+ private static final String SUBPART_OPTION_SELECT_ITEM = SUBPART_OPTION_SELECT
+ + "-item";
+ private static final String SUBPART_SELECTION_SELECT = "rightSelect";
+ private static final String SUBPART_SELECTION_SELECT_ITEM = SUBPART_SELECTION_SELECT
+ + "-item";
+ private static final String SUBPART_LEFT_CAPTION = "leftCaption";
+ private static final String SUBPART_RIGHT_CAPTION = "rightCaption";
+ private static final String SUBPART_ADD_BUTTON = "add";
+ private static final String SUBPART_REMOVE_BUTTON = "remove";
+
+ /** Primary style name for twin col select. */
+ public static final String CLASSNAME = "v-select-twincol";
+
+ private static final int VISIBLE_COUNT = 10;
+
+ private static final int DEFAULT_COLUMN_COUNT = 10;
+
+ private final DoubleClickListBox optionsListBox;
+
+ private final DoubleClickListBox selectionsListBox;
+
+ private final FlowPanel optionsContainer;
+
+ private final FlowPanel captionWrapper;
+
+ private final VButton addItemsLeftToRightButton;
+
+ private final VButton removeItemsRightToLeftButton;
+
+ private final FlowPanel buttons;
+
+ private final Panel panel;
+
+ private HTML optionsCaption = null;
+
+ private HTML selectionsCaption = null;
+
+ private List<BiConsumer<Set<String>, Set<String>>> selectionChangeListeners;
+
+ private boolean enabled;
+ private boolean readOnly;
+
+ private int rows = 0;
+
+ /**
+ * A multiselect ListBox which catches double clicks.
+ */
+ public class DoubleClickListBox extends ListBox
+ implements HasDoubleClickHandlers {
+ /**
+ * Constructs a new DoubleClickListBox.
+ */
+ public DoubleClickListBox() {
+ setMultipleSelect(true);
+ }
+
+ @Override
+ public HandlerRegistration addDoubleClickHandler(
+ DoubleClickHandler handler) {
+ return addDomHandler(handler, DoubleClickEvent.getType());
+ }
+ }
+
+ /**
+ * Constructs a new VTwinColSelect.
+ */
+ public VTwinColSelect() {
+ selectionChangeListeners = new ArrayList<>();
+
+ optionsContainer = new FlowPanel();
+ initWidget(optionsContainer);
+ optionsContainer.setStyleName(CLASSNAME);
+
+ captionWrapper = new FlowPanel();
+
+ optionsListBox = new DoubleClickListBox();
+ optionsListBox.addClickHandler(this);
+ optionsListBox.addDoubleClickHandler(this);
+ optionsListBox.setVisibleItemCount(VISIBLE_COUNT);
+ optionsListBox.setStyleName(CLASSNAME + "-options");
+
+ selectionsListBox = new DoubleClickListBox();
+ selectionsListBox.addClickHandler(this);
+ selectionsListBox.addDoubleClickHandler(this);
+ selectionsListBox.setVisibleItemCount(VISIBLE_COUNT);
+ selectionsListBox.setStyleName(CLASSNAME + "-selections");
+
+ buttons = new FlowPanel();
+ buttons.setStyleName(CLASSNAME + "-buttons");
+ addItemsLeftToRightButton = new VButton();
+ addItemsLeftToRightButton.setText(">>");
+ addItemsLeftToRightButton.addClickHandler(this);
+ removeItemsRightToLeftButton = new VButton();
+ removeItemsRightToLeftButton.setText("<<");
+ removeItemsRightToLeftButton.addClickHandler(this);
+
+ panel = optionsContainer;
+
+ panel.add(captionWrapper);
+ captionWrapper.getElement().getStyle().setOverflow(Overflow.HIDDEN);
+ // Hide until there actually is a caption to prevent IE from rendering
+ // extra empty space
+ captionWrapper.setVisible(false);
+
+ panel.add(optionsListBox);
+ buttons.add(addItemsLeftToRightButton);
+ final HTML br = new HTML("<span/>");
+ br.setStyleName(CLASSNAME + "-deco");
+ buttons.add(br);
+ buttons.add(removeItemsRightToLeftButton);
+ panel.add(buttons);
+ panel.add(selectionsListBox);
+
+ optionsListBox.addKeyDownHandler(this);
+ optionsListBox.addMouseDownHandler(this);
+
+ selectionsListBox.addMouseDownHandler(this);
+ selectionsListBox.addKeyDownHandler(this);
+
+ updateEnabledState();
+ }
+
+ /**
+ * Gets the options caption HTML Widget.
+ *
+ * @return the options caption widget
+ */
+ protected HTML getOptionsCaption() {
+ if (optionsCaption == null) {
+ optionsCaption = new HTML();
+ optionsCaption.setStyleName(CLASSNAME + "-caption-left");
+ optionsCaption.getElement().getStyle()
+ .setFloat(com.google.gwt.dom.client.Style.Float.LEFT);
+ captionWrapper.add(optionsCaption);
+ }
+
+ return optionsCaption;
+ }
+
+ /**
+ * Gets the selections caption HTML widget.
+ *
+ * @return the selections caption widget
+ */
+ protected HTML getSelectionsCaption() {
+ if (selectionsCaption == null) {
+ selectionsCaption = new HTML();
+ selectionsCaption.setStyleName(CLASSNAME + "-caption-right");
+ selectionsCaption.getElement().getStyle()
+ .setFloat(com.google.gwt.dom.client.Style.Float.RIGHT);
+ captionWrapper.add(selectionsCaption);
+ }
+
+ return selectionsCaption;
+ }
+
+ /**
+ * For internal use only. May be removed or replaced in the future.
+ *
+ * @return the caption wrapper widget
+ */
+ public Widget getCaptionWrapper() {
+ return captionWrapper;
+ }
+
+ /**
+ * Sets the number of visible items for the list boxes.
+ *
+ * @param rows
+ * the number of items to show
+ * @see ListBox#setVisibleItemCount(int)
+ */
+ public void setRows(int rows) {
+ if (this.rows != rows) {
+ this.rows = rows;
+ optionsListBox.setVisibleItemCount(rows);
+ selectionsListBox.setVisibleItemCount(rows);
+ }
+ }
+
+ /**
+ * Returns the number of visible items for the list boxes.
+ *
+ * @return the number of items to show
+ * @see ListBox#setVisibleItemCount(int)
+ */
+ public int getRows() {
+ return rows;
+ }
+
+ /**
+ * Updates the captions above the left (options) and right (selections)
+ * columns. {code null} value clear the caption.
+ *
+ * @param leftCaption
+ * the left caption to set, or {@code null} to clear
+ * @param rightCaption
+ * the right caption to set, or {@code null} to clear
+ */
+ public void updateCaptions(String leftCaption, String rightCaption) {
+ boolean hasCaptions = leftCaption != null || rightCaption != null;
+
+ if (leftCaption == null) {
+ removeOptionsCaption();
+ } else {
+ getOptionsCaption().setText(leftCaption);
+
+ }
+
+ if (rightCaption == null) {
+ removeSelectionsCaption();
+ } else {
+ getSelectionsCaption().setText(rightCaption);
+ }
+
+ captionWrapper.setVisible(hasCaptions);
+ }
+
+ private void removeOptionsCaption() {
+ if (optionsCaption == null) {
+ return;
+ }
+
+ if (optionsCaption.getParent() != null) {
+ captionWrapper.remove(optionsCaption);
+ }
+
+ optionsCaption = null;
+ }
+
+ private void removeSelectionsCaption() {
+ if (selectionsCaption == null) {
+ return;
+ }
+
+ if (selectionsCaption.getParent() != null) {
+ captionWrapper.remove(selectionsCaption);
+ }
+
+ selectionsCaption = null;
+ }
+
+ @Override
+ public Registration addSelectionChangeListener(
+ BiConsumer<Set<String>, Set<String>> listener) {
+ Objects.nonNull(listener);
+ selectionChangeListeners.add(listener);
+ return (Registration) () -> selectionChangeListeners.remove(listener);
+ }
+
+ @Override
+ public void setItems(List<JsonObject> items) {
+ // filter selected items
+ List<JsonObject> selection = items.stream()
+ .filter(item -> MultiSelectWidget.isSelected(item))
+ .collect(Collectors.toList());
+ items.removeAll(selection);
+
+ updateListBox(optionsListBox, items);
+ updateListBox(selectionsListBox, selection);
+ }
+
+ private static void updateListBox(ListBox listBox,
+ List<JsonObject> options) {
+ for (int i = 0; i < options.size(); i++) {
+ final JsonObject item = options.get(i);
+ // reuse existing option if possible
+ if (i < listBox.getItemCount()) {
+ listBox.setItemText(i, MultiSelectWidget.getCaption(item));
+ listBox.setValue(i, MultiSelectWidget.getKey(item));
+ } else {
+ listBox.addItem(MultiSelectWidget.getCaption(item),
+ MultiSelectWidget.getKey(item));
+ }
+ }
+ // remove extra
+ for (int i = listBox.getItemCount() - 1; i >= options.size(); i--) {
+ listBox.removeItem(i);
+ }
+ }
+
+ private static boolean[] getSelectionBitmap(ListBox listBox) {
+ final boolean[] selectedIndexes = new boolean[listBox.getItemCount()];
+ for (int i = 0; i < listBox.getItemCount(); i++) {
+ if (listBox.isItemSelected(i)) {
+ selectedIndexes[i] = true;
+ } else {
+ selectedIndexes[i] = false;
+ }
+ }
+ return selectedIndexes;
+ }
+
+ private void moveSelectedItemsLeftToRight() {
+ Set<String> movedItems = moveSelectedItems(optionsListBox,
+ selectionsListBox);
+ selectionChangeListeners
+ .forEach(e -> e.accept(movedItems, Collections.emptySet()));
+ }
+
+ private void moveSelectedItemsRightToLeft() {
+ Set<String> movedItems = moveSelectedItems(selectionsListBox,
+ optionsListBox);
+ selectionChangeListeners
+ .forEach(e -> e.accept(Collections.emptySet(), movedItems));
+ }
+
+ private static Set<String> moveSelectedItems(ListBox source,
+ ListBox target) {
+ final boolean[] sel = getSelectionBitmap(source);
+ final Set<String> movedItems = new HashSet<>();
+ for (int i = 0; i < sel.length; i++) {
+ if (sel[i]) {
+ final int optionIndex = i
+ - (sel.length - source.getItemCount());
+ movedItems.add(source.getValue(optionIndex));
+
+ // Move selection to another column
+ final String text = source.getItemText(optionIndex);
+ final String value = source.getValue(optionIndex);
+ target.addItem(text, value);
+ target.setItemSelected(target.getItemCount() - 1, true);
+ source.removeItem(optionIndex);
+ }
+ }
+
+ // If no items are left move the focus to the selections
+ if (source.getItemCount() == 0) {
+ target.setFocus(true);
+ } else {
+ source.setFocus(true);
+ }
+
+ return movedItems;
+ }
+
+ @Override
+ public void onClick(ClickEvent event) {
+ if (event.getSource() == addItemsLeftToRightButton) {
+ moveSelectedItemsLeftToRight();
+ } else if (event.getSource() == removeItemsRightToLeftButton) {
+ moveSelectedItemsRightToLeft();
+ } else if (event.getSource() == optionsListBox) {
+ // unselect all in other list, to avoid mistakes (i.e wrong button)
+ final int count = selectionsListBox.getItemCount();
+ for (int i = 0; i < count; i++) {
+ selectionsListBox.setItemSelected(i, false);
+ }
+ } else if (event.getSource() == selectionsListBox) {
+ // unselect all in other list, to avoid mistakes (i.e wrong button)
+ final int count = optionsListBox.getItemCount();
+ for (int i = 0; i < count; i++) {
+ optionsListBox.setItemSelected(i, false);
+ }
+ }
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void clearInternalHeights() {
+ selectionsListBox.setHeight("");
+ optionsListBox.setHeight("");
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void setInternalHeights() {
+ int captionHeight = WidgetUtil.getRequiredHeight(captionWrapper);
+ int totalHeight = getOffsetHeight();
+
+ String selectHeight = totalHeight - captionHeight + "px";
+
+ selectionsListBox.setHeight(selectHeight);
+ optionsListBox.setHeight(selectHeight);
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void clearInternalWidths() {
+ String colWidth = DEFAULT_COLUMN_COUNT + "em";
+ String containerWidth = 2 * DEFAULT_COLUMN_COUNT + 4 + "em";
+ // Caption wrapper width == optionsSelect + buttons +
+ // selectionsSelect
+ String captionWrapperWidth = 2 * DEFAULT_COLUMN_COUNT + 4 - 0.5 + "em";
+
+ optionsListBox.setWidth(colWidth);
+ if (optionsCaption != null) {
+ optionsCaption.setWidth(colWidth);
+ }
+ selectionsListBox.setWidth(colWidth);
+ if (selectionsCaption != null) {
+ selectionsCaption.setWidth(colWidth);
+ }
+ buttons.setWidth("3.5em");
+ optionsContainer.setWidth(containerWidth);
+ captionWrapper.setWidth(captionWrapperWidth);
+ }
+
+ /** For internal use only. May be removed or replaced in the future. */
+ public void setInternalWidths() {
+ getElement().getStyle().setPosition(Position.RELATIVE);
+ int bordersAndPaddings = WidgetUtil
+ .measureHorizontalPaddingAndBorder(buttons.getElement(), 0);
+
+ int buttonWidth = WidgetUtil.getRequiredWidth(buttons);
+ int totalWidth = getOffsetWidth();
+
+ int spaceForSelect = (totalWidth - buttonWidth - bordersAndPaddings)
+ / 2;
+
+ optionsListBox.setWidth(spaceForSelect + "px");
+ if (optionsCaption != null) {
+ optionsCaption.setWidth(spaceForSelect + "px");
+ }
+
+ selectionsListBox.setWidth(spaceForSelect + "px");
+ if (selectionsCaption != null) {
+ selectionsCaption.setWidth(spaceForSelect + "px");
+ }
+ captionWrapper.setWidth("100%");
+ }
+
+ /**
+ * Sets the tab index.
+ *
+ * @param tabIndex
+ * the tab index to set
+ */
+ public void setTabIndex(int tabIndex) {
+ optionsListBox.setTabIndex(tabIndex);
+ selectionsListBox.setTabIndex(tabIndex);
+ addItemsLeftToRightButton.setTabIndex(tabIndex);
+ removeItemsRightToLeftButton.setTabIndex(tabIndex);
+ }
+
+ /**
+ * Sets this twin column select as read only, meaning selection cannot be
+ * changed.
+ *
+ * @param readOnly
+ * {@code true} for read only, {@code false} for not read only
+ */
+ public void setReadOnly(boolean readOnly) {
+ if (this.readOnly != readOnly) {
+ this.readOnly = readOnly;
+ updateEnabledState();
+ }
+ }
+
+ /**
+ * Returns {@code true} if this twin column select is in read only mode,
+ * {@code false} if not.
+ *
+ * @return {@code true} for read only, {@code false} for not read only
+ */
+ public boolean isReadOnly() {
+ return readOnly;
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ if (this.enabled != enabled) {
+ this.enabled = enabled;
+ updateEnabledState();
+ }
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ private void updateEnabledState() {
+ boolean enabled = isEnabled() && !isReadOnly();
+ optionsListBox.setEnabled(enabled);
+ selectionsListBox.setEnabled(enabled);
+ addItemsLeftToRightButton.setEnabled(enabled);
+ removeItemsRightToLeftButton.setEnabled(enabled);
+ addItemsLeftToRightButton.setStyleName(StyleConstants.DISABLED,
+ !enabled);
+ removeItemsRightToLeftButton.setStyleName(StyleConstants.DISABLED,
+ !enabled);
+ }
+
+ @Override
+ public void focus() {
+ optionsListBox.setFocus(true);
+ }
+
+ /**
+ * Get the key that selects an item in the table. By default it is the Enter
+ * key but by overriding this you can change the key to whatever you want.
+ *
+ * @return the key that selects an item
+ */
+ protected int getNavigationSelectKey() {
+ return KeyCodes.KEY_ENTER;
+ }
+
+ @Override
+ public void onKeyDown(KeyDownEvent event) {
+ int keycode = event.getNativeKeyCode();
+
+ // Catch tab and move between select:s
+ if (keycode == KeyCodes.KEY_TAB
+ && event.getSource() == optionsListBox) {
+ // Prevent default behavior
+ event.preventDefault();
+
+ // Remove current selections
+ for (int i = 0; i < optionsListBox.getItemCount(); i++) {
+ optionsListBox.setItemSelected(i, false);
+ }
+
+ // Focus selections
+ selectionsListBox.setFocus(true);
+ }
+
+ if (keycode == KeyCodes.KEY_TAB && event.isShiftKeyDown()
+ && event.getSource() == selectionsListBox) {
+ // Prevent default behavior
+ event.preventDefault();
+
+ // Remove current selections
+ for (int i = 0; i < selectionsListBox.getItemCount(); i++) {
+ selectionsListBox.setItemSelected(i, false);
+ }
+
+ // Focus options
+ optionsListBox.setFocus(true);
+ }
+
+ if (keycode == getNavigationSelectKey()) {
+ // Prevent default behavior
+ event.preventDefault();
+
+ // Decide which select the selection was made in
+ if (event.getSource() == optionsListBox) {
+ // Prevents the selection to become a single selection when
+ // using Enter key
+ // as the selection key (default)
+ optionsListBox.setFocus(false);
+
+ moveSelectedItemsLeftToRight();
+
+ } else if (event.getSource() == selectionsListBox) {
+ // Prevents the selection to become a single selection when
+ // using Enter key
+ // as the selection key (default)
+ selectionsListBox.setFocus(false);
+
+ moveSelectedItemsRightToLeft();
+ }
+ }
+
+ }
+
+ @Override
+ public void onMouseDown(MouseDownEvent event) {
+ // Ensure that items are deselected when selecting
+ // from a different source. See #3699 for details.
+ if (event.getSource() == optionsListBox) {
+ for (int i = 0; i < selectionsListBox.getItemCount(); i++) {
+ selectionsListBox.setItemSelected(i, false);
+ }
+ } else if (event.getSource() == selectionsListBox) {
+ for (int i = 0; i < optionsListBox.getItemCount(); i++) {
+ optionsListBox.setItemSelected(i, false);
+ }
+ }
+
+ }
+
+ @Override
+ public void onDoubleClick(DoubleClickEvent event) {
+ if (event.getSource() == optionsListBox) {
+ moveSelectedItemsLeftToRight();
+ optionsListBox.setSelectedIndex(-1);
+ optionsListBox.setFocus(false);
+ } else if (event.getSource() == selectionsListBox) {
+ moveSelectedItemsRightToLeft();
+ selectionsListBox.setSelectedIndex(-1);
+ selectionsListBox.setFocus(false);
+ }
+
+ }
+
+ @Override
+ public com.google.gwt.user.client.Element getSubPartElement(
+ String subPart) {
+ if (SUBPART_OPTION_SELECT.equals(subPart)) {
+ return optionsListBox.getElement();
+ } else if (subPart.startsWith(SUBPART_OPTION_SELECT_ITEM)) {
+ String idx = subPart.substring(SUBPART_OPTION_SELECT_ITEM.length());
+ return (com.google.gwt.user.client.Element) optionsListBox
+ .getElement().getChild(Integer.parseInt(idx));
+ } else if (SUBPART_SELECTION_SELECT.equals(subPart)) {
+ return selectionsListBox.getElement();
+ } else if (subPart.startsWith(SUBPART_SELECTION_SELECT_ITEM)) {
+ String idx = subPart
+ .substring(SUBPART_SELECTION_SELECT_ITEM.length());
+ return (com.google.gwt.user.client.Element) selectionsListBox
+ .getElement().getChild(Integer.parseInt(idx));
+ } else if (optionsCaption != null
+ && SUBPART_LEFT_CAPTION.equals(subPart)) {
+ return optionsCaption.getElement();
+ } else if (selectionsCaption != null
+ && SUBPART_RIGHT_CAPTION.equals(subPart)) {
+ return selectionsCaption.getElement();
+ } else if (SUBPART_ADD_BUTTON.equals(subPart)) {
+ return addItemsLeftToRightButton.getElement();
+ } else if (SUBPART_REMOVE_BUTTON.equals(subPart)) {
+ return removeItemsRightToLeftButton.getElement();
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getSubPartName(
+ com.google.gwt.user.client.Element subElement) {
+ if (optionsCaption != null
+ && optionsCaption.getElement().isOrHasChild(subElement)) {
+ return SUBPART_LEFT_CAPTION;
+ } else if (selectionsCaption != null
+ && selectionsCaption.getElement().isOrHasChild(subElement)) {
+ return SUBPART_RIGHT_CAPTION;
+ } else if (optionsListBox.getElement().isOrHasChild(subElement)) {
+ if (optionsListBox.getElement() == subElement) {
+ return SUBPART_OPTION_SELECT;
+ } else {
+ int idx = WidgetUtil.getChildElementIndex(subElement);
+ return SUBPART_OPTION_SELECT_ITEM + idx;
+ }
+ } else if (selectionsListBox.getElement().isOrHasChild(subElement)) {
+ if (selectionsListBox.getElement() == subElement) {
+ return SUBPART_SELECTION_SELECT;
+ } else {
+ int idx = WidgetUtil.getChildElementIndex(subElement);
+ return SUBPART_SELECTION_SELECT_ITEM + idx;
+ }
+ } else if (addItemsLeftToRightButton.getElement()
+ .isOrHasChild(subElement)) {
+ return SUBPART_ADD_BUTTON;
+ } else if (removeItemsRightToLeftButton.getElement()
+ .isOrHasChild(subElement)) {
+ return SUBPART_REMOVE_BUTTON;
+ }
+
+ return null;
+ }
+}
diff --git a/client/src/main/java/com/vaadin/client/ui/optiongroup/CheckBoxGroupConnector.java b/client/src/main/java/com/vaadin/client/ui/optiongroup/CheckBoxGroupConnector.java
index a60a1b111f..254ae984d2 100644
--- a/client/src/main/java/com/vaadin/client/ui/optiongroup/CheckBoxGroupConnector.java
+++ b/client/src/main/java/com/vaadin/client/ui/optiongroup/CheckBoxGroupConnector.java
@@ -17,14 +17,16 @@
package com.vaadin.client.ui.optiongroup;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import com.vaadin.client.communication.StateChangeEvent;
import com.vaadin.client.connectors.AbstractListingConnector;
import com.vaadin.client.data.DataSource;
import com.vaadin.client.ui.VCheckBoxGroup;
+import com.vaadin.shared.data.selection.MultiSelectServerRpc;
import com.vaadin.shared.data.selection.SelectionModel;
-import com.vaadin.shared.data.selection.SelectionServerRpc;
import com.vaadin.shared.ui.Connect;
import com.vaadin.shared.ui.optiongroup.CheckBoxGroupState;
import com.vaadin.ui.CheckBoxGroup;
@@ -43,13 +45,14 @@ public class CheckBoxGroupConnector
}
private void selectionChanged(JsonObject changedItem, Boolean selected) {
- SelectionServerRpc rpc = getRpcProxy(SelectionServerRpc.class);
+ MultiSelectServerRpc rpc = getRpcProxy(MultiSelectServerRpc.class);
String key = getRowKey(changedItem);
-
+ HashSet<String> change = new HashSet<>();
+ change.add(key);
if (Boolean.TRUE.equals(selected)) {
- rpc.select(key);
+ rpc.updateSelection(change, Collections.emptySet());
} else {
- rpc.deselect(key);
+ rpc.updateSelection(Collections.emptySet(), change);
}
}
diff --git a/client/src/main/java/com/vaadin/client/ui/twincolselect/TwinColSelectConnector.java b/client/src/main/java/com/vaadin/client/ui/twincolselect/TwinColSelectConnector.java
new file mode 100644
index 0000000000..304f5dcab0
--- /dev/null
+++ b/client/src/main/java/com/vaadin/client/ui/twincolselect/TwinColSelectConnector.java
@@ -0,0 +1,95 @@
+/*
+ * 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.client.ui.twincolselect;
+
+import com.vaadin.client.DirectionalManagedLayout;
+import com.vaadin.client.annotations.OnStateChange;
+import com.vaadin.client.connectors.AbstractMultiSelectConnector;
+import com.vaadin.client.ui.VTwinColSelect;
+import com.vaadin.shared.ui.Connect;
+import com.vaadin.shared.ui.twincolselect.TwinColSelectState;
+import com.vaadin.ui.TwinColSelect;
+
+/**
+ * Client side connector for {@link TwinColSelect} component.
+ *
+ * @author Vaadin Ltd
+ *
+ */
+@Connect(TwinColSelect.class)
+public class TwinColSelectConnector extends AbstractMultiSelectConnector
+ implements DirectionalManagedLayout {
+
+ @Override
+ protected void init() {
+ super.init();
+ getLayoutManager().registerDependency(this,
+ getWidget().getCaptionWrapper().getElement());
+ }
+
+ @Override
+ public void onUnregister() {
+ getLayoutManager().unregisterDependency(this,
+ getWidget().getCaptionWrapper().getElement());
+ }
+
+ @Override
+ public VTwinColSelect getWidget() {
+ return (VTwinColSelect) super.getWidget();
+ }
+
+ @Override
+ public TwinColSelectState getState() {
+ return (TwinColSelectState) super.getState();
+ }
+
+ @OnStateChange(value = { "leftColumnCaption", "rightColumnCaption",
+ "caption" })
+ void updateCaptions() {
+ getWidget().updateCaptions(getState().leftColumnCaption,
+ getState().rightColumnCaption);
+
+ getLayoutManager().setNeedsHorizontalLayout(this);
+ }
+
+ @OnStateChange("readOnly")
+ void updateReadOnly() {
+ getWidget().setReadOnly(isReadOnly());
+ }
+
+ @Override
+ public void layoutVertically() {
+ if (isUndefinedHeight()) {
+ getWidget().clearInternalHeights();
+ } else {
+ getWidget().setInternalHeights();
+ }
+ }
+
+ @Override
+ public void layoutHorizontally() {
+ if (isUndefinedWidth()) {
+ getWidget().clearInternalWidths();
+ } else {
+ getWidget().setInternalWidths();
+ }
+ }
+
+ @Override
+ public MultiSelectWidget getMultiSelectWidget() {
+ return getWidget();
+ }
+}
diff --git a/pom.xml b/pom.xml
index eaeaf01c99..25c553bd32 100644
--- a/pom.xml
+++ b/pom.xml
@@ -170,7 +170,7 @@
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
- <version>4.11</version>
+ <version>4.12</version>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
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()));
- }
-}
diff --git a/shared/src/main/java/com/vaadin/shared/data/selection/MultiSelectServerRpc.java b/shared/src/main/java/com/vaadin/shared/data/selection/MultiSelectServerRpc.java
new file mode 100644
index 0000000000..a924f00d11
--- /dev/null
+++ b/shared/src/main/java/com/vaadin/shared/data/selection/MultiSelectServerRpc.java
@@ -0,0 +1,41 @@
+/*
+ * 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.shared.data.selection;
+
+import java.util.Set;
+
+import com.vaadin.shared.communication.ServerRpc;
+
+/**
+ * Transmits SelectionModel selection changes from the client to the server.
+ *
+ * @author Vaadin Ltd
+ *
+ * @since 8.0
+ */
+public interface MultiSelectServerRpc extends ServerRpc {
+
+ /**
+ * Updates the selected items based on their keys.
+ *
+ * @param addedItemKeys
+ * the item keys added to selection
+ * @param removedItemKeys
+ * the item keys removed from selection
+ */
+ void updateSelection(Set<String> addedItemKeys,
+ Set<String> removedItemKeys);
+}
diff --git a/shared/src/main/java/com/vaadin/shared/data/selection/SelectionModel.java b/shared/src/main/java/com/vaadin/shared/data/selection/SelectionModel.java
index 8711d6a9c8..c56fe8c4d9 100644
--- a/shared/src/main/java/com/vaadin/shared/data/selection/SelectionModel.java
+++ b/shared/src/main/java/com/vaadin/shared/data/selection/SelectionModel.java
@@ -16,9 +16,13 @@
package com.vaadin.shared.data.selection;
import java.io.Serializable;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.stream.Stream;
/**
* Models the selection logic of a {@code Listing} component. Determines how
@@ -60,7 +64,7 @@ public interface SelectionModel<T> extends Serializable {
/**
* Sets the current selection to the given item, or clears selection if
* given {@code null}.
- *
+ *
* @param item
* the item to select or {@code null} to clear selection
*/
@@ -78,11 +82,11 @@ public interface SelectionModel<T> extends Serializable {
*
* @return a singleton set of the selected item if any, an empty set
* otherwise
- *
+ *
* @see #getSelectedItem()
*/
@Override
- default Set<T> getSelectedItems() {
+ public default Set<T> getSelectedItems() {
return getSelectedItem().map(Collections::singleton)
.orElse(Collections.emptySet());
}
@@ -98,10 +102,77 @@ public interface SelectionModel<T> extends Serializable {
public interface Multi<T> extends SelectionModel<T> {
/**
- * Adds the given items to the set of currently selected items.
+ * Adds the given item to the set of currently selected items.
+ * <p>
+ * By default this does not clear any previous selection. To do that,
+ * use {@link #deselectAll()}.
+ * <p>
+ * If the the item was already selected, this is a NO-OP.
+ *
+ * @param item
+ * the item to add to selection, not {@code null}
*/
@Override
- public void select(T item);
+ public default void select(T item) {
+ Objects.requireNonNull(item);
+ selectItems(item);
+ };
+
+ /**
+ * Adds the given items to the set of currently selected items.
+ * <p>
+ * By default this does not clear any previous selection. To do that,
+ * use {@link #deselectAll()}.
+ * <p>
+ * If the all the items were already selected, this is a NO-OP.
+ * <p>
+ * This is a short-hand for {@link #updateSelection(Set, Set)} with
+ * nothing to deselect.
+ *
+ * @param items
+ * to add to selection, not {@code null}
+ */
+ public default void selectItems(T... items) {
+ Objects.requireNonNull(items);
+ Stream.of(items).forEach(Objects::requireNonNull);
+
+ updateSelection(new LinkedHashSet<>(Arrays.asList(items)),
+ Collections.emptySet());
+ }
+
+ /**
+ * Removes the given items from the set of currently selected items.
+ * <p>
+ * If the none of the items were selected, this is a NO-OP.
+ * <p>
+ * This is a short-hand for {@link #updateSelection(Set, Set)} with
+ * nothing to select.
+ *
+ * @param items
+ * to remove from selection, not {@code null}
+ */
+ public default void deselectItems(T... items) {
+ Objects.requireNonNull(items);
+ Stream.of(items).forEach(Objects::requireNonNull);
+
+ updateSelection(Collections.emptySet(),
+ new LinkedHashSet<>(Arrays.asList(items)));
+ }
+
+ /**
+ * Updates the selection by adding and removing the given items from it.
+ * <p>
+ * If all the added items were already selected and the removed items
+ * were not selected, this is a NO-OP.
+ * <p>
+ * Duplicate items (in both add & remove sets) are ignored.
+ *
+ * @param addedItems
+ * the items to add, not {@code null}
+ * @param removedItems
+ * the items to remove, not {@code null}
+ */
+ public void updateSelection(Set<T> addedItems, Set<T> removedItems);
}
/**
diff --git a/shared/src/main/java/com/vaadin/shared/ui/optiongroup/CheckBoxGroupConstants.java b/shared/src/main/java/com/vaadin/shared/ui/ListingJsonConstants.java
index 6bca43852a..82431bf7f1 100644
--- a/shared/src/main/java/com/vaadin/shared/ui/optiongroup/CheckBoxGroupConstants.java
+++ b/shared/src/main/java/com/vaadin/shared/ui/ListingJsonConstants.java
@@ -13,18 +13,16 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
-package com.vaadin.shared.ui.optiongroup;
+package com.vaadin.shared.ui;
import java.io.Serializable;
-public class CheckBoxGroupConstants implements Serializable {
+public class ListingJsonConstants implements Serializable {
public static final String JSONKEY_ITEM_DISABLED = "d";
public static final String JSONKEY_ITEM_ICON = "i";
public static final String JSONKEY_ITEM_VALUE = "v";
- public static final String JSONKEY_ITEM_KEY = "k";
-
public static final String JSONKEY_ITEM_SELECTED = "s";
}
diff --git a/shared/src/main/java/com/vaadin/shared/ui/optiongroup/RadioButtonGroupConstants.java b/shared/src/main/java/com/vaadin/shared/ui/twincolselect/TwinColSelectState.java
index 5278d211de..a6b09c0e91 100644
--- a/shared/src/main/java/com/vaadin/shared/ui/optiongroup/RadioButtonGroupConstants.java
+++ b/shared/src/main/java/com/vaadin/shared/ui/twincolselect/TwinColSelectState.java
@@ -13,18 +13,23 @@
* License for the specific language governing permissions and limitations under
* the License.
*/
-package com.vaadin.shared.ui.optiongroup;
+package com.vaadin.shared.ui.twincolselect;
-import java.io.Serializable;
+import com.vaadin.shared.annotations.DelegateToWidget;
+import com.vaadin.shared.ui.TabIndexState;
-public class RadioButtonGroupConstants implements Serializable {
- public static final String JSONKEY_ITEM_DISABLED = "d";
-
- public static final String JSONKEY_ITEM_ICON = "i";
-
- public static final String JSONKEY_ITEM_VALUE = "v";
-
- public static final String JSONKEY_ITEM_KEY = "k";
+/**
+ * Shared state for the TwinColSelect component.
+ *
+ * @since 7.0
+ */
+public class TwinColSelectState extends TabIndexState {
+ {
+ primaryStyleName = "v-select-twincol";
+ }
+ @DelegateToWidget
+ public int rows;
- public static final String JSONKEY_ITEM_SELECTED = "s";
+ public String leftColumnCaption;
+ public String rightColumnCaption;
}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/abstractlisting/AbstractMultiSelectTestUI.java b/uitest/src/main/java/com/vaadin/tests/components/abstractlisting/AbstractMultiSelectTestUI.java
new file mode 100644
index 0000000000..fadd94a4be
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/components/abstractlisting/AbstractMultiSelectTestUI.java
@@ -0,0 +1,83 @@
+package com.vaadin.tests.components.abstractlisting;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import com.vaadin.shared.data.selection.SelectionModel.Multi;
+import com.vaadin.ui.AbstractMultiSelect;
+
+public abstract class AbstractMultiSelectTestUI<MULTISELECT extends AbstractMultiSelect<Object>>
+ extends AbstractListingTestUI<MULTISELECT> {
+
+ protected final String selectionCategory = "Selection";
+
+ @Override
+ protected void createActions() {
+ super.createActions();
+ createItemCaptionGeneratorMenu();
+ createSelectionMenu();
+ createListenerMenu();
+ }
+
+ protected void createItemCaptionGeneratorMenu() {
+ createBooleanAction("Use Item Caption Generator", "Item Generator",
+ false, this::useItemCaptionProvider);
+ }
+
+ private void useItemCaptionProvider(MULTISELECT select, boolean activate,
+ Object data) {
+ if (activate) {
+ select.setItemCaptionGenerator(
+ item -> item.toString() + " Caption");
+ } else {
+ select.setItemCaptionGenerator(item -> item.toString());
+ }
+ select.getDataSource().refreshAll();
+ }
+
+ protected void createSelectionMenu() {
+ createClickAction(
+ "Clear selection", selectionCategory, (component, item,
+ data) -> component.getSelectionModel().deselectAll(),
+ "");
+
+ Command<MULTISELECT, String> toggleSelection = (component, item,
+ data) -> toggleSelection(item);
+
+ List<String> items = IntStream.of(0, 1, 5, 10, 25)
+ .mapToObj(i -> "Item " + i).collect(Collectors.toList());
+ items.forEach(item -> createClickAction("Toggle " + item,
+ selectionCategory, toggleSelection, item));
+
+ Command<MULTISELECT, Boolean> toggleMultiSelection = (component, i,
+ data) -> toggleMultiSelection(i, items);
+
+ createBooleanAction("Toggle items 0, 1, 5, 10, 25", selectionCategory,
+ false, toggleMultiSelection, items);
+ }
+
+ private void toggleSelection(String item) {
+ Multi<Object> selectionModel = getComponent().getSelectionModel();
+ if (selectionModel.isSelected(item)) {
+ selectionModel.deselect(item);
+ } else {
+ selectionModel.select(item);
+ }
+ }
+
+ private void toggleMultiSelection(boolean add, List<String> items) {
+ Multi<Object> selectionModel = getComponent().getSelectionModel();
+ if (add) {
+ selectionModel.selectItems(items.toArray());
+ } else {
+ selectionModel.deselectItems(items.toArray());
+ }
+ }
+
+ protected void createListenerMenu() {
+ createListenerAction("Selection listener", "Listeners",
+ c -> c.addSelectionListener(
+ e -> log("Selected: " + e.getNewSelection())));
+ }
+}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/checkbox/CheckBoxGroupTestUI.java b/uitest/src/main/java/com/vaadin/tests/components/checkbox/CheckBoxGroupTestUI.java
index 6553c0ae6e..95d2c016b1 100644
--- a/uitest/src/main/java/com/vaadin/tests/components/checkbox/CheckBoxGroupTestUI.java
+++ b/uitest/src/main/java/com/vaadin/tests/components/checkbox/CheckBoxGroupTestUI.java
@@ -15,14 +15,10 @@
*/
package com.vaadin.tests.components.checkbox;
-import java.util.function.Function;
-import java.util.stream.IntStream;
-
import com.vaadin.server.FontAwesome;
-import com.vaadin.server.Resource;
-import com.vaadin.shared.data.selection.SelectionModel.Multi;
-import com.vaadin.tests.components.abstractlisting.AbstractListingTestUI;
+import com.vaadin.tests.components.abstractlisting.AbstractMultiSelectTestUI;
import com.vaadin.ui.CheckBoxGroup;
+import com.vaadin.ui.IconGenerator;
/**
* Test UI for CheckBoxGroup component
@@ -30,11 +26,9 @@ import com.vaadin.ui.CheckBoxGroup;
* @author Vaadin Ltd
*/
public class CheckBoxGroupTestUI
- extends AbstractListingTestUI<CheckBoxGroup<Object>> {
-
- private final String selectionCategory = "Selection";
+ extends AbstractMultiSelectTestUI<CheckBoxGroup<Object>> {
- private static final Function<Object, Resource> DEFAULT_ICON_PROVIDER = item -> "Item 2"
+ private static final IconGenerator<Object> DEFAULT_ICON_GENERATOR = item -> "Item 2"
.equals(item) ? ICON_16_HELP_PNG_CACHEABLE : null;
@SuppressWarnings({ "unchecked", "rawtypes" })
@@ -46,7 +40,7 @@ public class CheckBoxGroupTestUI
@Override
protected CheckBoxGroup<Object> constructComponent() {
CheckBoxGroup<Object> checkBoxGroup = super.constructComponent();
- checkBoxGroup.setItemIconProvider(DEFAULT_ICON_PROVIDER);
+ checkBoxGroup.setItemIconGenerator(DEFAULT_ICON_GENERATOR);
checkBoxGroup.setItemEnabledProvider(item -> !"Item 10".equals(item));
return checkBoxGroup;
}
@@ -54,70 +48,25 @@ public class CheckBoxGroupTestUI
@Override
protected void createActions() {
super.createActions();
- createListenerMenu();
- createSelectionMenu();
- createItemProviderMenu();
- }
-
- protected void createSelectionMenu() {
- createClickAction(
- "Clear selection", selectionCategory, (component, item,
- data) -> component.getSelectionModel().deselectAll(),
- "");
-
- Command<CheckBoxGroup<Object>, String> toggleSelection = (component,
- item, data) -> toggleSelection(item);
-
- IntStream.of(0, 1, 5, 10, 25).mapToObj(i -> "Item " + i)
- .forEach(item -> {
- createClickAction("Toggle " + item, selectionCategory,
- toggleSelection, item);
- });
- }
-
- private void toggleSelection(String item) {
- Multi<Object> selectionModel = getComponent().getSelectionModel();
- if (selectionModel.isSelected(item)) {
- selectionModel.deselect(item);
- } else {
- selectionModel.select(item);
- }
+ createItemIconGenerator();
}
- private void createItemProviderMenu() {
- createBooleanAction("Use Item Caption Provider", "Item Provider", false,
- this::useItemCaptionProvider);
- createBooleanAction("Use Item Icon Provider", "Item Provider", false,
+ private void createItemIconGenerator() {
+ createBooleanAction("Use Item Icon Generator", "Item Generator", false,
this::useItemIconProvider);
}
- private void useItemCaptionProvider(CheckBoxGroup<Object> group,
- boolean activate, Object data) {
- if (activate) {
- group.setItemCaptionProvider(item -> item.toString() + " Caption");
- } else {
- group.setItemCaptionProvider(item -> item.toString());
- }
- group.getDataSource().refreshAll();
- }
-
private void useItemIconProvider(CheckBoxGroup<Object> group,
boolean activate, Object data) {
if (activate) {
- group.setItemIconProvider(
+ group.setItemIconGenerator(
item -> FontAwesome.values()[getIndex(item) + 1]);
} else {
- group.setItemIconProvider(DEFAULT_ICON_PROVIDER);
+ group.setItemIconGenerator(DEFAULT_ICON_GENERATOR);
}
group.getDataSource().refreshAll();
}
- protected void createListenerMenu() {
- createListenerAction("Selection listener", "Listeners",
- c -> c.addSelectionListener(
- e -> log("Selected: " + e.getNewSelection())));
- }
-
private int getIndex(Object item) {
int index = item.toString().indexOf(' ');
if (index < 0) {
diff --git a/uitest/src/main/java/com/vaadin/tests/components/select/TwinColSelectCaptionStyles.java b/uitest/src/main/java/com/vaadin/tests/components/select/TwinColSelectCaptionStyles.java
index 563e730681..6491651c4b 100644
--- a/uitest/src/main/java/com/vaadin/tests/components/select/TwinColSelectCaptionStyles.java
+++ b/uitest/src/main/java/com/vaadin/tests/components/select/TwinColSelectCaptionStyles.java
@@ -3,14 +3,15 @@ package com.vaadin.tests.components.select;
import com.vaadin.tests.components.TestBase;
import com.vaadin.ui.Button;
import com.vaadin.ui.Button.ClickEvent;
-import com.vaadin.v7.ui.TwinColSelect;
+import com.vaadin.ui.TwinColSelect;
public class TwinColSelectCaptionStyles extends TestBase {
@Override
protected void setup() {
setTheme("tests-tickets");
- final TwinColSelect sel = new TwinColSelect("Component caption");
+ final TwinColSelect<String> sel = new TwinColSelect<>(
+ "Component caption");
sel.setLeftColumnCaption("Left caption");
sel.setRightColumnCaption("Right caption");
sel.setStyleName("styled-twincol-captions");
diff --git a/uitest/src/main/java/com/vaadin/tests/components/select/TwinColSelects.java b/uitest/src/main/java/com/vaadin/tests/components/select/TwinColSelects.java
index 7a769de843..fbc2567096 100644
--- a/uitest/src/main/java/com/vaadin/tests/components/select/TwinColSelects.java
+++ b/uitest/src/main/java/com/vaadin/tests/components/select/TwinColSelects.java
@@ -1,7 +1,10 @@
package com.vaadin.tests.components.select;
+import java.util.ArrayList;
+import java.util.List;
+
import com.vaadin.tests.components.ComponentTestCase;
-import com.vaadin.v7.ui.TwinColSelect;
+import com.vaadin.ui.TwinColSelect;
public class TwinColSelects extends ComponentTestCase<TwinColSelect> {
@@ -13,7 +16,7 @@ public class TwinColSelects extends ComponentTestCase<TwinColSelect> {
@Override
protected void initializeComponents() {
- TwinColSelect tws = createTwinColSelect("400x<auto>");
+ TwinColSelect<String> tws = createTwinColSelect("400x<auto>");
tws.setWidth("400px");
tws.setHeight("-1px");
addTestComponent(tws);
@@ -34,14 +37,13 @@ public class TwinColSelects extends ComponentTestCase<TwinColSelect> {
}
- private TwinColSelect createTwinColSelect(String caption) {
- TwinColSelect select = new TwinColSelect(caption);
- select.addContainerProperty(CAPTION, String.class, null);
+ private TwinColSelect<String> createTwinColSelect(String caption) {
+ TwinColSelect<String> select = new TwinColSelect<>(caption);
+ List<String> items = new ArrayList<>();
for (int i = 0; i < 20; i++) {
- select.addItem("" + i).getItemProperty(CAPTION)
- .setValue("Item " + i);
+ items.add("Item " + i);
}
- select.setImmediate(true);
+ select.setItems(items);
return select;
}
diff --git a/uitest/src/main/java/com/vaadin/tests/components/twincolselect/TwinColSelectTestUI.java b/uitest/src/main/java/com/vaadin/tests/components/twincolselect/TwinColSelectTestUI.java
new file mode 100644
index 0000000000..761c412bd6
--- /dev/null
+++ b/uitest/src/main/java/com/vaadin/tests/components/twincolselect/TwinColSelectTestUI.java
@@ -0,0 +1,35 @@
+package com.vaadin.tests.components.twincolselect;
+
+import java.util.LinkedHashMap;
+
+import com.vaadin.tests.components.abstractlisting.AbstractMultiSelectTestUI;
+import com.vaadin.ui.TwinColSelect;
+
+public class TwinColSelectTestUI
+ extends AbstractMultiSelectTestUI<TwinColSelect<Object>> {
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ @Override
+ protected Class<TwinColSelect<Object>> getTestClass() {
+ return (Class) TwinColSelect.class;
+ }
+
+ @Override
+ protected void createActions() {
+ super.createActions();
+ createRows();
+ }
+
+ private void createRows() {
+ LinkedHashMap<String, Integer> options = new LinkedHashMap<>();
+ options.put("0", 0);
+ options.put("1", 1);
+ options.put("2", 2);
+ options.put("5", 5);
+ options.put("10 (default)", 10);
+ options.put("50", 50);
+
+ createSelectAction("Rows", CATEGORY_STATE, options, "10 (default)",
+ (c, value, data) -> c.setRows(value), null);
+ }
+}
diff --git a/uitest/src/test/java/com/vaadin/tests/components/checkboxgroup/CheckBoxGroupTest.java b/uitest/src/test/java/com/vaadin/tests/components/checkboxgroup/CheckBoxGroupTest.java
index a30f5d71e2..4bb1defbf6 100644
--- a/uitest/src/test/java/com/vaadin/tests/components/checkboxgroup/CheckBoxGroupTest.java
+++ b/uitest/src/test/java/com/vaadin/tests/components/checkboxgroup/CheckBoxGroupTest.java
@@ -1,6 +1,6 @@
/*
* 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
@@ -122,15 +122,16 @@ public class CheckBoxGroupTest extends MultiBrowserTest {
}
@Test
- public void itemCaptionProvider() {
- selectMenuPath("Component", "Item Provider",
- "Use Item Caption Provider");
+ public void itemCaptionGenerator() {
+ selectMenuPath("Component", "Item Generator",
+ "Use Item Caption Generator");
assertItems(20, " Caption");
}
@Test
- public void itemIconProvider() {
- selectMenuPath("Component", "Item Provider", "Use Item Icon Provider");
+ public void itemIconGenerator() {
+ selectMenuPath("Component", "Item Generator",
+ "Use Item Icon Generator");
assertItemSuffices(20);
List<WebElement> icons = getSelect()
.findElements(By.cssSelector(".v-icon.FontAwesome"));
diff --git a/uitest/src/test/java/com/vaadin/tests/components/twincolselect/TwinColSelectTest.java b/uitest/src/test/java/com/vaadin/tests/components/twincolselect/TwinColSelectTest.java
new file mode 100644
index 0000000000..42c8128d10
--- /dev/null
+++ b/uitest/src/test/java/com/vaadin/tests/components/twincolselect/TwinColSelectTest.java
@@ -0,0 +1,259 @@
+package com.vaadin.tests.components.twincolselect;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.Select;
+
+import com.vaadin.testbench.elements.TwinColSelectElement;
+import com.vaadin.tests.tb3.MultiBrowserTest;
+
+public class TwinColSelectTest extends MultiBrowserTest {
+
+ @Before
+ public void setUp() throws Exception {
+ openTestURL();
+ }
+
+ @Test
+ public void initialLoad_containsCorrectItems() {
+ assertItems(20);
+ }
+
+ @Test
+ public void initialItems_reduceItemCount_containsCorrectItems() {
+ selectMenuPath("Component", "Data source", "Items", "5");
+ assertItems(5);
+ }
+
+ @Test
+ public void initialItems_increaseItemCount_containsCorrectItems() {
+ selectMenuPath("Component", "Data source", "Items", "100");
+ assertItems(100);
+ }
+
+ @Test
+ public void itemsMovedFromLeftToRight() {
+ selectMenuPath("Component", "Data source", "Items", "5");
+ assertItems(5);
+
+ selectItems("Item 1", "Item 2", "Item 4");
+
+ assertSelected("Item 1", "Item 2", "Item 4");
+
+ assertOptionTexts("Item 0", "Item 3");
+
+ deselectItems("Item 1", "Item 4");
+
+ assertSelected("Item 2");
+
+ assertOptionTexts("Item 0", "Item 1", "Item 3", "Item 4");
+
+ selectItems("Item 0");
+
+ assertSelected("Item 0", "Item 2");
+ assertOptionTexts("Item 1", "Item 3", "Item 4");
+ }
+
+ @Test
+ public void clickToSelect() {
+ selectMenuPath("Component", "Listeners", "Selection listener");
+
+ selectItems("Item 4");
+ Assert.assertEquals("1. Selected: [Item 4]", getLogRow(0));
+ assertSelected("Item 4");
+
+ // the previous item stays selected
+ selectItems("Item 2");
+ // Selection order (most recently selected is last)
+ Assert.assertEquals("2. Selected: [Item 4, Item 2]", getLogRow(0));
+ assertSelected("Item 2", "Item 4");
+
+ deselectItems("Item 4");
+ Assert.assertEquals("3. Selected: [Item 2]", getLogRow(0));
+ assertSelected("Item 2");
+
+ selectItems("Item 10", "Item 0", "Item 9", "Item 4");
+
+ Assert.assertEquals(
+ "4. Selected: [Item 2, Item 0, Item 4, Item 10, Item 9]",
+ getLogRow(0));
+ assertSelected("Item 0", "Item 2", "Item 4", "Item 9", "Item 10");
+
+ deselectItems("Item 0", "Item 2", "Item 9");
+ Assert.assertEquals("5. Selected: [Item 4, Item 10]", getLogRow(0));
+ assertSelected("Item 4", "Item 10");
+ }
+
+ @Test
+ public void disabled_clickToSelect() {
+ selectMenuPath("Component", "State", "Enabled");
+
+ Assert.assertTrue(getTwinColSelect().findElements(By.tagName("input"))
+ .stream()
+ .allMatch(element -> element.getAttribute("disabled") != null));
+
+ selectMenuPath("Component", "Listeners", "Selection listener");
+
+ String lastLogRow = getLogRow(0);
+
+ selectItems("Item 4");
+ Assert.assertEquals(lastLogRow, getLogRow(0));
+ assertNothingSelected();
+
+ selectItems("Item 2");
+ // Selection order (most recently selected is last)
+ Assert.assertEquals(lastLogRow, getLogRow(0));
+ assertNothingSelected();
+
+ selectItems("Item 4");
+ Assert.assertEquals(lastLogRow, getLogRow(0));
+ assertNothingSelected();
+ }
+
+ @Test
+ public void clickToSelect_reenable() {
+ selectMenuPath("Component", "State", "Enabled");
+ selectMenuPath("Component", "Listeners", "Selection listener");
+
+ selectItems("Item 4");
+ assertNothingSelected();
+
+ selectMenuPath("Component", "State", "Enabled");
+
+ selectItems("Item 5");
+ Assert.assertEquals("3. Selected: [Item 5]", getLogRow(0));
+ assertSelected("Item 5");
+
+ selectItems("Item 2");
+ Assert.assertEquals("4. Selected: [Item 5, Item 2]", getLogRow(0));
+ assertSelected("Item 2", "Item 5");
+
+ deselectItems("Item 5");
+ Assert.assertEquals("5. Selected: [Item 2]", getLogRow(0));
+ assertSelected("Item 2");
+ }
+
+ @Test
+ public void itemCaptionProvider() {
+ selectMenuPath("Component", "Item Generator",
+ "Use Item Caption Generator");
+ assertItems(20, " Caption");
+ }
+
+ @Test
+ public void selectProgramatically() {
+ selectMenuPath("Component", "Listeners", "Selection listener");
+
+ selectMenuPath("Component", "Selection", "Toggle Item 5");
+ Assert.assertEquals("2. Selected: [Item 5]", getLogRow(0));
+ assertSelected("Item 5");
+
+ selectMenuPath("Component", "Selection", "Toggle Item 1");
+ // Selection order (most recently selected is last)
+ Assert.assertEquals("4. Selected: [Item 5, Item 1]", getLogRow(0));
+ // DOM order
+ assertSelected("Item 1", "Item 5");
+
+ selectMenuPath("Component", "Selection", "Toggle Item 5");
+ Assert.assertEquals("6. Selected: [Item 1]", getLogRow(0));
+ assertSelected("Item 1");
+
+ selectMenuPath("Component", "Selection",
+ "Toggle items 0, 1, 5, 10, 25");
+
+ // currently non-existing items are added to selection!
+ Assert.assertEquals(
+ "8. Selected: [Item 1, Item 0, Item 5, Item 10, Item 25]",
+ getLogRow(0));
+ assertSelected("Item 0", "Item 1", "Item 5", "Item 10");
+ }
+
+ private void assertSelected(String... expectedSelection) {
+ Assert.assertEquals(Arrays.asList(expectedSelection),
+ getTwinColSelect().getValues());
+ }
+
+ private void assertNothingSelected() {
+ Assert.assertEquals(0, getTwinColSelect().getValues().size());
+ }
+
+ @Override
+ protected Class<?> getUIClass() {
+ return TwinColSelectTestUI.class;
+ }
+
+ protected TwinColSelectElement getTwinColSelect() {
+ return $(TwinColSelectElement.class).first();
+ }
+
+ protected Select getOptionsElement() {
+ return new Select(getTwinColSelect().findElement(By.tagName("select")));
+ }
+
+ protected Select getSelectedOptionsElement() {
+ return new Select(
+ getTwinColSelect().findElements(By.tagName("select")).get(1));
+ }
+
+ protected WebElement getSelectButton() {
+ return getTwinColSelect().findElements(By.className("v-button")).get(0);
+ }
+
+ protected WebElement getDeselectButton() {
+ return getTwinColSelect().findElements(By.className("v-button")).get(1);
+ }
+
+ protected void selectItems(String... items) {
+ Select options = getOptionsElement();
+ options.deselectAll();
+ Stream.of(items).forEach(text -> options.selectByVisibleText(text));
+ getSelectButton().click();
+ }
+
+ protected void deselectItems(String... items) {
+ Select options = getSelectedOptionsElement();
+ options.deselectAll();
+ Stream.of(items).forEach(text -> options.selectByVisibleText(text));
+ getDeselectButton().click();
+ }
+
+ protected void assertItems(int count) {
+ assertItems(count, "");
+ }
+
+ protected void assertItems(int count, String suffix) {
+ int i = 0;
+ for (String text : getTwinColSelect().getOptions()) {
+ assertEquals("Item " + i + suffix, text);
+ i++;
+ }
+ assertEquals("Number of items", count, i);
+ }
+
+ protected void assertItemSuffices(int count) {
+ int i = 0;
+ for (String text : getTwinColSelect().getOptions()) {
+ assertTrue(text.endsWith("Item " + i));
+ i++;
+ }
+ assertEquals("Number of items", count, i);
+ }
+
+ protected void assertOptionTexts(String... items) {
+ List<String> optionTexts = getOptionsElement().getOptions().stream()
+ .map(element -> element.getText()).collect(Collectors.toList());
+ Assert.assertArrayEquals(items, optionTexts.toArray());
+ }
+
+}