Various small fixes

This commit is contained in:
Christopher Schnick 2022-10-22 20:00:19 +02:00
parent aa07f88e1d
commit 38efb368ec
13 changed files with 466 additions and 79 deletions

View file

@ -73,7 +73,7 @@ public final class XPipeConnection extends BeaconConnection {
} catch (InterruptedException ignored) { } catch (InterruptedException ignored) {
} }
var s = BeaconClient.tryConnect(); var s = BeaconClient.tryConnect(BeaconClient.ApiClientInformation.builder().version("?").language("Java").build());
if (s.isPresent()) { if (s.isPresent()) {
return s; return s;
} }
@ -114,7 +114,7 @@ public final class XPipeConnection extends BeaconConnection {
} }
try { try {
beaconClient = new BeaconClient(); beaconClient = BeaconClient.connect(BeaconClient.ApiClientInformation.builder().version("?").language("Java").build());
} catch (Exception ex) { } catch (Exception ex) {
throw new BeaconException("Unable to connect to running xpipe daemon", ex); throw new BeaconException("Unable to connect to running xpipe daemon", ex);
} }

View file

@ -37,7 +37,7 @@ public class XPipeDaemonController {
return; return;
} }
var client = new BeaconClient(); var client = BeaconClient.connect(BeaconClient.ApiClientInformation.builder().version("?").language("Java API Test").build());
if (!BeaconServer.tryStop(client)) { if (!BeaconServer.tryStop(client)) {
throw new AssertionError(); throw new AssertionError();
} }

View file

