diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java index 717416bc..dbc902c3 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconServer.java @@ -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)) { diff --git a/core/src/main/java/io/xpipe/core/store/DataStore.java b/core/src/main/java/io/xpipe/core/store/DataStore.java index 73fec67c..9d73a409 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStore.java +++ b/core/src/main/java/io/xpipe/core/store/DataStore.java @@ -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 { } diff --git a/core/src/main/java/io/xpipe/core/store/FileStore.java b/core/src/main/java/io/xpipe/core/store/FileStore.java index 48792cf2..0d710d14 100644 --- a/core/src/main/java/io/xpipe/core/store/FileStore.java +++ b/core/src/main/java/io/xpipe/core/store/FileStore.java @@ -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; } } diff --git a/core/src/main/java/io/xpipe/core/store/LocalMachineStore.java b/core/src/main/java/io/xpipe/core/store/LocalStore.java similarity index 56% rename from core/src/main/java/io/xpipe/core/store/LocalMachineStore.java rename to core/src/main/java/io/xpipe/core/store/LocalStore.java index a7fd7a02..d60491af 100644 --- a/core/src/main/java/io/xpipe/core/store/LocalMachineStore.java +++ b/core/src/main/java/io/xpipe/core/store/LocalStore.java @@ -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 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 createCommand(List cmd) { + return cmd; + } } diff --git a/core/src/main/java/io/xpipe/core/store/MachineStore.java b/core/src/main/java/io/xpipe/core/store/MachineFileStore.java similarity index 54% rename from core/src/main/java/io/xpipe/core/store/MachineStore.java rename to core/src/main/java/io/xpipe/core/store/MachineFileStore.java index e36c939a..ffe1b353 100644 --- a/core/src/main/java/io/xpipe/core/store/MachineStore.java +++ b/core/src/main/java/io/xpipe/core/store/MachineFileStore.java @@ -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; } diff --git a/core/src/main/java/io/xpipe/core/store/ShellStore.java b/core/src/main/java/io/xpipe/core/store/ShellStore.java new file mode 100644 index 00000000..afb5f8c5 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/ShellStore.java @@ -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 cmd) throws Exception { + var toExec = createCommand(cmd); + return new ProcessBuilder(toExec); + } + + String executeAndRead(List cmd) throws Exception; + + List createCommand(List cmd); +} diff --git a/core/src/main/java/io/xpipe/core/store/StandardShellStore.java b/core/src/main/java/io/xpipe/core/store/StandardShellStore.java new file mode 100644 index 00000000..614326d5 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/StandardShellStore.java @@ -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 createFileReadCommand(String file); + + List createFileWriteCommand(String file); + + List createFileExistsCommand(String file); + + Charset getCharset(); + + String getName(); + } + + default String executeAndRead(List 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 createCommand(List 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; + } +} diff --git a/core/src/main/java/io/xpipe/core/store/StreamDataStore.java b/core/src/main/java/io/xpipe/core/store/StreamDataStore.java index ed0ee6a9..0dae70b0 100644 --- a/core/src/main/java/io/xpipe/core/store/StreamDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StreamDataStore.java @@ -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; } diff --git a/core/src/main/java/io/xpipe/core/store/StringStore.java b/core/src/main/java/io/xpipe/core/store/StringStore.java index 5dd1d30b..8124ea27 100644 --- a/core/src/main/java/io/xpipe/core/store/StringStore.java +++ b/core/src/main/java/io/xpipe/core/store/StringStore.java @@ -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); } diff --git a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java index 670db378..c37b3f3a 100644 --- a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java +++ b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java @@ -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), diff --git a/extension/build.gradle b/extension/build.gradle index 3ce0f116..4a9790ca 100644 --- a/extension/build.gradle +++ b/extension/build.gradle @@ -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' } diff --git a/extension/src/main/java/io/xpipe/extension/ChainedValidator.java b/extension/src/main/java/io/xpipe/extension/ChainedValidator.java new file mode 100644 index 00000000..a0093296 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/ChainedValidator.java @@ -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 validators; + private final ReadOnlyObjectWrapper validationResultProperty = new ReadOnlyObjectWrapper<>(new ValidationResult()); + private final ReadOnlyBooleanWrapper containsErrorsProperty = new ReadOnlyBooleanWrapper(); + + public ChainedValidator(List 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(); + for (var val : validators) { + list.addAll(val.getValidationResult().getMessages()); + } + + var r = new ValidationResult(); + r.addAll(list); + return r; + } + + @Override + public ReadOnlyObjectProperty 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(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); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java index 2ed78f19..7531fd58 100644 --- a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java @@ -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 store) { - return null; + default GuiDialog guiDialog(Property 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(); diff --git a/extension/src/main/java/io/xpipe/extension/ExclusiveValidator.java b/extension/src/main/java/io/xpipe/extension/ExclusiveValidator.java new file mode 100644 index 00000000..cdc4e040 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/ExclusiveValidator.java @@ -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 implements io.xpipe.extension.Validator { + + private final Map validators; + private final ObservableValue obs; + + public ExclusiveValidator(Map validators, ObservableValue 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 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(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); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/GuiDialog.java b/extension/src/main/java/io/xpipe/extension/GuiDialog.java new file mode 100644 index 00000000..fbab46ff --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/GuiDialog.java @@ -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(); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/Properties.java b/extension/src/main/java/io/xpipe/extension/Properties.java new file mode 100644 index 00000000..5d79cb2a --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/Properties.java @@ -0,0 +1,17 @@ +package io.xpipe.extension; + +import javafx.beans.property.Property; + +import java.util.Map; + +public class Properties { + + public static void bindExclusive(Property selected, Map> map, Property toBind) { + selected.addListener((c,o,n) -> { + toBind.unbind(); + toBind.bind(map.get(n)); + }); + + toBind.bind(map.get(selected.getValue())); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/SimpleValidator.java b/extension/src/main/java/io/xpipe/extension/SimpleValidator.java new file mode 100644 index 00000000..51471c2a --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/SimpleValidator.java @@ -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> checks = new LinkedHashMap<>(); + private final ReadOnlyObjectWrapper 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 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 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 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); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/Validator.java b/extension/src/main/java/io/xpipe/extension/Validator.java new file mode 100644 index 00000000..54b35370 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/Validator.java @@ -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 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); +} diff --git a/extension/src/main/java/io/xpipe/extension/Validators.java b/extension/src/main/java/io/xpipe/extension/Validators.java new file mode 100644 index 00000000..6ab3d02d --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/Validators.java @@ -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 name, ObservableValue s) { + return v.createCheck().dependsOn("val", s).withMethod(c -> { + if (c.get("val") == null ) { + c.error(I18n.get("extension.mustNotBeEmpty", name.getValue())); + } + }); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java index 6776b606..77343ee5 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java +++ b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java @@ -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 { @@ -40,6 +43,17 @@ public class DynamicOptionsBuilder { this.title = title; } + public DynamicOptionsBuilder decorate(Check c) { + entries.get(entries.size() - 1).comp().apply(s -> c.decorates(s.get())); + return this; + } + + public DynamicOptionsBuilder 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 addNewLine(Property prop) { var map = new LinkedHashMap>(); for (var e : NewLine.values()) { @@ -79,6 +93,13 @@ public class DynamicOptionsBuilder { return this; } + public DynamicOptionsBuilder addString(String nameKey, Property prop) { + var comp = new TextFieldComp(prop); + entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp)); + props.add(prop); + return this; + } + public DynamicOptionsBuilder addString(ObservableValue name, Property prop) { var comp = new TextFieldComp(prop); entries.add(new DynamicOptionsComp.Entry(name, comp)); @@ -86,8 +107,8 @@ public class DynamicOptionsBuilder { return this; } - public DynamicOptionsBuilder addComp(Comp comp) { - entries.add(new DynamicOptionsComp.Entry(null, comp)); + public DynamicOptionsBuilder addComp(ObservableValue name, Comp comp) { + entries.add(new DynamicOptionsComp.Entry(name, comp)); return this; } @@ -105,19 +126,20 @@ public class DynamicOptionsBuilder { return this; } - public Comp buildComp(Function creator, Property toSet) { + public Comp buildComp(Supplier creator, Property 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 Region build(Function creator, Property toSet) { + public Region build(Supplier creator, Property toSet) { return buildComp(creator, toSet).createRegion(); } } diff --git a/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java b/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java index 8081b3cb..45b355dc 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java @@ -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> { @Override @@ -32,12 +36,24 @@ public class TabPaneComp extends Comp> { 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 selected; private final List entries; - public TabPaneComp(List entries) { + public TabPaneComp(Property selected, List entries) { + this.selected = selected; this.entries = entries; } diff --git a/extension/src/main/java/module-info.java b/extension/src/main/java/module-info.java index ddde3fcd..2f2c92d6 100644 --- a/extension/src/main/java/module-info.java +++ b/extension/src/main/java/module-info.java @@ -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; diff --git a/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_en.properties b/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_en.properties index 75bf68bd..86cbe912 100644 --- a/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_en.properties +++ b/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_en.properties @@ -3,4 +3,5 @@ newLine=Newline crlf=CRLF (Windows) lf=LF (Linux) none=None -nullPointer=Null Pointer: $MSG$ \ No newline at end of file +nullPointer=Null Pointer: $MSG$ +mustNotBeEmpty=$NAME$ must not be empty \ No newline at end of file