Add more exchanges + move some files

This commit is contained in:
Christopher Schnick 2022-01-18 09:55:43 +01:00
parent 774689e42a
commit caafb9d850
28 changed files with 775 additions and 76 deletions

View file

@ -23,7 +23,7 @@ import java.util.Optional;
import static io.xpipe.beacon.BeaconConfig.BODY_SEPARATOR;
public class BeaconClient {
public class BeaconClient implements AutoCloseable {
@FunctionalInterface
public interface FailableBiConsumer<T, U, E extends Throwable> {
@ -76,7 +76,7 @@ public class BeaconClient {
public <REQ extends RequestMessage, RES extends ResponseMessage> void exchange(
REQ req,
FailableConsumer<OutputStream, IOException> reqWriter,
FailableBiPredicate<RES, InputStream, IOException> resReader)
FailableBiConsumer<RES, InputStream, IOException> resReader)
throws ConnectorException, ClientException, ServerException {
try {
sendRequest(req);
@ -91,23 +91,16 @@ public class BeaconClient {
throw new ConnectorException("Invalid body separator");
}
if (resReader.test(res, in)) {
close();
}
resReader.accept(res, in);
} catch (IOException ex) {
close();
throw new ConnectorException("Couldn't communicate with socket", ex);
}
}
public <REQ extends RequestMessage, RES extends ResponseMessage> RES simpleExchange(REQ req)
throws ServerException, ConnectorException, ClientException {
try {
sendRequest(req);
return this.receiveResponse();
} finally {
close();
}
throws ServerException, ConnectorException, ClientException {
sendRequest(req);
return this.receiveResponse();
}
private <T extends RequestMessage> void sendRequest(T req) throws ClientException, ConnectorException {

View file

@ -16,7 +16,7 @@ public abstract class BeaconConnector {
protected <REQ extends RequestMessage, RES extends ResponseMessage> void performInputExchange(
BeaconClient socket,
REQ req,
BeaconClient.FailableBiPredicate<RES, InputStream, IOException> responseConsumer) throws ServerException, ConnectorException, ClientException {
BeaconClient.FailableBiConsumer<RES, InputStream, IOException> responseConsumer) throws ServerException, ConnectorException, ClientException {
performInputOutputExchange(socket, req, null, responseConsumer);
}
@ -24,7 +24,7 @@ public abstract class BeaconConnector {
BeaconClient socket,
REQ req,
BeaconClient.FailableConsumer<OutputStream, IOException> reqWriter,
BeaconClient.FailableBiPredicate<RES, InputStream, IOException> responseConsumer)
BeaconClient.FailableBiConsumer<RES, InputStream, IOException> responseConsumer)
throws ServerException, ConnectorException, ClientException {
socket.exchange(req, reqWriter, responseConsumer);
}
@ -37,7 +37,6 @@ public abstract class BeaconConnector {
AtomicReference<RES> response = new AtomicReference<>();
socket.exchange(req, reqWriter, (RES res, InputStream in) -> {
response.set(res);
return true;
});
return response.get();
}

View file

@ -0,0 +1,43 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class DialogExchange implements MessageExchange<DialogExchange.Request, DialogExchange.Response> {
@Override
public String getId() {
return "dialog";
}
@Override
public Class<DialogExchange.Request> getRequestClass() {
return DialogExchange.Request.class;
}
@Override
public Class<DialogExchange.Response> getResponseClass() {
return DialogExchange.Response.class;
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
DataSourceConfigInstance instance;
String key;
String value;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
DataSourceConfigInstance instance;
String errorMsg;
}
}

View file

@ -0,0 +1,45 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.source.DataSourceInfo;
import io.xpipe.core.store.DataStore;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class InfoExchange implements MessageExchange<InfoExchange.Request, InfoExchange.Response> {
@Override
public String getId() {
return "info";
}
@Override
public Class<InfoExchange.Request> getRequestClass() {
return InfoExchange.Request.class;
}
@Override
public Class<InfoExchange.Response> getResponseClass() {
return InfoExchange.Response.class;
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
DataSourceId id;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
DataSourceInfo info;
DataStore store;
DataSourceConfigInstance config;
}
}

View file

@ -0,0 +1,47 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.store.DataStore;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class ReadExecuteExchange implements MessageExchange<ReadExecuteExchange.Request, ReadExecuteExchange.Response> {
@Override
public String getId() {
return "readExecute";
}
@Override
public Class<ReadExecuteExchange.Request> getRequestClass() {
return ReadExecuteExchange.Request.class;
}
@Override
public Class<ReadExecuteExchange.Response> getResponseClass() {
return ReadExecuteExchange.Response.class;
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
@NonNull
DataStore dataStore;
@NonNull
DataSourceConfigInstance config;
@NonNull
DataSourceId targetId;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
}
}

View file

@ -0,0 +1,43 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.store.DataStore;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class ReadPreparationExchange implements MessageExchange<ReadPreparationExchange.Request, ReadPreparationExchange.Response> {
@Override
public String getId() {
return "readPreparation";
}
@Override
public Class<ReadPreparationExchange.Request> getRequestClass() {
return ReadPreparationExchange.Request.class;
}
@Override
public Class<ReadPreparationExchange.Response> getResponseClass() {
return ReadPreparationExchange.Response.class;
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
String providerType;
String dataStore;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
DataSourceConfigInstance config;
DataStore dataStore;
}
}

View file

@ -0,0 +1,39 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceId;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class SelectExchange implements MessageExchange<SelectExchange.Request, SelectExchange.Response> {
@Override
public String getId() {
return "select";
}
@Override
public Class<SelectExchange.Request> getRequestClass() {
return SelectExchange.Request.class;
}
@Override
public Class<SelectExchange.Response> getResponseClass() {
return SelectExchange.Response.class;
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
DataSourceId id;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
}
}

View file

@ -0,0 +1,47 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.store.DataStore;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class WriteExecuteExchange implements MessageExchange<WriteExecuteExchange.Request, WriteExecuteExchange.Response> {
@Override
public String getId() {
return "writeExecute";
}
@Override
public Class<WriteExecuteExchange.Request> getRequestClass() {
return WriteExecuteExchange.Request.class;
}
@Override
public Class<WriteExecuteExchange.Response> getResponseClass() {
return WriteExecuteExchange.Response.class;
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
@NonNull
DataSourceId sourceId;
@NonNull
DataStore dataStore;
@NonNull
DataSourceConfigInstance config;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
}
}

View file

@ -0,0 +1,50 @@
package io.xpipe.beacon.exchange;
import io.xpipe.beacon.message.RequestMessage;
import io.xpipe.beacon.message.ResponseMessage;
import io.xpipe.core.source.DataSourceConfigInstance;
import io.xpipe.core.source.DataSourceId;
import io.xpipe.core.store.DataStore;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class WritePreparationExchange implements MessageExchange<WritePreparationExchange.Request, WritePreparationExchange.Response> {
@Override
public String getId() {
return "writePreparation";
}
@Override
public Class<WritePreparationExchange.Request> getRequestClass() {
return WritePreparationExchange.Request.class;
}
@Override
public Class<WritePreparationExchange.Response> getResponseClass() {
return WritePreparationExchange.Response.class;
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {
String providerType;
String output;
@NonNull
DataSourceId sourceId;
}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
@NonNull
DataStore dataStore;
@NonNull
DataSourceConfigInstance config;
}
}

View file

@ -25,5 +25,12 @@ module io.xpipe.beacon {
StatusExchange,
StopExchange,
StoreResourceExchange,
WritePreparationExchange,
WriteExecuteExchange,
SelectExchange,
ReadPreparationExchange,
ReadExecuteExchange,
DialogExchange,
InfoExchange,
VersionExchange;
}

View file

@ -50,6 +50,10 @@ public class TupleType extends DataType {
return new TupleType(Collections.nCopies(types.size(), null), types);
}
public boolean hasAllNames() {
return names.stream().allMatch(Objects::nonNull);
}
@Override
public String getName() {
return "tuple";

View file

@ -1,49 +1,29 @@
package io.xpipe.core.source;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Singular;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.List;
@Value
@Builder
@Jacksonized
public class DataSourceConfig {
String description;
private String description;
private List<Option<?>> options;
@Singular
List<Option> options;
public DataSourceConfig(String description, List<Option<?>> options) {
this.description = description;
this.options = options;
}
public String getDescription() {
return description;
}
public List<Option<?>> getOptions() {
return options;
}
public abstract static class Option<T> {
private final String name;
protected T value;
public Option(String name) {
this.name = name;
this.value = null;
}
public Option(String name, T value) {
this.name = name;
this.value = value;
}
protected abstract String enterValue(String val);
public String getName() {
return name;
}
public T getValue() {
return value;
}
@Value
@Builder
@Jacksonized
@AllArgsConstructor
public static class Option {
String name;
String key;
}
}

View file

@ -0,0 +1,19 @@
package io.xpipe.core.source;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.Map;
@Value
@Builder
@Jacksonized
@AllArgsConstructor
public class DataSourceConfigInstance {
String provider;
DataSourceConfig config;
Map<String, String> currentValues;
}

View file

@ -22,8 +22,11 @@ import lombok.Getter;
public class DataSourceId {
public static final char SEPARATOR = ':';
public static final DataSourceId ANONYMOUS = new DataSourceId(null, null);
private final String collectionName;
private final String entryName;
@JsonCreator
private DataSourceId(String collectionName, String entryName) {
this.collectionName = collectionName;
@ -38,6 +41,10 @@ public class DataSourceId {
* @throws IllegalArgumentException if any name is not valid
*/
public static DataSourceId create(String collectionName, String entryName) {
if (collectionName == null && entryName == null) {
return ANONYMOUS;
}
if (collectionName != null && collectionName.trim().length() == 0) {
throw new IllegalArgumentException("Trimmed collection name is empty");
}
@ -70,6 +77,10 @@ public class DataSourceId {
throw new IllegalArgumentException("String is null");
}
if (s.equals(String.valueOf(SEPARATOR))) {
return ANONYMOUS;
}
var split = s.split(String.valueOf(SEPARATOR));
if (split.length != 2) {
throw new IllegalArgumentException("Data source id must contain exactly one " + SEPARATOR);
@ -87,6 +98,6 @@ public class DataSourceId {
@Override
public String toString() {
return (collectionName != null ? collectionName : "") + SEPARATOR + entryName;
return (collectionName != null ? collectionName.toLowerCase() : "") + SEPARATOR + (entryName != null ? entryName.toLowerCase() : "");
}
}

View file

@ -36,4 +36,9 @@ public class InputStreamDataStore implements StreamDataStore {
public OutputStream openOutput() throws Exception {
throw new UnsupportedOperationException("No output available");
}
@Override
public boolean exists() {
return true;
}
}

View file

@ -2,6 +2,7 @@ package io.xpipe.core.store;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonTypeName;
import lombok.EqualsAndHashCode;
import java.io.BufferedInputStream;
import java.io.IOException;
@ -13,6 +14,7 @@ import java.time.Instant;
import java.util.Optional;
@JsonTypeName("local")
@EqualsAndHashCode
public class LocalFileDataStore implements StreamDataStore {
private final Path file;
@ -50,4 +52,9 @@ public class LocalFileDataStore implements StreamDataStore {
public OutputStream openOutput() throws Exception {
return Files.newOutputStream(file);
}
@Override
public boolean exists() {
return Files.exists(file);
}
}

View file

@ -26,4 +26,9 @@ public class RemoteFileDataStore implements StreamDataStore {
public OutputStream openOutput() throws Exception {
return null;
}
@Override
public boolean exists() {
return false;
}
}

View file

@ -1,7 +1,14 @@
package io.xpipe.core.store;
import lombok.NonNull;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Optional;
/**
* A data store that can be accessed using InputStreams and/or OutputStreams.
@ -9,6 +16,21 @@ import java.io.OutputStream;
*/
public interface StreamDataStore extends DataStore {
static Optional<StreamDataStore> fromString(@NonNull String s) {
try {
var path = Path.of(s);
return Optional.of(new LocalFileDataStore(path));
} catch (InvalidPathException ignored) {
}
try {
var path = new URL(s);
} catch (MalformedURLException ignored) {
}
return Optional.empty();
}
/**
* Opens an input stream. This input stream does not necessarily have to be a new instance.
*/
@ -18,4 +40,6 @@ public interface StreamDataStore extends DataStore {
* Opens an output stream. This output stream does not necessarily have to be a new instance.
*/
OutputStream openOutput() throws Exception;
boolean exists();
}

View file

@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.module.SimpleModule;
import io.xpipe.core.data.type.ArrayType;
@ -35,6 +36,8 @@ public class CoreJacksonModule extends SimpleModule {
addSerializer(Path.class, new LocalPathSerializer());
addDeserializer(Path.class, new LocalPathDeserializer());
context.setMixInAnnotations(Throwable.class, ExceptionTypeMixIn.class);
}
public static class CharsetSerializer extends JsonSerializer<Charset> {
@ -70,4 +73,10 @@ public class CoreJacksonModule extends SimpleModule {
return Path.of(p.getValueAsString());
}
}
@JsonSerialize(as = Throwable.class)
public abstract static class ExceptionTypeMixIn {
}
}

View file

@ -7,9 +7,11 @@ plugins {
apply from: "$rootDir/deps/java.gradle"
apply from: "$rootDir/deps/javafx.gradle"
apply from: "$rootDir/deps/richtextfx.gradle"
apply from: "$rootDir/deps/jackson.gradle"
apply from: "$rootDir/deps/commons.gradle"
apply from: "$rootDir/deps/lombok.gradle"
apply from: "$rootDir/deps/ikonli.gradle"
apply from: 'publish.gradle'
apply from: "$rootDir/deps/publish-base.gradle"

View file

@ -1,19 +1,23 @@
package io.xpipe.extension;
import io.xpipe.core.source.DataSourceConfig;
import io.xpipe.core.source.DataSourceDescriptor;
import io.xpipe.core.source.DataSourceInfo;
import io.xpipe.core.source.TableReadConnection;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.StreamDataStore;
import javafx.beans.property.Property;
import javafx.scene.layout.Region;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
public interface DataSourceProvider {
interface FileProvider {
boolean supportsFile(Path file);
void write(StreamDataStore store, DataSourceDescriptor<StreamDataStore> desc, TableReadConnection con) throws Exception;
String getFileName();
@ -33,8 +37,45 @@ public interface DataSourceProvider {
interface CliProvider {
static String booleanName(String name) {
return name + " (y/n)";
}
static Function<String, Boolean> booleanConverter() {
return s -> {
if (s.equalsIgnoreCase("y") || s.equalsIgnoreCase("yes")) {
return true;
}
if (s.equalsIgnoreCase("n") || s.equalsIgnoreCase("no")) {
return false;
}
throw new IllegalArgumentException("Invalid boolean: " + s);
};
}
static Function<String, Character> characterConverter() {
return s -> {
if (s.length() != 1) {
throw new IllegalArgumentException("Invalid character: " + s);
}
return s.toCharArray()[0];
};
}
DataSourceConfig getConfig();
DataSourceDescriptor<?> toDescriptor(Map<String, String> values);
Map<String, String> toConfigOptions(DataSourceDescriptor<?> desc);
Map<DataSourceConfig.Option, Function<String, ?>> getConverters();
}
boolean supportsStore(DataStore store);
FileProvider getFileProvider();
GuiProvider getGuiProvider();
@ -49,5 +90,7 @@ public interface DataSourceProvider {
*/
DataSourceDescriptor<?> createDefaultDescriptor(DataStore input) throws Exception;
DataSourceDescriptor<?> createDefaultWriteDescriptor(DataStore input, DataSourceInfo info) throws Exception;
Class<? extends DataSourceDescriptor<?>> getDescriptorClass();
}

View file

@ -2,12 +2,10 @@ package io.xpipe.extension;
import io.xpipe.core.data.type.TupleType;
import io.xpipe.core.source.TableDataSourceDescriptor;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.LocalFileDataStore;
import io.xpipe.extension.event.ErrorEvent;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
@ -52,26 +50,13 @@ public class DataSourceProviders {
return ALL.stream().filter(d -> d.getId().equals(name)).findAny();
}
public static Optional<DataSourceProvider> byFile(Path file) {
public static Optional<DataSourceProvider> byStore(DataStore store) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
return ALL.stream().filter(d -> d.getFileProvider() != null)
.filter(d -> d.getFileProvider().supportsFile(file)).findAny();
}
public static Optional<DataSourceProvider> byURL(URL url) {
if (ALL == null) {
throw new IllegalStateException("Not initialized");
}
try {
var path = Path.of(url.toURI());
return byFile(path);
} catch (URISyntaxException e) {
return Optional.empty();
}
.filter(d -> d.supportsStore(store)).findAny();
}
public static Set<DataSourceProvider> getAll() {

View file

@ -8,6 +8,11 @@ import java.util.function.Supplier;
public interface SupportedApplicationProvider {
enum Category {
PROGRAMMING_LANGUAGE,
APPLICATION
}
Region createTableRetrieveInstructions(ObservableValue<DataSourceId> id);
Region createStructureRetrieveInstructions(ObservableValue<DataSourceId> id);
@ -19,4 +24,12 @@ public interface SupportedApplicationProvider {
String getId();
Supplier<String> getName();
Category getCategory();
String getSetupGuideURL();
default String getGraphicIcon() {
return null;
}
}

View file

@ -0,0 +1,99 @@
package io.xpipe.extension.comp;
import javafx.scene.paint.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public record CodeSnippet(List<CodeSnippet.Line> lines) {
public String getRawString() {
return lines.stream().map(line -> line.elements().stream()
.map(Element::text).collect(Collectors.joining()))
.collect(Collectors.joining(System.lineSeparator()));
}
public static Builder builder(CodeSnippets.ColorScheme scheme) {
return new Builder(scheme);
}
public static interface Element {
String text();
Color color();
}
public static class Builder {
private CodeSnippets.ColorScheme scheme;
private List<Line> lines;
private List<Element> currentLine;
public Builder(CodeSnippets.ColorScheme scheme) {
this.scheme = scheme;
lines = new ArrayList<>();
currentLine = new ArrayList<>();
}
public Builder keyword(String text) {
currentLine.add(new CodeSnippet.StaticElement(text, scheme.keyword()));
return this;
}
public Builder string(String text) {
currentLine.add(new CodeSnippet.StaticElement(text, scheme.string()));
return this;
}
public Builder identifier(String text) {
currentLine.add(new CodeSnippet.StaticElement(text, scheme.identifier()));
return this;
}
public Builder type(String text) {
currentLine.add(new CodeSnippet.StaticElement(text, scheme.type()));
return this;
}
public Builder text(String text, Color c) {
currentLine.add(new CodeSnippet.StaticElement(text, c));
return this;
}
public Builder space() {
currentLine.add(new CodeSnippet.StaticElement(" ", Color.TRANSPARENT));
return this;
}
public Builder space(int count) {
currentLine.add(new CodeSnippet.StaticElement(" ".repeat(count), Color.TRANSPARENT));
return this;
}
public Builder newLine() {
lines.add(new Line(new ArrayList<>(currentLine)));
currentLine.clear();
return this;
}
public CodeSnippet build() {
if (currentLine.size() > 0) {
newLine();
}
return new CodeSnippet(lines);
}
}
public static record StaticElement(String value, Color color) implements Element {
@Override
public String text() {
return value();
}
}
public static record Line(List<CodeSnippet.Element> elements) {
}
}

View file

@ -0,0 +1,113 @@
package io.xpipe.extension.comp;
import io.xpipe.fxcomps.Comp;
import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.util.PlatformUtil;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import org.fxmisc.richtext.InlineCssTextArea;
import org.kordamp.ikonli.javafx.FontIcon;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
public class CodeSnippetComp extends Comp<CompStructure<StackPane>> {
private final ObservableValue<CodeSnippet> value;
public CodeSnippetComp(ObservableValue<CodeSnippet> value) {
this.value = PlatformUtil.wrap(value);
}
private static String toRGBCode(Color color) {
return String.format("#%02X%02X%02X",
(int) (color.getRed() * 255),
(int) (color.getGreen() * 255),
(int) (color.getBlue() * 255));
}
private void fillArea(VBox lineNumbers, InlineCssTextArea s) {
lineNumbers.getChildren().clear();
s.clear();
int number = 1;
for (CodeSnippet.Line line : value.getValue().lines()) {
var numberLabel = new Label(String.valueOf(number));
numberLabel.getStyleClass().add("line-number");
lineNumbers.getChildren().add(numberLabel);
for (var el : line.elements()) {
String hex = toRGBCode(el.color());
s.append(el.text(), "-fx-fill: " + hex + ";");
}
boolean last = number == value.getValue().lines().size();
if (!last) {
s.appendText("\n");
}
number++;
}
}
private Region createCopyButton(Region container) {
var button = new Button();
button.setGraphic(new FontIcon("mdoal-content_copy"));
button.setOnAction(e -> {
var string = new StringSelection(value.getValue().getRawString());
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(string, string);
});
button.getStyleClass().add("copy");
button.getStyleClass().add("button-comp");
button.visibleProperty().bind(container.hoverProperty());
return button;
}
@Override
public CompStructure<StackPane> createBase() {
var s = new InlineCssTextArea();
s.setEditable(false);
s.setBackground(null);
s.getStyleClass().add("code-snippet");
s.addEventFilter(ScrollEvent.ANY, e -> {
s.getParent().fireEvent(e);
e.consume();
});
var lineNumbers = new VBox();
lineNumbers.getStyleClass().add("line-numbers");
fillArea(lineNumbers, s);
value.addListener((c,o,n) -> {
PlatformUtil.runLaterIfNeeded(() -> {
fillArea(lineNumbers, s);
});
});
var spacer = new Region();
spacer.getStyleClass().add("spacer");
var content = new HBox(lineNumbers, spacer, s);
HBox.setHgrow(s, Priority.ALWAYS);
var container = new ScrollPane(content);
container.setFitToWidth(true);
var c = new StackPane(container);
c.getStyleClass().add("code-snippet-container");
var copyButton = createCopyButton(c);
var pane = new AnchorPane(copyButton);
AnchorPane.setTopAnchor(copyButton, 10.0);
AnchorPane.setRightAnchor(copyButton, 10.0);
c.getChildren().add(pane);
return new CompStructure<>(c);
}
}

View file

@ -0,0 +1,18 @@
package io.xpipe.extension.comp;
import javafx.scene.paint.Color;
public class CodeSnippets {
public static final ColorScheme LIGHT_MODE = new ColorScheme(
Color.valueOf("0033B3"),
Color.valueOf("000000"),
Color.valueOf("000000"),
Color.valueOf("067D17")
);
public static record ColorScheme(
Color keyword, Color identifier, Color type, Color string) {
}
}

View file

@ -18,4 +18,12 @@ module io.xpipe.extension {
uses SupportedApplicationProvider;
uses io.xpipe.extension.I18n;
uses io.xpipe.extension.event.EventHandler;
requires java.desktop;
requires org.fxmisc.richtext;
requires org.fxmisc.flowless;
requires org.fxmisc.undofx;
requires org.fxmisc.wellbehavedfx;
requires org.reactfx;
requires org.kordamp.ikonli.javafx;
}

View file

@ -0,0 +1,41 @@
.code-snippet {
-fx-padding: 0.3em 0.5em 0.3em 0.5em;
-fx-border-width: 0 0 0 1px;
-fx-border-color: -xp-border;
-fx-spacing: 0;
-fx-font-family: Monospace;
}
.code-snippet-container {
-fx-border-width: 1px;
-fx-border-color: -xp-border;
-fx-background-color: transparent;
-fx-border-radius: 4px;
-fx-background-radius: 4px;
-fx-padding: 3px;
}
.copy {
}
.line-number {
-fx-alignment: center-right;
-fx-font-family: Monospace;
-fx-text-fill: grey;
}
.line-numbers {
-fx-padding: 0.3em 0.2em 0.3em 0.5em;
-fx-background-color: #073B4C11;
}
.code-snippet .line {
-fx-padding: 0.1em 0 0 0;
-fx-background-color: transparent;
}
.code-snippet .spacer {
-fx-pref-width: 1px;
-fx-background-color: #073B4C43;
}