com.dua3.utility.fx.controls.ComboBoxEx Maven / Gradle / Ivy
The newest version!
package com.dua3.utility.fx.controls;
import com.dua3.cabe.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;
public class ComboBoxEx extends CustomControl implements InputControl {
private static final Logger LOG = LogManager.getLogger(ComboBoxEx.class);
private Comparator super T> comparator = null;
private final UnaryOperator edit;
private final Supplier add;
private final 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));
}
public ComboBoxEx(@Nullable UnaryOperator edit, @Nullable Supplier add, @Nullable BiPredicate, T> remove, Function 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, ListCell> cellFactory = new Callback<>() {
@Override
public ListCell 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() {
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.get()).ifPresent(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));
}
}
}
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);
}
public static boolean alwaysRemoveSelectedItem(ComboBoxEx cb, T item) {
return true;
}
public Optional getSelectedItem() {
return Optional.ofNullable(comboBox.getSelectionModel().getSelectedItem());
}
public List getItems() {
return List.copyOf(items);
}
public ReadOnlyObjectProperty selectedItemProperty() {
return comboBox.selectionModelProperty().get().selectedItemProperty();
}
public void setComparator(Comparator super T> comparator) {
this.comparator = comparator;
sortItems();
}
public void sortItems() {
if (comparator == null) {
return;
}
Optional selectedItem = getSelectedItem();
items.sort(comparator);
selectedItem.ifPresent(item -> comboBox.selectionModelProperty().get().select(item));
}
@Override
public Node node() {
return this;
}
@Override
public Property valueProperty() {
return comboBox.valueProperty();
}
@Override
public void reset() {
}
@Override
public ReadOnlyBooleanProperty validProperty() {
return null;
}
@Override
public ReadOnlyStringProperty errorProperty() {
return null;
}
}