Lemur Listbox with multiple selection? How?

Hello guys,

currently I am trying to implement a Guide with a list inside from where I can select several objects at the same time. I tried first with ListBox by setting SelectionModel.SelectionMode.Multi mode but it doesn’t seem that I can select more than one elements at the same time. At least it is not visible that more than one element has been selected simultaneously.

At the moment I am using checkboxes in the render view as a workaround but this seems to me quite cumbersome.
Is there a possibility to change the list selection model in such a way that it can display multiple selections?

Regards,
Harry

I have questions that can be answered if you can show me some code. Maybe your cell renderer at a guess? In the line linked below it gives you a boolean that determines its selection state.

I think it’s true that I may not have implemented multi-selection. There may be places in ListBox that assume single selection… and I’m pretty sure that even if it supports displaying multiple selections that it won’t let you actually select more than one.

I’m not sure what it would take to implement at this point. I’d have to look much deeper than I have time for at the moment.

Hello guys,

thank you for your replies.

I have found a solution (= workaround) for this which works quite fine for me:

Step 1:
First I introduced a new wrapper data type which knows whether it is selected or not…

package sk.util.gui.elems;

import java.util.Objects;
import java.util.logging.Logger;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * This is a wrapper class for an arbitrary list data type which also knows
 * whether it has been selected or not.
 *
 * @author harryschwenk
 * @param <T>
 */
public class AdvListData<T> {

    private static final Logger LOG = Logger.getLogger(AdvListData.class.getName());

    private final T data;
    private boolean selected = false;

    /**
     *
     * @param data
     */
    public AdvListData(T data) {
        this.data = data;
    }

    /**
     * Get the value of selected
     *
     * @return the value of selected
     */
    public boolean isSelected() {
        return selected;
    }

    /**
     * Set the value of selected
     *
     * @param selected new value of selected
     */
    public void setSelected(boolean selected) {
        this.selected = selected;
    }

    /**
     * Get the value of data
     *
     * @return the value of data
     */
    public T getData() {
        return data;
    }

    /**
     *
     * @return
     */
    @Override
    public int hashCode() {
        int hash = 5;
        hash = 97 * hash + Objects.hashCode(this.data);
        return hash;
    }

    /**
     *
     *
     * @param obj
     * @return
     */
    @Override
    public boolean equals(@Nullable Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final AdvListData<?> other = (AdvListData<?>) obj;
        return Objects.equals(this.data, other.data);
    }
}

Step 2: This step I standardized the list container appearance. We have two columns here:
the left one is an empty container (for arbitrary content), the right is the checkbox for indicating/assinging selection state (AdvCheckbox is a slight adaption of mine which simplifies the addClickCommand callback).

package sk.util.gui.elems;

import com.jme3.scene.Spatial;
import com.simsilica.lemur.Command;
import com.simsilica.lemur.Container;
import com.simsilica.lemur.component.SpringGridLayout;
import java.util.logging.Logger;
import java.util.stream.Stream;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * The cell container standardizes the list cell appearance by defining a
 * selectable container which incorporates a checkbox on the right for cell
 * selection and an empty freely configurable container on the left which is
 * capable of conaitnain arbitrary gui elements for representing data.
 * <p>
 * the difference in the concept here between all other elements is here that
 * the selection command for the list have to be applied inside the
 * cellContainer rather than the listbox itslef.
 *
 * @author harryschwenk
 * @param <T> the type of the data which has to be displayed by the container.
 */
public abstract class AdvCellContainer<T> extends Container {

    private static final Logger LOG = Logger.getLogger(AdvCellContainer.class.getName());

    private final Container container = new Container("");
    private final AdvCheckbox checkbox = new AdvCheckbox("");

    /**
     *
     */
    public AdvCellContainer() {
        this("");
    }

    /**
     *
     * @param style
     */
    public AdvCellContainer(String style) {
        super(style);

        super.setLayout(new SpringGridLayout());
        super.addChild(container, 0, 0);
        super.addChild(checkbox, 0, 1);
    }

    /**
     *
     * @param commands
     */
    @SafeVarargs
    public final void addOnClickCommands(Command<@Nullable T>... commands) {
        Stream.of(commands).forEach((Command<@Nullable T> cmd) -> checkbox.addClickCommands(chk -> cmd.execute(getValue())));
    }

    /**
     * Get the value of data
     *
     * @return the value of data
     */
    public abstract @Nullable
    T getValue();

