All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.dua3.utility.fx.controls.ComboBoxEx Maven / Gradle / Ivy

There is a newer version: 15.0.2
Show newest version
package com.dua3.utility.fx.controls;

import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import org.jspecify.annotations.Nullable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.util.Callback;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

/**
 * A custom ComboBox control that supports additional features like editing, adding, and removing items.
 *
 * @param  the type of the items contained in the ComboBox
 */
public class ComboBoxEx extends CustomControl implements InputControl {
    private static final Logger LOG = LogManager.getLogger(ComboBoxEx.class);

    private @Nullable Comparator comparator = null;
    private final @Nullable UnaryOperator edit;
    private final @Nullable Supplier add;
    private final @Nullable BiPredicate, T> remove;
    private final Function format;
    private final ObservableList items;
    private final ComboBox comboBox;

    /**
     * Constructs a ComboBoxEx with the specified edit, add, remove, format, and items.
     *
     * @param edit    the unary operator to perform editing on the selected item (nullable)
     * @param add     the supplier to provide a new item to add (nullable)
     * @param remove  the bi-predicate to determine if an item should be removed (nullable)
     * @param format  the function to format the items as strings
     * @param items   the initial items to populate the ComboBox (variadic parameter)
     */
    @SafeVarargs
    public ComboBoxEx(@Nullable UnaryOperator edit, @Nullable Supplier add, @Nullable BiPredicate, T> remove, Function format, T... items) {
        this(edit, add, remove, format, Arrays.asList(items));
    }

    /**
     * Constructs a ComboBoxEx with the specified edit, add, remove, format, and items.
     *
     * @param edit    the unary operator to perform editing on the selected item (nullable)
     * @param add     the supplier to provide a new item to add (nullable)
     * @param remove  the bi-predicate to determine if an item should be removed (nullable)
     * @param format  the function to format the items as strings
     * @param items   the initial items to populate the ComboBox (variadic parameter)
     */
    public ComboBoxEx(@Nullable UnaryOperator edit, @Nullable Supplier add, @Nullable BiPredicate, T> remove, Function<@Nullable T, String> format, Collection items) {
        super(new HBox());
        container.setAlignment(Pos.CENTER_LEFT);

        getStyleClass().setAll("comboboxex");

        this.format = format;
        this.items = FXCollections.observableArrayList(List.copyOf(items));

        this.comboBox = new ComboBox<>(this.items);
        ObservableList children = container.getChildren();
        children.setAll(comboBox);

        if (edit != null) {
            this.edit = edit;
            Button buttonEdit = Controls.button().text("✎").action(this::editItem).build();
            children.add(buttonEdit);
            buttonEdit.disableProperty().bind(comboBox.selectionModelProperty().isNull());
        } else {
            this.edit = null;
        }

        if (add != null) {
            this.add = add;
            Button buttonAdd = Controls.button().text("+").action(this::addItem).build();
            children.add(buttonAdd);
        } else {
            this.add = null;
        }

        if (remove != null) {
            this.remove = remove;
            Button buttonRemove = Controls.button().text("-").action(this::removeItem).build();
            children.add(buttonRemove);
            buttonRemove.disableProperty().bind(Bindings.createBooleanBinding(
                    () -> comboBox.getSelectionModel().getSelectedItem() != null && this.items.size() > 1,
                    comboBox.selectionModelProperty(), this.items)
            );
            buttonRemove.disableProperty().bind(comboBox.selectionModelProperty().isNull().or(comboBox.valueProperty().isNull()));
        } else {
            this.remove = null;
        }

        Callback<@Nullable ListView<@Nullable T>, ListCell<@Nullable T>> cellFactory = new Callback<>() {

            @Override
            public ListCell<@Nullable T> call(@Nullable ListView lv) {
                return new ListCell<>() {

                    @Override
                    protected void updateItem(@Nullable T item, boolean empty) {
                        super.updateItem(item, empty);

                        String text = "";
                        if (!empty) {
                            try {
                                text = format.apply(item);
                            } catch (Exception e) {
                                LOG.warn("error during formatting", e);
                                text = String.valueOf(item);
                            }
                        }
                        setText(text);
                    }
                };
            }
        };

        comboBox.setButtonCell(cellFactory.call(null));
        comboBox.setCellFactory(cellFactory);
    }

