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

View file

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

View file

@ -13,22 +13,29 @@ import java.nio.file.Path;
public class FileStore implements StreamDataStore, FilenameStore { public class FileStore implements StreamDataStore, FilenameStore {
public static FileStore local(Path p) { 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) { public static FileStore local(String p) {
return new FileStore(MachineStore.local(), p); return new FileStore(MachineFileStore.local(), p);
} }
MachineStore machine; MachineFileStore machine;
String file; String file;
@JsonCreator @JsonCreator
public FileStore(MachineStore machine, String file) { public FileStore(MachineFileStore machine, String file) {
this.machine = machine; this.machine = machine;
this.file = file; 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 @Override
public InputStream openInput() throws Exception { public InputStream openInput() throws Exception {
return machine.openInput(file); return machine.openInput(file);
@ -40,7 +47,7 @@ public class FileStore implements StreamDataStore, FilenameStore {
} }
@Override @Override
public boolean canOpen() { public boolean canOpen() throws Exception {
return machine.exists(file); return machine.exists(file);
} }
@ -56,6 +63,6 @@ public class FileStore implements StreamDataStore, FilenameStore {
@Override @Override
public String getFileName() { public String getFileName() {
return Path.of(file).getFileName().toString(); return file;
} }
} }

View file

@ -1,14 +1,18 @@
package io.xpipe.core.store; package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.Value;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.List;
@JsonTypeName("local") @JsonTypeName("local")
public class LocalMachineStore implements MachineStore { @Value
public class LocalStore implements ShellStore {
@Override @Override
public boolean exists(String file) { public boolean exists(String file) {
@ -31,4 +35,19 @@ public class LocalMachineStore implements MachineStore {
var p = Path.of(file); var p = Path.of(file);
return Files.newOutputStream(p); 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.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
public interface MachineStore extends DataStore { public interface MachineFileStore extends DataStore {
static MachineStore local() { static MachineFileStore local() {
return new LocalMachineStore(); return new LocalStore();
} }
InputStream openInput(String file) throws Exception; InputStream openInput(String file) throws Exception;
OutputStream openOutput(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"); throw new UnsupportedOperationException("Can't open store output");
} }
default boolean canOpen() { default boolean canOpen() throws Exception {
return true; return true;
} }

View file

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

View file

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

View file

@ -30,10 +30,12 @@ repositories {
} }
dependencies { dependencies {
compileOnly 'net.synedra:validatorfx:0.3.1'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.8.2' compileOnly 'org.junit.jupiter:junit-jupiter-api:5.8.2'
compileOnly 'com.jfoenix:jfoenix:9.0.10' compileOnly 'com.jfoenix:jfoenix:9.0.10'
implementation project(':core') 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' 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.dialog.Dialog;
import io.xpipe.core.store.DataStore; 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 io.xpipe.core.store.StreamDataStore;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.scene.layout.Region;
import java.net.URI; import java.net.URI;
import java.util.List; import java.util.List;
@ -13,19 +14,25 @@ public interface DataStoreProvider {
enum Category { enum Category {
STREAM, STREAM,
MACHINE,
DATABASE; DATABASE;
} }
default Category getCategory() { default Category getCategory() {
if (StreamDataStore.class.isAssignableFrom(getStoreClasses().get(0))) { var c = getStoreClasses().get(0);
if (StreamDataStore.class.isAssignableFrom(c)) {
return Category.STREAM; return Category.STREAM;
} }
if (MachineFileStore.class.isAssignableFrom(c) || ShellStore.class.isAssignableFrom(c)) {
return Category.MACHINE;
}
throw new ExtensionException("Provider " + getId() + " has no set category"); throw new ExtensionException("Provider " + getId() + " has no set category");
} }
default Region createConfigGui(Property<DataStore> store) { default GuiDialog guiDialog(Property<DataStore> store) {
return null; throw new ExtensionException("Gui Dialog is not implemented by provider " + getId());
} }
default void init() throws Exception { default void init() throws Exception {
@ -65,7 +72,9 @@ public interface DataStoreProvider {
return null; return null;
} }
Dialog defaultDialog(); default Dialog defaultDialog() {
throw new ExtensionException("CLI Dialog not implemented by provider");
}
default String display(DataStore store) { default String display(DataStore store) {
return store.toDisplay(); 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.charsetter.NewLine;
import io.xpipe.core.util.Secret; import io.xpipe.core.util.Secret;
import io.xpipe.extension.I18n; import io.xpipe.extension.I18n;
import io.xpipe.extension.Validator;
import io.xpipe.extension.Validators;
import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.Comp;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import net.synedra.validatorfx.Check;
import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -15,7 +18,7 @@ import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Supplier;
public class DynamicOptionsBuilder<T> { public class DynamicOptionsBuilder<T> {
@ -40,6 +43,17 @@ public class DynamicOptionsBuilder<T> {
this.title = title; 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) { public DynamicOptionsBuilder<T> addNewLine(Property<NewLine> prop) {
var map = new LinkedHashMap<NewLine, ObservableValue<String>>(); var map = new LinkedHashMap<NewLine, ObservableValue<String>>();
for (var e : NewLine.values()) { for (var e : NewLine.values()) {
@ -79,6 +93,13 @@ public class DynamicOptionsBuilder<T> {
return this; 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) { public DynamicOptionsBuilder<T> addString(ObservableValue<String> name, Property<String> prop) {
var comp = new TextFieldComp(prop); var comp = new TextFieldComp(prop);
entries.add(new DynamicOptionsComp.Entry(name, comp)); entries.add(new DynamicOptionsComp.Entry(name, comp));
@ -86,8 +107,8 @@ public class DynamicOptionsBuilder<T> {
return this; return this;
} }
public DynamicOptionsBuilder<T> addComp(Comp<?> comp) { public DynamicOptionsBuilder<T> addComp(ObservableValue<String> name, Comp<?> comp) {
entries.add(new DynamicOptionsComp.Entry(null, comp)); entries.add(new DynamicOptionsComp.Entry(name, comp));
return this; return this;
} }
@ -105,19 +126,20 @@ public class DynamicOptionsBuilder<T> {
return this; 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 -> { props.forEach(prop -> {
prop.addListener((c,o,n) -> { prop.addListener((c,o,n) -> {
toSet.setValue(creator.apply(toSet.getValue())); toSet.setValue(creator.get());
}); });
}); });
toSet.setValue(creator.get());
if (title != null) { if (title != null) {
entries.add(0, new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title"))); entries.add(0, new DynamicOptionsComp.Entry(null, Comp.of(() -> new Label(title.getValue())).styleClass("title")));
} }
return new DynamicOptionsComp(entries, wrap); 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(); 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.Comp;
import io.xpipe.fxcomps.CompStructure; import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure; import io.xpipe.fxcomps.SimpleCompStructure;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.Tab; import javafx.scene.control.Tab;
import lombok.Getter;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List; import java.util.List;
@Getter
public class TabPaneComp extends Comp<CompStructure<JFXTabPane>> { public class TabPaneComp extends Comp<CompStructure<JFXTabPane>> {
@Override @Override
@ -32,12 +36,24 @@ public class TabPaneComp extends Comp<CompStructure<JFXTabPane>> {
content.prefWidthProperty().bind(tabPane.widthProperty()); 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); return new SimpleCompStructure<>(tabPane);
} }
private final Property<Entry> selected;
private final List<Entry> entries; private final List<Entry> entries;
public TabPaneComp(List<Entry> entries) { public TabPaneComp(Property<Entry> selected, List<Entry> entries) {
this.selected = selected;
this.entries = entries; this.entries = entries;
} }

View file

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

View file

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