    /**
     * Get the value of container
     *
     * @return the value of container
     */
    public Container getContainer() {
        return container;
    }

    /**
     * Get the value of checkbox
     *
     * @return the value of checkbox
     */
    public AdvCheckbox getCheckbox() {
        return checkbox;
    }

    /**
     *
     * @return
     */
    @Override
    public Spatial clone() {
        throw new AssertionError("Cloning not supported!");
    }

    /**
     * @param value the value to set
     */
    public abstract void setValue(@Nullable T value);
}

Step 3: The derived list box ready for single/multiple selection. This class contains static subclasses for the configurable cellRenderer and a default text container. If applied the appearance is the same as the default Listbox with the difference of the checkboxes at the right side of the cell containers. Please note that this class extends a AdvListData-ListBox (using the data type introduced above)…

package sk.util.gui.elems;

import com.jme3.scene.Spatial;
import com.simsilica.lemur.Command;
import com.simsilica.lemur.Label;
import com.simsilica.lemur.ListBox;
import com.simsilica.lemur.Panel;
import com.simsilica.lemur.core.VersionedList;
import com.simsilica.lemur.list.CellRenderer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * This listbox implementatino has to be supplied with a list for the complete
 * content and a value which specifies the current selection. A command object
 * can be specified which consumes the new selected value.
 *
 * @author harryschwenk
 * @param <T>
 */
public final class AdvListbox<T> extends ListBox<AdvListData<T>> {

    private static final Logger LOG = Logger.getLogger(AdvListbox.class.getName());

    private ExecutionMode mode = ExecutionMode.Serial;
    private Function<@Nullable T, String> toString = Objects::toString;
    private Comparator<@NonNull T> comparator = (a, b) -> 0;
    private CompletableFuture<@NonNull List<@NonNull AdvListData<T>>> ct = CompletableFuture.completedFuture(Collections.emptyList());
    private List<@NonNull T> input = Collections.emptyList();
    private boolean inputListUpdated = false;

    /**
     *
     * @param tag
     */
    public AdvListbox(Class<T> tag) {
        this(tag, "glass");
    }

    /**
     *
     * @param tag
     * @param style
     */
    public AdvListbox(Class<T> tag, String style) {
        super(new VersionedList<AdvListData<T>>(), style);

        setCellRenderer(new AdvCellRenderer<>());
    }

    /**
     * We do not want objects from these class being cloneable!
     *
     * @return
     */
    @Override
    @SuppressWarnings("CloneDoesntCallSuperClone")
    public AdvCheckbox clone() {
        throw new AssertionError("Do not clone objects from this class!");
    }

    /**
     * @return the mode
     */
    public ExecutionMode getMode() {
        return mode;
    }

    /**
     * @param mode the mode to set
     */
    public void setMode(ExecutionMode mode) {
        this.mode = mode;
    }

    /**
     * for sorting the list content.
     *
     * @return the comparator
     */
    public Comparator<@NonNull T> getComparator() {
        return comparator;
    }

    /**
     * @param comparator the comparator to set
     */
    public void setComparator(Comparator<@NonNull T> comparator) {
        this.comparator = comparator;
    }

    /**
     * A quirk of mine: Here I preferred passing streams instead of collections :-)
     * @param input
     */
    public void setList(Stream<@NonNull T> input) {
        this.input = input.collect(Collectors.toList());
        setInputListUpdated(true);

    }

    /**
     * Get the value of inputListUpdated
     *
     * @return the value of inputListUpdated
     */
    public boolean isInputListUpdated() {
        return inputListUpdated;
    }

    /**
     * Set the value of inputListUpdated
     *
     * @param inputListUpdated new value of inputListUpdated
     */
    public void setInputListUpdated(boolean inputListUpdated) {
        this.inputListUpdated = inputListUpdated;
    }

