From f0f14179807d430b630308d0ffec7e972af824b5 Mon Sep 17 00:00:00 2001 From: Christopher Schnick Date: Sun, 26 Jun 2022 20:49:51 +0200 Subject: [PATCH] Rework, add more comps, improve dialogs --- .../main/java/io/xpipe/api/DataStores.java | 21 +++++ .../io/xpipe/api/impl/DataSourceImpl.java | 6 +- .../api/impl/DataTableAccumulatorImpl.java | 11 ++- .../io/xpipe/api/util/QuietDialogHandler.java | 67 +++++++++++++++ .../java/io/xpipe/beacon/BeaconClient.java | 2 +- .../io/xpipe/beacon/BeaconConnection.java | 4 +- .../exchange/QueryDataSourceExchange.java | 7 +- ...arationExchange.java => ReadExchange.java} | 9 +- .../beacon/exchange/cli/ConvertExchange.java | 2 +- .../beacon/exchange/cli/DialogExchange.java | 3 + .../exchange/cli/WriteExecuteExchange.java | 5 ++ .../cli/WritePreparationExchange.java | 12 +-- beacon/src/main/java/module-info.java | 2 +- build.gradle | 6 ++ .../xpipe/core/dialog/BaseQueryElement.java | 17 +++- .../io/xpipe/core/dialog/BusyElement.java | 9 ++ .../java/io/xpipe/core/dialog/Choice.java | 19 +++++ .../io/xpipe/core/dialog/ChoiceElement.java | 52 ++++++++---- .../java/io/xpipe/core/dialog/Dialog.java | 62 +++++++++----- .../core/dialog/DialogCancelException.java | 23 +++++ .../io/xpipe/core/dialog/DialogElement.java | 4 + .../io/xpipe/core/dialog/DialogReference.java | 16 ++-- .../io/xpipe/core/dialog/HeaderElement.java | 9 ++ .../io/xpipe/core/dialog/QueryConverter.java | 4 +- .../io/xpipe/core/dialog/QueryElement.java | 4 +- .../java/io/xpipe/core/store/FileStore.java | 14 +++- .../java/io/xpipe/core/store/NamedStore.java | 50 +++++++++++ .../io/xpipe/core/store/StdinDataStore.java | 2 + .../io/xpipe/core/store/StdoutDataStore.java | 2 + .../io/xpipe/core/store/StreamDataStore.java | 14 ++++ .../java/io/xpipe/core/store/StringStore.java | 31 +++++++ .../io/xpipe/core/util/CoreJacksonModule.java | 18 ++-- .../main/java/io/xpipe/core/util/Secret.java | 18 +++- deps | 2 +- extension/build.gradle | 4 +- .../xpipe/extension/DataSourceProvider.java | 32 ++++--- .../xpipe/extension/DataSourceProviders.java | 5 +- .../io/xpipe/extension/DataStoreProvider.java | 32 +++++-- .../xpipe/extension/DataStoreProviders.java | 6 +- .../SimpleFileDataSourceProvider.java | 11 +++ .../SupportedApplicationProvider.java | 2 +- .../xpipe/extension/comp/CharChoiceComp.java | 3 +- .../io/xpipe/extension/comp/CharComp.java | 7 +- .../io/xpipe/extension/comp/ChoiceComp.java | 10 ++- .../xpipe/extension/comp/ChoicePaneComp.java | 71 ++++++++++++++++ .../xpipe/extension/comp/CodeSnippetComp.java | 11 +-- .../extension/comp/DynamicOptionsBuilder.java | 78 ++++++++++++----- .../extension/comp/DynamicOptionsComp.java | 50 +++++++---- .../io/xpipe/extension/comp/IntFieldComp.java | 84 +++++++++++++++++++ .../xpipe/extension/comp/SecretFieldComp.java | 34 ++++++++ .../io/xpipe/extension/comp/TabPaneComp.java | 47 +++++++++++ .../xpipe/extension/comp/TextFieldComp.java | 31 +++++++ .../xpipe/extension/comp/ToggleGroupComp.java | 7 +- .../io/xpipe/extension/event/ErrorEvent.java | 15 +++- .../extension/event/ExceptionConverter.java | 4 + .../io/xpipe/extension/event/TrackEvent.java | 4 + .../xpipe/extension/util/NamedCharacter.java | 55 ++++++++++++ extension/src/main/java/module-info.java | 5 +- .../resources/lang/translations_de.properties | 3 + .../resources/lang/translations_en.properties | 6 ++ 60 files changed, 967 insertions(+), 177 deletions(-) create mode 100644 api/src/main/java/io/xpipe/api/DataStores.java create mode 100644 api/src/main/java/io/xpipe/api/util/QuietDialogHandler.java rename beacon/src/main/java/io/xpipe/beacon/exchange/{ReadPreparationExchange.java => ReadExchange.java} (84%) create mode 100644 core/src/main/java/io/xpipe/core/dialog/DialogCancelException.java create mode 100644 core/src/main/java/io/xpipe/core/store/NamedStore.java create mode 100644 core/src/main/java/io/xpipe/core/store/StringStore.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/ChoicePaneComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/IntFieldComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/SecretFieldComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/comp/TextFieldComp.java create mode 100644 extension/src/main/java/io/xpipe/extension/util/NamedCharacter.java create mode 100644 extension/src/main/resources/io/xpipe/extension/resources/lang/translations_de.properties create mode 100644 extension/src/main/resources/io/xpipe/extension/resources/lang/translations_en.properties diff --git a/api/src/main/java/io/xpipe/api/DataStores.java b/api/src/main/java/io/xpipe/api/DataStores.java new file mode 100644 index 000000000..e55336733 --- /dev/null +++ b/api/src/main/java/io/xpipe/api/DataStores.java @@ -0,0 +1,21 @@ +package io.xpipe.api; + +import io.xpipe.api.connector.XPipeConnection; +import io.xpipe.api.util.QuietDialogHandler; +import io.xpipe.beacon.exchange.cli.StoreAddExchange; +import io.xpipe.core.store.DataStore; + +import java.util.Map; + +public class DataStores { + + public static void addNamedStore(DataStore store, String name) { + XPipeConnection.execute(con -> { + var req = StoreAddExchange.Request.builder() + .storeInput(store).name(name).build(); + StoreAddExchange.Response res = con.performSimpleExchange(req); + + new QuietDialogHandler(res.getConfig(), con, Map.of()).handle(); + }); + } +} 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 868bb2606..0b6e3dbd4 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataSourceImpl.java @@ -4,7 +4,7 @@ import io.xpipe.api.DataSource; import io.xpipe.api.DataSourceConfig; import io.xpipe.api.connector.XPipeConnection; import io.xpipe.beacon.exchange.QueryDataSourceExchange; -import io.xpipe.beacon.exchange.ReadPreparationExchange; +import io.xpipe.beacon.exchange.ReadExchange; import io.xpipe.beacon.exchange.StoreStreamExchange; import io.xpipe.core.source.DataSourceId; import io.xpipe.core.source.DataSourceReference; @@ -50,12 +50,12 @@ public abstract class DataSourceImpl implements DataSource { var store = res.getStore(); - var startReq = ReadPreparationExchange.Request.builder() + var startReq = ReadExchange.Request.builder() .provider(type) .store(store) .build(); var startRes = XPipeConnection.execute(con -> { - ReadPreparationExchange.Response r = con.performSimpleExchange(startReq); + ReadExchange.Response r = con.performSimpleExchange(startReq); return r; }); diff --git a/api/src/main/java/io/xpipe/api/impl/DataTableAccumulatorImpl.java b/api/src/main/java/io/xpipe/api/impl/DataTableAccumulatorImpl.java index d1c9e32d4..1c201cdba 100644 --- a/api/src/main/java/io/xpipe/api/impl/DataTableAccumulatorImpl.java +++ b/api/src/main/java/io/xpipe/api/impl/DataTableAccumulatorImpl.java @@ -5,7 +5,6 @@ import io.xpipe.api.DataTable; import io.xpipe.api.DataTableAccumulator; import io.xpipe.api.connector.XPipeConnection; import io.xpipe.api.util.TypeDescriptor; -import io.xpipe.beacon.exchange.ReadExecuteExchange; import io.xpipe.beacon.exchange.StoreStreamExchange; import io.xpipe.core.data.node.DataStructureNode; import io.xpipe.core.data.node.DataStructureNodeAcceptor; @@ -38,11 +37,11 @@ public class DataTableAccumulatorImpl implements DataTableAccumulator { StoreStreamExchange.Response res = connection.receiveResponse(); connection.close(); - var req = ReadExecuteExchange.Request.builder() - .target(id).dataStore(res.getStore()).build(); - XPipeConnection.execute(con -> { - con.performSimpleExchange(req); - }); +// var req = ReadExecuteExchange.Request.builder() +// .target(id).dataStore(res.getStore()).build(); +// XPipeConnection.execute(con -> { +// con.performSimpleExchange(req); +// }); return DataSource.get(DataSourceReference.id(id)).asTable(); } diff --git a/api/src/main/java/io/xpipe/api/util/QuietDialogHandler.java b/api/src/main/java/io/xpipe/api/util/QuietDialogHandler.java new file mode 100644 index 000000000..b1d453529 --- /dev/null +++ b/api/src/main/java/io/xpipe/api/util/QuietDialogHandler.java @@ -0,0 +1,67 @@ +package io.xpipe.api.util; + +import io.xpipe.beacon.BeaconConnection; +import io.xpipe.beacon.ClientException; +import io.xpipe.beacon.exchange.cli.DialogExchange; +import io.xpipe.core.dialog.BaseQueryElement; +import io.xpipe.core.dialog.ChoiceElement; +import io.xpipe.core.dialog.DialogElement; +import io.xpipe.core.dialog.DialogReference; + +import java.util.Map; +import java.util.UUID; + +public class QuietDialogHandler { + + private final UUID dialogKey; + private DialogElement element; + private final BeaconConnection connection; + private final Map overrides; + + public QuietDialogHandler(DialogReference ref, BeaconConnection connection, Map overrides) { + this.dialogKey = ref.getDialogId(); + this.element = ref.getStart(); + this.connection = connection; + this.overrides = overrides; + } + + public void handle() throws ClientException { + String response = null; + + if (element instanceof ChoiceElement c) { + response = handleChoice(c); + } + + if (element instanceof BaseQueryElement q) { + response = handleQuery(q); + } + + DialogExchange.Response res = connection.performSimpleExchange( + DialogExchange.Request.builder().dialogKey(dialogKey).value(response).build()); + if (res.getElement() != null && element.equals(res.getElement())) { + throw new ClientException("Invalid value for key " + res.getElement().toDisplayString()); + } + + element = res.getElement(); + + if (element != null) { + handle(); + } + } + + private String handleQuery(BaseQueryElement q) { + if (q.isRequired() && !overrides.containsKey(q.getDescription())) { + throw new IllegalStateException("Missing required config parameter: " + q.getDescription()); + } + + return overrides.get(q.getDescription()); + } + + private String handleChoice(ChoiceElement c) { + if (c.isRequired() && !overrides.containsKey(c.getDescription())) { + throw new IllegalStateException("Missing required config parameter: " + c.getDescription()); + } + + return overrides.get(c.getDescription()); + } +} diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java index b8f3d60df..169fb0cd1 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconClient.java @@ -126,7 +126,7 @@ public class BeaconClient implements AutoCloseable { var in = socket.getInputStream(); read = JacksonHelper.newMapper().disable(JsonParser.Feature.AUTO_CLOSE_SOURCE).readTree(in); } catch (SocketException ex) { - throw new ConnectorException("Connection to xpipe daemon closed unexpectedly"); + throw new ConnectorException("Connection to xpipe daemon closed unexpectedly", ex); } catch (IOException ex) { throw new ConnectorException("Couldn't read from socket", ex); } diff --git a/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java b/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java index 6adc47e0e..340d4c635 100644 --- a/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java +++ b/beacon/src/main/java/io/xpipe/beacon/BeaconConnection.java @@ -70,7 +70,7 @@ public abstract class BeaconConnection implements AutoCloseable { public void performInputExchange( REQ req, - BeaconClient.FailableBiConsumer responseConsumer) { + BeaconClient.FailableBiConsumer responseConsumer) { checkClosed(); performInputOutputExchange(req, null, responseConsumer); @@ -79,7 +79,7 @@ public abstract class BeaconConnection implements AutoCloseable { public void performInputOutputExchange( REQ req, BeaconClient.FailableConsumer reqWriter, - BeaconClient.FailableBiConsumer responseConsumer) { + BeaconClient.FailableBiConsumer responseConsumer) { checkClosed(); try { diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/QueryDataSourceExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/QueryDataSourceExchange.java index b7498bb4e..bc1697350 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/QueryDataSourceExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/QueryDataSourceExchange.java @@ -5,7 +5,6 @@ import io.xpipe.beacon.ResponseMessage; import io.xpipe.core.source.DataSourceId; import io.xpipe.core.source.DataSourceInfo; import io.xpipe.core.source.DataSourceReference; -import io.xpipe.core.store.DataStore; import lombok.Builder; import lombok.NonNull; import lombok.Value; @@ -35,13 +34,13 @@ public class QueryDataSourceExchange implements MessageExchange { @Builder @Value public static class Response implements ResponseMessage { - @NonNull DataSourceId id; + boolean disabled; @NonNull DataSourceInfo info; @NonNull - DataStore store; - @NonNull + String storeDisplay; + String provider; @NonNull Map config; diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/ReadPreparationExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/ReadExchange.java similarity index 84% rename from beacon/src/main/java/io/xpipe/beacon/exchange/ReadPreparationExchange.java rename to beacon/src/main/java/io/xpipe/beacon/exchange/ReadExchange.java index a54153a5c..b3f889f0f 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/ReadPreparationExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/ReadExchange.java @@ -3,6 +3,7 @@ package io.xpipe.beacon.exchange; import io.xpipe.beacon.RequestMessage; import io.xpipe.beacon.ResponseMessage; import io.xpipe.core.dialog.DialogReference; +import io.xpipe.core.source.DataSourceId; import io.xpipe.core.store.DataStore; import lombok.Builder; import lombok.NonNull; @@ -12,11 +13,11 @@ import lombok.extern.jackson.Jacksonized; /** * Prepares a client to send stream-based data to a daemon. */ -public class ReadPreparationExchange implements MessageExchange { +public class ReadExchange implements MessageExchange { @Override public String getId() { - return "readPreparation"; + return "read"; } @Jacksonized @@ -28,6 +29,8 @@ public class ReadPreparationExchange implements MessageExchange { @NonNull DataStore store; + DataSourceId target; + boolean configureAll; } @@ -35,8 +38,6 @@ public class ReadPreparationExchange implements MessageExchange { @Builder @Value public static class Response implements ResponseMessage { - String determinedType; - @NonNull DialogReference config; } diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ConvertExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ConvertExchange.java index d30087fd0..67f7a74d1 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ConvertExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/ConvertExchange.java @@ -28,7 +28,7 @@ public class ConvertExchange implements MessageExchange { String newProvider; - DataSourceType newType; + DataSourceType newCategory; DataSourceId copyId; } diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/DialogExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/DialogExchange.java index a25507b48..464197563 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/DialogExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/DialogExchange.java @@ -5,6 +5,7 @@ import io.xpipe.beacon.RequestMessage; import io.xpipe.beacon.ResponseMessage; import io.xpipe.core.dialog.DialogElement; import lombok.Builder; +import lombok.NonNull; import lombok.Value; import lombok.extern.jackson.Jacksonized; @@ -31,8 +32,10 @@ public class DialogExchange implements MessageExchange { @Builder @Value public static class Request implements RequestMessage { + @NonNull UUID dialogKey; String value; + boolean cancel; } @Jacksonized diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/WriteExecuteExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/WriteExecuteExchange.java index 11dda1ef5..9e846843e 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/WriteExecuteExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/WriteExecuteExchange.java @@ -9,6 +9,8 @@ import lombok.NonNull; import lombok.Value; import lombok.extern.jackson.Jacksonized; +import java.util.UUID; + /** * Output the data source contents. */ @@ -25,6 +27,9 @@ public class WriteExecuteExchange implements MessageExchange { public static class Request implements RequestMessage { @NonNull DataSourceReference ref; + + @NonNull + UUID id; } @Jacksonized diff --git a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/WritePreparationExchange.java b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/WritePreparationExchange.java index 3549b34a1..f9fd93fb9 100644 --- a/beacon/src/main/java/io/xpipe/beacon/exchange/cli/WritePreparationExchange.java +++ b/beacon/src/main/java/io/xpipe/beacon/exchange/cli/WritePreparationExchange.java @@ -1,11 +1,11 @@ package io.xpipe.beacon.exchange.cli; -import io.xpipe.beacon.exchange.MessageExchange; import io.xpipe.beacon.RequestMessage; import io.xpipe.beacon.ResponseMessage; +import io.xpipe.beacon.exchange.MessageExchange; import io.xpipe.core.dialog.DialogReference; import io.xpipe.core.source.DataSourceReference; -import io.xpipe.core.store.StreamDataStore; +import io.xpipe.core.store.DataStore; import lombok.Builder; import lombok.NonNull; import lombok.Value; @@ -18,7 +18,7 @@ public class WritePreparationExchange implements MessageExchange { @Override public String getId() { - return "writePreparation"; + return "write"; } @Jacksonized @@ -27,15 +27,17 @@ public class WritePreparationExchange implements MessageExchange { public static class Request implements RequestMessage { String type; @NonNull - StreamDataStore output; + DataStore output; @NonNull - DataSourceReference ref; + DataSourceReference source; } @Jacksonized @Builder @Value public static class Response implements ResponseMessage { + boolean hasBody; + @NonNull DialogReference config; } diff --git a/beacon/src/main/java/module-info.java b/beacon/src/main/java/module-info.java index 4e373d875..e835f4f22 100644 --- a/beacon/src/main/java/module-info.java +++ b/beacon/src/main/java/module-info.java @@ -33,7 +33,7 @@ module io.xpipe.beacon { WritePreparationExchange, WriteExecuteExchange, SelectExchange, - ReadPreparationExchange, + ReadExchange, QueryTextDataExchange, ReadExecuteExchange, ListStoresExchange, diff --git a/build.gradle b/build.gradle index b11c945e1..5e24c6109 100644 --- a/build.gradle +++ b/build.gradle @@ -2,5 +2,11 @@ plugins { id 'org.jreleaser' version '1.0.0' } +if (project == rootProject) { + plugins { + id "io.codearte.nexus-staging" version "0.30.0" + } +} + version file('misc/version').text apply from: 'jreleaser.gradle' \ No newline at end of file diff --git a/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java b/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java index 715703efb..419899ef8 100644 --- a/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/BaseQueryElement.java @@ -2,28 +2,39 @@ package io.xpipe.core.dialog; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.ToString; @JsonTypeName("query") +@EqualsAndHashCode(callSuper = true) +@ToString @Getter public class BaseQueryElement extends DialogElement { private final String description; private final boolean newLine; private final boolean required; - private final boolean hidden; + private final boolean secret; + private final boolean quiet; protected String value; @JsonCreator - public BaseQueryElement(String description, boolean newLine, boolean required, boolean hidden, String value) { + public BaseQueryElement(String description, boolean newLine, boolean required, boolean secret, boolean quiet, String value) { this.description = description; this.newLine = newLine; this.required = required; - this.hidden = hidden; + this.secret = secret; + this.quiet = quiet; this.value = value; } public boolean isNewLine() { return newLine; } + + @Override + public String toDisplayString() { + return description; + } } diff --git a/core/src/main/java/io/xpipe/core/dialog/BusyElement.java b/core/src/main/java/io/xpipe/core/dialog/BusyElement.java index 49a84bf0e..eef13393c 100644 --- a/core/src/main/java/io/xpipe/core/dialog/BusyElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/BusyElement.java @@ -1,10 +1,19 @@ package io.xpipe.core.dialog; import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.EqualsAndHashCode; +import lombok.ToString; @JsonTypeName("busy") +@EqualsAndHashCode(callSuper = true) +@ToString public class BusyElement extends DialogElement { + @Override + public String toDisplayString() { + return "busy"; + } + @Override public boolean apply(String value) { return true; diff --git a/core/src/main/java/io/xpipe/core/dialog/Choice.java b/core/src/main/java/io/xpipe/core/dialog/Choice.java index 50999ab38..797c3f487 100644 --- a/core/src/main/java/io/xpipe/core/dialog/Choice.java +++ b/core/src/main/java/io/xpipe/core/dialog/Choice.java @@ -12,4 +12,23 @@ import lombok.extern.jackson.Jacksonized; public class Choice { Character character; String description; + boolean disabled; + + public Choice(String description) { + this.description = description; + this.character = null; + this.disabled = false; + } + + public Choice(String description, boolean disabled) { + this.character = null; + this.description = description; + this.disabled = disabled; + } + + public Choice(Character character, String description) { + this.character = character; + this.description = description; + this.disabled = false; + } } diff --git a/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java b/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java index 166145ebc..9819a1b26 100644 --- a/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/ChoiceElement.java @@ -2,47 +2,67 @@ package io.xpipe.core.dialog; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.EqualsAndHashCode; +import lombok.ToString; import java.util.List; @JsonTypeName("choice") +@EqualsAndHashCode(callSuper = true) +@ToString public class ChoiceElement extends DialogElement { private final String description; private final List elements; + private final boolean required; private int selected; + @Override + public String toDisplayString() { + return description; + } + @Override public boolean apply(String value) { if (value == null) { return true; } - if (value.length() != 1) { - return true; - } - - var c = value.charAt(0); - if (Character.isDigit(c)) { - selected = Integer.parseInt(value) - 1; - return true; - } - - for (int i = 0; i < elements.size(); i++) { - if (elements.get(i).getCharacter() != null && elements.get(i).getCharacter().equals(c)) { - selected = i; + if (value.length() == 1) { + var c = value.charAt(0); + if (Character.isDigit(c)) { + selected = Integer.parseInt(value) - 1; return true; } + + for (int i = 0; i < elements.size(); i++) { + if (elements.get(i).getCharacter() != null && elements.get(i).getCharacter().equals(c)) { + selected = i; + return true; + } + } + } else { + for (int i = 0; i < elements.size(); i++) { + if (elements.get(i).getDescription().equalsIgnoreCase(value)) { + selected = i; + return true; + } + } } return false; } @JsonCreator - public ChoiceElement(String description, List elements, int selected) { + public ChoiceElement(String description, List elements, boolean required, int selected) { + if (elements.stream().allMatch(Choice::isDisabled)) { + throw new IllegalArgumentException("All choices are disabled"); + } + this.description = description; this.elements = elements; + this.required = required; this.selected = selected; } @@ -57,4 +77,8 @@ public class ChoiceElement extends DialogElement { public String getDescription() { return description; } + + public boolean isRequired() { + return required; + } } diff --git a/core/src/main/java/io/xpipe/core/dialog/Dialog.java b/core/src/main/java/io/xpipe/core/dialog/Dialog.java index c777518e2..dd851a28e 100644 --- a/core/src/main/java/io/xpipe/core/dialog/Dialog.java +++ b/core/src/main/java/io/xpipe/core/dialog/Dialog.java @@ -14,11 +14,13 @@ public abstract class Dialog { return new Dialog() { @Override public DialogElement start() throws Exception { + complete(); return null; } @Override protected DialogElement next(String answer) throws Exception { + complete(); return null; } }; @@ -28,8 +30,8 @@ public abstract class Dialog { private final ChoiceElement element; - private Choice(String description, List elements, int selected) { - this.element = new ChoiceElement(description, elements, selected); + private Choice(String description, List elements, boolean required, int selected) { + this.element = new ChoiceElement(description, elements, required, selected); } @Override @@ -51,18 +53,27 @@ public abstract class Dialog { } } - public static Dialog.Choice choice(String description, List elements, int selected) { - Dialog.Choice c = new Dialog.Choice(description, elements, selected); + public static Dialog.Choice choice(String description, List elements, boolean required, int selected) { + Dialog.Choice c = new Dialog.Choice(description, elements, required, selected); c.evaluateTo(c::getSelected); return c; } @SafeVarargs - public static Dialog.Choice choice(String description, Function toString, T def, T... vals) { + public static Dialog.Choice choice(String description, Function toString, boolean required, T def, T... vals) { var elements = Arrays.stream(vals).map(v -> new io.xpipe.core.dialog.Choice(null, toString.apply(v))).toList(); var index = Arrays.asList(vals).indexOf(def); - var c = choice(description, elements, index); - c.evaluateTo(() -> vals[c.getSelected()]); + if (def != null && index == -1) { + throw new IllegalArgumentException("Default value " + def.toString() + " is not in possible values"); + } + + var c = choice(description, elements, required, index); + c.evaluateTo(() -> { + if (c.getSelected() == -1) { + return null; + } + return vals[c.getSelected()]; + }); return c; } @@ -70,8 +81,8 @@ public abstract class Dialog { private final QueryElement element; - private Query(String description, boolean newLine, boolean required, Object value, QueryConverter converter, boolean hidden) { - this.element = new QueryElement(description, newLine, required,value, converter, hidden); + private Query(String description, boolean newLine, boolean required, boolean quiet, T value, QueryConverter converter, boolean hidden) { + this.element = new QueryElement(description, newLine, required, quiet, value, converter, hidden); } @Override @@ -98,13 +109,13 @@ public abstract class Dialog { } } - public static Dialog.Query query(String description, boolean newLine, boolean required, Object value, QueryConverter converter) { - var q = new Dialog.Query(description, newLine, required, value, converter, false); + public static Dialog.Query query(String description, boolean newLine, boolean required, boolean quiet, T value, QueryConverter converter) { + var q = new Dialog.Query(description, newLine, required, quiet, value, converter, false); q.evaluateTo(q::getConvertedValue); return q; } public static Dialog.Query querySecret(String description, boolean newLine, boolean required, Secret value) { - var q = new Dialog.Query(description, newLine, required, value, QueryConverter.SECRET, true); + var q = new Dialog.Query(description, newLine, required, false, value, QueryConverter.SECRET, true); q.evaluateTo(q::getConvertedValue); return q; } @@ -118,7 +129,16 @@ public abstract class Dialog { public DialogElement start() throws Exception { current = 0; eval = null; - return ds[0].start(); + DialogElement start; + do { + start = ds[current].start(); + if (start != null) { + return start; + } + } while (++current < ds.length); + + current = ds.length - 1; + return null; } @Override @@ -127,7 +147,6 @@ public abstract class Dialog { if (currentElement == null) { DialogElement next = null; while (current < ds.length - 1 && (next = ds[++current].start()) == null) { - }; return next; } @@ -200,8 +219,9 @@ public abstract class Dialog { public DialogElement start() throws Exception { eval = null; dialog = d.get(); + var start = dialog.start(); evaluateTo(dialog); - return dialog.start(); + return start; } @Override @@ -283,12 +303,16 @@ public abstract class Dialog { }.evaluateTo(d.evaluation).onCompletion(d.completion); } - public static Dialog fork(String description, List elements, int selected, Function c) { - var choice = new ChoiceElement(description, elements, selected); + public static Dialog fork(String description, List elements, boolean required, int selected, Function c) { + var choice = new ChoiceElement(description, elements, required, selected); return new Dialog() { private Dialog choiceMade; + { + evaluateTo(() -> choiceMade); + } + @Override public DialogElement start() throws Exception { choiceMade = null; @@ -310,7 +334,7 @@ public abstract class Dialog { return choice; } - }.evaluateTo(() -> choice.getSelected()); + }; } protected Object eval; @@ -324,7 +348,7 @@ public abstract class Dialog { } public Dialog evaluateTo(Dialog d) { - evaluation = d.evaluation; + evaluation = () -> d.evaluation.get(); return this; } diff --git a/core/src/main/java/io/xpipe/core/dialog/DialogCancelException.java b/core/src/main/java/io/xpipe/core/dialog/DialogCancelException.java new file mode 100644 index 000000000..00270b6f2 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/dialog/DialogCancelException.java @@ -0,0 +1,23 @@ +package io.xpipe.core.dialog; + +public class DialogCancelException extends Exception { + + public DialogCancelException() { + } + + public DialogCancelException(String message) { + super(message); + } + + public DialogCancelException(String message, Throwable cause) { + super(message, cause); + } + + public DialogCancelException(Throwable cause) { + super(cause); + } + + public DialogCancelException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/core/src/main/java/io/xpipe/core/dialog/DialogElement.java b/core/src/main/java/io/xpipe/core/dialog/DialogElement.java index 6e85b0ac5..103d9f701 100644 --- a/core/src/main/java/io/xpipe/core/dialog/DialogElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/DialogElement.java @@ -2,10 +2,12 @@ package io.xpipe.core.dialog; import com.fasterxml.jackson.annotation.JsonTypeInfo; import lombok.EqualsAndHashCode; +import lombok.ToString; import java.util.UUID; @EqualsAndHashCode +@ToString @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") public abstract class DialogElement { @@ -15,6 +17,8 @@ public abstract class DialogElement { this.id = UUID.randomUUID().toString(); } + public abstract String toDisplayString(); + public boolean apply(String value) { throw new UnsupportedOperationException(); } diff --git a/core/src/main/java/io/xpipe/core/dialog/DialogReference.java b/core/src/main/java/io/xpipe/core/dialog/DialogReference.java index 02419afb6..f376968cd 100644 --- a/core/src/main/java/io/xpipe/core/dialog/DialogReference.java +++ b/core/src/main/java/io/xpipe/core/dialog/DialogReference.java @@ -1,18 +1,22 @@ package io.xpipe.core.dialog; -import lombok.AllArgsConstructor; -import lombok.Builder; +import com.fasterxml.jackson.annotation.JsonCreator; +import lombok.NonNull; import lombok.Value; -import lombok.extern.jackson.Jacksonized; import java.util.UUID; @Value -@Builder -@Jacksonized -@AllArgsConstructor public class DialogReference { + @NonNull UUID dialogId; + DialogElement start; + + @JsonCreator + public DialogReference(UUID dialogId, DialogElement start) { + this.dialogId = dialogId; + this.start = start; + } } diff --git a/core/src/main/java/io/xpipe/core/dialog/HeaderElement.java b/core/src/main/java/io/xpipe/core/dialog/HeaderElement.java index 81d3c499d..55acf5c6f 100644 --- a/core/src/main/java/io/xpipe/core/dialog/HeaderElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/HeaderElement.java @@ -2,8 +2,12 @@ package io.xpipe.core.dialog; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.EqualsAndHashCode; +import lombok.ToString; @JsonTypeName("header") +@EqualsAndHashCode(callSuper = true) +@ToString public class HeaderElement extends DialogElement { protected String header; @@ -13,6 +17,11 @@ public class HeaderElement extends DialogElement { this.header = header; } + @Override + public String toDisplayString() { + return header; + } + @Override public boolean apply(String value) { return true; diff --git a/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java b/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java index cc5ac35c9..42f2ce393 100644 --- a/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java +++ b/core/src/main/java/io/xpipe/core/dialog/QueryConverter.java @@ -37,12 +37,12 @@ public abstract class QueryConverter { public static final QueryConverter SECRET = new QueryConverter() { @Override protected Secret fromString(String s) { - return Secret.parse(s); + return new Secret(s); } @Override protected String toString(Secret value) { - return value.getValue(); + return value.getEncryptedValue(); } }; diff --git a/core/src/main/java/io/xpipe/core/dialog/QueryElement.java b/core/src/main/java/io/xpipe/core/dialog/QueryElement.java index dec414cfe..d1620af77 100644 --- a/core/src/main/java/io/xpipe/core/dialog/QueryElement.java +++ b/core/src/main/java/io/xpipe/core/dialog/QueryElement.java @@ -7,8 +7,8 @@ public class QueryElement extends BaseQueryElement { private final QueryConverter converter; - public QueryElement(String description, boolean newLine, boolean required, Object value, QueryConverter converter, boolean hidden) { - super(description, newLine, required, hidden, value != null ? value.toString() : null); + public QueryElement(String description, boolean newLine, boolean required, boolean quiet, T value, QueryConverter converter, boolean hidden) { + super(description, newLine, required, hidden, quiet, value != null ? converter.toString(value) : null); this.converter = converter; } 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 91ac9b972..48792cf24 100644 --- a/core/src/main/java/io/xpipe/core/store/FileStore.java +++ b/core/src/main/java/io/xpipe/core/store/FileStore.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonTypeName; import lombok.Value; +import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Path; @Value @@ -27,6 +29,16 @@ public class FileStore implements StreamDataStore, FilenameStore { this.file = file; } + @Override + public InputStream openInput() throws Exception { + return machine.openInput(file); + } + + @Override + public OutputStream openOutput() throws Exception { + return machine.openOutput(file); + } + @Override public boolean canOpen() { return machine.exists(file); @@ -44,6 +56,6 @@ public class FileStore implements StreamDataStore, FilenameStore { @Override public String getFileName() { - return file; + return Path.of(file).getFileName().toString(); } } diff --git a/core/src/main/java/io/xpipe/core/store/NamedStore.java b/core/src/main/java/io/xpipe/core/store/NamedStore.java new file mode 100644 index 000000000..d64e438b7 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/NamedStore.java @@ -0,0 +1,50 @@ +package io.xpipe.core.store; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.Getter; + +import java.time.Instant; +import java.util.Optional; + +@JsonTypeName("named") +public final class NamedStore implements DataStore { + + @Getter + private final String name; + + @JsonCreator + public NamedStore(String name) { + this.name = name; + } + + @Override + public void validate() throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + public boolean delete() throws Exception { + throw new UnsupportedOperationException(); + } + + @Override + public String toDisplay() { + throw new UnsupportedOperationException(); + } + + @Override + public DS asNeeded() { + throw new UnsupportedOperationException(); + } + + @Override + public Optional determineDefaultName() { + throw new UnsupportedOperationException(); + } + + @Override + public Optional determineLastModified() { + throw new UnsupportedOperationException(); + } +} diff --git a/core/src/main/java/io/xpipe/core/store/StdinDataStore.java b/core/src/main/java/io/xpipe/core/store/StdinDataStore.java index 790a5282d..dcb279bdd 100644 --- a/core/src/main/java/io/xpipe/core/store/StdinDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StdinDataStore.java @@ -1,5 +1,6 @@ package io.xpipe.core.store; +import com.fasterxml.jackson.annotation.JsonTypeName; import lombok.EqualsAndHashCode; import lombok.Value; @@ -9,6 +10,7 @@ import java.io.OutputStream; @EqualsAndHashCode @Value +@JsonTypeName("stdin") public class StdinDataStore implements StreamDataStore { @Override diff --git a/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java b/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java index b3387d13e..8388f7130 100644 --- a/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StdoutDataStore.java @@ -1,5 +1,6 @@ package io.xpipe.core.store; +import com.fasterxml.jackson.annotation.JsonTypeName; import lombok.EqualsAndHashCode; import lombok.Value; @@ -8,6 +9,7 @@ import java.io.OutputStream; @EqualsAndHashCode @Value +@JsonTypeName("stdout") public class StdoutDataStore implements StreamDataStore { @Override 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 891572c3b..ed0ee6a9c 100644 --- a/core/src/main/java/io/xpipe/core/store/StreamDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StreamDataStore.java @@ -1,5 +1,6 @@ package io.xpipe.core.store; +import java.io.BufferedInputStream; import java.io.InputStream; import java.io.OutputStream; @@ -9,6 +10,10 @@ import java.io.OutputStream; */ public interface StreamDataStore extends DataStore { + default boolean isLocalOnly() { + return true; + } + /** * Opens an input stream. This input stream does not necessarily have to be a new instance. */ @@ -16,6 +21,15 @@ public interface StreamDataStore extends DataStore { throw new UnsupportedOperationException("Can't open store input"); } + default InputStream openBufferedInput() throws Exception { + var in = openInput(); + if (in.markSupported()) { + return in; + } + + return new BufferedInputStream(in); + } + /** * Opens an output stream. This output stream does not necessarily have to be a new instance. */ diff --git a/core/src/main/java/io/xpipe/core/store/StringStore.java b/core/src/main/java/io/xpipe/core/store/StringStore.java new file mode 100644 index 000000000..5dd1d30b8 --- /dev/null +++ b/core/src/main/java/io/xpipe/core/store/StringStore.java @@ -0,0 +1,31 @@ +package io.xpipe.core.store; + +import com.fasterxml.jackson.annotation.JsonTypeName; +import lombok.AllArgsConstructor; +import lombok.Value; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +@Value +@JsonTypeName("string") +@AllArgsConstructor +public class StringStore implements StreamDataStore { + + byte[] value; + + public StringStore(String s) { + value = s.getBytes(StandardCharsets.UTF_8); + } + + @Override + public InputStream openInput() throws Exception { + return new ByteArrayInputStream(value); + } + + @Override + public String toDisplay() { + return "string"; + } +} 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 9e4905b30..670db3786 100644 --- a/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java +++ b/core/src/main/java/io/xpipe/core/util/CoreJacksonModule.java @@ -21,10 +21,7 @@ import io.xpipe.core.dialog.ChoiceElement; import io.xpipe.core.dialog.HeaderElement; import io.xpipe.core.source.DataSourceInfo; import io.xpipe.core.source.DataSourceReference; -import io.xpipe.core.store.CollectionEntryDataStore; -import io.xpipe.core.store.FileStore; -import io.xpipe.core.store.LocalDirectoryDataStore; -import io.xpipe.core.store.LocalMachineStore; +import io.xpipe.core.store.*; import java.io.IOException; import java.nio.charset.Charset; @@ -36,21 +33,28 @@ public class CoreJacksonModule extends SimpleModule { public void setupModule(SetupContext context) { context.registerSubtypes( new NamedType(FileStore.class), + new NamedType(StdinDataStore.class), + new NamedType(StdoutDataStore.class), new NamedType(LocalDirectoryDataStore.class), new NamedType(CollectionEntryDataStore.class), + new NamedType(StringStore.class), + new NamedType(LocalMachineStore.class), + new NamedType(NamedStore.class), + new NamedType(ValueType.class), new NamedType(TupleType.class), new NamedType(ArrayType.class), new NamedType(WildcardType.class), + new NamedType(DataSourceInfo.Table.class), new NamedType(DataSourceInfo.Structure.class), new NamedType(DataSourceInfo.Text.class), new NamedType(DataSourceInfo.Collection.class), new NamedType(DataSourceInfo.Raw.class), + new NamedType(BaseQueryElement.class), new NamedType(ChoiceElement.class), new NamedType(BusyElement.class), - new NamedType(LocalMachineStore.class), new NamedType(HeaderElement.class) ); @@ -129,7 +133,7 @@ public class CoreJacksonModule extends SimpleModule { @Override public void serialize(Secret value, JsonGenerator jgen, SerializerProvider provider) throws IOException { - jgen.writeString(value.getValue()); + jgen.writeString(value.getEncryptedValue()); } } @@ -137,7 +141,7 @@ public class CoreJacksonModule extends SimpleModule { @Override public Secret deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { - return Secret.parse(p.getValueAsString()); + return Secret.create(p.getValueAsString()); } } diff --git a/core/src/main/java/io/xpipe/core/util/Secret.java b/core/src/main/java/io/xpipe/core/util/Secret.java index 29d7592d7..5d62349f7 100644 --- a/core/src/main/java/io/xpipe/core/util/Secret.java +++ b/core/src/main/java/io/xpipe/core/util/Secret.java @@ -10,7 +10,15 @@ import java.util.Base64; @EqualsAndHashCode public class Secret { - public static Secret parse(String s) { + public static Secret create(String s) { + if (s == null) { + return null; + } + + if (s.length() < 2) { + return new Secret(s); + } + return new Secret(Base64.getEncoder().encodeToString(s.getBytes(StandardCharsets.UTF_8))); } @@ -20,11 +28,15 @@ public class Secret { return "*".repeat(value.length()); } - public String getValue() { + public String getEncryptedValue() { return value; } - public String getSecret() { + public String getSecretValue() { + if (value.length() < 2) { + return value; + } + return new String(Base64.getDecoder().decode(value), StandardCharsets.UTF_8); } } diff --git a/deps b/deps index e2af7576c..49a1ad06b 160000 --- a/deps +++ b/deps @@ -1 +1 @@ -Subproject commit e2af7576c19161cdbb08d64b498b7e6c9ae7e4c6 +Subproject commit 49a1ad06bc6872f72c1d20ea864d24f3df59b7c5 diff --git a/extension/build.gradle b/extension/build.gradle index 14842902a..3ce0f1165 100644 --- a/extension/build.gradle +++ b/extension/build.gradle @@ -31,9 +31,9 @@ repositories { dependencies { 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.1' - implementation 'com.google.code.gson:gson:2.9.0' + implementation 'io.xpipe:fxcomps:0.2' implementation 'org.controlsfx:controlsfx:11.1.1' } diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java index 6f75a5f97..2c8c5875a 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProvider.java @@ -8,10 +8,8 @@ import io.xpipe.core.source.DataSourceType; import io.xpipe.core.store.DataStore; import javafx.beans.property.Property; import javafx.scene.layout.Region; -import lombok.SneakyThrows; import java.nio.charset.Charset; -import java.util.Arrays; import java.util.List; import java.util.Map; @@ -22,6 +20,11 @@ public interface DataSourceProvider> { DATABASE; } + default void validate() throws Exception { + getGeneralType(); + getSourceClass(); + } + default GeneralType getGeneralType() { if (getFileProvider() != null) { return GeneralType.FILE; @@ -34,12 +37,6 @@ public interface DataSourceProvider> { throw new ExtensionException("Provider has no general type"); } - @SneakyThrows - @SuppressWarnings("unchecked") - default T createDefault() { - return (T) getSourceClass().getDeclaredConstructors()[0].newInstance(); - } - default boolean supportsConversion(T in, DataSourceType t) { return false; } @@ -90,12 +87,16 @@ public interface DataSourceProvider> { } - public static Dialog charset(Charset c) { - return Dialog.query("charset", false, false, c, QueryConverter.CHARSET); + public static Dialog charset(Charset c, boolean all) { + return Dialog.query("charset", false, false, c != null &&!all, c, QueryConverter.CHARSET); } - public static Dialog newLine(NewLine l) { - return Dialog.query("newline", false, false, l, NEW_LINE_CONVERTER); + public static Dialog newLine(NewLine l, boolean all) { + return Dialog.query("newline", false, false, l != null &&!all, l, NEW_LINE_CONVERTER); + } + + static Dialog query(String desc, T value, boolean required, QueryConverter c, boolean all) { + return Dialog.query(desc, false, required, value != null && !all, value, c); } public static final QueryConverter NEW_LINE_CONVERTER = new QueryConverter() { @@ -174,12 +175,7 @@ public interface DataSourceProvider> { return createDefaultSource(input); } - @SuppressWarnings("unchecked") - default Class getSourceClass() { - return (Class) Arrays.stream(getClass().getDeclaredClasses()) - .filter(c -> c.getName().endsWith("Source")).findFirst() - .orElseThrow(() -> new ExtensionException("Descriptor class not found for " + getId())); - } + Class getSourceClass(); List getPossibleNames(); diff --git a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java index 0d6b94f29..04f2a9141 100644 --- a/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java +++ b/extension/src/main/java/io/xpipe/extension/DataSourceProviders.java @@ -19,11 +19,14 @@ public class DataSourceProviders { if (ALL == null) { ALL = ServiceLoader.load(layer, DataSourceProvider.class).stream() .map(p -> (DataSourceProvider) p.get()).collect(Collectors.toSet()); - ALL.forEach(p -> { + ALL.removeIf(p -> { try { p.init(); + p.validate(); + return false; } catch (Exception e) { ErrorEvent.fromThrowable(e).handle(); + return true; } }); } diff --git a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java index 945857697..2ed78f195 100644 --- a/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java +++ b/extension/src/main/java/io/xpipe/extension/DataStoreProvider.java @@ -2,12 +2,32 @@ package io.xpipe.extension; import io.xpipe.core.dialog.Dialog; import io.xpipe.core.store.DataStore; +import io.xpipe.core.store.StreamDataStore; +import javafx.beans.property.Property; +import javafx.scene.layout.Region; -import java.net.URL; +import java.net.URI; import java.util.List; public interface DataStoreProvider { + enum Category { + STREAM, + DATABASE; + } + + default Category getCategory() { + if (StreamDataStore.class.isAssignableFrom(getStoreClasses().get(0))) { + return Category.STREAM; + } + + throw new ExtensionException("Provider " + getId() + " has no set category"); + } + + default Region createConfigGui(Property store) { + return null; + } + default void init() throws Exception { } @@ -34,17 +54,17 @@ public interface DataStoreProvider { } default String getDisplayIconFileName() { - return getModuleName() + ":" + getId() + ".png"; - } - - default Dialog dialogForURL(URL url) { - return null; + return getModuleName() + ":" + getId() + "_icon.png"; } default Dialog dialogForString(String s) { return null; } + default Dialog dialogForURI(URI uri) { + return null; + } + Dialog defaultDialog(); default String display(DataStore store) { diff --git a/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java b/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java index 2dd52d504..512cc7fef 100644 --- a/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java +++ b/extension/src/main/java/io/xpipe/extension/DataStoreProviders.java @@ -3,7 +3,7 @@ package io.xpipe.extension; import io.xpipe.core.dialog.Dialog; import io.xpipe.extension.event.ErrorEvent; -import java.net.URL; +import java.net.URI; import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; @@ -37,12 +37,12 @@ public class DataStoreProviders { .anyMatch(s -> s.equalsIgnoreCase(name))).findAny(); } - public static Optional byURL(URL url) { + public static Optional byURI(URI uri) { if (ALL == null) { throw new IllegalStateException("Not initialized"); } - return ALL.stream().map(d -> d.dialogForURL(url)).filter(Objects::nonNull).findAny(); + return ALL.stream().map(d -> d.dialogForURI(uri)).filter(Objects::nonNull).findAny(); } public static Optional byString(String s) { diff --git a/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java b/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java index 61a550527..7ef4d103e 100644 --- a/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java +++ b/extension/src/main/java/io/xpipe/extension/SimpleFileDataSourceProvider.java @@ -1,6 +1,7 @@ package io.xpipe.extension; import io.xpipe.core.source.DataSource; +import io.xpipe.core.source.DataSourceType; import io.xpipe.core.store.DataStore; import io.xpipe.core.store.FileStore; import io.xpipe.core.store.StreamDataStore; @@ -11,6 +12,16 @@ import java.util.Map; public interface SimpleFileDataSourceProvider> extends DataSourceProvider { + @Override + default boolean supportsConversion(T in, DataSourceType t) { + return t == DataSourceType.RAW; + } + + @Override + default DataSource convert(T in, DataSourceType t) throws Exception { + return DataSourceProviders.byId("binary").createDefaultSource(in.getStore()); + } + @Override default boolean prefersStore(DataStore store) { for (var e : getSupportedExtensions().entrySet()) { diff --git a/extension/src/main/java/io/xpipe/extension/SupportedApplicationProvider.java b/extension/src/main/java/io/xpipe/extension/SupportedApplicationProvider.java index 504050dab..9113344ca 100644 --- a/extension/src/main/java/io/xpipe/extension/SupportedApplicationProvider.java +++ b/extension/src/main/java/io/xpipe/extension/SupportedApplicationProvider.java @@ -12,7 +12,7 @@ public interface SupportedApplicationProvider { APPLICATION } - Region createRetrieveInstructions(DataSourceProvider provider, ObservableValue id); + Region createRetrieveInstructions(ObservableValue id); String getId(); diff --git a/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java index e72cf3d42..7f03c3248 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/CharChoiceComp.java @@ -2,6 +2,7 @@ package io.xpipe.extension.comp; import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; +import io.xpipe.fxcomps.SimpleCompStructure; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; @@ -45,6 +46,6 @@ public class CharChoiceComp extends Comp> { box.setAlignment(Pos.CENTER); choiceR.prefWidthProperty().bind(box.widthProperty().subtract(charChoiceR.widthProperty())); box.getStyleClass().add("char-choice-comp"); - return new CompStructure<>(box); + return new SimpleCompStructure<>(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 index 4247be901..e765c663a 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/CharComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/CharComp.java @@ -2,7 +2,8 @@ package io.xpipe.extension.comp; import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; -import io.xpipe.fxcomps.util.PlatformUtil; +import io.xpipe.fxcomps.SimpleCompStructure; +import io.xpipe.fxcomps.util.PlatformThread; import javafx.beans.property.Property; import javafx.scene.control.TextField; @@ -24,10 +25,10 @@ public class CharComp extends Comp> { value.setValue(n != null && n.length() > 0 ? n.charAt(0) : null); }); value.addListener((c, o, n) -> { - PlatformUtil.runLaterIfNeeded(() -> { + PlatformThread.runLaterIfNeeded(() -> { text.setText(n != null ? n.toString() : null); }); }); - return new CompStructure<>(text); + return new SimpleCompStructure<>(text); } } diff --git a/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java b/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java index 4f4842f5a..e038972ae 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/ChoiceComp.java @@ -1,8 +1,10 @@ package io.xpipe.extension.comp; +import io.xpipe.extension.I18n; import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; -import io.xpipe.fxcomps.util.PlatformUtil; +import io.xpipe.fxcomps.SimpleCompStructure; +import io.xpipe.fxcomps.util.PlatformThread; import javafx.beans.property.Property; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -28,7 +30,7 @@ public class ChoiceComp extends Comp>> { @Override public String toString(T object) { if (object == null) { - return "null"; + return I18n.get("extension.none"); } return range.get(object).getValue(); @@ -39,8 +41,8 @@ public class ChoiceComp extends Comp>> { throw new UnsupportedOperationException(); } }); - PlatformUtil.connect(value, cb.valueProperty()); + PlatformThread.connect(value, cb.valueProperty()); cb.getStyleClass().add("choice-comp"); - return new CompStructure<>(cb); + return new SimpleCompStructure<>(cb); } } diff --git a/extension/src/main/java/io/xpipe/extension/comp/ChoicePaneComp.java b/extension/src/main/java/io/xpipe/extension/comp/ChoicePaneComp.java new file mode 100644 index 000000000..4dbd819d1 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/ChoicePaneComp.java @@ -0,0 +1,71 @@ +package io.xpipe.extension.comp; + +import io.xpipe.extension.I18n; +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.collections.FXCollections; +import javafx.scene.control.ComboBox; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import javafx.util.StringConverter; +import lombok.Value; + +import java.util.List; +import java.util.function.Function; + +@Value +public class ChoicePaneComp extends Comp> { + + List entries; + Property selected; + Function, Region> transformer = c -> c; + + @Override + public CompStructure createBase() { + var list = FXCollections.observableArrayList(entries); + var cb = new ComboBox(list); + cb.setConverter(new StringConverter<>() { + @Override + public String toString(Entry object) { + if (object == null) { + return I18n.get("extension.none"); + } + + return object.name().getValue(); + } + + @Override + public Entry fromString(String string) { + throw new UnsupportedOperationException(); + } + }); + + var vbox = new VBox(transformer.apply(cb)); + vbox.setFillWidth(true); + cb.prefWidthProperty().bind(vbox.widthProperty()); + cb.valueProperty().addListener((c,o,n) -> { + if (n == null) { + vbox.getChildren().remove(1); + } else { + if (vbox.getChildren().size() == 1) { + vbox.getChildren().add(n.comp().createRegion()); + } else { + vbox.getChildren().set(1, n.comp().createRegion()); + } + } + }); + + PlatformThread.connect(selected, cb.valueProperty()); + vbox.getStyleClass().add("choice-pane-comp"); + + return new SimpleCompStructure<>(vbox); + } + + public static record Entry(ObservableValue name, Comp comp) { + + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/CodeSnippetComp.java b/extension/src/main/java/io/xpipe/extension/comp/CodeSnippetComp.java index 4437c9b0d..dfad6daa7 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/CodeSnippetComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/CodeSnippetComp.java @@ -2,7 +2,8 @@ package io.xpipe.extension.comp; import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; -import io.xpipe.fxcomps.util.PlatformUtil; +import io.xpipe.fxcomps.SimpleCompStructure; +import io.xpipe.fxcomps.util.PlatformThread; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableValue; import javafx.scene.control.Button; @@ -26,12 +27,12 @@ public class CodeSnippetComp extends Comp> { public CodeSnippetComp(boolean showLineNumbers, ObservableValue value) { this.showLineNumbers = new SimpleBooleanProperty(showLineNumbers); - this.value = PlatformUtil.wrap(value); + this.value = PlatformThread.sync(value); } public CodeSnippetComp(ObservableValue showLineNumbers, ObservableValue value) { - this.showLineNumbers = PlatformUtil.wrap(showLineNumbers); - this.value = PlatformUtil.wrap(value); + this.showLineNumbers = PlatformThread.sync(showLineNumbers); + this.value = PlatformThread.sync(value); } private static String toRGBCode(Color color) { @@ -131,6 +132,6 @@ public class CodeSnippetComp extends Comp> { AnchorPane.setRightAnchor(copyButton, 10.0); c.getChildren().add(pane); - return new CompStructure<>(c); + return new SimpleCompStructure<>(c); } } 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 ca3e9c1ae..6776b6061 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java +++ b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsBuilder.java @@ -1,15 +1,12 @@ package io.xpipe.extension.comp; import io.xpipe.core.charsetter.NewLine; -import io.xpipe.core.source.DataSource; +import io.xpipe.core.util.Secret; import io.xpipe.extension.I18n; import io.xpipe.fxcomps.Comp; -import javafx.beans.Observable; -import javafx.beans.binding.Bindings; import javafx.beans.property.Property; -import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; -import javafx.scene.control.TextField; +import javafx.scene.control.Label; import javafx.scene.layout.Region; import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap; @@ -20,18 +17,36 @@ import java.util.List; import java.util.Map; import java.util.function.Function; -public class DynamicOptionsBuilder> { +public class DynamicOptionsBuilder { private final List entries = new ArrayList<>(); private final List> props = new ArrayList<>(); + private final ObservableValue title; + private final boolean wrap; + + public DynamicOptionsBuilder() { + this.wrap = true; + this.title = null; + } + + public DynamicOptionsBuilder(boolean wrap) { + this.wrap = wrap; + this.title = null; + } + + public DynamicOptionsBuilder(ObservableValue title) { + this.wrap = false; + this.title = title; + } + public DynamicOptionsBuilder addNewLine(Property prop) { var map = new LinkedHashMap>(); for (var e : NewLine.values()) { - map.put(e, new SimpleStringProperty(e.getId())); + map.put(e, I18n.observable("extension." + e.getId())); } var comp = new ChoiceComp<>(prop, new DualLinkedHashBidiMap<>(map)); - entries.add(new DynamicOptionsComp.Entry(I18n.observable("newLine"), comp)); + entries.add(new DynamicOptionsComp.Entry(I18n.observable("extension.newLine"), comp)); props.add(prop); return this; } @@ -59,29 +74,50 @@ public class DynamicOptionsBuilder> { public DynamicOptionsBuilder addCharset(Property prop) { var comp = new CharsetChoiceComp(prop); - entries.add(new DynamicOptionsComp.Entry(I18n.observable("charset"), comp)); + entries.add(new DynamicOptionsComp.Entry(I18n.observable("extension.charset"), comp)); props.add(prop); return this; } public DynamicOptionsBuilder addString(ObservableValue name, Property prop) { - var comp = Comp.of(() -> { - var tf = new TextField(prop.getValue()); - tf.textProperty().addListener((c, o, n) -> { - prop.setValue(n.length() > 0 ? n : null); - }); - return tf; - }); + var comp = new TextFieldComp(prop); entries.add(new DynamicOptionsComp.Entry(name, comp)); props.add(prop); return this; } - public Region build(Function creator, Property toBind) { - var bind = Bindings.createObjectBinding(() -> creator.apply(toBind.getValue()), props.toArray(Observable[]::new)); - bind.addListener((c,o, n) -> { - toBind.setValue(n); + public DynamicOptionsBuilder addComp(Comp comp) { + entries.add(new DynamicOptionsComp.Entry(null, comp)); + return this; + } + + public DynamicOptionsBuilder addSecret(ObservableValue name, Property prop) { + var comp = new SecretFieldComp(prop); + entries.add(new DynamicOptionsComp.Entry(name, comp)); + props.add(prop); + return this; + } + + public DynamicOptionsBuilder addInteger(ObservableValue name, Property prop) { + var comp = new IntFieldComp(prop); + entries.add(new DynamicOptionsComp.Entry(name, comp)); + props.add(prop); + return this; + } + + public Comp buildComp(Function creator, Property toSet) { + props.forEach(prop -> { + prop.addListener((c,o,n) -> { + toSet.setValue(creator.apply(toSet.getValue())); + }); }); - return new DynamicOptionsComp(entries).createRegion(); + 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) { + return buildComp(creator, toSet).createRegion(); } } diff --git a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java index ba20fd93a..356172a68 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/DynamicOptionsComp.java @@ -2,6 +2,7 @@ package io.xpipe.extension.comp; import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; +import io.xpipe.fxcomps.SimpleCompStructure; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; @@ -10,6 +11,7 @@ import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import java.util.ArrayList; @@ -18,9 +20,11 @@ import java.util.List; public class DynamicOptionsComp extends Comp> { private final List entries; + private final boolean wrap; - public DynamicOptionsComp(List entries) { + public DynamicOptionsComp(List entries, boolean wrap) { this.entries = entries; + this.wrap = wrap; } @Override @@ -35,32 +39,42 @@ public class DynamicOptionsComp extends Comp> { for (var entry : getEntries()) { var line = new HBox(); - line.setSpacing(5); + if (!wrap) { + line.prefWidthProperty().bind(flow.widthProperty()); + } + line.setSpacing(8); - var name = new Label(); - name.textProperty().bind(entry.name()); - name.prefHeightProperty().bind(line.heightProperty()); - name.setMinWidth(Region.USE_PREF_SIZE); - name.setAlignment(Pos.CENTER_LEFT); - nameRegions.add(name); - line.getChildren().add(name); + if (entry.name() != null) { + var name = new Label(); + name.textProperty().bind(entry.name()); + 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); + if (!wrap) { + HBox.setHgrow(r, Priority.ALWAYS); + } flow.getChildren().add(line); } - var compWidthBinding = Bindings.createDoubleBinding(() -> { - if (compRegions.stream().anyMatch(r -> r.getWidth() == 0)) { - return Region.USE_COMPUTED_SIZE; - } + if (wrap) { + 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 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)) { @@ -72,7 +86,7 @@ public class DynamicOptionsComp extends Comp> { }, nameRegions.stream().map(Region::widthProperty).toList().toArray(new Observable[0])); nameRegions.forEach(r -> r.prefWidthProperty().bind(nameWidthBinding)); - return new CompStructure<>(flow); + return new SimpleCompStructure<>(flow); } public List getEntries() { diff --git a/extension/src/main/java/io/xpipe/extension/comp/IntFieldComp.java b/extension/src/main/java/io/xpipe/extension/comp/IntFieldComp.java new file mode 100644 index 000000000..a7e169043 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/IntFieldComp.java @@ -0,0 +1,84 @@ +package io.xpipe.extension.comp; + +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.ChangeListener; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyEvent; +import lombok.Value; + +@Value +public class IntFieldComp extends Comp> { + + Property value; + int minValue; + int maxValue; + + public IntFieldComp(Property value) { + this.value = value; + this.minValue = 0; + this.maxValue = Integer.MAX_VALUE; + } + + public IntFieldComp(Property value, int minValue, int maxValue) { + this.value = value; + this.minValue = minValue; + this.maxValue = maxValue; + } + + @Override + public CompStructure createBase() { + var text = new TextField(value.getValue() != null ? value.getValue().toString() : null); + + value.addListener((ChangeListener) (observableValue, oldValue, newValue) -> { + PlatformThread.runLaterIfNeeded(() -> { + if (newValue == null) { + text.setText(""); + } else { + if (newValue.intValue() < minValue) { + value.setValue(minValue); + return; + } + + if (newValue.intValue() > maxValue) { + value.setValue(maxValue); + return; + } + + text.setText(newValue.toString()); + } + }); + }); + + text.addEventFilter(KeyEvent.KEY_TYPED, keyEvent -> { + if (minValue < 0) { + if (!"-0123456789".contains(keyEvent.getCharacter())) { + keyEvent.consume(); + } + } else { + if (!"0123456789".contains(keyEvent.getCharacter())) { + keyEvent.consume(); + } + } + }); + + text.textProperty().addListener((observableValue, oldValue, newValue) -> { + if (newValue == null || "".equals(newValue) || (minValue < 0 && "-".equals(newValue))) { + value.setValue(0); + return; + } + + int intValue = Integer.parseInt(newValue); + if (minValue > intValue || intValue > maxValue) { + text.textProperty().setValue(oldValue); + } + + value.setValue(Integer.parseInt(text.textProperty().get())); + }); + + return new SimpleCompStructure<>(text); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/SecretFieldComp.java b/extension/src/main/java/io/xpipe/extension/comp/SecretFieldComp.java new file mode 100644 index 000000000..e725d358d --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/SecretFieldComp.java @@ -0,0 +1,34 @@ +package io.xpipe.extension.comp; + +import io.xpipe.core.util.Secret; +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.scene.control.PasswordField; +import javafx.scene.control.TextField; + +public class SecretFieldComp extends Comp> { + + private final Property value; + + public SecretFieldComp(Property value) { + this.value = value; + } + + @Override + public CompStructure createBase() { + var text = new PasswordField(); + text.setText(value.getValue() != null ? value.getValue().getSecretValue() : null); + text.textProperty().addListener((c, o, n) -> { + value.setValue(n.length() > 0 ? Secret.create(n) : null); + }); + value.addListener((c, o, n) -> { + PlatformThread.runLaterIfNeeded(() -> { + text.setText(n != null ? n.getSecretValue() : null); + }); + }); + return new SimpleCompStructure<>(text); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java b/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java new file mode 100644 index 000000000..8081b3cb0 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/TabPaneComp.java @@ -0,0 +1,47 @@ +package io.xpipe.extension.comp; + +import com.jfoenix.controls.JFXTabPane; +import io.xpipe.fxcomps.Comp; +import io.xpipe.fxcomps.CompStructure; +import io.xpipe.fxcomps.SimpleCompStructure; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.Tab; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.List; + +public class TabPaneComp extends Comp> { + + @Override + public CompStructure createBase() { + JFXTabPane tabPane = new JFXTabPane(); + tabPane.getStyleClass().add("tab-pane-comp"); + + for (var e : entries) { + Tab tab = new Tab(); + var ll = new Label(null, new FontIcon(e.graphic())); + ll.textProperty().bind(e.name()); + ll.getStyleClass().add("name"); + ll.setAlignment(Pos.CENTER); + tab.setGraphic(ll); + var content = e.comp().createRegion(); + tab.setContent(content); + tabPane.getTabs().add(tab); + content.prefWidthProperty().bind(tabPane.widthProperty()); + } + + return new SimpleCompStructure<>(tabPane); + } + + private final List entries; + + public TabPaneComp(List entries) { + this.entries = entries; + } + + public static record Entry(ObservableValue name, String graphic, Comp comp) { + + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/TextFieldComp.java b/extension/src/main/java/io/xpipe/extension/comp/TextFieldComp.java new file mode 100644 index 000000000..8556c77e7 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/comp/TextFieldComp.java @@ -0,0 +1,31 @@ +package io.xpipe.extension.comp; + +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.scene.control.TextField; + +public class TextFieldComp extends Comp> { + + private final Property value; + + public TextFieldComp(Property value) { + this.value = value; + } + + @Override + public CompStructure createBase() { + var text = new TextField(value.getValue() != null ? value.getValue().toString() : null); + text.textProperty().addListener((c, o, n) -> { + value.setValue(n != null && n.length() > 0 ? n : null); + }); + value.addListener((c, o, n) -> { + PlatformThread.runLaterIfNeeded(() -> { + text.setText(n); + }); + }); + return new SimpleCompStructure<>(text); + } +} diff --git a/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java b/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java index 0bc54bbe2..8072a6c8b 100644 --- a/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java +++ b/extension/src/main/java/io/xpipe/extension/comp/ToggleGroupComp.java @@ -2,7 +2,8 @@ package io.xpipe.extension.comp; import io.xpipe.fxcomps.Comp; import io.xpipe.fxcomps.CompStructure; -import io.xpipe.fxcomps.util.PlatformUtil; +import io.xpipe.fxcomps.SimpleCompStructure; +import io.xpipe.fxcomps.util.PlatformThread; import javafx.beans.property.Property; import javafx.beans.value.ObservableValue; import javafx.scene.control.ToggleButton; @@ -38,7 +39,7 @@ public class ToggleGroupComp extends Comp> { box.getChildren().add(b); b.setToggleGroup(group); value.addListener((c, o, n) -> { - PlatformUtil.runLaterIfNeeded(() -> b.setSelected(entry.equals(n))); + PlatformThread.runLaterIfNeeded(() -> b.setSelected(entry.equals(n))); }); if (entry.getKey().equals(value.getValue())) { b.setSelected(true); @@ -55,6 +56,6 @@ public class ToggleGroupComp extends Comp> { oldVal.setSelected(true); }); - return new CompStructure<>(box); + return new SimpleCompStructure<>(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 index c99befa91..726ada190 100644 --- a/extension/src/main/java/io/xpipe/extension/event/ErrorEvent.java +++ b/extension/src/main/java/io/xpipe/extension/event/ErrorEvent.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.Singular; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; @Builder @@ -49,10 +50,16 @@ public class ErrorEvent { private Throwable throwable; @Singular - private List diagnostics; - - @Singular - private List sensitiveDiagnostics; + private List attachments; private final List trackEvents = EventHandler.get().snapshotEvents(); + + public void addAttachment(Path file) { + attachments = new ArrayList<>(attachments); + attachments.add(file); + } + + public void clearAttachments() { + attachments = new ArrayList<>(); + } } diff --git a/extension/src/main/java/io/xpipe/extension/event/ExceptionConverter.java b/extension/src/main/java/io/xpipe/extension/event/ExceptionConverter.java index 6c8897a2c..d946bc48c 100644 --- a/extension/src/main/java/io/xpipe/extension/event/ExceptionConverter.java +++ b/extension/src/main/java/io/xpipe/extension/event/ExceptionConverter.java @@ -16,6 +16,10 @@ public class ExceptionConverter { return I18n.get("classNotFound", msg); } + if (ex instanceof NullPointerException) { + return I18n.get("extension.nullPointer", 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 index 8bf5a4c46..5ec2628cb 100644 --- a/extension/src/main/java/io/xpipe/extension/event/TrackEvent.java +++ b/extension/src/main/java/io/xpipe/extension/event/TrackEvent.java @@ -6,6 +6,7 @@ import lombok.Singular; import java.time.Instant; import java.util.ArrayList; +import java.util.List; import java.util.Map; @Builder @@ -110,6 +111,9 @@ public class TrackEvent { @Singular private Map tags; + @Singular + private List elements; + public void handle() { EventHandler.get().handle(this); } diff --git a/extension/src/main/java/io/xpipe/extension/util/NamedCharacter.java b/extension/src/main/java/io/xpipe/extension/util/NamedCharacter.java new file mode 100644 index 000000000..3e38f7123 --- /dev/null +++ b/extension/src/main/java/io/xpipe/extension/util/NamedCharacter.java @@ -0,0 +1,55 @@ +package io.xpipe.extension.util; + +import io.xpipe.core.dialog.QueryConverter; +import lombok.Value; + +import java.util.List; + +@Value +public class NamedCharacter { + + public static QueryConverter converter(List chars, boolean allowOthers) { + return new QueryConverter() { + @Override + protected Character fromString(String s) { + if (s.length() == 0) { + throw new IllegalArgumentException("No character"); + } + + var byName = chars.stream().filter(nc -> nc.getNames().stream() + .anyMatch(n -> n.toLowerCase().contains(s.toLowerCase()))) + .findFirst().orElse(null); + if (byName != null) { + return byName.getCharacter(); + } + + var byChar = chars.stream().filter(nc -> String.valueOf(nc.getCharacter()).equalsIgnoreCase(s)) + .findFirst().orElse(null); + if (byChar != null) { + return byChar.getCharacter(); + } + + if (!allowOthers) { + throw new IllegalArgumentException("Unknown character: " + s); + } + + return QueryConverter.CHARACTER.convertFromString(s); + } + + @Override + protected String toString(Character value) { + var byChar = chars.stream().filter(nc -> value.equals(nc.getCharacter())) + .findFirst().orElse(null); + if (byChar != null) { + return byChar.getNames().get(0); + } + + return value.toString(); + } + }; + } + + char character; + List names; + String translationKey; +} diff --git a/extension/src/main/java/module-info.java b/extension/src/main/java/module-info.java index 84676f8a7..ddde3fcde 100644 --- a/extension/src/main/java/module-info.java +++ b/extension/src/main/java/module-info.java @@ -6,18 +6,18 @@ module io.xpipe.extension { exports io.xpipe.extension.comp; exports io.xpipe.extension.event; exports io.xpipe.extension.prefs; + exports io.xpipe.extension.util; requires transitive io.xpipe.core; requires transitive javafx.base; requires javafx.graphics; requires transitive javafx.controls; requires io.xpipe.fxcomps; - requires org.apache.commons.collections4; + requires static org.apache.commons.collections4; requires static lombok; requires static com.dlsc.preferencesfx; requires static com.dlsc.formsfx; requires static org.slf4j; - requires static com.google.gson; requires static org.controlsfx.controls; requires java.desktop; requires org.fxmisc.richtext; @@ -28,6 +28,7 @@ module io.xpipe.extension { requires org.kordamp.ikonli.javafx; requires com.fasterxml.jackson.databind; requires static org.junit.jupiter.api; + requires static com.jfoenix; uses DataSourceProvider; uses SupportedApplicationProvider; diff --git a/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_de.properties b/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_de.properties new file mode 100644 index 000000000..d86dcac53 --- /dev/null +++ b/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_de.properties @@ -0,0 +1,3 @@ +displayName=Mein Dateiformat +description=Meine Dateiformat-Beschreibung +fileName=Mein Dateiformat Datei \ No newline at end of file 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 new file mode 100644 index 000000000..75bf68bd1 --- /dev/null +++ b/extension/src/main/resources/io/xpipe/extension/resources/lang/translations_en.properties @@ -0,0 +1,6 @@ +charset=Charset +newLine=Newline +crlf=CRLF (Windows) +lf=LF (Linux) +none=None +nullPointer=Null Pointer: $MSG$ \ No newline at end of file