@ -1,5 +1,7 @@
package io.xpipe.beacon; package io.xpipe.beacon;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
@ -9,7 +11,12 @@ import com.fasterxml.jackson.databind.node.TextNode;
import io.xpipe.beacon.exchange.MessageExchanges; import io.xpipe.beacon.exchange.MessageExchanges;
import io.xpipe.beacon.exchange.data.ClientErrorMessage; import io.xpipe.beacon.exchange.data.ClientErrorMessage;
import io.xpipe.beacon.exchange.data.ServerErrorMessage; import io.xpipe.beacon.exchange.data.ServerErrorMessage;
import io.xpipe.core.store.ProcessControl;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.io.*; import java.io.*;
import java.net.InetAddress; import java.net.InetAddress;
@ -23,27 +30,90 @@ import static io.xpipe.beacon.BeaconConfig.BODY_SEPARATOR;
public class BeaconClient implements AutoCloseable { public class BeaconClient implements AutoCloseable {
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
public static abstract class ClientInformation {
public final CliClientInformation cli() {
return (CliClientInformation) this;
}
public abstract String toDisplayString();
}
@JsonTypeName("cli")
@Value
@Builder
@Jacksonized
@EqualsAndHashCode(callSuper = false)
public static class CliClientInformation extends ClientInformation {
String version;
int consoleWidth;
@Override
public String toDisplayString() {
return "X-Pipe CLI " + version;
}
}
@JsonTypeName("gateway")
@Value
@Builder
@Jacksonized
@EqualsAndHashCode(callSuper = false)
public static class GatewayClientInformation extends ClientInformation {
String version;
@Override
public String toDisplayString() {
return "X-Pipe Gateway " + version;
}
}
@JsonTypeName("api")
@Value
@Builder
@Jacksonized
@EqualsAndHashCode(callSuper = false)
public static class ApiClientInformation extends ClientInformation {
String version;
String language;
@Override
public String toDisplayString() {
return String.format("X-Pipe %s API v%s", language, version);
}
}
private final Closeable closeable; private final Closeable closeable;
private final InputStream in; private final InputStream in;
private final OutputStream out; private final OutputStream out;
public BeaconClient() throws IOException { private BeaconClient(Closeable closeable, InputStream in, OutputStream out) {
var socket = new Socket(InetAddress.getLoopbackAddress(), BeaconConfig.getUsedPort());
closeable = socket;
in = socket.getInputStream();
out = socket.getOutputStream();
}
public BeaconClient(Closeable closeable, InputStream in, OutputStream out) {
this.closeable = closeable; this.closeable = closeable;
this.in = in; this.in = in;
this.out = out; this.out = out;
} }
public static Optional<BeaconClient> tryConnect() { public static BeaconClient connect(ClientInformation information) throws Exception {
var socket = new Socket(InetAddress.getLoopbackAddress(), BeaconConfig.getUsedPort());
var client = new BeaconClient(socket, socket.getInputStream(), socket.getOutputStream());
client.sendObject(JacksonMapper.newMapper().valueToTree(information));
return client;
}
public static BeaconClient connectGateway(ProcessControl control, GatewayClientInformation information) throws Exception {
var client = new BeaconClient(() -> {}, control.getStdout(), control.getStdin());
client.sendObject(JacksonMapper.newMapper().valueToTree(information));
return client;
}
public static Optional<BeaconClient> tryConnect(ClientInformation information) {
try { try {
return Optional.of(new BeaconClient()); return Optional.of(connect(information));
} catch (IOException ex) { } catch (Exception ex) {
return Optional.empty(); return Optional.empty();
} }
} }
@ -95,10 +165,14 @@ public class BeaconClient implements AutoCloseable {
"Sending request to server of type " + req.getClass().getName()); "Sending request to server of type " + req.getClass().getName());
} }
sendObject(msg);
}
public void sendObject(JsonNode node) throws ConnectorException {
var writer = new StringWriter(); var writer = new StringWriter();
var mapper = JacksonMapper.newMapper(); var mapper = JacksonMapper.newMapper();
try (JsonGenerator g = mapper.createGenerator(writer).setPrettyPrinter(new DefaultPrettyPrinter())) { try (JsonGenerator g = mapper.createGenerator(writer).setPrettyPrinter(new DefaultPrettyPrinter())) {
g.writeTree(msg); g.writeTree(node);
} catch (IOException ex) { } catch (IOException ex) {
throw new ConnectorException("Couldn't serialize request", ex); throw new ConnectorException("Couldn't serialize request", ex);
} }

View file

@ -0,0 +1,15 @@
package io.xpipe.beacon;
import com.fasterxml.jackson.databind.jsontype.NamedType;
import com.fasterxml.jackson.databind.module.SimpleModule;
public class BeaconJacksonModule extends SimpleModule {
@Override
public void setupModule(SetupContext context) {
context.registerSubtypes(
new NamedType(BeaconClient.ApiClientInformation.class),
new NamedType(BeaconClient.CliClientInformation.class),
new NamedType(BeaconClient.GatewayClientInformation.class));
}
}

View file

@ -24,7 +24,7 @@ public class BeaconServer {
} }
public static boolean isRunning() { public static boolean isRunning() {
try (var socket = new BeaconClient()) { try (var socket = BeaconClient.connect(null)) {
return true; return true;
} catch (Exception e) { } catch (Exception e) {
return false; return false;

View file

@ -1,3 +1,5 @@
import com.fasterxml.jackson.databind.Module;
import io.xpipe.beacon.BeaconJacksonModule;
import io.xpipe.beacon.exchange.*; import io.xpipe.beacon.exchange.*;
import io.xpipe.beacon.exchange.api.QueryRawDataExchange; import io.xpipe.beacon.exchange.api.QueryRawDataExchange;
import io.xpipe.beacon.exchange.api.QueryTableDataExchange; import io.xpipe.beacon.exchange.api.QueryTableDataExchange;
@ -24,6 +26,7 @@ module io.xpipe.beacon {
uses MessageExchange; uses MessageExchange;
provides Module with BeaconJacksonModule;
provides io.xpipe.beacon.exchange.MessageExchange with provides io.xpipe.beacon.exchange.MessageExchange with
ForwardExchange, ForwardExchange,
InstanceExchange, InstanceExchange,

View file

@ -16,6 +16,7 @@ public abstract class ProcessControl {
pc.discardErr(); pc.discardErr();
var bytes = pc.getStdout().readAllBytes(); var bytes = pc.getStdout().readAllBytes();
var string = new String(bytes, pc.getCharset()); var string = new String(bytes, pc.getCharset());
pc.waitFor();
return string; return string;
} }
@ -27,6 +28,15 @@ public abstract class ProcessControl {
pc.waitFor(); pc.waitFor();
} }
public boolean executeAndCheckStatus() {
try {
executeOrThrow();
return true;
} catch (Exception ex) {
return false;
}
}
public Optional<String> executeAndReadStderrIfPresent() throws Exception { public Optional<String> executeAndReadStderrIfPresent() throws Exception {
var pc = this; var pc = this;
pc.start(); pc.start();

View file

@ -0,0 +1,48 @@
package io.xpipe.extension;
import io.xpipe.core.store.DataStore;
import io.xpipe.extension.event.ErrorEvent;
import javafx.scene.layout.Region;
import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
public interface DataStoreActionProvider<T extends DataStore> {
static List<DataStoreActionProvider<?>> ALL = new ArrayList<>();
public static void init(ModuleLayer layer) {
if (ALL.size() == 0) {
ALL.addAll(ServiceLoader.load(layer, DataStoreActionProvider.class).stream()
.map(p -> (DataStoreActionProvider<?>) p.get())
.filter(provider -> {
try {
return provider.isActive();
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return false;
}
})
.toList());
}
}
Class<T> getApplicableClass();
default boolean isActive() throws Exception {
return true;
}
default boolean isApplicable(T o) throws Exception {
return true;
}
default void applyToRegion(T store, Region region) {}
String getName(T store);
String getIcon(T store);
default void execute(T store) throws Exception {}
}

View file

@ -6,17 +6,204 @@ import io.xpipe.fxcomps.CompStructure;
import io.xpipe.fxcomps.augment.Augment; import io.xpipe.fxcomps.augment.Augment;
import io.xpipe.fxcomps.util.PlatformThread; import io.xpipe.fxcomps.util.PlatformThread;
import io.xpipe.fxcomps.util.Shortcuts; import io.xpipe.fxcomps.util.Shortcuts;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.event.WeakEventHandler;
import javafx.geometry.NodeOrientation;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.stage.Window; import javafx.stage.Window;
import javafx.util.Duration; import javafx.util.Duration;
public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<S> { public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<S> {
static { private static final TooltipBehavior BEHAVIOR = new TooltipBehavior(Duration.millis(400), Duration.INDEFINITE, Duration.millis(100));
JFXTooltip.setHoverDelay(Duration.millis(400));
JFXTooltip.setVisibleDuration(Duration.INDEFINITE); private static class TooltipBehavior {
private static String TOOLTIP_PROP = "jfoenix-tooltip";
private Timeline hoverTimer = new Timeline();
private Timeline visibleTimer = new Timeline();
private Timeline leftTimer = new Timeline();
/**
* the currently hovered node
*/
private Node hoveredNode;
/**
* the next tooltip to be shown
*/
private JFXTooltip nextTooltip;
/**
* the current showing tooltip
*/
private JFXTooltip currentTooltip;
private TooltipBehavior(Duration hoverDelay, Duration visibleDuration, Duration leftDelay) {
setHoverDelay(hoverDelay);
hoverTimer.setOnFinished(event -> {
ensureHoveredNodeIsVisible(() -> {
// set tooltip orientation
NodeOrientation nodeOrientation = hoveredNode.getEffectiveNodeOrientation();
nextTooltip.getScene().setNodeOrientation(nodeOrientation);
//show tooltip
showTooltip(nextTooltip);
currentTooltip = nextTooltip;
hoveredNode = null;
// start visible timer
visibleTimer.playFromStart();
});
// clear next tooltip
nextTooltip = null;
});
setVisibleDuration(visibleDuration);
visibleTimer.setOnFinished(event -> hideCurrentTooltip());
setLeftDelay(leftDelay);
leftTimer.setOnFinished(event -> hideCurrentTooltip());
}
private void setHoverDelay(Duration duration) {
hoverTimer.getKeyFrames().setAll(new KeyFrame(duration));
}
private void setVisibleDuration(Duration duration) {
visibleTimer.getKeyFrames().setAll(new KeyFrame(duration));
}
private void setLeftDelay(Duration duration) {
leftTimer.getKeyFrames().setAll(new KeyFrame(duration));
}
private void hideCurrentTooltip() {
currentTooltip.hide();
currentTooltip = null;
hoveredNode = null;
}
private void showTooltip(JFXTooltip tooltip) {
// anchors are computed differently for each tooltip
tooltip.show(hoveredNode, -1, -1);
}
private EventHandler<MouseEvent> moveHandler = (MouseEvent event) -> {
// if tool tip is already showing, do nothing
if (visibleTimer.getStatus() == Timeline.Status.RUNNING) {
return;
}
hoveredNode = (Node) event.getSource();
Object property = hoveredNode.getProperties().get(TOOLTIP_PROP);
if (property instanceof JFXTooltip) {
JFXTooltip tooltip = (JFXTooltip) property;
ensureHoveredNodeIsVisible(() -> {
// if a tooltip is already showing then show this tooltip immediately
if (leftTimer.getStatus() == Timeline.Status.RUNNING) {
if (currentTooltip != null) {
currentTooltip.hide();
}
currentTooltip = tooltip;
// show the tooltip
showTooltip(tooltip);
// stop left timer and start the visible timer to hide the tooltip
// once finished
leftTimer.stop();
visibleTimer.playFromStart();
} else {
// else mark the tooltip as the next tooltip to be shown once the hover
// timer is finished (restart the timer)
// t.setActivated(true);
nextTooltip = tooltip;
hoverTimer.stop();
hoverTimer.playFromStart();
}
});
} else {
uninstall(hoveredNode);
}
};
private WeakEventHandler<MouseEvent> weakMoveHandler = new WeakEventHandler<>(moveHandler);
private EventHandler<MouseEvent> exitHandler = (MouseEvent event) -> {
// stop running hover timer as the mouse exited the node
if (hoverTimer.getStatus() == Timeline.Status.RUNNING) {
hoverTimer.stop();
} else if (visibleTimer.getStatus() == Timeline.Status.RUNNING) {
// if tool tip was already showing, stop the visible timer
// and start the left timer to hide the current tooltip
visibleTimer.stop();
leftTimer.playFromStart();
}
hoveredNode = null;
nextTooltip = null;
};
private WeakEventHandler<MouseEvent> weakExitHandler = new WeakEventHandler<>(exitHandler);
// if mouse is pressed then stop all timers / clear all fields
private EventHandler<MouseEvent> pressedHandler = (MouseEvent event) -> {
// stop timers
hoverTimer.stop();
visibleTimer.stop();
leftTimer.stop();
// hide current tooltip
if (currentTooltip != null) {
currentTooltip.hide();
}
// clear fields
hoveredNode = null;
currentTooltip = null;
nextTooltip = null;
};
private WeakEventHandler<MouseEvent> weakPressedHandler = new WeakEventHandler<>(pressedHandler);
private void install(Node node, JFXTooltip tooltip) {
if (node == null) {
return;
}
if (tooltip == null) {
uninstall(node);
return;
}
node.removeEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler);
node.removeEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler);
node.removeEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler);
node.addEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler);
node.addEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler);
node.addEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler);
node.getProperties().put(TOOLTIP_PROP, tooltip);
}
private void uninstall(Node node) {
if (node == null) {
return;
}
node.removeEventHandler(MouseEvent.MOUSE_MOVED, weakMoveHandler);
node.removeEventHandler(MouseEvent.MOUSE_EXITED, weakExitHandler);
node.removeEventHandler(MouseEvent.MOUSE_PRESSED, weakPressedHandler);
Object tooltip = node.getProperties().get(TOOLTIP_PROP);
if (tooltip != null) {
node.getProperties().remove(TOOLTIP_PROP);
if (tooltip.equals(currentTooltip) || tooltip.equals(nextTooltip)) {
weakPressedHandler.handle(null);
}
}
}
private void ensureHoveredNodeIsVisible(Runnable visibleRunnable) {
final Window owner = getWindow(hoveredNode);
if (owner != null && owner.isShowing()) {
final boolean treeVisible = true; // NodeHelper.isTreeVisible(hoveredNode);
if (treeVisible && owner.isFocused()) {
visibleRunnable.run();
}
}
}
private Window getWindow(final Node node) {
final Scene scene = node == null ? null : node.getScene();
return scene == null ? null : scene.getWindow();
}
} }
private final ObservableValue<String> text; private final ObservableValue<String> text;
@ -31,21 +218,11 @@ public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<
@Override @Override
public void augment(S struc) { public void augment(S struc) {
var tt = new FocusTooltip(); augment(struc.get());
var toDisplay = text.getValue();
if (Shortcuts.getShortcut(struc.get()) != null) {
toDisplay = toDisplay + " (" + Shortcuts.getShortcut(struc.get()).getDisplayText() + ")";
}
tt.textProperty().setValue(toDisplay);
tt.setStyle("-fx-font-size: 11pt;");
JFXTooltip.install(struc.get(), tt);
tt.setWrapText(true);
tt.setMaxWidth(400);
tt.getStyleClass().add("fancy-tooltip");
} }
public void augment(Node region) { public void augment(Node region) {
var tt = new FocusTooltip(); var tt = new JFXTooltip();
var toDisplay = text.getValue(); var toDisplay = text.getValue();
if (Shortcuts.getShortcut((Region) region) != null) { if (Shortcuts.getShortcut((Region) region) != null) {
toDisplay = toDisplay =
@ -53,57 +230,10 @@ public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<
} }
tt.textProperty().setValue(toDisplay); tt.textProperty().setValue(toDisplay);
tt.setStyle("-fx-font-size: 11pt;"); tt.setStyle("-fx-font-size: 11pt;");
JFXTooltip.install(region, tt);
tt.setWrapText(true); tt.setWrapText(true);
tt.setMaxWidth(400); tt.setMaxWidth(400);
tt.getStyleClass().add("fancy-tooltip"); tt.getStyleClass().add("fancy-tooltip");
}
private static class FocusTooltip extends JFXTooltip { BEHAVIOR.install(region, tt);
public FocusTooltip() {}
public FocusTooltip(String string) {
super(string);
}
@Override
protected void show() {
Window owner = getOwnerWindow();
if (owner == null || owner.isFocused()) {
super.show();
}
}
@Override
public void show(Node ownerNode, double anchorX, double anchorY) {
Window owner = getOwnerWindow();
if (owner == null || owner.isFocused()) {
super.show(ownerNode, anchorX, anchorY);
}
}
@Override
public void showOnAnchors(Node ownerNode, double anchorX, double anchorY) {
Window owner = getOwnerWindow();
if (owner == null || owner.isFocused()) {
super.showOnAnchors(ownerNode, anchorX, anchorY);
}
}
@Override
public void show(Window owner) {
if (owner == null || owner.isFocused()) {
super.show(owner);
}
}
@Override
public void show(Window ownerWindow, double anchorX, double anchorY) {
Window owner = getOwnerWindow();
if (owner == null || owner.isFocused()) {
super.show(ownerWindow, anchorX, anchorY);
}
}
} }
} }

