Add validators, rework machine stores

This commit is contained in:
Christopher Schnick 2022-07-03 20:41:42 +02:00
parent f0f1417980
commit 4eb0c80d60
23 changed files with 608 additions and 33 deletions

View file

@ -114,9 +114,9 @@ public class BeaconServer {
// Prepare for invalid XPIPE_HOME path value
try {
if (System.getProperty("os.name").startsWith("Windows")) {
file = Path.of(env, "app", "xpipe.exe");
file = Path.of(env, "app", "xpiped.exe");
} else {
file = Path.of(env, "app", "bin", "xpipe");
file = Path.of(env, "app", "bin", "xpiped");
}
return Files.exists(file) ? Optional.of(file) : Optional.empty();
} catch (Exception ex) {
@ -132,9 +132,9 @@ public class BeaconServer {
Path file;
if (System.getProperty("os.name").startsWith("Windows")) {
file = Path.of(System.getenv("LOCALAPPDATA"), "X-Pipe", "app", "xpipe.exe");
file = Path.of(System.getenv("LOCALAPPDATA"), "X-Pipe", "app", "xpiped.exe");
} else {
file = Path.of("/opt/xpipe/app/bin/xpipe");
file = Path.of("/opt/xpipe/app/bin/xpiped");
}
if (Files.exists(file)) {

View file

@ -16,6 +16,10 @@ import java.util.Optional;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public interface DataStore {
default boolean isComplete() {
return true;
}
default void validate() throws Exception {
}

View file

@ -13,22 +13,29 @@ import java.nio.file.Path;
public class FileStore implements StreamDataStore, FilenameStore {
public static FileStore local(Path p) {
return new FileStore(MachineStore.local(), p.toString());
return new FileStore(MachineFileStore.local(), p.toString());
}
public static FileStore local(String p) {
return new FileStore(MachineStore.local(), p);
return new FileStore(MachineFileStore.local(), p);
}
MachineStore machine;
MachineFileStore machine;
String file;
@JsonCreator
public FileStore(MachineStore machine, String file) {
public FileStore(MachineFileStore machine, String file) {
this.machine = machine;
this.file = file;
}
@Override
public void validate() throws Exception {
if (!machine.exists(file)) {
throw new IllegalStateException("File " + file + " could not be found on machine " + machine.toDisplay());
}
}
@Override
public InputStream openInput() throws Exception {
return machine.openInput(file);
@ -40,7 +47,7 @@ public class FileStore implements StreamDataStore, FilenameStore {
}
@Override
public boolean canOpen() {
public boolean canOpen() throws Exception {
return machine.exists(file);
}
@ -56,6 +63,6 @@ public class FileStore implements StreamDataStore, FilenameStore {
@Override
public String getFileName() {
return Path.of(file).getFileName().toString();
return file;
}
}

View file

@ -1,14 +1,18 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Value;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@JsonTypeName("local")
public class LocalMachineStore implements MachineStore {
@Value
public class LocalStore implements ShellStore {
@Override
public boolean exists(String file) {
@ -31,4 +35,19 @@ public class LocalMachineStore implements MachineStore {
var p = Path.of(file);
return Files.newOutputStream(p);
}
@Override
public String executeAndRead(List<String> cmd) throws Exception {
var p = prepare(cmd).redirectErrorStream(true);
var proc = p.start();
var b = proc.getInputStream().readAllBytes();
proc.waitFor();
//TODO
return new String(b, StandardCharsets.UTF_16LE);
}
@Override
public List<String> createCommand(List<String> cmd) {
return cmd;
}
}

View file

@ -3,15 +3,15 @@ package io.xpipe.core.store;
import java.io.InputStream;
import java.io.OutputStream;
public interface MachineStore extends DataStore {
public interface MachineFileStore extends DataStore {
static MachineStore local() {
return new LocalMachineStore();
static MachineFileStore local() {
return new LocalStore();
}
InputStream openInput(String file) throws Exception;
OutputStream openOutput(String file) throws Exception;
public boolean exists(String file);
public boolean exists(String file) throws Exception;
}

View file

@ -0,0 +1,19 @@
package io.xpipe.core.store;
import java.util.List;
public interface ShellStore extends MachineFileStore {
static ShellStore local() {
return new LocalStore();
}
default ProcessBuilder prepare(List<String> cmd) throws Exception {
var toExec = createCommand(cmd);
return new ProcessBuilder(toExec);
}
String executeAndRead(List<String> cmd) throws Exception;
List<String> createCommand(List<String> cmd);
}

View file

@ -0,0 +1,61 @@
package io.xpipe.core.store;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.List;
public interface StandardShellStore extends ShellStore {
static interface ShellType {
List<String> createFileReadCommand(String file);
List<String> createFileWriteCommand(String file);
List<String> createFileExistsCommand(String file);
Charset getCharset();
String getName();
}
default String executeAndRead(List<String> cmd) throws Exception {
var type = determineType();
var p = prepare(cmd).redirectErrorStream(true);
var proc = p.start();
var s = new String(proc.getInputStream().readAllBytes(), type.getCharset());
return s;
}
List<String> createCommand(List<String> cmd);
ShellType determineType();
@Override
default InputStream openInput(String file) throws Exception {
var type = determineType();
var cmd = type.createFileReadCommand(file);
var p = prepare(cmd).redirectErrorStream(true);
var proc = p.start();
return proc.getInputStream();
}
@Override
default OutputStream openOutput(String file) throws Exception {
var type = determineType();
var cmd = type.createFileWriteCommand(file);
var p = prepare(cmd).redirectErrorStream(true);
var proc = p.start();
return proc.getOutputStream();
}
@Override
default boolean exists(String file) throws Exception {
var type = determineType();
var cmd = type.createFileExistsCommand(file);
var p = prepare(cmd).redirectErrorStream(true);
var proc = p.start();
return proc.waitFor() == 0;
}
}

View file

@ -37,7 +37,7 @@ public interface StreamDataStore extends DataStore {
throw new UnsupportedOperationException("Can't open store output");
}
default boolean canOpen() {
default boolean canOpen() throws Exception {
return true;
}

View file

@ -1,7 +1,7 @@
package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.AllArgsConstructor;
import lombok.Value;
import java.io.ByteArrayInputStream;
@ -10,11 +10,15 @@ import java.nio.charset.StandardCharsets;
@Value
@JsonTypeName("string")
@AllArgsConstructor
public class StringStore implements StreamDataStore {
byte[] value;
@JsonCreator
public StringStore(byte[] value) {
this.value = value;
}
public StringStore(String s) {
value = s.getBytes(StandardCharsets.UTF_8);
}

View file

@ -38,7 +38,7 @@ public class CoreJacksonModule extends SimpleModule {
new NamedType(LocalDirectoryDataStore.class),
new NamedType(CollectionEntryDataStore.class),
new NamedType(StringStore.class),
new NamedType(LocalMachineStore.class),
new NamedType(LocalStore.class),
new NamedType(NamedStore.class),
new NamedType(ValueType.class),

View file

@ -30,10 +30,12 @@ repositories {
}
dependencies {
compileOnly 'net.synedra:validatorfx:0.3.1'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.8.2'
compileOnly 'com.jfoenix:jfoenix:9.0.10'
implementation project(':core')
implementation 'io.xpipe:fxcomps:0.2'
implementation project(':fxcomps')
//implementation 'io.xpipe:fxcomps:0.2'
implementation 'org.controlsfx:controlsfx:11.1.1'
}

View file

@ -0,0 +1,103 @@
package io.xpipe.extension;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import net.synedra.validatorfx.Check;
import net.synedra.validatorfx.ValidationMessage;
import net.synedra.validatorfx.ValidationResult;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class ChainedValidator implements io.xpipe.extension.Validator {
private final List<Validator> validators;
private final ReadOnlyObjectWrapper<ValidationResult> validationResultProperty = new ReadOnlyObjectWrapper<>(new ValidationResult());
private final ReadOnlyBooleanWrapper containsErrorsProperty = new ReadOnlyBooleanWrapper();
public ChainedValidator(List<Validator> validators) {
this.validators = validators;
validators.forEach(v -> {
v.containsErrorsProperty().addListener((c,o,n) -> {
containsErrorsProperty.set(containsErrors());
});
v.validationResultProperty().addListener((c,o,n) -> {
validationResultProperty.set(getValidationResult());
});
});
}
@Override
public Check createCheck() {
throw new UnsupportedOperationException();
}
@Override
public void add(Check check) {
throw new UnsupportedOperationException();
}
@Override
public void remove(Check check) {
throw new UnsupportedOperationException();
}
@Override
public ValidationResult getValidationResult() {
var list = new ArrayList<ValidationMessage>();
for (var val : validators) {
list.addAll(val.getValidationResult().getMessages());
}
var r = new ValidationResult();
r.addAll(list);
return r;
}
@Override
public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {
return validationResultProperty;
}
@Override
public ReadOnlyBooleanProperty containsErrorsProperty() {
return containsErrorsProperty;
}
@Override
public boolean containsErrors() {
return validators.stream().anyMatch(Validator::containsErrors);
}
@Override
public boolean validate() {
for (var val : validators) {
if (!val.validate()) {
return false;
}
}
return true;
}
@Override
public StringBinding createStringBinding() {
return createStringBinding("- ", "\n");
}
@Override
public StringBinding createStringBinding(String prefix, String separator) {
var list = new ArrayList<Observable>(validators.stream().map(Validator::createStringBinding).toList());
Observable[] observables = list.toArray(Observable[]::new);
return Bindings.createStringBinding(() -> {
return validators.stream().map(v -> v.createStringBinding(prefix, separator).get()).collect(Collectors.joining("\n"));
}, observables);
}
}

View file

@ -2,9 +2,10 @@ package io.xpipe.extension;
import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.MachineFileStore;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.store.StreamDataStore;
import javafx.beans.property.Property;
import javafx.scene.layout.Region;
import java.net.URI;
import java.util.List;
@ -13,19 +14,25 @@ public interface DataStoreProvider {
enum Category {
STREAM,
MACHINE,
DATABASE;
}
default Category getCategory() {
if (StreamDataStore.class.isAssignableFrom(getStoreClasses().get(0))) {
var c = getStoreClasses().get(0);
if (StreamDataStore.class.isAssignableFrom(c)) {
return Category.STREAM;
}
if (MachineFileStore.class.isAssignableFrom(c) || ShellStore.class.isAssignableFrom(c)) {
return Category.MACHINE;
}
throw new ExtensionException("Provider " + getId() + " has no set category");
}
default Region createConfigGui(Property<DataStore> store) {
return null;
default GuiDialog guiDialog(Property<DataStore> store) {
throw new ExtensionException("Gui Dialog is not implemented by provider " + getId());
}
default void init() throws Exception {
@ -65,7 +72,9 @@ public interface DataStoreProvider {
return null;
}
Dialog defaultDialog();
default Dialog defaultDialog() {
throw new ExtensionException("CLI Dialog not implemented by provider");
}
default String display(DataStore store) {
return store.toDisplay();

View file

@ -0,0 +1,83 @@
package io.xpipe.extension;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.value.ObservableValue;
import net.synedra.validatorfx.Check;
import net.synedra.validatorfx.ValidationResult;
import java.util.ArrayList;
import java.util.Map;
public final class ExclusiveValidator<T> implements io.xpipe.extension.Validator {
private final Map<T, ? extends Validator> validators;
private final ObservableValue<T> obs;
public ExclusiveValidator(Map<T, ? extends Validator> validators, ObservableValue<T> obs) {
this.validators = validators;
this.obs = obs;
}
private Validator get() {
return validators.get(obs.getValue());
}
@Override
public Check createCheck() {
throw new UnsupportedOperationException();
}
@Override
public void add(Check check) {
throw new UnsupportedOperationException();
}
@Override
public void remove(Check check) {
throw new UnsupportedOperationException();
}
@Override
public ValidationResult getValidationResult() {
return get().getValidationResult();
}
@Override
public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {
return get().validationResultProperty();
}
@Override
public ReadOnlyBooleanProperty containsErrorsProperty() {
return get().containsErrorsProperty();
}
@Override
public boolean containsErrors() {
return get().containsErrors();
}
@Override
public boolean validate() {
return get().validate();
}
@Override
public StringBinding createStringBinding() {
return createStringBinding("- ", "\n");
}
@Override
public StringBinding createStringBinding(String prefix, String separator) {
var list = new ArrayList<Observable>(validators.values().stream().map(Validator::createStringBinding).toList());
list.add(obs);
Observable[] observables = list.toArray(Observable[]::new);
return Bindings.createStringBinding(() -> {
return get().createStringBinding(prefix, separator).get();
}, observables);
}
}

View file

@ -0,0 +1,18 @@
package io.xpipe.extension;
import io.xpipe.fxcomps.Comp;
import lombok.AllArgsConstructor;
import lombok.Value;
@Value
@AllArgsConstructor
public class GuiDialog {
Comp<?> comp;
Validator validator;
public GuiDialog(Comp<?> comp) {
this.comp = comp;
this.validator = new SimpleValidator();
}
}

View file

@ -0,0 +1,17 @@
package io.xpipe.extension;
import javafx.beans.property.Property;
import java.util.Map;
public class Properties {
public static <T, V> void bindExclusive(Property<V> selected, Map<V, ? extends Property<T>> map, Property<T> toBind) {
selected.addListener((c,o,n) -> {
toBind.unbind();
toBind.bind(map.get(n));
});
toBind.bind(map.get(selected.getValue()));
}
}

View file

@ -0,0 +1,118 @@
package io.xpipe.extension;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.value.ChangeListener;
import net.synedra.validatorfx.Check;
import net.synedra.validatorfx.Severity;
import net.synedra.validatorfx.ValidationMessage;
import net.synedra.validatorfx.ValidationResult;
import java.util.*;
public class SimpleValidator implements Validator {
private final Map<Check, ChangeListener<ValidationResult>> checks = new LinkedHashMap<>();
private final ReadOnlyObjectWrapper<ValidationResult> validationResultProperty = new ReadOnlyObjectWrapper<>(new ValidationResult());
private final ReadOnlyBooleanWrapper containsErrorsProperty = new ReadOnlyBooleanWrapper();
/** Create a check that lives within this checker's domain.
* @return A check object whose dependsOn, decorates, etc. methods can be called
*/
public Check createCheck() {
Check check = new Check();
add(check);
return check;
}
/** Add another check to the checker. Changes in the check's validationResultProperty will be reflected in the checker.
* @param check The check to add.
*/
public void add(Check check) {
ChangeListener<ValidationResult> listener = (obs, oldv, newv) -> refreshProperties();
checks.put(check, listener);
check.validationResultProperty().addListener(listener);
}
/** Removes a check from this validator.
* @param check The check to remove from this validator.
*/
public void remove(Check check) {
ChangeListener<ValidationResult> listener = checks.remove(check);
if (listener != null) {
check.validationResultProperty().removeListener(listener);
}
refreshProperties();
}
/** Retrieves current validation result
* @return validation result
*/
public ValidationResult getValidationResult() {
return validationResultProperty.get();
}
/** Can be used to track validation result changes
* @return The Validation result property.
*/
public ReadOnlyObjectProperty<ValidationResult> validationResultProperty() {
return validationResultProperty.getReadOnlyProperty();
}
/** A read-only boolean property indicating whether any of the checks of this validator emitted an error. */
public ReadOnlyBooleanProperty containsErrorsProperty() {
return containsErrorsProperty.getReadOnlyProperty();
}
public boolean containsErrors() {
return containsErrorsProperty().get();
}
/** Run all checks (decorating nodes if appropriate)
* @return true if no errors were found, false otherwise
*/
public boolean validate() {
for (Check check : checks.keySet()) {
check.recheck();
}
return ! containsErrors();
}
private void refreshProperties() {
ValidationResult nextResult = new ValidationResult();
for (Check check : checks.keySet()) {
nextResult.addAll(check.getValidationResult().getMessages());
}
validationResultProperty.set(nextResult);
boolean hasErrors = false;
for (ValidationMessage msg : nextResult.getMessages()) {
hasErrors = hasErrors || msg.getSeverity() == Severity.ERROR;
}
containsErrorsProperty.set(hasErrors);
}
/** Create a string property that depends on the validation result.
* Each error message will be displayed on a separate line prefixed with a bullet.
*/
public StringBinding createStringBinding() {
return createStringBinding("- ", "\n");
}
@Override
public StringBinding createStringBinding(String prefix, String separator) {
return Bindings.createStringBinding( () -> {
StringBuilder str = new StringBuilder();
for (ValidationMessage msg : validationResultProperty.get().getMessages()) {
if (str.length() > 0) {
str.append(separator);
}
str.append(prefix).append(msg.getText());
}
return str.toString();
}, validationResultProperty);
}
}

View file

@ -0,0 +1,56 @@
package io.xpipe.extension;
import javafx.beans.binding.StringBinding;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import net.synedra.validatorfx.Check;
import net.synedra.validatorfx.ValidationResult;
public interface Validator {
public Check createCheck();
/** Add another check to the checker. Changes in the check's validationResultProperty will be reflected in the checker.
* @param check The check to add.
*/
public void add(Check check);
/** Removes a check from this validator.
* @param check The check to remove from this validator.
*/
public void remove(Check check);
/** Retrieves current validation result
* @return validation result
*/
public ValidationResult getValidationResult();
/** Can be used to track validation result changes
* @return The Validation result property.
*/
public ReadOnlyObjectProperty<ValidationResult> validationResultProperty();
/** A read-only boolean property indicating whether any of the checks of this validator emitted an error. */
public ReadOnlyBooleanProperty containsErrorsProperty();
public boolean containsErrors();
/** Run all checks (decorating nodes if appropriate)
* @return true if no errors were found, false otherwise
*/
public boolean validate();
/** Create a string property that depends on the validation result.
* Each error message will be displayed on a separate line prefixed with a bullet.
* @return
*/
public StringBinding createStringBinding();
/** Create a string property that depends on the validation result.
* @param prefix The string to prefix each validation message with
* @param separator The string to separate consecutive validation messages with
* @param severities The severities to consider; If none is given, only Severity.ERROR will be considered
* @return
*/
public StringBinding createStringBinding(String prefix, String separator);
}

View file

@ -0,0 +1,15 @@
package io.xpipe.extension;
import javafx.beans.value.ObservableValue;
import net.synedra.validatorfx.Check;
public class Validators {
public static Check nonNull(Validator v, ObservableValue<String> name, ObservableValue<?> s) {
return v.createCheck().dependsOn("val", s).withMethod(c -> {
if (c.get("val") == null ) {
c.error(I18n.get("extension.mustNotBeEmpty", name.getValue()));
}
});
}
}

View file

@ -3,11 +3,14 @@ package io.xpipe.extension.comp;
import io.xpipe.core.charsetter.NewLine;
import io.xpipe.core.util.Secret;
import io.xpipe.extension.I18n;
import io.xpipe.extension.Validator;
import io.xpipe.extension.Validators;
import io.xpipe.fxcomps.Comp;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
import net.synedra.validatorfx.Check;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
import java.nio.charset.Charset;
@ -15,7 +18,7 @@ import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
public class DynamicOptionsBuilder<T> {
@ -40,6 +43,17 @@ public class DynamicOptionsBuilder<T> {
this.title = title;
}
public DynamicOptionsBuilder<T> decorate(Check c) {
entries.get(entries.size() - 1).comp().apply(s -> c.decorates(s.get()));
return this;
}
public DynamicOptionsBuilder<T> nonNull(Validator v) {
var e = entries.get(entries.size() - 1);
var p = props.get(props.size() - 1);
return decorate(Validators.nonNull(v, e.name(), p));
}
public DynamicOptionsBuilder<T> addNewLine(Property<NewLine> prop) {
var map = new LinkedHashMap<NewLine, ObservableValue<String>>();
for (var e : NewLine.values()) {
@ -79,6 +93,13 @@ public class DynamicOptionsBuilder<T> {
return this;
}
public DynamicOptionsBuilder<T> addString(String nameKey, Property<String> prop) {
var comp = new TextFieldComp(prop);
entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp));
props.add(prop);
return this;
}
public DynamicOptionsBuilder<T> addString(ObservableValue<String> name, Property<String> prop) {
var comp = new TextFieldComp(prop);
entries.add(new DynamicOptionsComp.Entry(name, comp));
@ -86,8 +107,8 @@ public class DynamicOptionsBuilder<T> {
return this;
}
public DynamicOptionsBuilder<T> addComp(Comp<?> comp) {
entries.add(new DynamicOptionsComp.Entry(null, comp));
public DynamicOptionsBuilder<T> addComp(ObservableValue<String> name, Comp<?> comp) {
entries.add(new DynamicOptionsComp.Entry(name, comp));
return this;
}
@ -105,19 +126,20 @@ public class DynamicOptionsBuilder<T> {
return this;
}
public <V extends T> Comp<?> buildComp(Function<T, V> creator, Property<T> toSet) {
public <V extends T> Comp<?> buildComp(Supplier<V> creator, Property<T> toSet) {
props.forEach(prop -> {
prop.addListener((c,o,n) -> {
toSet.setValue(creator.apply(toSet.getValue()));
toSet.setValue(creator.get());
});
});
toSet.setValue(creator.get());
if (title != null) {
entries.add(0, new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title")));
}
return new DynamicOptionsComp(entries, wrap);
}
public <V extends T> Region build(Function<T, V> creator, Property<T> toSet) {
public <V extends T> Region build(Supplier<V> creator, Property<T> toSet) {
return buildComp(creator, toSet).createRegion();
}
}

View file

@ -4,14 +4,18 @@ import com.jfoenix.controls.JFXTabPane;
import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
@Getter
public class TabPaneComp extends Comp<CompStructure<JFXTabPane>> {
@Override
@ -32,12 +36,24 @@ public class TabPaneComp extends Comp<CompStructure<JFXTabPane>> {
content.prefWidthProperty().bind(tabPane.widthProperty());
}
tabPane.getSelectionModel().select(entries.indexOf(selected.getValue()));
tabPane.getSelectionModel().selectedIndexProperty().addListener((c,o,n) -> {
selected.setValue(entries.get(n.intValue()));
});
selected.addListener((c,o,n) -> {
PlatformThread.runLaterIfNeeded(() -> {
tabPane.getSelectionModel().select(entries.indexOf(n));
});
});
return new SimpleCompStructure<>(tabPane);
}
private final Property<Entry> selected;
private final List<Entry> entries;
public TabPaneComp(List<Entry> entries) {
public TabPaneComp(Property<Entry> selected, List<Entry> entries) {
this.selected = selected;
this.entries = entries;
}

View file

@ -21,6 +21,7 @@ module io.xpipe.extension {
requires static org.controlsfx.controls;
requires java.desktop;
requires org.fxmisc.richtext;
requires static net.synedra.validatorfx;
requires org.fxmisc.flowless;
requires org.fxmisc.undofx;
requires org.fxmisc.wellbehavedfx;

View file

@ -3,4 +3,5 @@ newLine=Newline
crlf=CRLF (Windows)
lf=LF (Linux)
none=None
nullPointer=Null Pointer: $MSG$
nullPointer=Null Pointer: $MSG$
mustNotBeEmpty=$NAME$ must not be empty