    /**
     * this class provides synchronous/aysnchronous update on the list content.
     * Asynchronous update can be advantageous for large lists (due to sorting).
     * But theres always the possiblity to switch to serial execution.
     * <p>
     * the list content is updated here within the render thread here with the
     * already sorted list.
     *
     * @param tpf
     */
    @Override
    @SuppressWarnings("methodref.inference.unimplemented")
    public void updateLogicalState(float tpf) {
        if (isInputListUpdated()) {
            if (mode.equals(ExecutionMode.Parallel)) {
                if (ct.isDone()) {
                    List<AdvListData<T>> list = ct.join();
                    // Storing the selection before clearing the list.
                    Collection<T> selectionTmp = getCompleteSelection();
                    super.getModel().clear();
                    super.getModel().addAll(list);
                    ct = CompletableFuture
                            .supplyAsync(() -> input.stream()
                            .sorted(getComparator())
                            .map(AdvListData::new)
                            .peek(listData
                                    -> listData.setSelected(Objects.equals(listData.getData(), selectionTmp)))
                            .collect(Collectors.toList()));
                    // Updating the selection. ONly elements available in list can
                    // be selected!
                    setCompleteSelection(selectionTmp);
                    setInputListUpdated(false);
                }
            } else {
                // Store selection before clearing the list.
                Collection<T> selectionTmp = getCompleteSelection();
                super.getModel().clear();
                List<AdvListData<T>> list = input
                        .stream()
                        .sorted(getComparator())
                        .map(AdvListData::new)
                        .peek(listData
                                -> listData.setSelected(Objects.equals(listData.getData(), selectionTmp)))
                        .collect(Collectors.toList());
                super.getModel().addAll(list);
                // Updating the selection. ONly elements available in list can
                // be selected!
                setCompleteSelection(selectionTmp);
                setInputListUpdated(false);
            }

        }
        super.updateLogicalState(tpf); //To change body of generated methods, choose Tools | Templates.
    }

    /**
     * @return the toString
     */
    public Function<@Nullable T, String> getToString() {
        return toString;
    }

    /**
     * @param toString the toString to set
     */
    public void setToString(Function<@Nullable T, String> toString) {
        this.toString = toString;
    }

    /**
     * Setting the selection state of a list member in case it is a part of the
     * list.
     *
     * @param value
     * @param selected
     */
    public void setSelection(@Nullable T value, boolean selected) {
        getModel().stream().filter(data -> Objects.equals(data.getData(), value))
                .forEach(data -> data.setSelected(selected));
    }

    /**
     * Indicates wether a value is member of the selection.
     *
     * @param value
     * @return
     */
    public boolean isSelected(@Nullable T value) {
        return getModel().stream().filter(data -> Objects.equals(data.getData(), value))
                .map(data -> data.isSelected())
                .filter(selected -> selected)
                .findFirst()
                .orElse(false);
    }

    /**
     * Sets the value for exclusive selection. All other selections are cleared.
     *
     * @param value
     */
    public void setSelection(@Nullable T value) {
        getModel().stream().peek(data -> data.setSelected(false))
                .filter(data -> Objects.equals(data.getData(), value))
                .forEach(targetData -> targetData.setSelected(true));
    }

    /**
     * the advanced cell renderer can be accessed right here.
     *
     * @return
     */
    @Override
    @SuppressWarnings("unchecked")
    public AdvCellRenderer<T> getCellRenderer() {
        return (AdvCellRenderer<T>) super.getCellRenderer(); //To change body of generated methods, choose Tools | Templates.
    }

    /**
     * Get the first selected element of the list.
     *
     * @return
     */
    @Nullable
    @SuppressWarnings("cast.unsafe")
    public T getSelection() {
        return getModel().stream().filter(data -> data.isSelected())
                .map(data -> data.getData())
                .findFirst()
                .orElse((@NonNull T) null);
    }

    /**
     * Get all selected elements of the list.
     *
     * @return
     */
    public Collection<T> getCompleteSelection() {
        return getModel().stream().filter(data -> data.isSelected())
                .map(data -> data.getData())
                .collect(Collectors.toList());
    }

    /**
     * Set the selection of the list. Please note that only list members can be
     * selected.
     *
     * @param selection
     */
    public void setCompleteSelection(Collection<T> selection) {
        getModel().stream().forEach(data -> data.setSelected(selection.contains(data.getData())));
    }

    /**
     * The advanced cell renderers task is managing the appearance of the cells
     * of the list by loading custom cell containers and performing updates on
     * them. Normally this class is intantiated together wit the AdvListBox and
     * shall not be instantiated twice on the same list.
     * <p>
     * in order to operate this class the instance of the cell renderer can be
     * retrieved at any time using:
     * <p>
     * myAdvCellRenderer = myAdvListBox.getCellRenderer()
     * <p>
     * The default behaviour is similar to the standard listbox using a text
     * label to describe the corresponding object.
     * <p>
     * Because the cell renderers resonsibility to generate new cell containers
     * the cell rederer is also resonsible for forwarding click commands to the
     * container. This can be specified by callling <b>addClickCommands
     * method.</b>
     * <p>
     * Advanced behaviour can be archived by configuring the AdvCellRenderers
     * cellSupplier and cellUpdater method.
     *
     *
     * @author harryschwenk
     * @param <T> the data type
     */
    public static final class AdvCellRenderer<T> implements CellRenderer<AdvListData<T>> {