View file

@ -0,0 +1,62 @@
package io.xpipe.extension.util;
import io.xpipe.extension.event.ErrorEvent;
import org.apache.commons.lang3.SystemUtils;
import java.awt.*;
import java.nio.file.Path;
import java.nio.file.Paths;
public class OsHelper {
public static String getFileSystemCompatibleName(String name) {
return name.replaceAll("[\\\\/:*?\"<>|]", "_");
}
public static Path getUserDocumentsPath() {
if (SystemUtils.IS_OS_WINDOWS) {
return Paths.get(System.getProperty("user.home"));
} else {
return Paths.get(System.getProperty("user.home"), ".local", "share");
}
}
public static void browseFile(Path file) {
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
return;
}
ThreadHelper.run(() -> {
try {
Desktop.getDesktop().open(file.toFile());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
});
}
public static void browseFileInDirectory(Path file) {
if (!Desktop.getDesktop().isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
if (!Desktop.getDesktop().isSupported(Desktop.Action.OPEN)) {
return;
}
ThreadHelper.run(() -> {
try {
Desktop.getDesktop().open(file.getParent().toFile());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
});
return;
}
ThreadHelper.run(() -> {
try {
Desktop.getDesktop().browseFileDirectory(file.toFile());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).omit().handle();
}
});
}
}

