From b192fd5b095a465ec61c1237eb7ce355cc0b4b16 Mon Sep 17 00:00:00 2001 From: Christopher Schnick Date: Sat, 8 Jan 2022 04:30:16 +0100 Subject: [PATCH] Rework many parts and move some components into the core module --- api/src/main/java/io/xpipe/api/DataTable.java | 4 +- .../io/xpipe/api/impl/DataSourceImpl.java | 28 +----- .../java/io/xpipe/api/impl/DataTableImpl.java | 62 ++++++++---- .../java/io/xpipe/beacon/BeaconClient.java | 20 +++- .../java/io/xpipe/beacon/BeaconConnector.java | 5 +- .../java/io/xpipe/beacon/BeaconHandler.java | 2 - .../exchange/StoreResourceExchange.java | 11 +-- .../xpipe/core/data/node/SimpleTupleNode.java | 7 +- .../core/source/DataSourceDescriptor.java | 2 + .../io/xpipe/core/source/DataSourceId.java | 2 +- .../io/xpipe/core/source/DataSourceInfo.java | 41 ++++++++ .../java/io/xpipe/core/store/DataStore.java | 5 + .../io/xpipe/core/util/CoreJacksonModule.java | 5 +- extension/build.gradle | 3 + .../main/java/io/xpipe/extension/I18n.java | 9 ++ .../xpipe/extension/comp/CharChoiceComp.java | 52 ++++++++++ .../io/xpipe/extension/comp/CharComp.java | 23 +++++ .../extension/comp/CharsetChoiceComp.java | 19 ++++ .../io/xpipe/extension/comp/ChoiceComp.java | 50 ++++++++++ .../extension/comp/DynamicOptionsComp.java | 84 ++++++++++++++++ .../xpipe/extension/comp/ToggleGroupComp.java | 55 +++++++++++ .../io/xpipe/extension/event/ErrorEvent.java | 47 +++++++++ .../xpipe/extension/event/EventHandler.java | 26 +++++ .../extension/event/ExceptionConverter.java | 17 ++++ .../io/xpipe/extension/event/TrackEvent.java | 98 +++++++++++++++++++ extension/src/main/java/module-info.java | 7 ++ library/build.gradle | 8 ++ library/module-info.java | 17 +++- samples/sample_program/build.gradle | 2 + 29 files changed, 644 insertions(+), 67 deletions(-) create mode 100644 core/src/main/java/io/xpipe/core/source/DataSourceInfo.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/CharComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/event/ErrorEvent.java create mode 100644 extension/src/main/java/io/xpipe/extension/event/EventHandler.java create mode 100644 extension/src/main/java/io/xpipe/extension/event/ExceptionConverter.java create mode 100644 extension/src/main/java/io/xpipe/extension/event/TrackEvent.java diff --git a/api/src/main/java/io/xpipe/api/DataTable.java b/api/src/main/java/io/xpipe/api/DataTable.java index 16de64a7..1522e50e 100644 --- a/api/src/main/java/io/xpipe/api/DataTable.java +++ b/api/src/main/java/io/xpipe/api/DataTable.java @@ -2,7 +2,7 @@ package io.xpipe.api; import io.xpipe.core.data.node.ArrayNode; import io.xpipe.core.data.node.TupleNode; -import io.xpipe.core.data.type.DataType; +import io.xpipe.core.data.type.TupleType; import java.util.OptionalInt; import java.util.stream.Stream; @@ -15,7 +15,7 @@ public interface DataTable extends Iterable, DataSource { OptionalInt getRowCountIfPresent(); - DataType getDataType(); + TupleType getDataType(); ArrayNode readAll(); diff --git a/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java b/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java index 4484281f..dfa82a3f 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java @@ -25,16 +25,7 @@ public abstract class DataSourceImpl implements DataSource { protected void handle(BeaconClient sc) throws ClientException, ServerException, ConnectorException { var req = ReadInfoExchange.Request.builder().sourceId(ds).build(); ReadInfoExchange.Response res = performSimpleExchange(sc, req); - switch (res.getType()) { - case TABLE -> { - var data = res.getTableData(); - source[0] = new DataTableImpl(res.getSourceId(), res.getConfig(), data.getRowCount(), data.getDataType()); - } - case STRUCTURE -> { - } - case RAW -> { - } - } + } }.execute(); return source[0]; @@ -53,10 +44,10 @@ public abstract class DataSourceImpl implements DataSource { s.transferTo(out); } }); - switch (res.getSourceType()) { + switch (res.getInfo().getType()) { case TABLE -> { - var data = res.getTableData(); - source[0] = new DataTableImpl(res.getSourceId(), res.getConfig(), data.getRowCount(), data.getDataType()); + var data = res.getInfo().asTable(); + source[0] = new DataTableImpl(res.getSourceId(), res.getConfig(), data); } case STRUCTURE -> { } @@ -75,16 +66,7 @@ public abstract class DataSourceImpl implements DataSource { protected void handle(BeaconClient sc) throws ClientException, ServerException, ConnectorException { var req = StoreStreamExchange.Request.builder().type(type).build(); StoreStreamExchange.Response res = performOutputExchange(sc, req, in::transferTo); - switch (res.getSourceType()) { - case TABLE -> { - var data = res.getTableData(); - source[0] = new DataTableImpl(res.getSourceId(), res.getConfig(), data.getRowCount(), data.getDataType()); - } - case STRUCTURE -> { - } - case RAW -> { - } - } + } }.execute(); return source[0]; diff --git a/api/src/main/java/io/xpipe/api/impl/DataTableImpl.java b/api/src/main/java/io/xpipe/api/impl/DataTableImpl.java index e96a321c..ada67d19 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataTableImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataTableImpl.java @@ -10,13 +10,14 @@ import io.xpipe.beacon.exchange.ReadTableDataExchange; import io.xpipe.core.data.node.ArrayNode; import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.data.node.TupleNode; -import io.xpipe.core.data.type.DataType; +import io.xpipe.core.data.type.TupleType; import io.xpipe.core.data.typed.TypedAbstractReader; import io.xpipe.core.data.typed.TypedDataStreamParser; import io.xpipe.core.data.typed.TypedDataStructureNodeReader; import io.xpipe.core.data.typed.TypedReusableDataStructureNodeReader; import io.xpipe.core.source.DataSourceConfig; import io.xpipe.core.source.DataSourceId; +import io.xpipe.core.source.DataSourceInfo; import io.xpipe.core.source.DataSourceType; import java.io.IOException; @@ -29,14 +30,17 @@ import java.util.stream.StreamSupport; public class DataTableImpl extends DataSourceImpl implements DataTable { private final DataSourceId id; - private final int size; - private final DataType dataType; + private final DataSourceInfo.Table info; - public DataTableImpl(DataSourceId id, DataSourceConfig sourceConfig, int size, DataType dataType) { + public DataTableImpl(DataSourceId id, DataSourceConfig sourceConfig, DataSourceInfo.Table info) { super(id, sourceConfig); this.id = id; - this.size = size; - this.dataType = dataType; + this.info = info; + } + + @Override + public DataTable asTable() { + return this; } public Stream stream() { @@ -56,21 +60,21 @@ public class DataTableImpl extends DataSourceImpl implements DataTable { @Override public int getRowCount() { - if (size == -1) { + if (info.getRowCount() == -1) { throw new UnsupportedOperationException("Row count is unknown"); } - return size; + return info.getRowCount(); } @Override public OptionalInt getRowCountIfPresent() { - return size != -1 ? OptionalInt.of(size) : OptionalInt.empty(); + return info.getRowCount() != -1 ? OptionalInt.of(info.getRowCount()) : OptionalInt.empty(); } @Override - public DataType getDataType() { - return dataType; + public TupleType getDataType() { + return info.getDataType(); } @Override @@ -80,7 +84,7 @@ public class DataTableImpl extends DataSourceImpl implements DataTable { @Override public ArrayNode read(int maxRows) { - int maxToRead = size == -1 ? maxRows : Math.min(size, maxRows); + int maxToRead = info.getRowCount() == -1 ? maxRows : Math.min(info.getRowCount(), maxRows); List nodes = new ArrayList<>(); new XPipeApiConnector() { @@ -89,8 +93,9 @@ public class DataTableImpl extends DataSourceImpl implements DataTable { var req = ReadTableDataExchange.Request.builder() .sourceId(id).maxRows(maxToRead).build(); performInputExchange(sc, req, (ReadTableDataExchange.Response res, InputStream in) -> { - var r = new TypedDataStreamParser(dataType); - r.parseStructures(in, TypedDataStructureNodeReader.immutable(dataType), nodes::add); + var r = new TypedDataStreamParser(info.getDataType()); + r.parseStructures(in, TypedDataStructureNodeReader.immutable(info.getDataType()), nodes::add); + return true; }); } }.execute(); @@ -103,7 +108,7 @@ public class DataTableImpl extends DataSourceImpl implements DataTable { private InputStream input; private int read; - private final int toRead = size; + private final int toRead = info.getRowCount(); private final TypedDataStreamParser parser; private final TypedAbstractReader nodeReader; @@ -114,21 +119,33 @@ public class DataTableImpl extends DataSourceImpl implements DataTable { var req = ReadTableDataExchange.Request.builder() .sourceId(id).maxRows(Integer.MAX_VALUE).build(); performInputExchange(sc, req, - (ReadTableDataExchange.Response res, InputStream in) -> input = in); + (ReadTableDataExchange.Response res, InputStream in) -> { + input = in; + return false; + }); } }.execute(); - nodeReader = TypedReusableDataStructureNodeReader.create(dataType); - parser = new TypedDataStreamParser(dataType); + nodeReader = TypedReusableDataStructureNodeReader.create(info.getDataType()); + parser = new TypedDataStreamParser(info.getDataType()); + } + + private void finish() { + try { + input.close(); + } catch (IOException e) { + e.printStackTrace(); + } } private boolean hasKnownSize() { - return size != -1; + return info.getRowCount() != -1; } @Override public boolean hasNext() { if (hasKnownSize() && read == toRead) { + finish(); return false; } @@ -137,8 +154,13 @@ public class DataTableImpl extends DataSourceImpl implements DataTable { } try { - return parser.hasNext(input); + var hasNext = parser.hasNext(input); + if (!hasNext) { + finish(); + } + return hasNext; } catch (IOException ex) { + finish(); throw new UncheckedIOException(ex); } } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java index b2791252..840ac9e8 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -12,6 +12,8 @@ import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; import io.xpipe.beacon.message.ServerErrorMessage; import io.xpipe.core.util.JacksonHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; @@ -25,12 +27,20 @@ import static io.xpipe.beacon.BeaconConfig.BODY_SEPARATOR; public class BeaconClient { + private static final Logger log = LoggerFactory.getLogger("beacon"); + @FunctionalInterface public interface FailableBiConsumer { void accept(T var1, U var2) throws E; } + @FunctionalInterface + public interface FailableBiPredicate { + + boolean test(T var1, U var2) throws E; + } + @FunctionalInterface public interface FailableConsumer { @@ -70,7 +80,7 @@ public class BeaconClient { public void exchange( REQ req, FailableConsumer reqWriter, - FailableBiConsumer resReader) + FailableBiPredicate resReader) throws ConnectorException, ClientException, ServerException { try { sendRequest(req); @@ -85,11 +95,12 @@ public class BeaconClient { throw new ConnectorException("Invalid body separator"); } - resReader.accept(res, in); + if (resReader.test(res, in)) { + close(); + } } catch (IOException ex) { - throw new ConnectorException("Couldn't communicate with socket", ex); - } finally { close(); + throw new ConnectorException("Couldn't communicate with socket", ex); } } @@ -116,6 +127,7 @@ public class BeaconClient { var msg = JsonNodeFactory.instance.objectNode(); msg.set("xPipeMessage", json); + log.atTrace().addKeyValue("class", req.getClass().getSimpleName()).log("Sending request to server"); try { var mapper = JacksonHelper.newMapper().disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET); diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConnector.java b/beacon/src/main/java/io/xpipe/beacon/BeaconConnector.java index 5e6e96ab..4685e24c 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconConnector.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconConnector.java @@ -16,7 +16,7 @@ public abstract class BeaconConnector { protected void performInputExchange( BeaconClient socket, REQ req, - BeaconClient.FailableBiConsumer responseConsumer) throws ServerException, ConnectorException, ClientException { + BeaconClient.FailableBiPredicate responseConsumer) throws ServerException, ConnectorException, ClientException { performInputOutputExchange(socket, req, null, responseConsumer); } @@ -24,7 +24,7 @@ public abstract class BeaconConnector { BeaconClient socket, REQ req, BeaconClient.FailableConsumer reqWriter, - BeaconClient.FailableBiConsumer responseConsumer) + BeaconClient.FailableBiPredicate responseConsumer) throws ServerException, ConnectorException, ClientException { socket.exchange(req, reqWriter, responseConsumer); } @@ -37,6 +37,7 @@ public abstract class BeaconConnector { AtomicReference response = new AtomicReference<>(); socket.exchange(req, reqWriter, (RES res, InputStream in) -> { response.set(res); + return true; }); return response.get(); } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java b/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java index ca7a3f7b..e7819d06 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconHandler.java @@ -18,7 +18,5 @@ public interface BeaconHandler { public void sendServerErrorResponse(Throwable ex) throws Exception; - InputStream getInputStream() throws Exception; - OutputStream getOutputStream() throws Exception; } diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/StoreResourceExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/StoreResourceExchange.java index ed4ed356..94bceb95 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/StoreResourceExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/StoreResourceExchange.java @@ -2,9 +2,9 @@ package io.xpipe.beacon.exchange; import io.xpipe.beacon.message.RequestMessage; import io.xpipe.beacon.message.ResponseMessage; -import io.xpipe.core.source.DataSourceId; -import io.xpipe.core.source.DataSourceType; import io.xpipe.core.source.DataSourceConfig; +import io.xpipe.core.source.DataSourceId; +import io.xpipe.core.source.DataSourceInfo; import lombok.Builder; import lombok.Value; import lombok.extern.jackson.Jacksonized; @@ -41,12 +41,7 @@ public class StoreResourceExchange implements MessageExchange { return Optional.empty(); } + DataSourceInfo determineInfo(DS store) throws Exception; + DataSourceType getType(); } diff --git a/core/src/main/java/io/xpipe/core/source/DataSourceId.java b/core/src/main/java/io/xpipe/core/source/DataSourceId.java index b68e53d0..f228a83b 100644 --- a/core/src/main/java/io/xpipe/core/source/DataSourceId.java +++ b/core/src/main/java/io/xpipe/core/source/DataSourceId.java @@ -87,6 +87,6 @@ public class DataSourceId { @Override public String toString() { - return collectionName + SEPARATOR + entryName; + return (collectionName != null ? collectionName : "") + SEPARATOR + entryName; } } diff --git a/core/src/main/java/io/xpipe/core/source/DataSourceInfo.java b/core/src/main/java/io/xpipe/core/source/DataSourceInfo.java new file mode 100644 index 00000000..5798d159 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/source/DataSourceInfo.java @@ -0,0 +1,41 @@ +package io.xpipe.core.source; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.xpipe.core.data.type.TupleType; +import lombok.EqualsAndHashCode; +import lombok.Value; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +public abstract class DataSourceInfo { + + public abstract DataSourceType getType(); + + @EqualsAndHashCode(callSuper = false) + @Value + @JsonTypeName("table") + public static class Table extends DataSourceInfo { + TupleType dataType; + int rowCount; + + @JsonCreator + public Table(TupleType dataType, int rowCount) { + this.dataType = dataType; + this.rowCount = rowCount; + } + + @Override + public DataSourceType getType() { + return DataSourceType.TABLE; + } + } + + public Table asTable() { + if (!getType().equals(DataSourceType.TABLE)) { + throw new IllegalStateException("Not a table"); + } + + return (Table) this; + } +} 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 104586f5..c7550e3d 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStore.java +++ b/core/src/main/java/io/xpipe/core/store/DataStore.java @@ -8,6 +8,11 @@ import java.util.Optional; @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public interface DataStore { + @SuppressWarnings("unchecked") + default DS asNeeded() { + return (DS) this; + } + default Optional determineDefaultName() { return Optional.empty(); } 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 a5b47e88..980c822d 100644 --- a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java +++ b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java @@ -2,7 +2,6 @@ package io.xpipe.core.util; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; @@ -12,6 +11,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import io.xpipe.core.data.type.ArrayType; import io.xpipe.core.data.type.TupleType; import io.xpipe.core.data.type.ValueType; +import io.xpipe.core.source.DataSourceInfo; import io.xpipe.core.store.LocalFileDataStore; import java.io.IOException; @@ -26,7 +26,8 @@ public class CoreJacksonModule extends SimpleModule { new NamedType(LocalFileDataStore.class), new NamedType(ValueType.class), new NamedType(TupleType.class), - new NamedType(ArrayType.class) + new NamedType(ArrayType.class), + new NamedType(DataSourceInfo.Table.class) ); addSerializer(Charset.class, new CharsetSerializer()); diff --git a/extension/build.gradle b/extension/build.gradle index 2823de72..ef808e79 100644 --- a/extension/build.gradle +++ b/extension/build.gradle @@ -11,6 +11,8 @@ java { apply from: "$rootDir/deps/javafx.gradle" apply from: "$rootDir/deps/jackson.gradle" +apply from: "$rootDir/deps/commons.gradle" +apply from: "$rootDir/deps/lombok.gradle" repositories { mavenCentral() @@ -18,4 +20,5 @@ repositories { dependencies { implementation project(':core') + compileOnly project(':fxcomps') } diff --git a/extension/src/main/java/io/xpipe/extension/I18n.java b/extension/src/main/java/io/xpipe/extension/I18n.java index 1f8d6e7d..d3fe9e2b 100644 --- a/extension/src/main/java/io/xpipe/extension/I18n.java +++ b/extension/src/main/java/io/xpipe/extension/I18n.java @@ -1,5 +1,8 @@ package io.xpipe.extension; +import javafx.beans.binding.Bindings; +import javafx.beans.value.ObservableValue; + import java.util.ServiceLoader; import java.util.function.Supplier; @@ -11,6 +14,12 @@ public interface I18n { return () -> get(s, vars); } + public static ObservableValue observable(String s, Object... vars) { + return Bindings.createStringBinding(() -> { + return get(s, vars); + }); + } + public static String get(String s, Object... vars) { return INSTANCE.getLocalised(s, vars); } diff --git a/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java new file mode 100644 index 00000000..c2f26c53 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java @@ -0,0 +1,52 @@ +package io.xpipe.extension.comp; + +import io.xpipe.fxcomps.CompStructure; +import io.xpipe.fxcomps.store.DefaultValueStoreComp; +import io.xpipe.fxcomps.util.StrongBindings; +import javafx.geometry.Pos; +import javafx.scene.layout.HBox; +import org.apache.commons.collections4.BidiMap; +import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; + +import java.util.function.Supplier; + +public class CharChoiceComp extends DefaultValueStoreComp, Character> { + + private final BidiMap> range; + private final Supplier customName; + + public CharChoiceComp(Character defaultVal, BidiMap> range, Supplier customName) { + super(defaultVal); + this.range = range; + this.customName = customName; + } + + @Override + public CompStructure createBase() { + var charChoice = new CharComp(); + StrongBindings.bind(charChoice.valueProperty(), valueProperty()); + + var rangeCopy = new DualLinkedHashBidiMap<>(range); + rangeCopy.put(null, customName); + var choice = new ChoiceComp(value.getValue(), rangeCopy); + choice.set(getValue()); + choice.valueProperty().addListener((c, o, n) -> { + set(n); + }); + valueProperty().addListener((c, o, n) -> { + if (n != null && !range.containsKey(n)) { + choice.set(null); + } else { + choice.set(n); + } + }); + + var charChoiceR = charChoice.createRegion(); + var choiceR = choice.createRegion(); + var box = new HBox(charChoiceR, choiceR); + box.setAlignment(Pos.CENTER); + choiceR.prefWidthProperty().bind(box.widthProperty().subtract(charChoiceR.widthProperty())); + box.getStyleClass().add("char-choice-comp"); + return new CompStructure<>(box); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/CharComp.java b/extension/src/main/java/io/xpipe/extension/comp/CharComp.java new file mode 100644 index 00000000..c99d04dd --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/CharComp.java @@ -0,0 +1,23 @@ +package io.xpipe.extension.comp; + +import io.xpipe.fxcomps.CompStructure; +import io.xpipe.fxcomps.store.ValueStoreComp; +import javafx.scene.control.TextField; + +public class CharComp extends ValueStoreComp, Character> { + + @Override + public CompStructure createBase() { + var text = new TextField(getValue() != null ? getValue().toString() : null); + text.setOnKeyTyped(e -> { + text.setText(e.getCharacter()); + }); + text.textProperty().addListener((c, o, n) -> { + this.set(n != null && n.length() > 0 ? n.charAt(0) : null); + }); + valueProperty().addListener((c, o, n) -> { + text.setText(n != null ? n.toString() : null); + }); + return new CompStructure<>(text); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java new file mode 100644 index 00000000..b5235b1f --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/CharsetChoiceComp.java @@ -0,0 +1,19 @@ +package io.xpipe.extension.comp; + +import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.function.Supplier; + +public class CharsetChoiceComp { + + public static ChoiceComp create() { + var map = new LinkedHashMap>(); + for (var e : Charset.availableCharsets().entrySet()) { + map.put(e.getValue(), () -> e.getKey()); + } + return new ChoiceComp<>(StandardCharsets.UTF_8, new DualLinkedHashBidiMap<>(map)); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java new file mode 100644 index 00000000..de0042e3 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java @@ -0,0 +1,50 @@ +package io.xpipe.extension.comp; + +import io.xpipe.fxcomps.CompStructure; +import io.xpipe.fxcomps.store.DefaultValueStoreComp; +import javafx.collections.FXCollections; +import javafx.scene.control.ChoiceBox; +import javafx.util.StringConverter; +import org.apache.commons.collections4.BidiMap; + +import java.util.function.Supplier; + +public class ChoiceComp extends DefaultValueStoreComp>, T> { + + private final BidiMap> range; + + public ChoiceComp(T defaultVal, BidiMap> range) { + super(defaultVal); + this.range = range; + } + + @Override + protected boolean isValid(T newValue) { + return range.containsKey(newValue); + } + + public BidiMap> getRange() { + return range; + } + + @Override + public CompStructure> createBase() { + var comp = this; + var list = FXCollections.observableArrayList(comp.getRange().keySet()); + var cb = new ChoiceBox<>(list); + cb.setConverter(new StringConverter<>() { + @Override + public String toString(T object) { + return comp.getRange().get(object).get(); + } + + @Override + public T fromString(String string) { + throw new UnsupportedOperationException(); + } + }); + cb.valueProperty().bindBidirectional(comp.valueProperty()); + cb.getStyleClass().add("choice-comp"); + return new CompStructure<>(cb); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java new file mode 100644 index 00000000..8a36adf5 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java @@ -0,0 +1,84 @@ +package io.xpipe.extension.comp; + +import io.xpipe.fxcomps.Comp; +import io.xpipe.fxcomps.CompStructure; +import javafx.beans.Observable; +import javafx.beans.binding.Bindings; +import javafx.geometry.Orientation; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Region; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +public class DynamicOptionsComp extends Comp> { + + private final List entries; + + public DynamicOptionsComp(List entries) { + this.entries = entries; + } + + @Override + public CompStructure createBase() { + var flow = new FlowPane(Orientation.HORIZONTAL); + flow.setAlignment(Pos.CENTER); + flow.setHgap(7); + flow.setVgap(7); + + var nameRegions = new ArrayList(); + var compRegions = new ArrayList(); + + for (var entry : getEntries()) { + var line = new HBox(); + line.setSpacing(5); + + var name = new Label(entry.name().get()); + name.prefHeightProperty().bind(line.heightProperty()); + name.setMinWidth(Region.USE_PREF_SIZE); + name.setAlignment(Pos.CENTER_LEFT); + nameRegions.add(name); + line.getChildren().add(name); + + var r = entry.comp().createRegion(); + compRegions.add(r); + line.getChildren().add(r); + + flow.getChildren().add(line); + } + + var compWidthBinding = Bindings.createDoubleBinding(() -> { + if (compRegions.stream().anyMatch(r -> r.getWidth() == 0)) { + return Region.USE_COMPUTED_SIZE; + } + + var m = compRegions.stream().map(Region::getWidth).max(Double::compareTo).orElse(0.0); + return m; + }, compRegions.stream().map(Region::widthProperty).toList().toArray(new Observable[0])); + compRegions.forEach(r -> r.prefWidthProperty().bind(compWidthBinding)); + + var nameWidthBinding = Bindings.createDoubleBinding(() -> { + if (nameRegions.stream().anyMatch(r -> r.getWidth() == 0)) { + return Region.USE_COMPUTED_SIZE; + } + + var m = nameRegions.stream().map(Region::getWidth).max(Double::compareTo).orElse(0.0); + return m; + }, nameRegions.stream().map(Region::widthProperty).toList().toArray(new Observable[0])); + nameRegions.forEach(r -> r.prefWidthProperty().bind(nameWidthBinding)); + + return new CompStructure<>(flow); + } + + public List getEntries() { + return entries; + } + + public static record Entry(Supplier name, Comp comp) { + + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java b/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java new file mode 100644 index 00000000..f3136e6d --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java @@ -0,0 +1,55 @@ +package io.xpipe.extension.comp; + +import io.xpipe.fxcomps.CompStructure; +import io.xpipe.fxcomps.store.DefaultValueStoreComp; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.HBox; +import org.apache.commons.collections4.BidiMap; + +import java.util.function.Supplier; + +public class ToggleGroupComp extends DefaultValueStoreComp, T> { + + private final BidiMap> range; + + public ToggleGroupComp(T defaultVal, BidiMap> range) { + super(defaultVal); + this.range = range; + } + + public BidiMap> getRange() { + return range; + } + + @Override + public CompStructure createBase() { + var box = new HBox(); + box.getStyleClass().add("toggle-group-comp"); + ToggleGroup group = new ToggleGroup(); + for (var entry : range.entrySet()) { + var b = new ToggleButton(entry.getValue().get()); + b.setOnAction(e -> { + set(entry.getKey()); + e.consume(); + }); + box.getChildren().add(b); + b.setToggleGroup(group); + valueProperty().addListener((c, o, n) -> { + b.setSelected(entry.equals(n)); + }); + if (entry.getKey().equals(getValue())) { + b.setSelected(true); + } + } + box.getChildren().get(0).getStyleClass().add("first"); + box.getChildren().get(box.getChildren().size() - 1).getStyleClass().add("last"); + + group.selectedToggleProperty().addListener((obsVal, oldVal, newVal) -> { + if (newVal == null) + oldVal.setSelected(true); + }); + + return new CompStructure<>(box); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/event/ErrorEvent.java b/extension/src/main/java/io/xpipe/extension/event/ErrorEvent.java new file mode 100644 index 00000000..c4719252 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/event/ErrorEvent.java @@ -0,0 +1,47 @@ +package io.xpipe.extension.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +import java.nio.file.Path; +import java.util.List; + +@Builder +@Getter +public class ErrorEvent { + + public static ErrorEventBuilder fromThrowable(Throwable t) { + return builder().throwable(t) + .description(ExceptionConverter.convertMessage(t)); + } + + public static ErrorEventBuilder fromThrowable(String msg, Throwable t) { + return builder().throwable(t) + .description(msg); + } + + public void handle() { + EventHandler.get().handle(this); + } + + private String description; + + private boolean terminal; + + @Builder.Default + private boolean omitted = false; + + @Builder.Default + private boolean reportable = true; + + private Throwable throwable; + + @Singular + private List diagnostics; + + @Singular + private List sensitiveDiagnostics; + + private final List trackEvents = EventHandler.get().snapshotEvents(); +} diff --git a/extension/src/main/java/io/xpipe/extension/event/EventHandler.java b/extension/src/main/java/io/xpipe/extension/event/EventHandler.java new file mode 100644 index 00000000..240ea881 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/event/EventHandler.java @@ -0,0 +1,26 @@ +package io.xpipe.extension.event; + +import java.util.List; +import java.util.ServiceLoader; + +public abstract class EventHandler { + + private static EventHandler INSTANCE; + + private static void init() { + if (INSTANCE == null) { + INSTANCE = ServiceLoader.load(EventHandler.class).findFirst().orElseThrow(); + } + } + + public static EventHandler get() { + init(); + return INSTANCE; + } + + public abstract List snapshotEvents(); + + public abstract void handle(TrackEvent te); + + public abstract void handle(ErrorEvent ee); +} diff --git a/extension/src/main/java/io/xpipe/extension/event/ExceptionConverter.java b/extension/src/main/java/io/xpipe/extension/event/ExceptionConverter.java new file mode 100644 index 00000000..f13a0deb --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/event/ExceptionConverter.java @@ -0,0 +1,17 @@ +package io.xpipe.extension.event; + +import io.xpipe.extension.I18n; + +import java.io.FileNotFoundException; + +public class ExceptionConverter { + + public static String convertMessage(Throwable ex) { + var msg = ex.getLocalizedMessage(); + if (ex instanceof FileNotFoundException) { + return I18n.get("fileNotFound", msg); + } + + return msg; + } +} diff --git a/extension/src/main/java/io/xpipe/extension/event/TrackEvent.java b/extension/src/main/java/io/xpipe/extension/event/TrackEvent.java new file mode 100644 index 00000000..5bf59a82 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/event/TrackEvent.java @@ -0,0 +1,98 @@ +package io.xpipe.extension.event; + +import lombok.Builder; +import lombok.Getter; +import lombok.Singular; + +import java.time.Instant; +import java.util.Map; + +@Builder +@Getter +public class TrackEvent { + + public static class TrackEventBuilder { + + public TrackEventBuilder windowCategory() { + this.category("window"); + return this; + } + } + + public static TrackEventBuilder fromMessage(String type, String message) { + return builder().type(type).message(message); + } + + public static void simple(String type, String message) { + builder().type(type).message(message).build().handle(); + } + + public static TrackEventBuilder withInfo(String message) { + return builder().type("info").message(message); + } + + public static TrackEventBuilder withTrace(String message) { + return builder().type("trace").message(message); + } + + public static void info(String message) { + builder().type("info").message(message).build().handle(); + } + + public static void warn(String message) { + builder().type("warn").message(message).build().handle(); + } + + public static TrackEventBuilder withDebug(String message) { + return builder().type("debug").message(message); + } + + public static void debug(String message) { + builder().type("debug").message(message).build().handle(); + } + + public static void trace(String message) { + builder().type("trace").message(message).build().handle(); + } + + public static void trace(String cat, String message) { + builder().category(cat).type("trace").message(message).build().handle(); + } + + public static TrackEventBuilder withError(String message) { + return builder().type("error").message(message); + } + + public static void error(String message) { + builder().type("error").message(message).build().handle(); + } + + private final Thread thread = Thread.currentThread(); + private final Instant instant = Instant.now(); + + private String type; + + private String message; + + private String category; + + @Singular + private Map tags; + + public void handle() { + EventHandler.get().handle(this); + } + + @Override + public String toString() { + var s = message; + if (tags.size() > 0) { + s += " {\n"; + for (var e : tags.entrySet()) { + s += " " + e.getKey() + "=" + e.getValue() + "\n"; + } + s += "}"; + } + return s; + } +} diff --git a/extension/src/main/java/module-info.java b/extension/src/main/java/module-info.java index a5c58e64..d30b8833 100644 --- a/extension/src/main/java/module-info.java +++ b/extension/src/main/java/module-info.java @@ -6,11 +6,18 @@ module io.xpipe.extension { requires io.xpipe.core; requires javafx.base; requires javafx.graphics; + requires javafx.controls; + requires io.xpipe.fxcomps; + requires org.apache.commons.collections4; + requires static lombok; exports io.xpipe.extension; + exports io.xpipe.extension.comp; + exports io.xpipe.extension.event; uses DataSourceProvider; uses DataSourceGuiProvider; uses SupportedApplicationProvider; uses io.xpipe.extension.I18n; + uses io.xpipe.extension.event.EventHandler; } \ No newline at end of file diff --git a/library/build.gradle b/library/build.gradle index 7edfcec5..d7b31f17 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -2,11 +2,15 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' + id "org.moditect.gradleplugin" version "1.0.0-rc3" } apply from: 'publish.gradle' apply from: "$rootDir/deps/jackson.gradle" +apply from: "$rootDir/deps/slf4j.gradle" apply from: "$rootDir/deps/lombok.gradle" +apply from: "$rootDir/deps/commons.gradle" +apply from: "$rootDir/deps/javafx.gradle" apply from: "$rootDir/deps/javafx-static.gradle" version '0.1' @@ -19,6 +23,10 @@ java { targetCompatibility = JavaVersion.VERSION_17 } +dependencies { + compileOnly project(':fxcomps') +} + repositories { mavenCentral() } diff --git a/library/module-info.java b/library/module-info.java index c23c12ea..769d949c 100644 --- a/library/module-info.java +++ b/library/module-info.java @@ -31,15 +31,28 @@ module io.xpipe { opens io.xpipe.core.store; opens io.xpipe.core.source; opens io.xpipe.core.data.typed; + opens io.xpipe.core.data.type; // core services uses com.fasterxml.jackson.databind.Module; provides com.fasterxml.jackson.databind.Module with io.xpipe.core.util.CoreJacksonModule; + // extension + requires static io.xpipe.fxcomps; + requires static javafx.base; + requires static javafx.graphics; + requires static javafx.controls; + requires static org.apache.commons.collections4; + exports io.xpipe.extension; + exports io.xpipe.extension.comp; + uses io.xpipe.extension.DataSourceProvider; + uses io.xpipe.extension.DataSourceGuiProvider; + uses io.xpipe.extension.SupportedApplicationProvider; + uses io.xpipe.extension.I18n; + requires com.fasterxml.jackson.core; requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.module.paramnames; + requires org.slf4j; requires static lombok; - requires static javafx.base; - requires static javafx.graphics; } \ No newline at end of file diff --git a/samples/sample_program/build.gradle b/samples/sample_program/build.gradle index 07de0fad..6b200fc7 100644 --- a/samples/sample_program/build.gradle +++ b/samples/sample_program/build.gradle @@ -12,9 +12,11 @@ repositories { mavenCentral() } apply from: "$rootDir/deps/jackson.gradle" +apply from: "$rootDir/deps/slf4j.gradle" dependencies { implementation files(project(':library').jar.archivePath) + implementation group: 'org.slf4j', name: 'slf4j-simple', version: '2.0.0-alpha1' } compileJava.dependsOn(project(':library').jar)