Various fixes

This commit is contained in:
Christopher Schnick 2022-10-18 01:37:21 +02:00
parent 9e1363c5df
commit a2238be4cd
22 changed files with 323 additions and 190 deletions

View file

@ -1,12 +1,12 @@
[![Build Status](https://github.com/xpipe-io/xpipe_java/actions/workflows/build.yml/badge.svg)](https://github.com/xpipe-io/xpipe_java/actions/workflows/build.yml)
[![Publish Status](https://github.com/xpipe-io/xpipe_java/actions/workflows/publishb.yml/badge.svg)](https://github.com/xpipe-io/xpipe_java/actions/workflows/publish.yml)
[![Publish Status](https://github.com/xpipe-io/xpipe_java/actions/workflows/publish.yml/badge.svg)](https://github.com/xpipe-io/xpipe_java/actions/workflows/publish.yml)
## X-Pipe Java
The fundamental components of the [X-Pipe project](https://docs.xpipe.io).
This repository contains the following four modules:
- Core - Shared core classes of the Java API and the X-Pipe daemon implementation
- Core - Shared core classes of the Java API, extensions, and the X-Pipe daemon implementation
- API - The API that can be used to interact with X-Pipe from any JVM-based languages
- Beacon - The X-Pipe beacon component is responsible for handling all communications between the X-Pipe daemon
and the client applications, for example the various programming language APIs and the CLI

View file

@ -68,7 +68,7 @@ public abstract class Charsetter {
if (charset.hasByteOrderMark()) {
var bom = stream.readNBytes(charset.getByteOrderMark().length);
if (bom.length != 0 && !Arrays.equals(bom, charset.getByteOrderMark())) {
throw new IllegalStateException("Invalid charset: " + toString());
throw new IllegalStateException("Charset does not match: " + charset.toString());
}
}

View file

@ -16,31 +16,45 @@ public class StreamCharset {
public static final StreamCharset UTF8 = new StreamCharset(StandardCharsets.UTF_8, null);
public static final StreamCharset UTF8_BOM =
new StreamCharset(StandardCharsets.UTF_8, new byte[] {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF});
public static final StreamCharset UTF16 = new StreamCharset(StandardCharsets.UTF_16, null);
public static final StreamCharset UTF16_BOM =
new StreamCharset(StandardCharsets.UTF_16, new byte[] {(byte) 0xFE, (byte) 0xFF});
public static final StreamCharset UTF16_BE = new StreamCharset(StandardCharsets.UTF_16BE, null);
public static final StreamCharset UTF16_BE_BOM =
new StreamCharset(StandardCharsets.UTF_16BE, new byte[] {(byte) 0xFE, (byte) 0xFF});
public static final StreamCharset UTF16_LE = new StreamCharset(StandardCharsets.UTF_16LE, null);
public static final StreamCharset UTF16_LE_BOM =
new StreamCharset(StandardCharsets.UTF_16LE, new byte[] {(byte) 0xFF, (byte) 0xFE});
public static final StreamCharset UTF32 = new StreamCharset(Charset.forName("utf-32"), null);
public static final StreamCharset UTF32_BOM =
new StreamCharset(Charset.forName("utf-32"), new byte[] {0x00, 0x00, (byte) 0xFE, (byte) 0xFF});
public static final StreamCharset UTF32_LE = new StreamCharset(Charset.forName("utf-32le"), null);
public static final StreamCharset UTF32_LE_BOM =
new StreamCharset(Charset.forName("utf-32le"), new byte[] {0x00, 0x00, (byte) 0xFE, (byte) 0xFF});
public static final StreamCharset UTF32_BE = new StreamCharset(Charset.forName("utf-32be"), null);
public static final StreamCharset UTF32_BE_BOM =
new StreamCharset(Charset.forName("utf-32be"), new byte[] {(byte) 0xFF, (byte) 0xFE, 0x00, 0x00, });
public static final List<StreamCharset> COMMON = List.of(
UTF8,
UTF8_BOM,
UTF16,
UTF16_BOM,
UTF16_BE,
UTF16_BE_BOM,
UTF16_LE,
UTF16_LE_BOM,
UTF32,
UTF32_BOM,
new StreamCharset(StandardCharsets.US_ASCII, null),
new StreamCharset(StandardCharsets.ISO_8859_1, null),
new StreamCharset(Charset.forName("Windows-1251"), null),
new StreamCharset(Charset.forName("Windows-1252"), null));
public static final List<StreamCharset> RARE = Charset.availableCharsets().values().stream()
.filter(charset -> COMMON.stream().noneMatch(c -> c.getCharset().equals(charset)))
.map(charset -> new StreamCharset(charset, null))
private static final List<StreamCharset> RARE_KNOWN = List.of(UTF32_LE, UTF32_LE_BOM, UTF32_BE, UTF32_BE_BOM);
public static final List<StreamCharset> RARE = Stream.concat(
RARE_KNOWN.stream(),
Charset.availableCharsets().values().stream()
.filter(charset -> !charset.equals(StandardCharsets.UTF_16)
&& !charset.equals(Charset.forName("utf-32"))
&& !charset.displayName().startsWith("x-")
&& !charset.displayName().startsWith("X-")
&& !charset.displayName().endsWith("-BOM")
&& COMMON.stream()
.noneMatch(c -> c.getCharset().equals(charset))
&& RARE_KNOWN.stream()
.noneMatch(c -> c.getCharset().equals(charset)))
.map(charset -> new StreamCharset(charset, null)))
.toList();
Charset charset;
byte[] byteOrderMark;

View file

@ -45,7 +45,6 @@ public abstract class DataSource<DS extends DataStore> extends JacksonizedValue
throw new AssertionError(ex);
}
}
public void test() throws Exception {
store.validate();
}

View file

@ -51,14 +51,14 @@ public class LocalStore extends JacksonizedValue implements MachineFileStore, St
}
@Override
public ProcessControl prepareCommand(List<SecretValue> input, List<String> cmd, Integer timeout) {
return new LocalProcessControl(input, cmd, getEffectiveTimeOut(timeout));
public ProcessControl prepareCommand(List<SecretValue> input, List<String> cmd, Integer timeout, Charset charset) {
return new LocalProcessControl(input, cmd, getEffectiveTimeOut(timeout), charset);
}
@Override
public ProcessControl preparePrivilegedCommand(List<SecretValue> input, List<String> cmd, Integer timeOut)
public ProcessControl preparePrivilegedCommand(List<SecretValue> input, List<String> cmd, Integer timeOut, Charset charset)
throws Exception {
return new LocalProcessControl(input, cmd, getEffectiveTimeOut(timeOut));
return new LocalProcessControl(input, cmd, getEffectiveTimeOut(timeOut), charset);
}
@Override
@ -71,14 +71,15 @@ public class LocalStore extends JacksonizedValue implements MachineFileStore, St
private final List<SecretValue> input;
private final Integer timeout;
private final List<String> command;
private Charset charset;
private final Charset charset;
private Process process;
LocalProcessControl(List<SecretValue> input, List<String> cmd, Integer timeout) {
LocalProcessControl(List<SecretValue> input, List<String> cmd, Integer timeout, Charset charset) {
this.input = input;
this.timeout = timeout;
this.command = cmd;
this.charset = charset;
}
private InputStream createInputStream() {
@ -93,7 +94,6 @@ public class LocalStore extends JacksonizedValue implements MachineFileStore, St
var l = type.switchTo(command);
var builder = new ProcessBuilder(l);
process = builder.start();
charset = type.determineCharset(LocalStore.this);
var t = new Thread(() -> {
try (var inputStream = createInputStream()) {

View file

@ -10,22 +10,54 @@ import java.util.concurrent.atomic.AtomicReference;
public abstract class ProcessControl {
public String readOutOnly() throws Exception {
start();
var errT = discardErr();
var string = new String(getStdout().readAllBytes(), getCharset());
waitFor();
return string.trim();
public String executeAndReadStdout() throws Exception {
var pc = this;
pc.start();
pc.discardErr();
var bytes = pc.getStdout().readAllBytes();
var string = new String(bytes, pc.getCharset());
return string;
}
public Optional<String> readErrOnly() throws Exception {
start();
var outT = discardOut();
public void executeOrThrow() throws Exception {
var pc = this;
pc.start();
pc.discardOut();
pc.discardErr();
pc.waitFor();
}
public Optional<String> executeAndReadStderrIfPresent() throws Exception {
var pc = this;
pc.start();
pc.discardOut();
var bytes = pc.getStderr().readAllBytes();
var string = new String(bytes, pc.getCharset());
var ec = pc.waitFor();
return ec != 0 ? Optional.of(string) : Optional.empty();
}
public String executeAndReadStdoutOrThrow()
throws Exception {
var pc = this;
pc.start();
AtomicReference<String> readError = new AtomicReference<>();
var errorThread = new Thread(() -> {
try {
readError.set(new String(pc.getStderr().readAllBytes(), pc.getCharset()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
errorThread.setDaemon(true);
errorThread.start();
AtomicReference<String> read = new AtomicReference<>();
var t = new Thread(() -> {
try {
read.set(new String(getStderr().readAllBytes(), getCharset()));
read.set(new String(pc.getStdout().readAllBytes(), pc.getCharset()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@ -33,8 +65,17 @@ public abstract class ProcessControl {
t.setDaemon(true);
t.start();
var ec = waitFor();
return ec != 0 ? Optional.of(read.get().trim()) : Optional.empty();
var ec = pc.waitFor();
if (ec == -1) {
throw new ProcessOutputException("Command timed out");
}
if (ec == 0 && !(read.get().isEmpty() && !readError.get().isEmpty())) {
return read.get().trim();
} else {
throw new ProcessOutputException(
"Command returned with " + ec + ": " + readError.get().trim());
}
}
public Thread discardOut() {

View file

@ -2,11 +2,8 @@ package io.xpipe.core.store;
import io.xpipe.core.util.SecretValue;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
public interface ShellStore extends DataStore {
@ -18,78 +15,6 @@ public interface ShellStore extends DataStore {
return List.of();
}
public default String executeAndRead(List<String> cmd, Integer timeout) throws Exception {
var pc = prepareCommand(List.of(), cmd, getEffectiveTimeOut(timeout));
pc.start();
pc.discardErr();
var string = new String(pc.getStdout().readAllBytes(), pc.getCharset());
return string;
}
public default String executeAndCheckOut(List<SecretValue> in, List<String> cmd, Integer timeout)
throws ProcessOutputException, Exception {
var pc = prepareCommand(in, cmd, getEffectiveTimeOut(timeout));
pc.start();
AtomicReference<String> readError = new AtomicReference<>();
var errorThread = new Thread(() -> {
try {
readError.set(new String(pc.getStderr().readAllBytes(), pc.getCharset()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
errorThread.setDaemon(true);
errorThread.start();
AtomicReference<String> read = new AtomicReference<>();
var t = new Thread(() -> {
try {
read.set(new String(pc.getStdout().readAllBytes(), pc.getCharset()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
t.setDaemon(true);
t.start();
var ec = pc.waitFor();
if (ec == -1) {
throw new ProcessOutputException("Command timed out");
}
if (ec == 0 && !(read.get().isEmpty() && !readError.get().isEmpty())) {
return read.get().trim();
} else {
throw new ProcessOutputException(
"Command returned with " + ec + ": " + readError.get().trim());
}
}
public default Optional<String> executeAndCheckErr(List<SecretValue> in, List<String> cmd) throws Exception {
var pc = prepareCommand(in, cmd, getTimeout());
pc.start();
var outT = pc.discardOut();
AtomicReference<String> read = new AtomicReference<>();
var t = new Thread(() -> {
try {
read.set(new String(pc.getStderr().readAllBytes(), pc.getCharset()));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
t.setDaemon(true);
t.start();
outT.join();
t.join();
var ec = pc.waitFor();
return ec != 0 ? Optional.of(read.get()) : Optional.empty();
}
public default Integer getEffectiveTimeOut(Integer timeout) {
if (this.getTimeout() == null) {
return timeout;
@ -100,18 +25,18 @@ public interface ShellStore extends DataStore {
return Math.min(getTimeout(), timeout);
}
public default ProcessControl prepareCommand(List<String> cmd, Integer timeout) throws Exception {
return prepareCommand(List.of(), cmd, timeout);
public default ProcessControl prepareCommand(List<String> cmd, Integer timeout, Charset charset) throws Exception {
return prepareCommand(List.of(), cmd, timeout, charset);
}
public abstract ProcessControl prepareCommand(List<SecretValue> input, List<String> cmd, Integer timeout)
public abstract ProcessControl prepareCommand(List<SecretValue> input, List<String> cmd, Integer timeout, Charset charset)
throws Exception;
public default ProcessControl preparePrivilegedCommand(List<String> cmd, Integer timeout) throws Exception {
return preparePrivilegedCommand(List.of(), cmd, timeout);
public default ProcessControl preparePrivilegedCommand(List<String> cmd, Integer timeout, Charset charset) throws Exception {
return preparePrivilegedCommand(List.of(), cmd, timeout, charset);
}
public default ProcessControl preparePrivilegedCommand(List<SecretValue> input, List<String> cmd, Integer timeout)
public default ProcessControl preparePrivilegedCommand(List<SecretValue> input, List<String> cmd, Integer timeout, Charset charset)
throws Exception {
throw new UnsupportedOperationException();
}

View file

@ -18,11 +18,18 @@ public class ShellTypes {
public static final StandardShellStore.ShellType SH = new Sh();
public static StandardShellStore.ShellType determine(ShellStore store) throws Exception {
var o = store.executeAndCheckOut(List.of(), List.of("echo", "$0"), null).strip();
var o = store.prepareCommand(List.of(), List.of("echo", "$0"), null, StandardCharsets.US_ASCII)
.executeAndReadStdoutOrThrow()
.strip();
if (!o.equals("$0")) {
return SH;
} else {
o = store.executeAndCheckOut(List.of(), List.of("(dir 2>&1 *`|echo CMD);&<# rem #>echo PowerShell"), null)
o = store.prepareCommand(
List.of(),
List.of("(dir 2>&1 *`|echo CMD);&<# rem #>echo PowerShell"),
null,
StandardCharsets.UTF_16LE)
.executeAndReadStdoutOrThrow()
.trim();
if (o.equals("PowerShell")) {
return POWERSHELL;
@ -33,7 +40,8 @@ public class ShellTypes {
}
public static StandardShellStore.ShellType[] getAvailable(ShellStore store) throws Exception {
var o = store.executeAndCheckOut(List.of(), List.of("echo", "$0"), null);
var o = store.prepareCommand(List.of(), List.of("echo", "$0"), null, StandardCharsets.US_ASCII)
.executeAndReadStdoutOrThrow();
if (!o.trim().equals("$0")) {
return getLinuxShells();
} else {
@ -43,7 +51,7 @@ public class ShellTypes {
public static StandardShellStore.ShellType getDefault() {
if (System.getProperty("os.name").startsWith("Windows")) {
return CMD;
return POWERSHELL;
} else {
return SH;
}
@ -71,18 +79,20 @@ public class ShellTypes {
var l = new ArrayList<>(cmd);
l.add(0, "cmd.exe");
l.add(1, "/c");
l.add(2, "@chcp 65001");
l.add(3, ">");
l.add(4, "nul");
l.add(5, "&&");
l.add(2, "@chcp");
l.add(3, "65001");
l.add(4, ">");
l.add(5, "nul");
l.add(6, "&&");
return l;
}
@Override
public ProcessControl prepareElevatedCommand(
ShellStore st, List<SecretValue> in, List<String> cmd, Integer timeout, String pw) throws Exception {
ShellStore st, List<SecretValue> in, List<String> cmd, Integer timeout, String pw, Charset charset)
throws Exception {
var l = List.of("net", "session", ";", "if", "%errorLevel%", "!=", "0");
return st.prepareCommand(List.of(), l, timeout);
return st.prepareCommand(List.of(), l, timeout, charset);
}
@Override
@ -184,12 +194,13 @@ public class ShellTypes {
@Override
public ProcessControl prepareElevatedCommand(
ShellStore st, List<SecretValue> in, List<String> cmd, Integer timeout, String pw) throws Exception {
ShellStore st, List<SecretValue> in, List<String> cmd, Integer timeout, String pw, Charset charset)
throws Exception {
var l = new ArrayList<>(cmd);
l.add(0, "sudo");
l.add(1, "-S");
var pws = new ByteArrayInputStream(pw.getBytes(determineCharset(st)));
return st.prepareCommand(List.of(SecretValue.createForSecretValue(pw)), l, timeout);
return st.prepareCommand(List.of(SecretValue.createForSecretValue(pw)), l, timeout, charset);
}
@Override

View file

@ -22,7 +22,12 @@ public interface StandardShellStore extends MachineFileStore, ShellStore {
public abstract ShellType determineType() throws Exception;
public default String querySystemName() throws Exception {
var result = executeAndCheckOut(List.of(), determineType().getOperatingSystemNameCommand(), getTimeout());
var result = prepareCommand(
List.of(),
determineType().getOperatingSystemNameCommand(),
getTimeout(),
determineType().determineCharset(this))
.executeAndReadStdoutOrThrow();
return result.strip();
}
@ -30,7 +35,7 @@ public interface StandardShellStore extends MachineFileStore, ShellStore {
public default InputStream openInput(String file) throws Exception {
var type = determineType();
var cmd = type.createFileReadCommand(file);
var p = prepareCommand(List.of(), cmd, null);
var p = prepareCommand(List.of(), cmd, null, type.determineCharset(this));
p.start();
return p.getStdout();
}
@ -49,7 +54,7 @@ public interface StandardShellStore extends MachineFileStore, ShellStore {
public default boolean exists(String file) throws Exception {
var type = determineType();
var cmd = type.createFileExistsCommand(file);
var p = prepareCommand(List.of(), cmd, null);
var p = prepareCommand(List.of(), cmd, null, type.determineCharset(this));
p.start();
return p.waitFor() == 0;
}
@ -63,8 +68,9 @@ public interface StandardShellStore extends MachineFileStore, ShellStore {
List<String> switchTo(List<String> cmd);
default ProcessControl prepareElevatedCommand(
ShellStore st, List<SecretValue> in, List<String> cmd, Integer timeout, String pw) throws Exception {
return st.prepareCommand(in, cmd, timeout);
ShellStore st, List<SecretValue> in, List<String> cmd, Integer timeout, String pw, Charset charset)
throws Exception {
return st.prepareCommand(in, cmd, timeout, charset);
}
List<String> createFileReadCommand(String file);

View file

@ -19,7 +19,7 @@ public interface DataSourceProvider<T extends DataSource<?>> {
default Category getCategory() {
if (getFileProvider() != null) {
return Category.FILE;
return Category.STREAM;
}
throw new ExtensionException("Provider has no set general type");
@ -43,7 +43,7 @@ public interface DataSourceProvider<T extends DataSource<?>> {
return getId() + "." + key;
}
default Region configGui(Property<T> source, Property<T> appliedSource, boolean all) {
default Region configGui(Property<T> source, boolean all) {
return null;
}
@ -120,7 +120,7 @@ public interface DataSourceProvider<T extends DataSource<?>> {
List<String> getPossibleNames();
static enum Category {
FILE,
STREAM,
DATABASE;
}

View file

@ -46,6 +46,11 @@ public interface SupportedApplicationProvider {
PASSIVE
}
enum Direction {
RETRIEVE,
UPDATE
}
@Value
@AllArgsConstructor
public static class InstructionsDisplay {

View file

@ -26,18 +26,20 @@ public class CharsetChoiceComp extends SimpleComp {
},
new Label(I18n.get("extension.none")),
null);
builder.addFilter((charset, filter) -> {
return charset.getCharset().displayName().contains(filter);
});
builder.addHeader(I18n.get("extension.common"));
for (var e : StreamCharset.COMMON) {
builder.add(e);
}
builder.addHeader(I18n.get("extension.other"));
builder.addFilter((charset, filter) -> {
return charset.getCharset().displayName().contains(filter);
});
for (var e : StreamCharset.RARE) {
builder.add(e);
}
return builder.build();
var comboBox = builder.build();
comboBox.setVisibleRowCount(16);
return comboBox;
}
}

View file

@ -8,7 +8,6 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
@ -19,7 +18,7 @@ import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
public class CodeSnippetComp extends Comp<CompStructure<StackPane>> {
public class CodeSnippetComp extends Comp<CompStructure<?>> {
private final ObservableValue<Boolean> showLineNumbers;
private final ObservableValue<CodeSnippet> value;
@ -79,7 +78,7 @@ public class CodeSnippetComp extends Comp<CompStructure<StackPane>> {
}
@Override
public CompStructure<StackPane> createBase() {
public CompStructure<?> createBase() {
var s = new InlineCssTextArea();
s.setEditable(false);
s.setBackground(null);
@ -88,6 +87,7 @@ public class CodeSnippetComp extends Comp<CompStructure<StackPane>> {
s.getParent().fireEvent(e);
e.consume();
});
s.prefHeightProperty().setValue(20* this.value.getValue().lines().stream().count());
var lineNumbers = new VBox();
lineNumbers.getStyleClass().add("line-numbers");
@ -115,13 +115,10 @@ public class CodeSnippetComp extends Comp<CompStructure<StackPane>> {
}
});
HBox.setHgrow(s, Priority.ALWAYS);
var container = new ScrollPane(content);
container.setFitToWidth(true);
container.setFitToHeight(true);
var c = new StackPane(container);
container.prefHeightProperty().bind(c.heightProperty());
var c = new StackPane(content);
c.getStyleClass().add("code-snippet-container");
c.prefHeightProperty().bind(content.prefHeightProperty());
var copyButton = createCopyButton(c);
var pane = new AnchorPane(copyButton);

View file

@ -6,7 +6,6 @@ import io.xpipe.fxcomps.SimpleCompStructure;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
@ -34,7 +33,6 @@ public class DynamicOptionsComp extends Comp<CompStructure<FlowPane>> {
flow.setAlignment(Pos.CENTER);
flow.setHgap(7);
flow.setVgap(7);
flow.setPadding(new Insets(8, 0, 0, 0));
var nameRegions = new ArrayList<Region>();
var compRegions = new ArrayList<Region>();

View file

@ -0,0 +1,97 @@
package io.xpipe.extension.comp;
import io.xpipe.extension.util.XPipeDaemon;
import io.xpipe.fxcomps.SimpleComp;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.web.WebView;
public class PrettyImageComp extends SimpleComp {
private final ObservableValue<String> value;
private final double width;
private final double height;
public PrettyImageComp(ObservableValue<String> value, double width, double height) {
this.value = value;
this.width = width;
this.height = height;
}
@Override
protected Region createSimple() {
var aspectRatioProperty = new SimpleDoubleProperty(1);
var widthProperty = Bindings.createDoubleBinding(
() -> {
boolean widthLimited = width / height < aspectRatioProperty.doubleValue();
if (widthLimited) {
return width;
} else {
return height * aspectRatioProperty.doubleValue();
}
},
aspectRatioProperty);
var heightProperty = Bindings.createDoubleBinding(
() -> {
boolean widthLimited = width / height < aspectRatioProperty.doubleValue();
if (widthLimited) {
return width / aspectRatioProperty.doubleValue();
} else {
return height;
}
},
aspectRatioProperty);
Node node;
if (value.getValue().endsWith(".svg")) {
var storeIcon =
SvgComp.create(Bindings.createStringBinding(() -> XPipeDaemon.getInstance().svgImage(value.getValue()), value));
aspectRatioProperty.bind(Bindings.createDoubleBinding(
() -> {
return storeIcon.getWidth().getValue().doubleValue()
/ storeIcon.getHeight().getValue().doubleValue();
},
storeIcon.getWidth(),
storeIcon.getHeight()));
node = storeIcon.createWebview();
((WebView) node).prefWidthProperty().bind(widthProperty);
((WebView) node).maxWidthProperty().bind(widthProperty);
((WebView) node).minWidthProperty().bind(widthProperty);
((WebView) node).prefHeightProperty().bind(heightProperty);
((WebView) node).maxHeightProperty().bind(heightProperty);
((WebView) node).minHeightProperty().bind(heightProperty);
} else {
var storeIcon = new ImageView();
storeIcon
.imageProperty()
.bind(Bindings.createObjectBinding(
() -> {
var image = XPipeDaemon.getInstance().image(value.getValue());
aspectRatioProperty.set(image.getWidth() / image.getHeight());
return image;
},
PlatformThread.sync(value)));
;
storeIcon.fitWidthProperty().bind(widthProperty);
storeIcon.fitHeightProperty().bind(heightProperty);
storeIcon.setSmooth(true);
node = storeIcon;
}
var stack = new StackPane(node);
stack.setPrefWidth(width);
stack.setMinWidth(width);
stack.setPrefHeight(height);
stack.setMinHeight(height);
stack.setAlignment(Pos.CENTER);
return stack;
}
}

View file

@ -100,6 +100,10 @@ public class SvgComp {
wv.maxWidthProperty().bind(wv.prefWidthProperty());
wv.maxHeightProperty().bind(wv.prefHeightProperty());
wv.minWidthProperty().bind(wv.prefWidthProperty());
wv.minHeightProperty().bind(wv.prefHeightProperty());
return wv;
}

View file

@ -5,27 +5,47 @@ import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.TextArea;
public class TextAreaComp extends Comp<CompStructure<TextArea>> {
private final Property<String> value;
private final Property<String> lazyValue = new SimpleStringProperty();
private final boolean lazy;
public TextAreaComp(Property<String> value) {
this(value, false);
}
public TextAreaComp(Property<String> value, boolean lazy) {
this.value = value;
this.lazy = lazy;
if (!lazy) {
value.bind(lazyValue);
}
}
@Override
public CompStructure<TextArea> createBase() {
var text = new TextArea(value.getValue() != null ? value.getValue().toString() : null);
text.textProperty().addListener((c, o, n) -> {
value.setValue(n != null && n.length() > 0 ? n : null);
lazyValue.setValue(n != null && n.length() > 0 ? n : null);
});
value.addListener((c, o, n) -> {
lazyValue.setValue(n);
PlatformThread.runLaterIfNeeded(() -> {
text.setText(n);
});
});
text.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
value.setValue(lazyValue.getValue());
}
});
return new SimpleCompStructure<>(text);
}
}

View file

@ -5,6 +5,7 @@ import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.SimpleCompStructure;
import io.xpipe.fxcomps.util.PlatformThread;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.event.EventHandler;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
@ -12,39 +13,57 @@ import javafx.scene.input.KeyEvent;
public class TextFieldComp extends Comp<CompStructure<TextField>> {
private final Property<String> value;
private final Property<String> lazyValue;
private final Property<String> lastAppliedValue;
private final Property<String> currentValue;
private final boolean lazy;
public TextFieldComp(Property<String> value) {
this.value = value;
this.lazyValue = value;
this(value, false);
}
public TextFieldComp(Property<String> value, Property<String> lazyValue) {
this.value = value;
this.lazyValue = lazyValue;
public TextFieldComp(Property<String> value, boolean lazy) {
this.lastAppliedValue = value;
this.currentValue = new SimpleStringProperty(value.getValue());
this.lazy = lazy;
if (!lazy) {
value.bind(currentValue);
}
}
@Override
public CompStructure<TextField> createBase() {
var text = new TextField(value.getValue() != null ? value.getValue().toString() : null);
var text = new TextField(
currentValue.getValue() != null ? currentValue.getValue().toString() : null);
text.textProperty().addListener((c, o, n) -> {
value.setValue(n != null && n.length() > 0 ? n : null);
currentValue.setValue(n != null && n.length() > 0 ? n : null);
});
value.addListener((c, o, n) -> {
lastAppliedValue.addListener((c, o, n) -> {
currentValue.setValue(n);
PlatformThread.runLaterIfNeeded(() -> {
text.setText(n);
});
});
text.setOnKeyPressed(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent ke) {
if (ke.getCode().equals(KeyCode.ENTER)) {
lazyValue.setValue(value.getValue());
text.getScene().getRoot().requestFocus();
}
if (lazy && ke.getCode().equals(KeyCode.ENTER)) {
lastAppliedValue.setValue(currentValue.getValue());
}
ke.consume();
}
});
text.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue) {
lastAppliedValue.setValue(currentValue.getValue());
}
});
return new SimpleCompStructure<>(text);
}
}

View file

@ -23,8 +23,6 @@ public class DynamicOptionsBuilder {
private final List<DynamicOptionsComp.Entry> entries = new ArrayList<>();
private final List<Property<?>> props = new ArrayList<>();
private final List<Property<?>> lazyProperties = new ArrayList<>();
private final ObservableValue<String> title;
private final boolean wrap;
@ -53,13 +51,6 @@ public class DynamicOptionsBuilder {
return this;
}
public DynamicOptionsBuilder makeLazy() {
var p = props.get(props.size() - 1);
props.remove(p);
lazyProperties.add(p);
return this;
}
public DynamicOptionsBuilder decorate(Check c) {
entries.get(entries.size() - 1).comp().apply(s -> c.decorates(s.get()));
@ -133,8 +124,8 @@ public class DynamicOptionsBuilder {
return this;
}
public DynamicOptionsBuilder addStringArea(String nameKey, Property<String> prop) {
var comp = new TextAreaComp(prop);
public DynamicOptionsBuilder addStringArea(String nameKey, Property<String> prop, boolean lazy) {
var comp = new TextAreaComp(prop, lazy);
entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp));
props.add(prop);
return this;
@ -147,11 +138,10 @@ public class DynamicOptionsBuilder {
return this;
}
public DynamicOptionsBuilder addLazyString(String nameKey, Property<String> prop, Property<String> lazy) {
public DynamicOptionsBuilder addString(String nameKey, Property<String> prop, boolean lazy) {
var comp = new TextFieldComp(prop, lazy);
entries.add(new DynamicOptionsComp.Entry(I18n.observable(nameKey), comp));
props.add(prop);
lazyProperties.add(lazy);
return this;
}
@ -162,6 +152,13 @@ public class DynamicOptionsBuilder {
return this;
}
public DynamicOptionsBuilder addString(ObservableValue<String> name, Property<String> prop, boolean lazy) {
var comp = new TextFieldComp(prop, lazy);
entries.add(new DynamicOptionsComp.Entry(name, comp));
props.add(prop);
return this;
}
public DynamicOptionsBuilder addComp(Comp<?> comp) {
return addComp((ObservableValue<String>) null, comp, null);
}
@ -220,17 +217,6 @@ public class DynamicOptionsBuilder {
return this;
}
public <T, V extends T> DynamicOptionsBuilder bindLazy(Supplier<V> creator, Property<T> toLazySet) {
lazyProperties.forEach(prop -> {
prop.addListener((c, o, n) -> {
toLazySet.setValue(creator.get());
});
});
toLazySet.setValue(creator.get());
return this;
}
public final <T, V extends T> DynamicOptionsBuilder bindChoice(
Supplier<Property<? extends V>> creator, Property<T> toSet) {
props.forEach(prop -> {

View file

@ -24,6 +24,11 @@ public class Validators {
throw new IllegalArgumentException(I18n.get("extension.null", name));
}
}
public static void notEmpty(String string, String name) {
if (string.trim().length() == 0) {
throw new IllegalArgumentException(I18n.get("extension.empty", name));
}
}
public static void namedStoreExists(DataStore store, String name) {
if (!XPipeDaemon.getInstance().getNamedStores().contains(store) && !(store instanceof LocalStore)) {

View file

@ -23,7 +23,9 @@ public interface XPipeDaemon {
List<DataStore> getNamedStores();
public Image image(String file);
Image image(String file);
String svgImage(String file);
<T extends Comp<?> & Validatable> T streamStoreChooser(
Property<DataStore> storeProperty,
@ -36,7 +38,7 @@ public interface XPipeDaemon {
Property<? extends DataStore> selected,
DataStoreProvider.Category category);
Comp<?> namedSourceChooser(
<T extends Comp<?> & Validatable> T namedSourceChooser(
ObservableValue<Predicate<DataSource<?>>> filter,
Property<? extends DataSource<?>> selected,
DataSourceProvider.Category category);

View file

@ -3,6 +3,8 @@ newLine=Newline
crlf=CRLF (Windows)
lf=LF (Linux)
none=None
common=Common
other=Other
nullPointer=Null Pointer: $MSG$
mustNotBeEmpty=$NAME$ must not be empty
null=$VALUE$ must be not null