org.controlsfx.control.ListSelectionView Maven / Gradle / Ivy
Show all versions of controlsfx Show documentation
/**
* Copyright (c) 2014, 2018 ControlsFX
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of ControlsFX, any associated website, nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL CONTROLSFX BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.controlsfx.control;
import impl.org.controlsfx.skin.ListSelectionViewSkin;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Orientation;
import javafx.scene.Node;
import javafx.scene.control.Cell;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Skin;
import javafx.scene.input.KeyCombination;
import javafx.util.Callback;
import org.controlsfx.control.action.Action;
import org.controlsfx.glyphfont.FontAwesome;
import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import static impl.org.controlsfx.i18n.Localization.asKey;
import static impl.org.controlsfx.i18n.Localization.localize;
import static javafx.geometry.Orientation.HORIZONTAL;
import static org.controlsfx.glyphfont.FontAwesome.Glyph.*;
/**
* A control used to perform a multi-selection via the help of two list views.
* Items can be moved from one list (source) to the other (target). This can be
* done by either double clicking on the list items or by using one of the
* "move" buttons between the two lists. Buttons can be added or removed by
* altering the {@link #getActions() list of actions}.
* Each list can be decorated with a header and a footer node.
* The default header nodes are simply two labels ("Available", "Selected").
*
* Screenshot
*
*
*
* Code Example
*
*
* ListSelectionView<String> view = new ListSelectionView<>();
* view.getSourceItems().add("One", "Two", "Three");
* view.getTargetItems().add("Four", "Five");
*
*
* @param
* the type of the list items
*/
public class ListSelectionView extends ControlsFXControl {
private static final String DEFAULT_STYLECLASS = "list-selection-view";
/**
* Constructs a new dual list view.
*/
public ListSelectionView() {
getStyleClass().add(DEFAULT_STYLECLASS);
Label sourceHeader = new Label(
localize(asKey("listSelectionView.header.source")));
sourceHeader.getStyleClass().add("list-header-label");
sourceHeader.setId("source-header-label");
setSourceHeader(sourceHeader);
Label targetHeader = new Label(
localize(asKey("listSelectionView.header.target")));
targetHeader.getStyleClass().add("list-header-label");
targetHeader.setId("target-header-label");
setTargetHeader(targetHeader);
}
@Override
protected Skin> createDefaultSkin() {
return new ListSelectionViewSkin<>(this);
}
/** {@inheritDoc} */
@Override public String getUserAgentStylesheet() {
return getUserAgentStylesheet(ListSelectionView.class, "listselectionview.css");
}
private final ObjectProperty sourceHeader = new SimpleObjectProperty<>(
this, "sourceHeader");
/**
* A property used to store a reference to a node that will be displayed
* above the source list view. The default node is a {@link Label}
* displaying the text "Available".
*
* @return the property used to store the source header node
*/
public final ObjectProperty sourceHeaderProperty() {
return sourceHeader;
}
/**
* Returns the value of {@link #sourceHeaderProperty()}.
*
* @return the source header node
*/
public final Node getSourceHeader() {
return sourceHeader.get();
}
/**
* Sets the value of {@link #sourceHeaderProperty()}.
*
* @param node
* the new header node to use for the source list
*/
public final void setSourceHeader(Node node) {
sourceHeader.set(node);
}
private final ObjectProperty sourceFooter = new SimpleObjectProperty<>(
this, "sourceFooter");
/**
* A property used to store a reference to a node that will be displayed
* below the source list view. The default node is a node with two buttons
* for easily selecting / deselecting all elements in the list view.
*
* @return the property used to store the source footer node
*/
public final ObjectProperty sourceFooterProperty() {
return sourceFooter;
}
/**
* Returns the value of {@link #sourceFooterProperty()}.
*
* @return the source footer node
*/
public final Node getSourceFooter() {
return sourceFooter.get();
}
/**
* Sets the value of {@link #sourceFooterProperty()}.
*
* @param node
* the new node shown below the source list
*/
public final void setSourceFooter(Node node) {
sourceFooter.set(node);
}
private final ObjectProperty targetHeader = new SimpleObjectProperty<>(
this, "targetHeader");
/**
* A property used to store a reference to a node that will be displayed
* above the target list view. The default node is a {@link Label}
* displaying the text "Selected".
*
* @return the property used to store the target header node
*/
public final ObjectProperty targetHeaderProperty() {
return targetHeader;
}
/**
* Returns the value of {@link #targetHeaderProperty()}.
*
* @return the source header node
*/
public final Node getTargetHeader() {
return targetHeader.get();
}
/**
* Sets the value of {@link #targetHeaderProperty()}.
*
* @param node
* the new node shown above the target list
*/
public final void setTargetHeader(Node node) {
targetHeader.set(node);
}
private final ObjectProperty targetFooter = new SimpleObjectProperty<>(
this, "targetFooter");
/**
* A property used to store a reference to a node that will be displayed
* below the target list view. The default node is a node with two buttons
* for easily selecting / deselecting all elements in the list view.
*
* @return the property used to store the source footer node
*/
public final ObjectProperty targetFooterProperty() {
return targetFooter;
}
/**
* Returns the value of {@link #targetFooterProperty()}.
*
* @return the source header node
*/
public final Node getTargetFooter() {
return targetFooter.get();
}
/**
* Sets the value of {@link #targetFooterProperty()}.
*
* @param node
* the new node shown below the target list
*/
public final void setTargetFooter(Node node) {
targetFooter.set(node);
}
private ObjectProperty> sourceItems;
/**
* Sets the underlying data model for the ListView. Note that it has a
* generic type that must match the type of the ListView itself.
*/
public final void setSourceItems(ObservableList value) {
sourceItemsProperty().set(value);
}
/**
* Returns an {@link ObservableList} that contains the items currently being
* shown to the user in the source list. This may be null if
* {@link #setSourceItems(javafx.collections.ObservableList)} has previously
* been called, however, by default it is an empty ObservableList.
*
* @return An ObservableList containing the items to be shown to the user in
* the source list, or null if the items have previously been set to
* null.
*/
public final ObservableList getSourceItems() {
return sourceItemsProperty().get();
}
/**
* The underlying data model for the source list view. Note that it has a
* generic type that must match the type of the source list view itself.
*/
public final ObjectProperty> sourceItemsProperty() {
if (sourceItems == null) {
sourceItems = new SimpleObjectProperty<>(this, "sourceItems",
FXCollections.observableArrayList());
}
return sourceItems;
}
private ObjectProperty> targetItems;
/**
* Sets the underlying data model for the ListView. Note that it has a
* generic type that must match the type of the ListView itself.
*/
public final void setTargetItems(ObservableList value) {
targetItemsProperty().set(value);
}
/**
* Returns an {@link ObservableList} that contains the items currently being
* shown to the user in the target list. This may be null if
* {@link #setTargetItems(javafx.collections.ObservableList)} has previously
* been called, however, by default it is an empty ObservableList.
*
* @return An ObservableList containing the items to be shown to the user in
* the target list, or null if the items have previously been set to
* null.
*/
public final ObservableList getTargetItems() {
return targetItemsProperty().get();
}
/**
* The underlying data model for the target list view. Note that it has a
* generic type that must match the type of the source list view itself.
*/
public final ObjectProperty> targetItemsProperty() {
if (targetItems == null) {
targetItems = new SimpleObjectProperty<>(this, "targetItems",
FXCollections.observableArrayList());
}
return targetItems;
}
// --- Orientation
private final ObjectProperty orientation = new SimpleObjectProperty<>(
this, "orientation", HORIZONTAL); //$NON-NLS-1$;
/**
* The {@link Orientation} of the {@code ListSelectionView} - this can
* either be horizontal or vertical.
*/
public final ObjectProperty orientationProperty() {
return orientation;
}
/**
* Sets the {@link Orientation} of the {@code ListSelectionView} - this can
* either be horizontal or vertical.
*/
public final void setOrientation(Orientation value) {
orientationProperty().set(value);
};
/**
* Returns the {@link Orientation} of the {@code ListSelectionView} - this
* can either be horizontal or vertical.
*/
public final Orientation getOrientation() {
return orientation.get();
}
// --- Cell Factory
private ObjectProperty, ListCell>> cellFactory;
/**
* Sets a new cell factory to use by both list views. This forces all old
* {@link ListCell}'s to be thrown away, and new ListCell's created with the
* new cell factory.
*/
public final void setCellFactory(Callback, ListCell> value) {
cellFactoryProperty().set(value);
}
/**
* Returns the current cell factory.
*/
public final Callback, ListCell> getCellFactory() {
return cellFactory == null ? null : cellFactory.get();
}
/**
*
* Setting a custom cell factory has the effect of deferring all cell
* creation, allowing for total customization of the cell. Internally, the
* ListView is responsible for reusing ListCells - all that is necessary is
* for the custom cell factory to return from this function a ListCell which
* might be usable for representing any item in the ListView.
*
*
* Refer to the {@link Cell} class documentation for more detail.
*/
public final ObjectProperty, ListCell>> cellFactoryProperty() {
if (cellFactory == null) {
cellFactory = new SimpleObjectProperty<>(this, "cellFactory");
}
return cellFactory;
}
// -- actions
private ObservableList actions = FXCollections.observableArrayList(new MoveToTarget(), new MoveToTargetAll(), new MoveToSource(), new MoveToSourceAll());
/**
* The list of actions to be shown in between the two list views.
* All actions except, {@link org.controlsfx.control.action.ActionUtils#ACTION_SEPARATOR}
* and {@link org.controlsfx.control.action.ActionUtils#ACTION_SPAN}, are represented as buttons.
*
* For actions dependent on both the internal list views, an instance of {@link ListSelectionAction} should
* be used.
*
* By default, the list has 4 actions - {@link MoveToTarget}, {@link MoveToTargetAll},
* {@link MoveToSource} and {@link MoveToSourceAll}. A user may choose to add on top of
* these actions or replace them depending on their use case.
*
* @return An ObservableList of actions.
*/
public final ObservableList getActions() {
return actions;
}
// -- source actions
private ObservableList sourceActions = FXCollections.observableArrayList();
/**
* These actions are shown beside the source list view. An instance of
* {@link ListActionView.ListAction} should be used where actions are
* dependent on the source ListView.
*
* @return An ObservableList of actions.
* @see ListActionView.ListAction
*/
public final ObservableList getSourceActions() {
return sourceActions;
}
// -- target actions
private ObservableList targetActions = FXCollections.observableArrayList();
/**
* These actions are shown beside the target list view. An instance of
* {@link ListActionView.ListAction} should be used where actions are
* dependent on the target ListView.
*
* @return An ObservableList of actions.
* @see ListActionView.ListAction
*/
public final ObservableList getTargetActions() {
return targetActions;
}
/**
* Specialized actions for ListSelectionView which get access to both the internal list views.
* A user can add a custom action to the control by extending this class and adding its instance
* to the {@link ListSelectionView#getActions() action list}.
*
* @param Type of ListSelectionView to which this ListSelectionAction will be added.
*/
public static abstract class ListSelectionAction extends Action {
/**
* Creates a new instance of ListSelectionAction with the graphic node.
* @param graphic Graphic to be shown in relation to this action.
*/
public ListSelectionAction(Node graphic) {
this(graphic, "");
}
/**
* Creates a new instance of ListSelectionAction with the provided graphic and text.
* @param graphic Graphic to be shown in relation to this action.
* @param text The text for the Action.
*/
public ListSelectionAction(Node graphic, String text) {
super(text);
setGraphic(graphic);
}
/**
* Can be used to define properties or bindings for actions which are directly dependent
* on the list views.
* @param sourceListView The source list view
* @param targetListView The target list view
*/
public abstract void initialize(ListView sourceListView, ListView targetListView);
@Override
protected final void setEventHandler(Consumer eventHandler) {
super.setEventHandler(eventHandler);
}
}
/**
* Action use to move the selected items from the
* source list view to the target list view.
*/
public class MoveToTarget extends ListSelectionAction {
public MoveToTarget() {
super(getGlyph(ANGLE_RIGHT));
getStyleClass().add("move-to-target-button");
setAccelerator(KeyCombination.keyCombination("CTRL+RIGHT"));
graphicProperty().bind(Bindings.createObjectBinding(() -> (getOrientation() == HORIZONTAL ? getGlyph(ANGLE_RIGHT) : getGlyph(ANGLE_DOWN)), orientationProperty()));
}
@Override
public void initialize(ListView sourceListView, ListView targetListView) {
disabledProperty().bind(Bindings.isEmpty(sourceListView.getSelectionModel().getSelectedItems()));
setEventHandler(ae -> moveToTarget(sourceListView, targetListView));
}
}
/**
* Action use to move all the items from the
* source list view to the target list view.
*/
public class MoveToTargetAll extends ListSelectionAction {
public MoveToTargetAll() {
super(getGlyph(ANGLE_DOUBLE_RIGHT));
getStyleClass().add("move-to-target-all-button");
graphicProperty().bind(Bindings.createObjectBinding(() -> (getOrientation() == HORIZONTAL ? getGlyph(ANGLE_DOUBLE_RIGHT) : getGlyph(ANGLE_DOUBLE_DOWN)), orientationProperty()));
setAccelerator(KeyCombination.keyCombination("CTRL+SHIFT+RIGHT"));
}
@Override
public void initialize(ListView sourceListView, ListView targetListView) {
disabledProperty().bind(Bindings.isEmpty(sourceListView.getItems()));
setEventHandler(ae -> moveToTargetAll(sourceListView, targetListView));
}
}
/**
* Action use to move the selected items from the
* target list view to the source list view.
*/
public class MoveToSource extends ListSelectionAction {
public MoveToSource() {
super(getGlyph(ANGLE_LEFT));
getStyleClass().add("move-to-source-button");
graphicProperty().bind(Bindings.createObjectBinding(() -> (getOrientation() == HORIZONTAL ? getGlyph(ANGLE_LEFT) : getGlyph(ANGLE_UP)), orientationProperty()));
setAccelerator(KeyCombination.keyCombination("CTRL+LEFT"));
}
@Override
public void initialize(ListView sourceListView, ListView targetListView) {
disabledProperty().bind(Bindings.isEmpty(targetListView.getSelectionModel().getSelectedItems()));
setEventHandler(ae -> moveToSource(sourceListView, targetListView));
}
}
/**
* Action use to all the items from the
* target list view to the source list view.
*/
public class MoveToSourceAll extends ListSelectionAction {
public MoveToSourceAll() {
super(getGlyph(ANGLE_DOUBLE_LEFT));
getStyleClass().add("move-to-source-all-button");
graphicProperty().bind(Bindings.createObjectBinding(() -> (getOrientation() == HORIZONTAL ? getGlyph(ANGLE_DOUBLE_LEFT) : getGlyph(ANGLE_DOUBLE_UP)), orientationProperty()));
setAccelerator(KeyCombination.keyCombination("CTRL+SHIFT+LEFT"));
}
@Override
public void initialize(ListView sourceListView, ListView targetListView) {
disabledProperty().bind(Bindings.isEmpty(targetListView.getItems()));
setEventHandler(ae -> moveToSourceAll(sourceListView, targetListView));
}
}
private static Glyph getGlyph(FontAwesome.Glyph angleDoubleDown) {
return new FontAwesome().create(angleDoubleDown);
}
private static void moveToTarget(ListView sourceListView, ListView targetListView) {
move(sourceListView, targetListView);
sourceListView.getSelectionModel().clearSelection();
}
private static void moveToTargetAll(ListView sourceListView, ListView targetListView) {
move(sourceListView, targetListView, new ArrayList<>(sourceListView.getItems()));
sourceListView.getSelectionModel().clearSelection();
}
private static void moveToSource(ListView sourceListView, ListView targetListView) {
move(targetListView, sourceListView);
targetListView.getSelectionModel().clearSelection();
}
private static void moveToSourceAll(ListView sourceListView, ListView targetListView) {
move(targetListView, sourceListView, new ArrayList<>(targetListView.getItems()));
targetListView.getSelectionModel().clearSelection();
}
private static void move(ListView source, ListView target) {
List selectedItems = new ArrayList<>(source.getSelectionModel().getSelectedItems());
move(source, target, selectedItems);
}
private static void move(ListView source, ListView target, List items) {
source.getItems().removeAll(items);
target.getItems().addAll(items);
}
}