View file

@ -0,0 +1,42 @@
package io.xpipe.extension.util;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
public class ThreadHelper {
public static Thread run(Runnable r) {
var t = new Thread(r);
t.setDaemon(true);
t.start();
return t;
}
public static <T> T run(Supplier<T> r) {
AtomicReference<T> ret = new AtomicReference<>();
var t = new Thread(() -> ret.set(r.get()));
t.setDaemon(true);
t.start();
try {
t.join();
} catch (InterruptedException e) {
return null;
}
return ret.get();
}
public static Thread create(String name, boolean daemon, Runnable r) {
var t = new Thread(r);
t.setDaemon(daemon);
t.setName(name);
return t;
}
public static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

View file

@ -1,4 +1,5 @@
import io.xpipe.extension.DataSourceProvider; import io.xpipe.extension.DataSourceProvider;
import io.xpipe.extension.DataStoreActionProvider;
import io.xpipe.extension.SupportedApplicationProvider; import io.xpipe.extension.SupportedApplicationProvider;
import io.xpipe.extension.util.XPipeDaemon; import io.xpipe.extension.util.XPipeDaemon;
@ -31,6 +32,7 @@ open module io.xpipe.extension {
uses DataSourceProvider; uses DataSourceProvider;
uses SupportedApplicationProvider; uses SupportedApplicationProvider;
uses DataStoreActionProvider;
uses io.xpipe.extension.I18n; uses io.xpipe.extension.I18n;
uses io.xpipe.extension.event.EventHandler; uses io.xpipe.extension.event.EventHandler;
uses io.xpipe.extension.prefs.PrefsProvider; uses io.xpipe.extension.prefs.PrefsProvider;

View file

@ -0,0 +1 @@
io.xpipe.beacon.BeaconJacksonModule