diff options
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(); + } +} @@ -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()); + } + +} |