        private final List<Command<T>> commandList = new ArrayList<>(16);

        private Supplier<AdvCellContainer<T>> cellSupplier = DefaultListCellContainer::new;
        private BiConsumer<AdvCellContainer<T>, T> cellUpdater = (cell, val) -> {
            cell.setValue(val);
        };

        /**
         * Get the value of cellUpdater. This member is responsible for updating
         * the cell content from an outside source.
         *
         * @return the value of cellUpdater
         */
        public BiConsumer<AdvCellContainer<T>, T> getCellUpdater() {
            return cellUpdater;
        }

        /**
         * Set the value of cellUpdater.
         *
         * @param cellUpdater new value of cellUpdater
         */
        public void setCellUpdater(BiConsumer<AdvCellContainer<T>, T> cellUpdater) {
            this.cellUpdater = cellUpdater;
        }

        /**
         * Get the value of cellSupplier which stores the constructor of the
         * cell container derived from AdvCellContainer<T>. This method is
         * called by the "getView" method for accesing the container
         * constructor.
         *
         * @return the value of cellSupplier
         */
        public Supplier<AdvCellContainer<T>> getCellSupplier() {
            return cellSupplier;
        }

        /**
         *
         * @param data
         * @param selected
         * @param existing
         * @return
         */
        @Override
        @SuppressWarnings({"unchecked"})
        public Panel getView(AdvListData<T> data, boolean selected, Panel existing) {
            final AdvCellContainer<T> cellContainer;
            if (existing != null) {
                cellContainer = (AdvCellContainer<T>) existing;
            } else {
                cellContainer = getCellSupplier().get();
                cellContainer.addOnClickCommands(t -> getCommandList().forEach((Command<T> command) -> {
                    if (t != null) {
                        command.execute(t);
                    }
                    //This is default selectio reaction.
//                    cellContainer.getCheckbox().addOnClickCommand(chk -> data.setSelected(Objects.equals(t, data.getData())));
                }));
            }
            cellContainer.getCheckbox().setChecked(data.isSelected());
            getCellUpdater().accept(cellContainer, data.getData());
            return cellContainer;
        }

        /**
         * Click commands are managed by the cellRenderer directly because it
         * assigns the list data to the cell elements rendered on the listBox.
         * By browsing the list the values move through the cells. This has to
         * be considered also for the click events.
         *
         * @param commands
         */
        @SafeVarargs
        public final void addOnClickCommands(Command<T>... commands) {
            commandList.addAll(Arrays.asList(commands));
        }

        /**
         * Assign the constructor of the container derved from AdvCellContainer
         * here.
         *
         * @param cellSupplier the cellSupplier to set
         */
        public void setCellSupplier(Supplier<AdvCellContainer<T>> cellSupplier) {
            this.cellSupplier = cellSupplier;
        }

        /**
         * @return the commandList
         */
        private List<Command<T>> getCommandList() {
            return Collections.unmodifiableList(commandList);
        }
    }

    /**
     * the default list cell contaier is a default implementation of the
     * AdvCellContainer which adds a label to the empty container.
     *
     * @param <T>
     */
    public static class DefaultListCellContainer<T> extends AdvCellContainer<T> {

        private static final Logger LOG = Logger.getLogger(DefaultListCellContainer.class.getName());

        private final Label label = new Label("");
        private @Nullable
        T value = null;

        DefaultListCellContainer() {
            super.getContainer().addChild(label);
        }

        /**
         * Get the value of value
         *
         * @return the value of value
         */
        public @Nullable
        @Override
        T getValue() {
            return value;
        }

        /**
         * Set the value of value
         *
         * @param value new value of value
         */
        @Override
        public void setValue(@Nullable T value) {
            this.value = value;
        }

        /**
         * Text update is performed here.
         *
         * @param tpf
         */
        @Override
        public void updateLogicalState(float tpf) {
            label.setText("" + value);

            super.updateLogicalState(tpf); //To change body of generated methods, choose Tools | Templates.
        }

        /**
         *
         * @return
         */
        @Override
        public Spatial clone() {
            throw new AssertionError("Clone is not allowed here!");
        }
    }
}

I am sorry but it seem that I cannot post pictures (due to an error). I would have added a screenshot of the list appearance.

Regards,
harry