Rework comboboxes

This commit is contained in:
crschnick 2024-01-06 16:54:57 +00:00
parent aee7b65bce
commit 0f9cae0681
6 changed files with 41 additions and 406 deletions

View file

@ -1,17 +1,16 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.CustomComboBoxBuilder;
import io.xpipe.app.util.JfxHelper;
import javafx.beans.property.Property;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Region;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
@ -19,27 +18,23 @@ import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
public class DsStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<Node>>> {
public class DataStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataStoreProvider>>> {
Predicate<DataStoreProvider> filter;
Property<DataStoreProvider> provider;
boolean staticDisplay;
private Region createDefaultNode() {
return JfxHelper.createNamedEntry(
AppI18n.get("selectType"), AppI18n.get("selectTypeDescription"), "connection_icon.svg");
}
private List<DataStoreProvider> getProviders() {
return DataStoreProviders.getAll().stream().filter(filter).toList();
}
private Region createGraphic(DataStoreProvider provider) {
if (provider == null) {
return createDefaultNode();
return null;
}
var graphic = provider.getDisplayIconFileName(null);
@ -47,20 +42,40 @@ public class DsStoreProviderChoiceComp extends Comp<CompStructure<ComboBox<Node>
}
@Override
public CompStructure<ComboBox<Node>> createBase() {
var comboBox = new CustomComboBoxBuilder<>(provider, this::createGraphic, createDefaultNode(), v -> true);
comboBox.setAccessibleNames(dataStoreProvider -> dataStoreProvider.getDisplayName());
public CompStructure<ComboBox<DataStoreProvider>> createBase() {
Supplier<ListCell<DataStoreProvider>> cellFactory = () -> new ListCell<>() {
@Override
protected void updateItem(DataStoreProvider item, boolean empty) {
super.updateItem(item, empty);
setGraphic(createGraphic(item));
setAccessibleText(item != null ? item.getDisplayName() : null);
setAccessibleHelp(item != null ? item.getDisplayDescription() : null);
}
};
var cb = new ComboBox<DataStoreProvider>();
cb.setCellFactory(param -> {
return cellFactory.get();
});
cb.setButtonCell(cellFactory.get());
var l = getProviders().stream()
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.getCreationCategory() != null || staticDisplay).toList();
l
.forEach(comboBox::add);
if (l.size() == 1) {
provider.setValue(l.get(0));
.filter(p -> AppPrefs.get().developerShowHiddenProviders().get() || p.getCreationCategory() != null || staticDisplay)
.toList();
l.forEach(dataStoreProvider -> cb.getItems().add(dataStoreProvider));
if (provider.getValue() == null) {
provider.setValue(l.getFirst());
}
ComboBox<Node> cb = comboBox.build();
cb.getStyleClass().add("data-source-type");
cb.setValue(provider.getValue());
provider.bind(cb.valueProperty());
cb.getStyleClass().add("choice-comp");
cb.setAccessibleText("Choose connection type");
cb.setOnKeyPressed(event -> {
if (!event.getCode().equals(KeyCode.ENTER)) {
return;
}
cb.show();
event.consume();
});
return new SimpleCompStructure<>(cb);
}
}

View file

@ -276,7 +276,7 @@ public class GuiDsStoreCreator extends MultiStepComp.Step<CompStructure<?>> {
var layout = new BorderPane();
layout.getStyleClass().add("store-creator");
layout.setPadding(new Insets(20));
var providerChoice = new DsStoreProviderChoiceComp(filter, provider, staticDisplay);
var providerChoice = new DataStoreProviderChoiceComp(filter, provider, staticDisplay);
if (staticDisplay) {
providerChoice.apply(struc -> struc.get().setDisable(true));
}

View file

@ -1,46 +0,0 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.util.CustomComboBoxBuilder;
import io.xpipe.core.charsetter.StreamCharset;
import javafx.beans.property.Property;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
public class CharsetChoiceComp extends SimpleComp {
private final Property<StreamCharset> charset;
public CharsetChoiceComp(Property<StreamCharset> charset) {
this.charset = charset;
}
@Override
protected Region createSimple() {
var builder = new CustomComboBoxBuilder<>(
charset,
streamCharset -> {
return new Label(streamCharset.getCharset().displayName()
+ (streamCharset.hasByteOrderMark() ? " (BOM)" : ""));
},
new Label(AppI18n.get("app.none")),
null);
builder.setAccessibleNames(streamCharset -> streamCharset.getNames().get(0));
builder.addFilter((charset, filter) -> {
return charset.getCharset().displayName().contains(filter);
});
builder.addHeader(AppI18n.get("app.common"));
for (var e : StreamCharset.COMMON) {
builder.add(e);
}
builder.addHeader(AppI18n.get("app.other"));
for (var e : StreamCharset.RARE) {
builder.add(e);
}
var comboBox = builder.build();
comboBox.setVisibleRowCount(16);
return comboBox;
}
}

View file

@ -8,7 +8,6 @@ import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.HBox;
@ -18,14 +17,12 @@ import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
public class FileReferenceChoiceComp extends SimpleComp {
public class ContextualFileReferenceChoiceComp extends SimpleComp {
private final boolean hideFileSystem;
private final Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem;
private final Property<String> filePath;
public <T extends FileSystemStore> FileReferenceChoiceComp(ObservableValue<DataStoreEntryRef<T>> fileSystem, Property<String> filePath) {
this.hideFileSystem = true;
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(ObservableValue<DataStoreEntryRef<T>> fileSystem, Property<String> filePath) {
this.fileSystem = new SimpleObjectProperty<>();
SimpleChangeListener.apply(fileSystem, val -> {
this.fileSystem.setValue(val);
@ -33,27 +30,15 @@ public class FileReferenceChoiceComp extends SimpleComp {
this.filePath = filePath;
}
public FileReferenceChoiceComp(boolean hideFileSystem, Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem, Property<String> filePath) {
this.hideFileSystem = hideFileSystem;
this.fileSystem = fileSystem != null ? fileSystem : new SimpleObjectProperty<>();
this.filePath = filePath;
}
@Override
protected Region createSimple() {
var fileSystemChoiceComp =
new FileSystemStoreChoiceComp(fileSystem).grow(false, true).styleClass(Styles.LEFT_PILL);
if (hideFileSystem) {
fileSystemChoiceComp.hide(new SimpleBooleanProperty(true));
}
var fileNameComp = new TextFieldComp(filePath)
.apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS))
.styleClass(hideFileSystem ? Styles.LEFT_PILL : Styles.CENTER_PILL)
.styleClass(Styles.LEFT_PILL)
.grow(false, true);
var fileBrowseButton = new ButtonComp(null, new FontIcon("mdi2f-folder-open-outline"), () -> {
StandaloneFileBrowser.openSingleFile(() -> hideFileSystem ? fileSystem.getValue() : null, fileStore -> {
StandaloneFileBrowser.openSingleFile(() -> fileSystem.getValue(), fileStore -> {
if (fileStore == null) {
filePath.setValue(null);
fileSystem.setValue(null);
@ -66,7 +51,7 @@ public class FileReferenceChoiceComp extends SimpleComp {
.styleClass(Styles.RIGHT_PILL)
.grow(false, true);
var layout = new HorizontalComp(List.of(fileSystemChoiceComp, fileNameComp, fileBrowseButton))
var layout = new HorizontalComp(List.of(fileNameComp, fileBrowseButton))
.apply(struc -> struc.get().setFillHeight(true));
layout.apply(struc -> {

View file

@ -1,53 +0,0 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.CustomComboBoxBuilder;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.Property;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
public class FileSystemStoreChoiceComp extends SimpleComp {
private final Property<DataStoreEntryRef<? extends FileSystemStore>> selected;
public FileSystemStoreChoiceComp(Property<DataStoreEntryRef<? extends FileSystemStore>> selected) {
this.selected = selected;
}
private static String getName(DataStoreEntryRef<? extends FileSystemStore> store) {
return store.get().getName();
}
private Region createGraphic(DataStoreEntryRef<? extends FileSystemStore> s) {
var provider = s.get().getProvider();
var img = PrettyImageHelper.ofFixedSquare(provider.getDisplayIconFileName(s.getStore()), 16);
return new Label(getName(s), img.createRegion());
}
private Region createDisplayGraphic(DataStoreEntryRef<? extends FileSystemStore> s) {
var provider = s.get().getProvider();
var img = PrettyImageHelper.ofFixedSquare(provider.getDisplayIconFileName(s.getStore()), 16);
return new Label(null, img.createRegion());
}
@Override
protected Region createSimple() {
var comboBox = new CustomComboBoxBuilder<>(selected, this::createGraphic, null, v -> true);
comboBox.setAccessibleNames(FileSystemStoreChoiceComp::getName);
comboBox.setSelectedDisplay(this::createDisplayGraphic);
DataStorage.get().getUsableEntries().stream()
.filter(e -> e.getStore() instanceof FileSystemStore)
.map(DataStoreEntry::<FileSystemStore>ref)
.forEach(comboBox::add);
ComboBox<Node> cb = comboBox.build();
cb.getStyleClass().add("choice-comp");
cb.setMaxWidth(45);
return cb;
}
}

View file

@ -1,266 +0,0 @@
package io.xpipe.app.util;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.Separator;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
public class CustomComboBoxBuilder<T> {
private final Property<T> selected;
private final Function<T, Node> nodeFunction;
private ObservableValue<String> emptyAccessibilityName = AppI18n.observable("none");
private Function<T, String> accessibleNameFunction;
private Function<T, Node> selectedDisplayNodeFunction;
private final Map<Node, T> nodeMap = new HashMap<>();
private final Map<Node, Runnable> actionsMap = new HashMap<>();
private final List<Node> nodes = new ArrayList<>();
private final Set<Node> disabledNodes = new HashSet<>();
private final Node emptyNode;
private final Predicate<T> veto;
private final Property<String> filterString = new SimpleStringProperty();
private final List<T> filterable = new ArrayList<>();
private BiPredicate<T, String> filterPredicate;
private Node filterNode;
private Function<T, Node> unknownNode;
public CustomComboBoxBuilder(
Property<T> selected, Function<T, Node> nodeFunction, Node emptyNode, Predicate<T> veto) {
this.selected = selected;
this.nodeFunction = nodeFunction;
this.selectedDisplayNodeFunction = nodeFunction;
this.emptyNode = emptyNode;
this.veto = veto;
}
public void setSelectedDisplay(Function<T, Node> nodeFunction) {
selectedDisplayNodeFunction = nodeFunction;
}
public void setAccessibleNames(Function<T, String> function) {
accessibleNameFunction = function;
}
public void setEmptyAccessibilityName(ObservableValue<String> n) {
emptyAccessibilityName = n;
}
public void addAction(Node node, Runnable run) {
nodes.add(node);
actionsMap.put(node, run);
}
public void disable(Node node) {
disabledNodes.add(node);
}
public void setUnknownNode(Function<T, Node> node) {
unknownNode = node;
}
public Node add(T val) {
var node = nodeFunction.apply(val);
nodeMap.put(node, val);
nodes.add(node);
if (filterPredicate != null) {
filterable.add(val);
}
return node;
}
public void addSeparator() {
var sep = new Separator(Orientation.HORIZONTAL);
nodes.add(sep);
disabledNodes.add(sep);
}
public void addHeader(String name) {
var spacer = new Region();
spacer.setPrefHeight(10);
var header = new Label(name);
header.setAlignment(Pos.CENTER);
var v = new VBox(spacer, header, new Separator(Orientation.HORIZONTAL));
v.setAccessibleText(name);
v.setAlignment(Pos.CENTER);
nodes.add(v);
disabledNodes.add(v);
}
public void addFilter(BiPredicate<T, String> filterPredicate) {
this.filterPredicate = filterPredicate;
var spacer = new Region();
spacer.setPrefHeight(10);
var header = new FilterComp(filterString).createStructure();
var v = new VBox(header.get());
v.setAlignment(Pos.CENTER);
nodes.add(v);
filterNode = header.getText();
}
public ComboBox<Node> build() {
var cb = new ComboBox<Node>();
cb.getItems().addAll(nodes);
cb.setCellFactory((lv) -> {
return new Cell();
});
cb.setButtonCell(new SelectedCell());
SimpleChangeListener.apply(selected, c -> {
var item = nodeMap.entrySet().stream()
.filter(e -> Objects.equals(c, e.getValue()))
.map(e -> e.getKey())
.findAny()
.orElse(c == null || unknownNode == null ? emptyNode : unknownNode.apply(c));
cb.setValue(item);
});
cb.valueProperty().addListener((c, o, n) -> {
if (nodeMap.containsKey(n)) {
if (veto != null && !veto.test(nodeMap.get(n))) {
return;
}
selected.setValue(nodeMap.get(n));
}
if (actionsMap.containsKey(n)) {
cb.setValue(o);
actionsMap.get(n).run();
}
});
if (filterPredicate != null) {
SimpleChangeListener.apply(filterString, c -> {
var filteredNodes = nodes.stream()
.filter(e -> e.equals(cb.getValue())
|| !(nodeMap.get(e) != null
&& (filterable.contains(nodeMap.get(e))
&& filterString.getValue() != null
&& !filterPredicate.test(nodeMap.get(e), c))))
.toList();
cb.setItems(FXCollections.observableList(filteredNodes));
});
filterNode.sceneProperty().addListener((c, o, n) -> {
if (n != null) {
n.getWindow().focusedProperty().addListener((c2, o2, n2) -> {
Platform.runLater(() -> {
filterNode.requestFocus();
});
});
}
Platform.runLater(() -> {
filterNode.requestFocus();
});
});
}
if (emptyNode != null) {
emptyNode.setAccessibleText(emptyAccessibilityName.getValue());
}
if (accessibleNameFunction != null) {
nodes.forEach(node -> node.setAccessibleText(accessibleNameFunction.apply(nodeMap.get(node))));
}
return cb;
}
private class SelectedCell extends ListCell<Node> {
@Override
protected void updateItem(Node item, boolean empty) {
super.updateItem(item, empty);
accessibleTextProperty().unbind();
if (empty || item.equals(emptyNode)) {
if (emptyAccessibilityName != null) {
accessibleTextProperty().bind(emptyAccessibilityName);
} else {
setAccessibleText(null);
}
}
if (empty) {
return;
}
if (item.equals(emptyNode)) {
setGraphic(item);
return;
}
// Case for dynamically created unknown nodes
if (!nodeMap.containsKey(item)) {
setGraphic(item);
// Don't expect the accessible name function to properly map this item
setAccessibleText(null);
return;
}
var val = nodeMap.get(item);
var newNode = selectedDisplayNodeFunction.apply(val);
setGraphic(newNode);
setAccessibleText(newNode.getAccessibleText());
}
}
private class Cell extends ListCell<Node> {
public Cell() {
addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (!nodeMap.containsKey(getItem())) {
event.consume();
}
});
addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ENTER && !nodeMap.containsKey(getItem())) {
event.consume();
}
});
}
@Override
protected void updateItem(Node item, boolean empty) {
setGraphic(item);
if (getItem() == item) {
return;
}
super.updateItem(item, empty);
if (item == null) {
return;
}
setGraphic(item);
if (disabledNodes.contains(item)) {
this.setDisable(true);
this.setFocusTraversable(false);
// this.setPadding(Insets.EMPTY);
} else {
this.setDisable(false);
this.setFocusTraversable(true);
setAccessibleText(item.getAccessibleText());
}
}
}
}