    private void editItem() {
        if (edit == null) {
            LOG.warn("editing not supported");
            return;
        }

        int idx = comboBox.getSelectionModel().getSelectedIndex();
        if (idx >= 0) {
            T item = items.get(idx);
            item = edit.apply(item);
            if (item != null) {
                items.remove(idx);
                items.add(idx, item);
                comboBox.getSelectionModel().select(idx);
                sortItems();
            }
        }
    }

    private void addItem() {
        Optional.ofNullable(add).map(Supplier::get).ifPresent((T item) -> {
            items.add(item);
            comboBox.getSelectionModel().select(item);
            sortItems();
        });
    }

    private void removeItem() {
        T item = comboBox.getSelectionModel().getSelectedItem();
        if (Optional.ofNullable(remove).orElse(ComboBoxEx::alwaysRemoveSelectedItem).test(this, item)) {
            int idx = items.indexOf(item);
            items.remove(idx);
            idx = Math.min(idx, items.size() - 1);
            if (idx >= 0) {
                set(items.get(idx));
            }
        }
    }

    /**
     * Prompts the user with a confirmation dialogue to verify whether to remove the selected item.
     *
     * 

Pass this as the {@code remove} parameter to the constructor to show a confirmation dialog * when the user wants to remove an item. * * @param item the item to be removed * @return true if the user confirms the removal, false otherwise */ public boolean askBeforeRemoveSelectedItem(T item) { return Dialogs.confirmation(Optional.ofNullable(getScene()).map(Scene::getWindow).orElse(null)) .header("Remove %s?", format.apply(item)) .defaultButton(ButtonType.YES) .build() .showAndWait() .map(bt -> bt == ButtonType.YES || bt == ButtonType.OK) .orElse(false); } /** * Remove the selected item without showing a confirmation dialog. * *

Pass this as the {@code remove} parameter to the constructor to remove items without showing * a confirmation dialog. * * @param the type of items contained in the {@code ComboBoxEx} * @param cb the {@code ComboBoxEx} to remove the item from * @param item the item to be removed * @return true if the user confirms the removal, false otherwise */ public static boolean alwaysRemoveSelectedItem(ComboBoxEx cb, T item) { return true; } /** * Retrieves the currently selected item from the ComboBoxEx. * * @return an Optional containing the selected item if one is selected, or an empty Optional if no item is selected */ public Optional getSelectedItem() { //noinspection OptionalOfNullableMisuse - false positive; from reading the JavaFX source, the selected item might well be null return Optional.ofNullable(comboBox.getSelectionModel().getSelectedItem()); } /** * Retrieves a copy of the items in the ComboBoxEx. * * @return an immutable list containing the current items in the ComboBoxEx */ public List getItems() { return List.copyOf(items); } /** * Returns the property containing the selected item in the ComboBoxEx. * * @return the ReadOnlyObjectProperty representing the selected item property */ public ReadOnlyObjectProperty selectedItemProperty() { return comboBox.selectionModelProperty().get().selectedItemProperty(); } /** * Sets the comparator for the ComboBoxEx and sorts the items accordingly. * If the comparator is null, the items will not be sorted. * * @param comparator the comparator to set, which is used for sorting the items */ public void setComparator(Comparator comparator) { this.comparator = comparator; sortItems(); } /** * Sorts the items in the ComboBoxEx using the defined comparator. * If the comparator is null, the method returns without performing any action. * The currently selected item, if any, will remain selected after sorting. */ public void sortItems() { if (comparator == null) { return; } Optional selectedItem = getSelectedItem(); items.sort(comparator); selectedItem.ifPresent((T item) -> comboBox.selectionModelProperty().get().select(item)); } @Override public Node node() { return this; } @Override public Property<@Nullable T> valueProperty() { return comboBox.valueProperty(); } @Override public void reset() { } @Override public ReadOnlyBooleanProperty validProperty() { return new SimpleBooleanProperty(true); } @Override public ReadOnlyStringProperty errorProperty() { return new SimpleStringProperty(""); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy