diff options
author | Pekka Hyvönen <pekka@vaadin.com> | 2016-09-22 14:19:34 +0300 |
---|---|---|
committer | Vaadin Code Review <review@vaadin.com> | 2016-09-27 07:23:36 +0000 |
commit | 7e78d52dfe72678cf275585bc552b1612844da44 (patch) | |
tree | a6d3454fdb59f6a2e69c0bab125c228dca93a169 /client | |
parent | 0052d59a318075a3ce8202b2eab84e9d643fc544 (diff) | |
download | vaadin-framework-7e78d52dfe72678cf275585bc552b1612844da44.tar.gz vaadin-framework-7e78d52dfe72678cf275585bc552b1612844da44.zip |
TwinColSelect with new databinding API
Removes feature for adding new items.
Introduces a AbstractMultiSelect-abstraction layer,
which is used in server side by TwinColSelect & CheckBoxGroup and
on client side only TwinColSelect for now. Plan is to use it for
ListSelect too.
Further improvement would be to make AbstractMultiSelect use
SelectionModel that extends AbstractSelectionModel and is thus used
as an extension both as client & server side.
Updates to JUnit 4.12 for easier use of @Parameterized test..
Change-Id: I64258c2229b9514d382693748e2ca562a1e448d4
Diffstat (limited to 'client')
6 files changed, 1040 insertions, 46 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(); + } +} |