mirror of
https://github.com/xpipe-io/xpipe.git
synced 2026-05-29 07:20:35 +00:00
Merge branch 16-relase into master
This commit is contained in:
+9
-19
@@ -9,8 +9,7 @@ There are no real formal contribution guidelines right now, they will maybe come
|
||||
This mainly concerns API classes not a lot of implementation.
|
||||
- [beacon](beacon) - The XPipe beacon component is responsible for handling all communications between the XPipe
|
||||
daemon and the client applications, for example APIs and the CLI
|
||||
- [app](app) - Contains the XPipe daemon implementation, the XPipe desktop application, and an
|
||||
API to create all different kinds of extensions for the XPipe platform
|
||||
- [app](app) - Contains the XPipe daemon implementation and the XPipe desktop application
|
||||
- [dist](dist) - Tools to create a distributable package of XPipe
|
||||
- [ext](ext) - Available XPipe extensions. Essentially every concrete feature implementation is implemented as an extension
|
||||
|
||||
@@ -25,15 +24,15 @@ components from it when it is run in a development environment.
|
||||
Note that in case the current master branch is ahead of the latest release, it might happen that there are some incompatibilities when loading data from your local XPipe installation.
|
||||
You should therefore always check out the matching version tag for your local repository and local XPipe installation.
|
||||
You can find the available version tags at https://github.com/xpipe-io/xpipe/tags.
|
||||
So for example if you currently have XPipe `13.0` installed, you should run `git reset --hard 13.0` first to properly compile against it.
|
||||
So for example if you currently have XPipe `16.0` installed, you should run `git reset --hard 16.0` first to properly compile against it.
|
||||
|
||||
You need to have JDK for Java 22 installed to compile the project.
|
||||
You need to have JDK for Java 24 installed to compile the project.
|
||||
If you are on Linux or macOS, you can easily accomplish that by running
|
||||
```bash
|
||||
curl -s "https://get.sdkman.io" | bash
|
||||
. "$HOME/.sdkman/bin/sdkman-init.sh"
|
||||
sdk install java 22.0.2-graalce
|
||||
sdk default java 22.0.2-graalce
|
||||
sdk install java 24-graalce
|
||||
sdk default java 24-graalce
|
||||
```
|
||||
.
|
||||
On Windows, you have to manually install a JDK, e.g. from [Adoptium](https://adoptium.net/temurin/releases/?version=21).
|
||||
@@ -47,17 +46,12 @@ You can use the gradle wrapper to build and run the project:
|
||||
- `gradlew clean dist` will create a distributable production version in `dist/build/dist/base`.
|
||||
- `gradlew <project>:test` will run the tests of the specified project.
|
||||
|
||||
You are also able to properly debug the built production application through two different methods:
|
||||
You are also able to properly debug the built production application:
|
||||
- The `dist/build/dist/base/app/scripts/xpiped_debug` script will launch the application in debug mode and with a console attached to it
|
||||
- The `dist/build/dist/base/app/scripts/xpiped_debug_attach` script attaches a debugger with the help of [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme).
|
||||
Just make sure that the attachme process is running within IntelliJ, and the debugger should launch automatically once you start up the application.
|
||||
|
||||
Note that when any unit test is run using a debugger, the XPipe daemon process that is started will also attempt
|
||||
to connect to that debugger through [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme) as well.
|
||||
|
||||
## Modularity and IDEs
|
||||
|
||||
All XPipe components target [Java 22](https://openjdk.java.net/projects/jdk/22/) and make full use of the Java Module System (JPMS).
|
||||
All XPipe components target [Java 24](https://openjdk.java.net/projects/jdk/24/) and make full use of the Java Module System (JPMS).
|
||||
All components are modularized, including all their dependencies.
|
||||
In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
|
||||
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
|
||||
@@ -65,7 +59,7 @@ many IDEs still have problems building this project properly.
|
||||
|
||||
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
|
||||
The tested and recommended IDE is IntelliJ.
|
||||
When setting up the project in IntelliJ, make sure that the correct JDK (Java 22)
|
||||
When setting up the project in IntelliJ, make sure that the correct JDK (Java 24)
|
||||
is selected both for the project and for gradle itself.
|
||||
|
||||
## Contributing guide
|
||||
@@ -75,7 +69,7 @@ Especially when starting out, it might be a good idea to start with easy tasks f
|
||||
### Interacting via the HTTP API
|
||||
|
||||
You can create clients that communicate with the XPipe daemon via its HTTP API.
|
||||
To get started, see the [OpenAPI spec](/openapi.yaml).
|
||||
To get started, see the [OpenAPI spec](https://docs.xpipe.io/api).
|
||||
|
||||
### Implementing support for a new editor
|
||||
|
||||
@@ -98,10 +92,6 @@ All actions that you can perform for certain connections in the connection overv
|
||||
|
||||
You can add custom script definitions [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/java/io/xpipe/ext/base/script/PredefinedScriptStore.java) and [here](https://github.com/xpipe-io/xpipe/tree/master/ext/base/src/main/resources/io/xpipe/ext/base/resources/scripts).
|
||||
|
||||
### Adding more system icons for system autodetection
|
||||
|
||||
You can register new system types [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/java/io/xpipe/app/resources/SystemIcons.java) and add the respective icons [here](https://github.com/xpipe-io/xpipe/tree/master/app/src/main/resources/io/xpipe/app/resources/img/system).
|
||||
|
||||
### Adding more file icons for specific types
|
||||
|
||||
You can register file types [here](https://github.com/xpipe-io/xpipe/blob/master/app/src/main/resources/io/xpipe/app/resources/file_list.txt) and add the respective icons [here](https://github.com/xpipe-io/xpipe/tree/master/app/src/main/resources/io/xpipe/app/resources/img/browser).
|
||||
|
||||
+3
-14
@@ -49,18 +49,14 @@ dependencies {
|
||||
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
|
||||
|
||||
api("com.github.weisj:jsvg:1.7.1")
|
||||
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
|
||||
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
|
||||
api 'org.bouncycastle:bcprov-jdk18on:1.80'
|
||||
api 'info.picocli:picocli:4.7.6'
|
||||
api ('org.kohsuke:github-api:1.326') {
|
||||
exclude group: 'org.apache.commons', module: 'commons-lang3'
|
||||
}
|
||||
api 'org.apache.commons:commons-lang3:3.17.0'
|
||||
api 'io.sentry:sentry:7.20.0'
|
||||
api 'io.sentry:sentry:8.7.0'
|
||||
api 'commons-io:commons-io:2.18.0'
|
||||
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.2"
|
||||
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.2"
|
||||
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.3"
|
||||
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.3"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
|
||||
@@ -160,13 +156,6 @@ processResources {
|
||||
into resourcesDir
|
||||
}
|
||||
}
|
||||
|
||||
doLast {
|
||||
copy {
|
||||
from file("$rootDir/openapi.yaml")
|
||||
into file("${sourceSets.main.output.resourcesDir}/io/xpipe/app/resources/misc");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
distTar {
|
||||
|
||||
@@ -2,8 +2,7 @@ package io.xpipe.app.beacon;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.resources.AppResources;
|
||||
import io.xpipe.app.util.MarkdownHelper;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.beacon.BeaconConfig;
|
||||
import io.xpipe.beacon.BeaconInterface;
|
||||
import io.xpipe.core.process.OsType;
|
||||
@@ -14,16 +13,15 @@ import com.sun.net.httpserver.HttpServer;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AppBeaconServer {
|
||||
|
||||
@@ -48,9 +46,6 @@ public class AppBeaconServer {
|
||||
@Getter
|
||||
private String localAuthSecret;
|
||||
|
||||
private String notFoundHtml;
|
||||
private final Map<String, String> resources = new HashMap<>();
|
||||
|
||||
public static void setupPort() {
|
||||
int port;
|
||||
boolean propertyPort;
|
||||
@@ -112,7 +107,8 @@ public class AppBeaconServer {
|
||||
executor.shutdown();
|
||||
try {
|
||||
executor.awaitTermination(30, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException ignored) {}
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void initAuthSecret() throws IOException {
|
||||
@@ -150,17 +146,6 @@ public class AppBeaconServer {
|
||||
});
|
||||
server.setExecutor(executor);
|
||||
|
||||
var resourceMap = Map.of(
|
||||
"openapi.yaml", "misc/openapi.yaml",
|
||||
"markdown.css", "misc/github-markdown-dark.css",
|
||||
"highlight.min.js", "misc/highlight.min.js",
|
||||
"github-dark.min.css", "misc/github-dark.min.css");
|
||||
resourceMap.forEach((s, s2) -> {
|
||||
server.createContext("/" + s, exchange -> {
|
||||
handleResource(exchange, s2);
|
||||
});
|
||||
});
|
||||
|
||||
server.createContext("/", exchange -> {
|
||||
handleCatchAll(exchange);
|
||||
});
|
||||
@@ -169,53 +154,9 @@ public class AppBeaconServer {
|
||||
running = true;
|
||||
}
|
||||
|
||||
private void handleResource(HttpExchange exchange, String resource) throws IOException {
|
||||
if (!resources.containsKey(resource)) {
|
||||
AppResources.with(AppResources.XPIPE_MODULE, resource, file -> {
|
||||
resources.put(resource, Files.readString(file));
|
||||
});
|
||||
}
|
||||
var body = resources.get(resource).getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
try (var out = exchange.getResponseBody()) {
|
||||
out.write(body);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCatchAll(HttpExchange exchange) throws IOException {
|
||||
if (notFoundHtml == null) {
|
||||
AppResources.with(AppResources.XPIPE_MODULE, "misc/api.md", file -> {
|
||||
var md = Files.readString(file);
|
||||
md = md.replaceAll(
|
||||
Pattern.quote(
|
||||
"""
|
||||
> 400 Response
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "string"
|
||||
}
|
||||
```
|
||||
"""),
|
||||
"");
|
||||
notFoundHtml = MarkdownHelper.toHtml(
|
||||
md,
|
||||
head -> {
|
||||
return head + "\n" + "<link rel=\"stylesheet\" href=\"markdown.css\">"
|
||||
+ "\n" + "<link rel=\"stylesheet\" href=\"github-dark.min.css\">"
|
||||
+ "\n" + "<script src=\"highlight.min.js\"></script>"
|
||||
+ "\n" + "<script>hljs.highlightAll();</script>";
|
||||
},
|
||||
s -> {
|
||||
return "<div style=\"max-width: 800px;margin: auto;\">" + s + "</div>";
|
||||
},
|
||||
"standalone");
|
||||
});
|
||||
}
|
||||
var body = notFoundHtml.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, body.length);
|
||||
try (var out = exchange.getResponseBody()) {
|
||||
out.write(body);
|
||||
}
|
||||
exchange.getResponseHeaders().add("Location", DocumentationLink.API.getLink());
|
||||
exchange.sendResponseHeaders(HttpURLConnection.HTTP_SEE_OTHER, 0);
|
||||
exchange.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +111,9 @@ public class BeaconRequestHandler<T> implements HttpHandler {
|
||||
return;
|
||||
} catch (BeaconServerException serverException) {
|
||||
var cause = serverException.getCause() != null ? serverException.getCause() : serverException;
|
||||
ErrorEvent.fromThrowable(cause).omit().handle();
|
||||
writeError(exchange, new BeaconServerErrorResponse(cause), 500);
|
||||
var event = ErrorEvent.fromThrowable(cause).omit().handle();
|
||||
var link = event.getLink();
|
||||
writeError(exchange, new BeaconServerErrorResponse(cause, link), 500);
|
||||
return;
|
||||
} catch (IOException ex) {
|
||||
// Handle serialization errors as normal exceptions and other IO exceptions as assuming that the connection
|
||||
@@ -132,8 +133,9 @@ public class BeaconRequestHandler<T> implements HttpHandler {
|
||||
}
|
||||
return;
|
||||
} catch (Throwable other) {
|
||||
ErrorEvent.fromThrowable(other).omit().expected().handle();
|
||||
writeError(exchange, new BeaconServerErrorResponse(other), 500);
|
||||
var event = ErrorEvent.fromThrowable(other).omit().expected().handle();
|
||||
var link = event.getLink();
|
||||
writeError(exchange, new BeaconServerErrorResponse(other, link), 500);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -160,8 +162,9 @@ public class BeaconRequestHandler<T> implements HttpHandler {
|
||||
ErrorEvent.fromThrowable(ioException).omit().expected().handle();
|
||||
}
|
||||
} catch (Throwable other) {
|
||||
ErrorEvent.fromThrowable(other).handle();
|
||||
writeError(exchange, new BeaconServerErrorResponse(other), 500);
|
||||
var event = ErrorEvent.fromThrowable(other).handle();
|
||||
var link = event.getLink();
|
||||
writeError(exchange, new BeaconServerErrorResponse(other, link), 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.prefs.ExternalApplicationType;
|
||||
import io.xpipe.app.terminal.TerminalView;
|
||||
import io.xpipe.app.util.AskpassAlert;
|
||||
import io.xpipe.app.util.SecretManager;
|
||||
import io.xpipe.app.util.SecretQueryState;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.api.AskpassExchange;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
@@ -58,19 +55,7 @@ public class AskpassExchangeImpl extends AskpassExchange {
|
||||
if (term.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var control = term.get().controllable();
|
||||
if (control.isPresent()) {
|
||||
control.get().focus();
|
||||
} else {
|
||||
if (OsType.getLocal() == OsType.MACOS) {
|
||||
// Just focus the app, this is correct most of the time
|
||||
var terminalType = AppPrefs.get().terminalType().getValue();
|
||||
if (terminalType instanceof ExternalApplicationType.MacApplication m) {
|
||||
m.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
TerminalView.focus(term.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -14,7 +14,13 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) throws Throwable {
|
||||
var found = DataStorage.get().getStoreEntryIfPresent(msg.getData(), false);
|
||||
if (found.isEmpty()) {
|
||||
found = DataStorage.get().getStoreEntryIfPresent(msg.getName());
|
||||
}
|
||||
|
||||
if (found.isPresent()) {
|
||||
var data = msg.getData();
|
||||
found.get().setStoreInternal(data, true);
|
||||
return Response.builder().connection(found.get().getUuid()).build();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,10 @@ package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStorageQuery;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.beacon.api.ConnectionQueryExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -22,14 +22,14 @@ public class FsReadExchangeImpl extends FsReadExchange {
|
||||
var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
|
||||
var fs = new ConnectionFileSystem(shell.getControl());
|
||||
|
||||
if (!fs.fileExists(msg.getPath().toString())) {
|
||||
if (!fs.fileExists(msg.getPath())) {
|
||||
throw new BeaconClientException("File does not exist");
|
||||
}
|
||||
|
||||
var size = fs.getFileSize(msg.getPath().toString());
|
||||
var size = fs.getFileSize(msg.getPath());
|
||||
if (size > 100_000_000) {
|
||||
var file = BlobManager.get().newBlobFile();
|
||||
try (var in = fs.openInput(msg.getPath().toString())) {
|
||||
try (var in = fs.openInput(msg.getPath())) {
|
||||
var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), size);
|
||||
try (var fileOut =
|
||||
Files.newOutputStream(file.resolve(msg.getPath().getFileName()))) {
|
||||
@@ -45,7 +45,7 @@ public class FsReadExchangeImpl extends FsReadExchange {
|
||||
}
|
||||
} else {
|
||||
byte[] bytes;
|
||||
try (var in = fs.openInput(msg.getPath().toString())) {
|
||||
try (var in = fs.openInput(msg.getPath())) {
|
||||
var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), size);
|
||||
bytes = fixedIn.readAllBytes();
|
||||
in.transferTo(OutputStream.nullOutputStream());
|
||||
|
||||
@@ -21,9 +21,7 @@ public class FsScriptExchangeImpl extends FsScriptExchange {
|
||||
data = new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
data = shell.getControl().getShellDialect().prepareScriptContent(data);
|
||||
var file = ScriptHelper.getExecScriptFile(shell.getControl());
|
||||
shell.getControl().view().writeScriptFile(file, data);
|
||||
file = ScriptHelper.fixScriptPermissions(shell.getControl(), file);
|
||||
var file = ScriptHelper.createExecScript(shell.getControl(), data);
|
||||
return Response.builder().path(file).build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public class FsWriteExchangeImpl extends FsWriteExchange {
|
||||
var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
|
||||
var fs = new ConnectionFileSystem(shell.getControl());
|
||||
try (var in = BlobManager.get().getBlob(msg.getBlob());
|
||||
var os = fs.openOutput(msg.getPath().toString(), in.available())) {
|
||||
var os = fs.openOutput(msg.getPath(), in.available())) {
|
||||
in.transferTo(os);
|
||||
}
|
||||
return Response.builder().build();
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import atlantafx.base.layout.ModalBox;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
@@ -14,6 +11,8 @@ import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.BeaconServerException;
|
||||
import io.xpipe.beacon.api.TerminalExternalLaunchExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchExchange {
|
||||
@@ -26,13 +25,15 @@ public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchEx
|
||||
}
|
||||
|
||||
if (found.size() > 1) {
|
||||
throw new BeaconServerException("Multiple connections found: " + found.stream().map(DataStoreEntry::getName).toList());
|
||||
throw new BeaconServerException("Multiple connections found: "
|
||||
+ found.stream().map(DataStoreEntry::getName).toList());
|
||||
}
|
||||
|
||||
var e = found.getFirst();
|
||||
var isShell = e.getStore() instanceof ShellStore;
|
||||
if (!isShell) {
|
||||
throw new BeaconClientException("Connection " + DataStorage.get().getStorePath(e).toString() + " is not a shell connection");
|
||||
throw new BeaconClientException(
|
||||
"Connection " + DataStorage.get().getStorePath(e).toString() + " is not a shell connection");
|
||||
}
|
||||
|
||||
if (!checkPermission()) {
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.terminal.TerminalLauncherManager;
|
||||
import io.xpipe.app.terminal.TerminalView;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.api.TerminalPrepareExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
@@ -11,9 +8,7 @@ import com.sun.net.httpserver.HttpExchange;
|
||||
public class TerminalPrepareExchangeImpl extends TerminalPrepareExchange {
|
||||
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
|
||||
TerminalView.get().open(msg.getRequest(), msg.getPid());
|
||||
TerminalLauncherManager.registerPid(msg.getRequest(), msg.getPid());
|
||||
public Object handle(HttpExchange exchange, Request msg) {
|
||||
var term = AppPrefs.get().terminalType().getValue();
|
||||
var unicode = term.supportsUnicode();
|
||||
var escapes = term.supportsEscapes();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import io.xpipe.app.terminal.TerminalLauncherManager;
|
||||
import io.xpipe.app.terminal.TerminalView;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.api.TerminalRegisterExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public class TerminalRegisterExchangeImpl extends TerminalRegisterExchange {
|
||||
|
||||
@Override
|
||||
public Object handle(HttpExchange exchange, Request msg) throws BeaconClientException {
|
||||
TerminalView.get().open(msg.getRequest(), msg.getPid());
|
||||
TerminalLauncherManager.registerPid(msg.getRequest(), msg.getPid());
|
||||
return Response.builder().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresEnabledApi() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,10 @@ import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabComp;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.base.DialogComp;
|
||||
import io.xpipe.app.comp.base.LeftSplitPaneComp;
|
||||
import io.xpipe.app.comp.base.StackComp;
|
||||
import io.xpipe.app.comp.base.VerticalComp;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
@@ -21,16 +20,12 @@ import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.Window;
|
||||
import javafx.stage.WindowEvent;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
@@ -38,65 +33,53 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class BrowserFileChooserSessionComp extends DialogComp {
|
||||
public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
|
||||
|
||||
private final Stage stage;
|
||||
private final BrowserFileChooserSessionModel model;
|
||||
|
||||
public BrowserFileChooserSessionComp(Stage stage, BrowserFileChooserSessionModel model) {
|
||||
this.stage = stage;
|
||||
public BrowserFileChooserSessionComp(BrowserFileChooserSessionModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public static void openSingleFile(
|
||||
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file, boolean save) {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
var lastWindow = Window.getWindows().stream()
|
||||
.filter(window -> window.isFocused())
|
||||
.findFirst();
|
||||
var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE);
|
||||
DialogComp.showWindow(save ? "saveFileTitle" : "openFileTitle", stage -> {
|
||||
stage.addEventFilter(WindowEvent.WINDOW_HIDDEN, event -> {
|
||||
lastWindow.ifPresent(window -> window.requestFocus());
|
||||
});
|
||||
var comp = new BrowserFileChooserSessionComp(stage, model);
|
||||
comp.apply(struc -> struc.get().setPrefSize(1200, 700))
|
||||
.styleClass("browser")
|
||||
.styleClass("chooser");
|
||||
return comp;
|
||||
});
|
||||
model.setOnFinish(fileStores -> {
|
||||
file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);
|
||||
});
|
||||
ThreadHelper.runAsync(() -> {
|
||||
model.openFileSystemAsync(store.get(), null, null);
|
||||
});
|
||||
var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE);
|
||||
model.setOnFinish(fileStores -> {
|
||||
file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);
|
||||
});
|
||||
var comp =
|
||||
new BrowserFileChooserSessionComp(model).styleClass("browser").styleClass("chooser");
|
||||
var selection = new SimpleStringProperty();
|
||||
model.getFileSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
|
||||
selection.set(
|
||||
c.getList().size() > 0
|
||||
? c.getList().getFirst().getRawFileEntry().getPath().toString()
|
||||
: null);
|
||||
});
|
||||
var selectionField = new TextFieldComp(selection);
|
||||
selectionField.apply(struc -> {
|
||||
struc.get().setEditable(false);
|
||||
AppFontSizes.base(struc.get());
|
||||
});
|
||||
selectionField.styleClass("chooser-selection");
|
||||
selectionField.hgrow();
|
||||
var modal = ModalOverlay.of(save ? "saveFileTitle" : "openFileTitle", comp);
|
||||
modal.setRequireCloseButtonForClose(true);
|
||||
modal.addButtonBarComp(selectionField);
|
||||
modal.addButton(new ModalButton("select", () -> model.finishChooser(), true, true));
|
||||
modal.show();
|
||||
ThreadHelper.runAsync(() -> {
|
||||
model.openFileSystemAsync(store.get(), null, null);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String finishKey() {
|
||||
return "select";
|
||||
protected void onClose() {
|
||||
model.closeFileSystem();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Comp<?> pane(Comp<?> content) {
|
||||
return content;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finish() {
|
||||
stage.close();
|
||||
model.finishChooser();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void discard() {
|
||||
model.finishWithoutChoice();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comp<?> content() {
|
||||
protected Region createSimple() {
|
||||
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
|
||||
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore)
|
||||
&& storeEntryWrapper.getEntry().getValidity().isUsable();
|
||||
@@ -120,14 +103,17 @@ public class BrowserFileChooserSessionComp extends DialogComp {
|
||||
});
|
||||
};
|
||||
|
||||
var bookmarkTopBar = new BrowserConnectionListFilterComp();
|
||||
var category = new SimpleObjectProperty<>(
|
||||
StoreViewState.get().getActiveCategory().getValue());
|
||||
var filter = new SimpleStringProperty();
|
||||
var bookmarkTopBar = new BrowserConnectionListFilterComp(category, filter);
|
||||
var bookmarksList = new BrowserConnectionListComp(
|
||||
BindingsHelper.map(
|
||||
model.getSelectedEntry(), v -> v != null ? v.getEntry().get() : null),
|
||||
applicable,
|
||||
action,
|
||||
bookmarkTopBar.getCategory(),
|
||||
bookmarkTopBar.getFilter());
|
||||
category,
|
||||
filter);
|
||||
var bookmarksContainer = new StackComp(List.of(bookmarksList)).styleClass("bookmarks-container");
|
||||
bookmarksContainer
|
||||
.apply(struc -> {
|
||||
@@ -157,39 +143,10 @@ public class BrowserFileChooserSessionComp extends DialogComp {
|
||||
var vertical = new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer)).styleClass("left");
|
||||
var splitPane = new LeftSplitPaneComp(vertical, stack)
|
||||
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
|
||||
.withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
|
||||
.styleClass("background")
|
||||
.apply(struc -> {
|
||||
struc.getLeft().setMinWidth(200);
|
||||
struc.getLeft().setMaxWidth(500);
|
||||
});
|
||||
return splitPane;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comp<?> bottom() {
|
||||
return Comp.of(() -> {
|
||||
var selected = new HBox();
|
||||
selected.setAlignment(Pos.CENTER_LEFT);
|
||||
model.getFileSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
selected.getChildren()
|
||||
.setAll(c.getList().stream()
|
||||
.map(s -> {
|
||||
var field = new TextField(
|
||||
s.getRawFileEntry().getPath());
|
||||
field.setEditable(false);
|
||||
field.getStyleClass().add("chooser-selection");
|
||||
HBox.setHgrow(field, Priority.ALWAYS);
|
||||
return field;
|
||||
})
|
||||
.toList());
|
||||
});
|
||||
});
|
||||
var bottomBar = new HBox(selected);
|
||||
HBox.setHgrow(selected, Priority.ALWAYS);
|
||||
bottomBar.setAlignment(Pos.CENTER);
|
||||
return bottomBar;
|
||||
});
|
||||
return splitPane.createRegion();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.util.FileReference;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
import io.xpipe.core.util.FailableFunction;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import lombok.Getter;
|
||||
@@ -40,8 +40,10 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
|
||||
return;
|
||||
}
|
||||
|
||||
var l = new DerivedObservableList<>(fileSelection, true);
|
||||
l.bindContent(newValue.getFileList().getSelection());
|
||||
fileSelection.setAll(newValue.getFileList().getSelection());
|
||||
newValue.getFileList().getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
|
||||
fileSelection.setAll(newValue.getFileList().getSelection());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -65,7 +67,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
|
||||
onFinish.accept(stores);
|
||||
}
|
||||
|
||||
public void finishWithoutChoice() {
|
||||
public void closeFileSystem() {
|
||||
synchronized (BrowserFileChooserSessionModel.this) {
|
||||
var open = selectedEntry.getValue();
|
||||
if (open != null) {
|
||||
@@ -78,7 +80,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
|
||||
|
||||
public void openFileSystemAsync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
|
||||
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
|
||||
BooleanProperty externalBusy) {
|
||||
if (store == null) {
|
||||
return;
|
||||
@@ -96,7 +98,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
|
||||
sessionEntries.add(model);
|
||||
}
|
||||
if (path != null) {
|
||||
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
|
||||
model.initWithGivenDirectory(path.apply(model).toDirectory());
|
||||
} else {
|
||||
model.initWithDefaultDirectory();
|
||||
}
|
||||
|
||||
@@ -4,15 +4,12 @@ import io.xpipe.app.browser.file.BrowserConnectionListComp;
|
||||
import io.xpipe.app.browser.file.BrowserConnectionListFilterComp;
|
||||
import io.xpipe.app.browser.file.BrowserTransferComp;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.AnchorComp;
|
||||
import io.xpipe.app.comp.base.LeftSplitPaneComp;
|
||||
import io.xpipe.app.comp.base.LoadingOverlayComp;
|
||||
import io.xpipe.app.comp.base.StackComp;
|
||||
import io.xpipe.app.comp.base.VerticalComp;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.window.AppMainWindow;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
@@ -22,10 +19,11 @@ import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
import java.util.HashMap;
|
||||
@@ -43,7 +41,7 @@ public class BrowserFullSessionComp extends SimpleComp {
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var vertical = createLeftSide();
|
||||
var left = Comp.of(() -> createLeftSide());
|
||||
|
||||
var leftSplit = new SimpleDoubleProperty();
|
||||
var rightSplit = new SimpleDoubleProperty();
|
||||
@@ -57,7 +55,7 @@ public class BrowserFullSessionComp extends SimpleComp {
|
||||
AnchorPane.setRightAnchor(struc.get(), 0.0);
|
||||
});
|
||||
|
||||
vertical.apply(struc -> {
|
||||
left.apply(struc -> {
|
||||
struc.get()
|
||||
.paddingProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
@@ -74,10 +72,15 @@ public class BrowserFullSessionComp extends SimpleComp {
|
||||
|
||||
var loadingStack = new AnchorComp(List.of(tabs, pinnedStack, loadingIndicator));
|
||||
loadingStack.apply(struc -> struc.get().setPickOnBounds(false));
|
||||
var splitPane = new LeftSplitPaneComp(vertical, loadingStack)
|
||||
var delayedStack = new DelayedInitComp(
|
||||
left, () -> StoreViewState.get() != null && StoreViewState.get().isInitialized());
|
||||
delayedStack.hide(AppMainWindow.getInstance().getStage().widthProperty().lessThan(1000));
|
||||
var splitPane = new LeftSplitPaneComp(delayedStack, loadingStack)
|
||||
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
|
||||
.withOnDividerChange(d -> {
|
||||
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);
|
||||
if (d > 0.0) {
|
||||
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);
|
||||
}
|
||||
leftSplit.set(d);
|
||||
});
|
||||
splitPane.apply(struc -> {
|
||||
@@ -104,7 +107,7 @@ public class BrowserFullSessionComp extends SimpleComp {
|
||||
return r;
|
||||
}
|
||||
|
||||
private Comp<CompStructure<VBox>> createLeftSide() {
|
||||
private Region createLeftSide() {
|
||||
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
|
||||
if (!storeEntryWrapper.getEntry().getValidity().isUsable()) {
|
||||
return false;
|
||||
@@ -131,7 +134,10 @@ public class BrowserFullSessionComp extends SimpleComp {
|
||||
});
|
||||
};
|
||||
|
||||
var bookmarkTopBar = new BrowserConnectionListFilterComp();
|
||||
var category = new SimpleObjectProperty<>(
|
||||
StoreViewState.get().getActiveCategory().getValue());
|
||||
var filter = new SimpleStringProperty();
|
||||
var bookmarkTopBar = new BrowserConnectionListFilterComp(category, filter);
|
||||
var bookmarksList = new BrowserConnectionListComp(
|
||||
BindingsHelper.map(
|
||||
model.getSelectedEntry(),
|
||||
@@ -140,8 +146,8 @@ public class BrowserFullSessionComp extends SimpleComp {
|
||||
: null),
|
||||
applicable,
|
||||
action,
|
||||
bookmarkTopBar.getCategory(),
|
||||
bookmarkTopBar.getFilter());
|
||||
category,
|
||||
filter);
|
||||
var bookmarksContainer = new StackComp(List.of(bookmarksList)).styleClass("bookmarks-container");
|
||||
bookmarksContainer
|
||||
.apply(struc -> {
|
||||
@@ -168,7 +174,7 @@ public class BrowserFullSessionComp extends SimpleComp {
|
||||
localDownloadStage.maxHeight(200);
|
||||
var vertical =
|
||||
new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
|
||||
return vertical;
|
||||
return vertical.createRegion();
|
||||
}
|
||||
|
||||
private StackComp createSplitStack(SimpleDoubleProperty rightSplit, BrowserSessionTabsComp tabs) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
import io.xpipe.core.util.FailableFunction;
|
||||
|
||||
@@ -199,7 +199,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
|
||||
public void openFileSystemAsync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
|
||||
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
|
||||
BooleanProperty externalBusy) {
|
||||
if (store == null) {
|
||||
return;
|
||||
@@ -212,7 +212,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
|
||||
public BrowserFileSystemTabModel openFileSystemSync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
|
||||
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
|
||||
BooleanProperty externalBusy,
|
||||
boolean select)
|
||||
throws Exception {
|
||||
@@ -232,7 +232,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
|
||||
}
|
||||
}
|
||||
if (path != null) {
|
||||
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
|
||||
model.initWithGivenDirectory(path.apply(model).toDirectory());
|
||||
} else {
|
||||
model.initWithDefaultDirectory();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
@@ -34,7 +34,7 @@ public abstract class BrowserSessionTab {
|
||||
|
||||
public abstract String getIcon();
|
||||
|
||||
public abstract DataColor getColor();
|
||||
public abstract DataStoreColor getColor();
|
||||
|
||||
public boolean isCloseable() {
|
||||
return true;
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.xpipe.app.browser;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.PrettyImageHelper;
|
||||
import io.xpipe.app.core.App;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
@@ -217,8 +218,27 @@ public class BrowserSessionTabsComp extends SimpleComp {
|
||||
headerArea
|
||||
.paddingProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> new Insets(2, 0, 4, -leftPadding.get() + 3), leftPadding));
|
||||
tabs.setPadding(new Insets(0, 0, 0, -5));
|
||||
() -> {
|
||||
var w = App.getApp().getStage().getWidth();
|
||||
if (w >= 1000) {
|
||||
return new Insets(2, 0, 4, -leftPadding.get() + 3);
|
||||
} else {
|
||||
return new Insets(2, 0, 4, -leftPadding.get() - 4);
|
||||
}
|
||||
},
|
||||
App.getApp().getStage().widthProperty(),
|
||||
leftPadding));
|
||||
tabs.paddingProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
var w = App.getApp().getStage().getWidth();
|
||||
if (w >= 1000) {
|
||||
return new Insets(0, 0, 0, -5);
|
||||
} else {
|
||||
return new Insets(0, 0, 0, 5);
|
||||
}
|
||||
},
|
||||
App.getApp().getStage().widthProperty()));
|
||||
headerHeight.bind(headerArea.heightProperty());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
@@ -42,7 +42,7 @@ public abstract class BrowserStoreSessionTab<T extends DataStore> extends Browse
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataColor getColor() {
|
||||
public DataStoreColor getColor() {
|
||||
return DataStorage.get().getEffectiveColor(entry.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package io.xpipe.app.browser.action;
|
||||
|
||||
import io.xpipe.app.browser.file.BrowserEntry;
|
||||
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
|
||||
import io.xpipe.app.comp.base.TooltipAugment;
|
||||
import io.xpipe.app.comp.base.TooltipHelper;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
@@ -10,6 +10,7 @@ import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
@@ -38,7 +39,7 @@ public interface BrowserLeafAction extends BrowserAction {
|
||||
event.consume();
|
||||
});
|
||||
var name = getName(model, selected);
|
||||
new TooltipAugment<>(name, getShortcut()).augment(b);
|
||||
Tooltip.install(b, TooltipHelper.create(name, getShortcut()));
|
||||
var graphic = getIcon(model, selected);
|
||||
if (graphic != null) {
|
||||
b.setGraphic(graphic);
|
||||
|
||||
@@ -2,7 +2,7 @@ package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
@@ -17,7 +17,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class BrowserAlerts {
|
||||
|
||||
public static FileConflictChoice showFileConflictAlert(String file, boolean multiple) {
|
||||
public static FileConflictChoice showFileConflictAlert(FilePath file, boolean multiple) {
|
||||
var map = new LinkedHashMap<ButtonType, FileConflictChoice>();
|
||||
map.put(new ButtonType(AppI18n.get("cancel"), ButtonBar.ButtonData.CANCEL_CLOSE), FileConflictChoice.CANCEL);
|
||||
if (multiple) {
|
||||
@@ -74,8 +74,10 @@ public class BrowserAlerts {
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
public static boolean showDeleteAlert(List<FileEntry> source) {
|
||||
if (!AppPrefs.get().confirmDeletions().get()
|
||||
public static boolean showDeleteAlert(BrowserFileSystemTabModel model, List<FileEntry> source) {
|
||||
var config =
|
||||
DataStorage.get().getEffectiveCategoryConfig(model.getEntry().get());
|
||||
if (!Boolean.TRUE.equals(config.getConfirmAllModifications())
|
||||
&& source.stream().noneMatch(entry -> entry.getKind() == FileKind.DIRECTORY)) {
|
||||
return true;
|
||||
}
|
||||
@@ -96,7 +98,7 @@ public class BrowserAlerts {
|
||||
var names = namesHeader + "\n"
|
||||
+ source.stream()
|
||||
.limit(10)
|
||||
.map(entry -> "- " + new FilePath(entry.getPath()).getFileName())
|
||||
.map(entry -> "- " + entry.getPath().getFileName())
|
||||
.collect(Collectors.joining("\n"));
|
||||
if (source.size() > 10) {
|
||||
names += "\n+ " + (source.size() - 10) + " ...";
|
||||
|
||||
@@ -64,9 +64,9 @@ public class BrowserBreadcrumbBar extends SimpleComp {
|
||||
});
|
||||
}
|
||||
|
||||
var elements = FileNames.splitHierarchy(val);
|
||||
var elements = val.splitHierarchy();
|
||||
var modifiedElements = new ArrayList<>(elements);
|
||||
if (val.startsWith("/")) {
|
||||
if (val.toString().startsWith("/")) {
|
||||
modifiedElements.addFirst("/");
|
||||
}
|
||||
Breadcrumbs.BreadCrumbItem<String> items =
|
||||
|
||||
@@ -16,6 +16,7 @@ import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@@ -67,9 +68,11 @@ public final class BrowserConnectionListComp extends SimpleComp {
|
||||
var section = new StoreSectionMiniComp(
|
||||
StoreSection.createTopLevel(
|
||||
StoreViewState.get().getAllEntries(),
|
||||
Set.of(),
|
||||
this::filter,
|
||||
filter,
|
||||
category,
|
||||
StoreViewState.get().getEntriesListVisibilityObservable(),
|
||||
StoreViewState.get().getEntriesListUpdateObservable()),
|
||||
augment,
|
||||
selectedAction -> {
|
||||
@@ -82,7 +85,8 @@ public final class BrowserConnectionListComp extends SimpleComp {
|
||||
busyEntries.remove(selectedAction);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
false);
|
||||
|
||||
var r = section.vgrow().createRegion();
|
||||
r.getStyleClass().add("bookmark-list");
|
||||
|
||||
@@ -9,21 +9,20 @@ import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public final class BrowserConnectionListFilterComp extends SimpleComp {
|
||||
|
||||
private final Property<StoreCategoryWrapper> category =
|
||||
new SimpleObjectProperty<>(StoreViewState.get().getActiveCategory().getValue());
|
||||
private final Property<String> filter = new SimpleStringProperty();
|
||||
private final Property<StoreCategoryWrapper> category;
|
||||
private final Property<String> filter;
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
|
||||
@@ -4,7 +4,6 @@ import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
|
||||
import io.xpipe.app.browser.icon.BrowserIconFileType;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -74,6 +73,6 @@ public class BrowserEntry {
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return FileNames.getFileName(getRawFileEntry().getPath());
|
||||
return getRawFileEntry().getPath().getFileName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileInfo;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -46,7 +45,6 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
|
||||
private final BrowserFileListModel fileList;
|
||||
private final StringProperty typedSelection = new SimpleStringProperty("");
|
||||
private final DoubleProperty ownerWidth = new SimpleDoubleProperty();
|
||||
|
||||
public BrowserFileListComp(BrowserFileListModel fileList) {
|
||||
this.fileList = fileList;
|
||||
@@ -63,8 +61,7 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
filenameCol.textProperty().bind(AppI18n.observable("name"));
|
||||
filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
|
||||
param.getValue() != null
|
||||
? FileNames.getFileName(
|
||||
param.getValue().getRawFileEntry().getPath())
|
||||
? param.getValue().getRawFileEntry().getPath().getFileName()
|
||||
: null));
|
||||
filenameCol.setComparator(Comparator.comparing(String::toLowerCase));
|
||||
filenameCol.setSortType(ASCENDING);
|
||||
@@ -73,9 +70,9 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
filenameCol.setReorderable(false);
|
||||
filenameCol.setResizable(false);
|
||||
|
||||
var sizeCol = new TableColumn<BrowserEntry, Number>();
|
||||
var sizeCol = new TableColumn<BrowserEntry, String>();
|
||||
sizeCol.textProperty().bind(AppI18n.observable("size"));
|
||||
sizeCol.setCellValueFactory(param -> new SimpleLongProperty(
|
||||
sizeCol.setCellValueFactory(param -> new ReadOnlyStringWrapper(
|
||||
param.getValue().getRawFileEntry().resolved().getSize()));
|
||||
sizeCol.setCellFactory(col -> new FileSizeCell());
|
||||
sizeCol.setResizable(false);
|
||||
@@ -183,6 +180,10 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (unix.getUid() == null && unix.getGid() == null && unix.getUser() == null && unix.getGroup() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var m = fileList.getFileSystemModel();
|
||||
var user = unix.getUser() != null
|
||||
? unix.getUser()
|
||||
@@ -487,12 +488,18 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
mtimeCol.setVisible(true);
|
||||
}
|
||||
|
||||
ownerWidth.set(fileList.getAll().getValue().stream()
|
||||
var hasOwner = fileList.getAll().getValue().stream()
|
||||
.map(browserEntry -> formatOwner(browserEntry))
|
||||
.map(s -> s != null ? s.length() * 9 : 0)
|
||||
.max(Comparator.naturalOrder())
|
||||
.orElse(150));
|
||||
ownerCol.setPrefWidth(ownerWidth.get());
|
||||
.anyMatch(s -> s != null);
|
||||
if (hasOwner) {
|
||||
ownerCol.setPrefWidth(fileList.getAll().getValue().stream()
|
||||
.map(browserEntry -> formatOwner(browserEntry))
|
||||
.map(s -> s != null ? s.length() * 9 : 0)
|
||||
.max(Comparator.naturalOrder())
|
||||
.orElse(150));
|
||||
} else {
|
||||
ownerCol.setPrefWidth(0);
|
||||
}
|
||||
|
||||
if (fileList.getFileSystemModel().getFileSystem() != null) {
|
||||
var shell = fileList.getFileSystemModel()
|
||||
@@ -505,7 +512,9 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
} else {
|
||||
modeCol.setVisible(true);
|
||||
if (table.getWidth() > 1000) {
|
||||
ownerCol.setVisible(true);
|
||||
ownerCol.setVisible(hasOwner);
|
||||
} else if (!hasOwner) {
|
||||
ownerCol.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -572,19 +581,24 @@ public final class BrowserFileListComp extends SimpleComp {
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileSizeCell extends TableCell<BrowserEntry, Number> {
|
||||
private static class FileSizeCell extends TableCell<BrowserEntry, String> {
|
||||
|
||||
@Override
|
||||
protected void updateItem(Number fileSize, boolean empty) {
|
||||
protected void updateItem(String fileSize, boolean empty) {
|
||||
super.updateItem(fileSize, empty);
|
||||
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
|
||||
setText(null);
|
||||
} else {
|
||||
var path = getTableRow().getItem();
|
||||
if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
|
||||
setText("");
|
||||
} else {
|
||||
setText(byteCount(fileSize.longValue()));
|
||||
setText(null);
|
||||
} else if (fileSize != null) {
|
||||
try {
|
||||
var l = Long.parseLong(fileSize);
|
||||
setText(byteCount(l));
|
||||
} catch (NumberFormatException e) {
|
||||
setText(fileSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.xpipe.app.browser.file;
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
|
||||
@@ -16,23 +17,20 @@ import javafx.scene.input.*;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.File;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
@Getter
|
||||
public class BrowserFileListCompEntry {
|
||||
|
||||
public static final Timer DROP_TIMER = new Timer("dnd", true);
|
||||
|
||||
private final TableView<BrowserEntry> tv;
|
||||
private final Node row;
|
||||
private final BrowserEntry item;
|
||||
private final BrowserFileListModel model;
|
||||
|
||||
private Point2D lastOver = new Point2D(-1, -1);
|
||||
private TimerTask activeTask;
|
||||
private Runnable activeTask;
|
||||
private ContextMenu lastContextMenu;
|
||||
|
||||
public BrowserFileListCompEntry(
|
||||
@@ -283,7 +281,7 @@ public class BrowserFileListCompEntry {
|
||||
}
|
||||
|
||||
lastOver = (new Point2D(event.getX(), event.getY()));
|
||||
activeTask = new TimerTask() {
|
||||
activeTask = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (activeTask != this) {
|
||||
@@ -294,10 +292,11 @@ public class BrowserFileListCompEntry {
|
||||
return;
|
||||
}
|
||||
|
||||
model.getFileSystemModel().cdAsync(item.getRawFileEntry().getPath());
|
||||
model.getFileSystemModel()
|
||||
.cdAsync(item.getRawFileEntry().getPath().toString());
|
||||
}
|
||||
};
|
||||
DROP_TIMER.schedule(activeTask, 1200);
|
||||
GlobalTimer.delayAsync(activeTask, Duration.ofMillis(1200));
|
||||
}
|
||||
|
||||
public void onDragEntered(DragEvent event) {
|
||||
|
||||
@@ -3,7 +3,8 @@ package io.xpipe.app.browser.file;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.base.TextFieldComp;
|
||||
import io.xpipe.app.comp.base.TooltipAugment;
|
||||
import io.xpipe.app.comp.base.TooltipHelper;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
@@ -11,6 +12,7 @@ import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
@@ -45,8 +47,11 @@ public class BrowserFileListFilterComp extends Comp<BrowserFileListFilterComp.St
|
||||
button.fire();
|
||||
keyEvent.consume();
|
||||
});
|
||||
new TooltipAugment<>("app.search", new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN))
|
||||
.augment(button);
|
||||
Tooltip.install(
|
||||
button,
|
||||
TooltipHelper.create(
|
||||
AppI18n.observable("app.search"),
|
||||
new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)));
|
||||
text.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (!newValue && filterString.getValue() == null) {
|
||||
if (button.isFocused()) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
@@ -66,8 +65,9 @@ public final class BrowserFileListModel {
|
||||
List<BrowserEntry> filtered = fileSystemModel.getFilter().getValue() != null
|
||||
? all.getValue().stream()
|
||||
.filter(entry -> {
|
||||
var name = FileNames.getFileName(
|
||||
entry.getRawFileEntry().getPath())
|
||||
var name = entry.getRawFileEntry()
|
||||
.getPath()
|
||||
.getFileName()
|
||||
.toLowerCase(Locale.ROOT);
|
||||
var filterString =
|
||||
fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT);
|
||||
@@ -99,8 +99,8 @@ public final class BrowserFileListModel {
|
||||
return old;
|
||||
}
|
||||
|
||||
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), old.getFileName());
|
||||
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
|
||||
var fullPath = fileSystemModel.getCurrentPath().get().join(old.getFileName());
|
||||
var newFullPath = fileSystemModel.getCurrentPath().get().join(newName);
|
||||
|
||||
// This check will fail on case-insensitive file systems when changing the case of the file
|
||||
// So skip it in this case
|
||||
@@ -144,7 +144,7 @@ public final class BrowserFileListModel {
|
||||
public void onDoubleClick(BrowserEntry entry) {
|
||||
var r = entry.getRawFileEntry().resolved();
|
||||
if (r.getKind() == FileKind.DIRECTORY) {
|
||||
fileSystemModel.cdAsync(r.getPath());
|
||||
fileSystemModel.cdAsync(r.getPath().toString());
|
||||
}
|
||||
|
||||
if (AppPrefs.get().editFilesWithDoubleClick().get() && r.getKind() == FileKind.FILE) {
|
||||
|
||||
@@ -82,6 +82,10 @@ class BrowserFileListNameCell extends TableCell<BrowserEntry, String> {
|
||||
}
|
||||
|
||||
var item = getTableRow().getItem();
|
||||
if (item == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY;
|
||||
var isParentLink = item.getRawFileEntry()
|
||||
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.ext.ConnectionFileSystem;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.FileBridge;
|
||||
import io.xpipe.app.util.FileOpener;
|
||||
import io.xpipe.core.process.CommandBuilder;
|
||||
import io.xpipe.core.process.ElevationFunction;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileInfo;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
@@ -32,15 +35,12 @@ public class BrowserFileOpener {
|
||||
}
|
||||
|
||||
var info = (FileInfo.Unix) file.getInfo();
|
||||
var zero = Integer.valueOf(0);
|
||||
var otherWrite = info.getPermissions().charAt(7) == 'w';
|
||||
var requiresRoot = zero.equals(info.getUid()) && zero.equals(info.getGid()) && !otherWrite;
|
||||
if (!requiresRoot || model.getCache().isRoot()) {
|
||||
var requiresSudo = requiresSudo(model, info, file.getPath());
|
||||
if (!requiresSudo) {
|
||||
return fileSystem.openOutput(file.getPath(), totalBytes);
|
||||
}
|
||||
|
||||
var elevate = AppWindowHelper.showConfirmationAlert(
|
||||
"app.fileWriteSudoTitle", "app.fileWriteSudoHeader", "app.fileWriteSudoContent");
|
||||
var elevate = AppDialog.confirm("fileWriteSudo");
|
||||
if (!elevate) {
|
||||
return fileSystem.openOutput(file.getPath(), totalBytes);
|
||||
}
|
||||
@@ -63,16 +63,48 @@ public class BrowserFileOpener {
|
||||
}
|
||||
}
|
||||
|
||||
private static int calculateKey(FileEntry entry) {
|
||||
return Objects.hash(entry.getPath(), entry.getFileSystem(), entry.getKind(), entry.getInfo());
|
||||
private static boolean requiresSudo(BrowserFileSystemTabModel model, FileInfo.Unix info, FilePath filePath)
|
||||
throws Exception {
|
||||
if (model.getCache().isRoot()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info != null) {
|
||||
var otherWrite = info.getPermissions().charAt(7) == 'w';
|
||||
if (otherWrite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var userOwned = info.getUid() != null
|
||||
&& model.getCache().getUidForUser(model.getCache().getUsername()) == info.getUid()
|
||||
|| info.getUser() != null && model.getCache().getUsername().equals(info.getUser());
|
||||
var userWrite = info.getPermissions().charAt(1) == 'w';
|
||||
if (userOwned && userWrite) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var test = model.getFileSystem()
|
||||
.getShell()
|
||||
.orElseThrow()
|
||||
.command(CommandBuilder.of().add("test", "-w").addFile(filePath))
|
||||
.executeAndCheck();
|
||||
return !test;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
private static int calculateKey(BrowserFileSystemTabModel model, FileEntry entry) {
|
||||
// Use different key for empty / non-empty files to prevent any issues from blanked files when transfer fails
|
||||
var empty = model.getFileSystem().getFileSize(entry.getPath()) == 0;
|
||||
return Objects.hash(entry.getPath(), entry.getFileSystem(), entry.getKind(), entry.getInfo(), empty);
|
||||
}
|
||||
|
||||
public static void openWithAnyApplication(BrowserFileSystemTabModel model, FileEntry entry) {
|
||||
var file = entry.getPath();
|
||||
var key = calculateKey(entry);
|
||||
var key = calculateKey(model, entry);
|
||||
FileBridge.get()
|
||||
.openIO(
|
||||
FileNames.getFileName(file),
|
||||
file.getFileName(),
|
||||
key,
|
||||
new BooleanScope(model.getBusy()).exclusive(),
|
||||
() -> {
|
||||
@@ -90,10 +122,10 @@ public class BrowserFileOpener {
|
||||
|
||||
public static void openInDefaultApplication(BrowserFileSystemTabModel model, FileEntry entry) {
|
||||
var file = entry.getPath();
|
||||
var key = calculateKey(entry);
|
||||
var key = calculateKey(model, entry);
|
||||
FileBridge.get()
|
||||
.openIO(
|
||||
FileNames.getFileName(file),
|
||||
file.getFileName(),
|
||||
key,
|
||||
new BooleanScope(model.getBusy()).exclusive(),
|
||||
() -> {
|
||||
@@ -116,10 +148,10 @@ public class BrowserFileOpener {
|
||||
}
|
||||
|
||||
var file = entry.getPath();
|
||||
var key = calculateKey(entry);
|
||||
var key = calculateKey(model, entry);
|
||||
FileBridge.get()
|
||||
.openIO(
|
||||
FileNames.getFileName(file),
|
||||
file.getFileName(),
|
||||
key,
|
||||
new BooleanScope(model.getBusy()).exclusive(),
|
||||
() -> {
|
||||
|
||||
@@ -35,10 +35,10 @@ public class BrowserFileOverviewComp extends SimpleComp {
|
||||
var graphic = new HorizontalComp(List.of(
|
||||
icon,
|
||||
new BrowserQuickAccessButtonComp(() -> new BrowserEntry(entry, model.getFileList()), model)));
|
||||
var l = new Button(entry.getPath(), graphic.createRegion());
|
||||
var l = new Button(entry.getPath().toString(), graphic.createRegion());
|
||||
l.setGraphicTextGap(1);
|
||||
l.setOnAction(event -> {
|
||||
model.cdAsync(entry.getPath());
|
||||
model.cdAsync(entry.getPath().toString());
|
||||
event.consume();
|
||||
});
|
||||
l.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
@@ -2,10 +2,7 @@ package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
import io.xpipe.core.store.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
@@ -67,7 +64,7 @@ public class BrowserFileSystemHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static String resolveDirectoryPath(BrowserFileSystemTabModel model, String path, boolean allowRewrite)
|
||||
public static FilePath resolveDirectoryPath(BrowserFileSystemTabModel model, FilePath path, boolean allowRewrite)
|
||||
throws Exception {
|
||||
if (path == null) {
|
||||
return null;
|
||||
@@ -82,23 +79,23 @@ public class BrowserFileSystemHelper {
|
||||
return path;
|
||||
}
|
||||
|
||||
var resolved = shell.get()
|
||||
var resolved = FilePath.of(shell.get()
|
||||
.getShellDialect()
|
||||
.resolveDirectory(shell.get(), path)
|
||||
.readStdoutOrThrow();
|
||||
.resolveDirectory(shell.get(), path.toString())
|
||||
.readStdoutOrThrow());
|
||||
|
||||
if (!FileNames.isAbsolute(resolved)) {
|
||||
if (!resolved.isAbsolute()) {
|
||||
throw new IllegalArgumentException(String.format("Directory %s is not absolute", resolved));
|
||||
}
|
||||
|
||||
if (allowRewrite && model.getFileSystem().fileExists(path)) {
|
||||
return FileNames.toDirectory(FileNames.getParent(path));
|
||||
if (allowRewrite && model.getFileSystem().fileExists(resolved)) {
|
||||
return resolved.getParent().toDirectory();
|
||||
}
|
||||
|
||||
return FileNames.toDirectory(resolved);
|
||||
return resolved.toDirectory();
|
||||
}
|
||||
|
||||
public static void validateDirectoryPath(BrowserFileSystemTabModel model, String path, boolean verifyExists)
|
||||
public static void validateDirectoryPath(BrowserFileSystemTabModel model, FilePath path, boolean verifyExists)
|
||||
throws Exception {
|
||||
if (path == null) {
|
||||
return;
|
||||
@@ -114,7 +111,8 @@ public class BrowserFileSystemHelper {
|
||||
}
|
||||
|
||||
if (verifyExists && !model.getFileSystem().directoryExists(path)) {
|
||||
throw ErrorEvent.expected(new IllegalArgumentException(String.format("Directory %s does not exist", path)));
|
||||
throw ErrorEvent.expected(new IllegalArgumentException(
|
||||
String.format("Directory %s does not exist or is not accessible", path)));
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -125,12 +123,12 @@ public class BrowserFileSystemHelper {
|
||||
}
|
||||
}
|
||||
|
||||
public static FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
|
||||
public static FileEntry getRemoteWrapper(FileSystem fileSystem, FilePath file) throws Exception {
|
||||
return new FileEntry(
|
||||
fileSystem,
|
||||
file,
|
||||
Instant.now(),
|
||||
fileSystem.getFileSize(file),
|
||||
"" + fileSystem.getFileSize(file),
|
||||
null,
|
||||
fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
@@ -12,33 +14,37 @@ import java.util.Objects;
|
||||
public final class BrowserFileSystemHistory {
|
||||
|
||||
private final IntegerProperty cursor = new SimpleIntegerProperty(-1);
|
||||
private final List<String> history = new ArrayList<>();
|
||||
private final List<FilePath> history = new ArrayList<>();
|
||||
private final BooleanBinding canGoBack =
|
||||
Bindings.createBooleanBinding(() -> cursor.get() > 0 && history.size() > 1, cursor);
|
||||
private final BooleanBinding canGoForth =
|
||||
Bindings.createBooleanBinding(() -> cursor.get() < history.size() - 1, cursor);
|
||||
|
||||
public List<String> getForwardHistory(int max) {
|
||||
var l = new ArrayList<String>();
|
||||
public List<FilePath> getForwardHistory(int max) {
|
||||
var l = new ArrayList<FilePath>();
|
||||
for (var i = cursor.get() + 1; i < Math.min(history.size(), cursor.get() + max); i++) {
|
||||
l.add(history.get(i));
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
public List<String> getBackwardHistory(int max) {
|
||||
var l = new ArrayList<String>();
|
||||
public List<FilePath> getBackwardHistory(int max) {
|
||||
var l = new ArrayList<FilePath>();
|
||||
for (var i = cursor.get() - 1; i >= Math.max(0, cursor.get() - max); i--) {
|
||||
l.add(history.get(i));
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
public String getCurrent() {
|
||||
public FilePath getCurrent() {
|
||||
return history.size() > 0 ? history.get(cursor.get()) : null;
|
||||
}
|
||||
|
||||
public void updateCurrent(String s) {
|
||||
public void updateCurrent(FilePath s) {
|
||||
if (s == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lastString = getCurrent();
|
||||
if (cursor.get() != -1 && Objects.equals(lastString, s)) {
|
||||
return;
|
||||
@@ -52,11 +58,11 @@ public final class BrowserFileSystemHistory {
|
||||
cursor.set(history.size() - 1);
|
||||
}
|
||||
|
||||
public String back() {
|
||||
public FilePath back() {
|
||||
return back(1);
|
||||
}
|
||||
|
||||
public String back(int i) {
|
||||
public FilePath back(int i) {
|
||||
if (!canGoBack.get()) {
|
||||
return null;
|
||||
}
|
||||
@@ -64,7 +70,7 @@ public final class BrowserFileSystemHistory {
|
||||
return history.get(cursor.get());
|
||||
}
|
||||
|
||||
public String forth(int i) {
|
||||
public FilePath forth(int i) {
|
||||
if (!canGoForth.get()) {
|
||||
return history.getLast();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.util.JacksonMapper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
@@ -23,8 +24,10 @@ import lombok.*;
|
||||
import lombok.extern.jackson.Jacksonized;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -35,25 +38,24 @@ import java.util.stream.Collectors;
|
||||
@JsonDeserialize(using = BrowserFileSystemSavedState.Deserializer.class)
|
||||
public class BrowserFileSystemSavedState {
|
||||
|
||||
private static final Timer TIMEOUT_TIMER = new Timer(true);
|
||||
private static final int STORED = 15;
|
||||
|
||||
@Setter
|
||||
private BrowserFileSystemTabModel model;
|
||||
|
||||
private String lastDirectory;
|
||||
private FilePath lastDirectory;
|
||||
|
||||
@NonNull
|
||||
private ObservableList<RecentEntry> recentDirectories;
|
||||
|
||||
public BrowserFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
|
||||
public BrowserFileSystemSavedState(FilePath lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
|
||||
this.lastDirectory = lastDirectory;
|
||||
this.recentDirectories = recentDirectories;
|
||||
}
|
||||
|
||||
public BrowserFileSystemSavedState() {
|
||||
lastDirectory = null;
|
||||
recentDirectories = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<>(STORED)));
|
||||
recentDirectories = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
|
||||
}
|
||||
|
||||
static BrowserFileSystemSavedState loadForStore(BrowserFileSystemTabModel model) {
|
||||
@@ -73,7 +75,7 @@ public class BrowserFileSystemSavedState {
|
||||
AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
|
||||
}
|
||||
|
||||
public void cd(String dir, boolean delay) {
|
||||
public void cd(FilePath dir, boolean delay) {
|
||||
if (dir == null) {
|
||||
lastDirectory = null;
|
||||
return;
|
||||
@@ -83,8 +85,8 @@ public class BrowserFileSystemSavedState {
|
||||
|
||||
if (delay) {
|
||||
// After 10 seconds
|
||||
TIMEOUT_TIMER.schedule(
|
||||
new TimerTask() {
|
||||
GlobalTimer.delayAsync(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Synchronize with platform thread
|
||||
@@ -100,18 +102,22 @@ public class BrowserFileSystemSavedState {
|
||||
});
|
||||
}
|
||||
},
|
||||
10000);
|
||||
Duration.ofMillis(10000));
|
||||
} else {
|
||||
updateRecent(dir);
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void updateRecent(String dir) {
|
||||
var without = FileNames.removeTrailingSlash(dir);
|
||||
var with = FileNames.toDirectory(dir);
|
||||
recentDirectories.removeIf(recentEntry ->
|
||||
Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with));
|
||||
private synchronized void updateRecent(FilePath dir) {
|
||||
var without = dir.removeTrailingSlash();
|
||||
var with = dir.toDirectory();
|
||||
var copy = new ArrayList<>(recentDirectories);
|
||||
for (RecentEntry recentEntry : copy) {
|
||||
if (Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with)) {
|
||||
recentDirectories.remove(recentEntry);
|
||||
}
|
||||
}
|
||||
|
||||
var o = new RecentEntry(with, Instant.now());
|
||||
if (recentDirectories.size() < STORED) {
|
||||
@@ -161,10 +167,10 @@ public class BrowserFileSystemSavedState {
|
||||
recentDirectories = List.of();
|
||||
}
|
||||
var cleaned = recentDirectories.stream()
|
||||
.map(recentEntry -> new RecentEntry(FileNames.toDirectory(recentEntry.directory), recentEntry.time))
|
||||
.map(recentEntry -> new RecentEntry(recentEntry.directory.toDirectory(), recentEntry.time))
|
||||
.filter(distinctBy(recentEntry -> recentEntry.getDirectory()))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
return new BrowserFileSystemSavedState(null, FXCollections.synchronizedObservableList(FXCollections.observableList(cleaned)));
|
||||
.collect(Collectors.toCollection(CopyOnWriteArrayList::new));
|
||||
return new BrowserFileSystemSavedState(null, FXCollections.observableList(cleaned));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +179,7 @@ public class BrowserFileSystemSavedState {
|
||||
@Builder
|
||||
public static class RecentEntry {
|
||||
|
||||
String directory;
|
||||
FilePath directory;
|
||||
Instant time;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,16 @@ import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.comp.augment.ContextMenuAugment;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.MenuButton;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
@@ -49,9 +52,11 @@ public class BrowserFileSystemTabComp extends SimpleComp {
|
||||
private Region createContent() {
|
||||
var root = new VBox();
|
||||
var overview = new Button(null, new FontIcon("mdi2m-monitor"));
|
||||
overview.setOnAction(e -> model.cdAsync(null));
|
||||
new TooltipAugment<>("overview", new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN))
|
||||
.augment(overview);
|
||||
overview.setOnAction(e -> model.cdAsync((FilePath) null));
|
||||
Tooltip.install(
|
||||
overview,
|
||||
TooltipHelper.create(
|
||||
AppI18n.observable("overview"), new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN)));
|
||||
overview.disableProperty().bind(model.getInOverview());
|
||||
overview.setAccessibleText("System overview");
|
||||
InputHelper.onKeyCombination(
|
||||
@@ -158,14 +163,14 @@ public class BrowserFileSystemTabComp extends SimpleComp {
|
||||
root, new KeyCodeCombination(KeyCode.UP, KeyCombination.ALT_DOWN), true, keyEvent -> {
|
||||
var p = model.getCurrentParentDirectory();
|
||||
if (p != null) {
|
||||
model.cdAsync(p.getPath());
|
||||
model.cdAsync(p.getPath().toString());
|
||||
}
|
||||
keyEvent.consume();
|
||||
});
|
||||
InputHelper.onKeyCombination(root, new KeyCodeCombination(KeyCode.BACK_SPACE), false, keyEvent -> {
|
||||
var p = model.getCurrentParentDirectory();
|
||||
if (p != null) {
|
||||
model.cdAsync(p.getPath());
|
||||
model.cdAsync(p.getPath().toString());
|
||||
}
|
||||
keyEvent.consume();
|
||||
});
|
||||
@@ -203,11 +208,13 @@ public class BrowserFileSystemTabComp extends SimpleComp {
|
||||
});
|
||||
|
||||
var home = new BrowserOverviewComp(model).styleClass("browser-overview");
|
||||
var stack = new MultiContentComp(Map.of(
|
||||
home,
|
||||
model.getCurrentPath().isNull(),
|
||||
fileList,
|
||||
model.getCurrentPath().isNull().not()), false);
|
||||
var stack = new MultiContentComp(
|
||||
Map.of(
|
||||
home,
|
||||
model.getCurrentPath().isNull(),
|
||||
fileList,
|
||||
model.getCurrentPath().isNull().not()),
|
||||
false);
|
||||
var r = stack.styleClass("browser-content-container").createRegion();
|
||||
r.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue) {
|
||||
|
||||
@@ -41,7 +41,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
|
||||
private final Property<String> filter = new SimpleStringProperty();
|
||||
private final BrowserFileListModel fileList;
|
||||
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
|
||||
private final ReadOnlyObjectWrapper<FilePath> currentPath = new ReadOnlyObjectWrapper<>();
|
||||
private final BrowserFileSystemHistory history = new BrowserFileSystemHistory();
|
||||
private final BooleanProperty inOverview = new SimpleBooleanProperty();
|
||||
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
|
||||
@@ -64,9 +64,9 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
fileList = new BrowserFileListModel(selectionMode, this);
|
||||
}
|
||||
|
||||
public Optional<FileEntry> findFile(String path) {
|
||||
public Optional<FileEntry> findFile(FilePath path) {
|
||||
return getFileList().getAll().getValue().stream()
|
||||
.filter(browserEntry -> browserEntry.getFileName().equals(path)
|
||||
.filter(browserEntry -> browserEntry.getFileName().equals(path.toString())
|
||||
|| browserEntry.getRawFileEntry().getPath().equals(path))
|
||||
.findFirst()
|
||||
.map(browserEntry -> browserEntry.getRawFileEntry());
|
||||
@@ -193,12 +193,12 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
return null;
|
||||
}
|
||||
|
||||
var parent = FileNames.getParent(currentPath.get());
|
||||
var parent = currentPath.get().getParent();
|
||||
if (parent == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new FileEntry(fileSystem, parent, null, 0, null, FileKind.DIRECTORY);
|
||||
return new FileEntry(fileSystem, parent, null, null, null, FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
public FileEntry getCurrentDirectory() {
|
||||
@@ -210,7 +210,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
return null;
|
||||
}
|
||||
|
||||
return new FileEntry(fileSystem, currentPath.get(), null, 0, null, FileKind.DIRECTORY);
|
||||
return new FileEntry(fileSystem, currentPath.get(), null, null, null, FileKind.DIRECTORY);
|
||||
}
|
||||
|
||||
public void cdAsync(FilePath path) {
|
||||
cdAsync(path != null ? path.toString() : null);
|
||||
}
|
||||
|
||||
public void cdAsync(String path) {
|
||||
@@ -260,7 +264,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
}
|
||||
|
||||
public Optional<String> cdSyncOrRetry(String path, boolean customInput) {
|
||||
if (Objects.equals(path, currentPath.get())) {
|
||||
var cps = currentPath.get() != null ? currentPath.get().toString() : null;
|
||||
if (Objects.equals(path, cps)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@@ -273,7 +278,12 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
startIfNeeded();
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
|
||||
if (path == null) {
|
||||
currentPath.set(null);
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Fix common issues with paths
|
||||
@@ -288,12 +298,15 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
evaluatedPath = BrowserFileSystemHelper.evaluatePath(this, adjustedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
|
||||
if (evaluatedPath == null) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
// Handle commands typed into navigation bar
|
||||
if (customInput
|
||||
&& evaluatedPath != null
|
||||
&& !evaluatedPath.isBlank()
|
||||
&& !FileNames.isAbsolute(evaluatedPath)
|
||||
&& fileSystem.getShell().isPresent()) {
|
||||
@@ -324,34 +337,34 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
openTerminalAsync(name, directory, cc, true);
|
||||
}
|
||||
});
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
|
||||
// Evaluate optional links
|
||||
String resolvedPath;
|
||||
FilePath resolvedPath;
|
||||
try {
|
||||
resolvedPath = BrowserFileSystemHelper.resolveDirectoryPath(this, evaluatedPath, customInput);
|
||||
resolvedPath = BrowserFileSystemHelper.resolveDirectoryPath(this, FilePath.of(evaluatedPath), customInput);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
|
||||
if (!Objects.equals(path, resolvedPath)) {
|
||||
return Optional.ofNullable(resolvedPath);
|
||||
if (!Objects.equals(path, resolvedPath.toString())) {
|
||||
return Optional.ofNullable(resolvedPath.toString());
|
||||
}
|
||||
|
||||
try {
|
||||
BrowserFileSystemHelper.validateDirectoryPath(this, resolvedPath, true);
|
||||
cdSyncWithoutCheck(path);
|
||||
cdSyncWithoutCheck(resolvedPath);
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
return Optional.ofNullable(currentPath.get());
|
||||
return Optional.ofNullable(cps);
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private void cdSyncWithoutCheck(String path) throws Exception {
|
||||
private void cdSyncWithoutCheck(FilePath path) throws Exception {
|
||||
if (fileSystem == null) {
|
||||
var fs = entry.getStore().createFileSystem();
|
||||
fs.open();
|
||||
@@ -368,7 +381,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
loadFilesSync(path);
|
||||
}
|
||||
|
||||
public void withFiles(String dir, FailableConsumer<Stream<FileEntry>, Exception> consumer) throws Exception {
|
||||
public void withFiles(FilePath dir, FailableConsumer<Stream<FileEntry>, Exception> consumer) throws Exception {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
if (dir != null) {
|
||||
startIfNeeded();
|
||||
@@ -385,7 +398,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
});
|
||||
}
|
||||
|
||||
private boolean loadFilesSync(String dir) {
|
||||
private boolean loadFilesSync(FilePath dir) {
|
||||
try {
|
||||
startIfNeeded();
|
||||
var fs = getFileSystem();
|
||||
@@ -456,7 +469,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
}
|
||||
|
||||
startIfNeeded();
|
||||
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
|
||||
var abs = getCurrentDirectory().getPath().join(name);
|
||||
if (fileSystem.directoryExists(abs)) {
|
||||
throw ErrorEvent.expected(
|
||||
new IllegalStateException(String.format("Directory %s already exists", abs)));
|
||||
@@ -468,8 +481,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
});
|
||||
}
|
||||
|
||||
public void createLinkAsync(String linkName, String targetFile) {
|
||||
if (linkName == null || linkName.isBlank() || targetFile == null || targetFile.isBlank()) {
|
||||
public void createLinkAsync(String linkName, FilePath targetFile) {
|
||||
if (linkName == null || linkName.isBlank() || targetFile == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -484,7 +497,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
}
|
||||
|
||||
startIfNeeded();
|
||||
var abs = FileNames.join(getCurrentDirectory().getPath(), linkName);
|
||||
var abs = getCurrentDirectory().getPath().join(linkName);
|
||||
fileSystem.symbolicLink(abs, targetFile);
|
||||
refreshSync();
|
||||
});
|
||||
@@ -549,7 +562,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
return;
|
||||
}
|
||||
|
||||
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
|
||||
var abs = getCurrentDirectory().getPath().join(name);
|
||||
fileSystem.touch(abs);
|
||||
refreshSync();
|
||||
});
|
||||
@@ -560,8 +573,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
return fileSystem == null;
|
||||
}
|
||||
|
||||
public void initWithGivenDirectory(String dir) {
|
||||
cdSync(dir);
|
||||
public void initWithGivenDirectory(FilePath dir) {
|
||||
cdSync(dir != null ? dir.toString() : null);
|
||||
}
|
||||
|
||||
public void initWithDefaultDirectory() {
|
||||
@@ -570,7 +583,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
}
|
||||
|
||||
public void openTerminalAsync(
|
||||
String name, String directory, ProcessControl processControl, boolean dockIfPossible) {
|
||||
String name, FilePath directory, ProcessControl processControl, boolean dockIfPossible) {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
if (fileSystem == null) {
|
||||
return;
|
||||
@@ -597,11 +610,17 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
|
||||
}
|
||||
|
||||
public void backSync(int i) {
|
||||
cdSync(history.back(i));
|
||||
var b = history.back(i);
|
||||
if (b != null) {
|
||||
cdSync(b.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public void forthSync(int i) {
|
||||
cdSync(history.forth(i));
|
||||
var f = history.forth(i);
|
||||
if (f != null) {
|
||||
cdSync(f.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
|
||||
@@ -12,7 +12,6 @@ import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
@@ -73,7 +72,7 @@ public class BrowserFileTransferOperation {
|
||||
this.progress.accept(progress);
|
||||
}
|
||||
|
||||
private BrowserAlerts.FileConflictChoice handleChoice(FileSystem fileSystem, String target, boolean multiple)
|
||||
private BrowserAlerts.FileConflictChoice handleChoice(FileSystem fileSystem, FilePath target, boolean multiple)
|
||||
throws Exception {
|
||||
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) {
|
||||
return BrowserAlerts.FileConflictChoice.CANCEL;
|
||||
@@ -177,7 +176,7 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
|
||||
var sourceFile = source.getPath();
|
||||
var targetFile = FileNames.join(target.getPath(), FileNames.getFileName(sourceFile));
|
||||
var targetFile = target.getPath().join(sourceFile.getFileName());
|
||||
|
||||
if (sourceFile.equals(targetFile)) {
|
||||
// Duplicate file by renaming it
|
||||
@@ -209,7 +208,7 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
}
|
||||
|
||||
private String renameFileLoop(FileSystem fileSystem, String target, boolean dir) throws Exception {
|
||||
private FilePath renameFileLoop(FileSystem fileSystem, FilePath target, boolean dir) throws Exception {
|
||||
// Who has more than 10 copies?
|
||||
for (int i = 0; i < 10; i++) {
|
||||
target = renameFile(target);
|
||||
@@ -220,23 +219,21 @@ public class BrowserFileTransferOperation {
|
||||
return target;
|
||||
}
|
||||
|
||||
private String renameFile(String target) {
|
||||
var targetFile = new FilePath(target);
|
||||
var name = targetFile.getFileName();
|
||||
private FilePath renameFile(FilePath target) {
|
||||
var name = target.getFileName();
|
||||
var pattern = Pattern.compile("(.+) \\((\\d+)\\)\\.(.+?)");
|
||||
var matcher = pattern.matcher(name);
|
||||
if (matcher.matches()) {
|
||||
try {
|
||||
var number = Integer.parseInt(matcher.group(2));
|
||||
var newFile =
|
||||
targetFile.getParent().join(matcher.group(1) + " (" + (number + 1) + ")." + matcher.group(3));
|
||||
return newFile.toString();
|
||||
var newFile = target.getParent().join(matcher.group(1) + " (" + (number + 1) + ")." + matcher.group(3));
|
||||
return newFile;
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
var noExt = targetFile.getFileName().equals(targetFile.getExtension());
|
||||
return targetFile.getBaseName() + " (" + 1 + ")" + (noExt ? "" : "." + targetFile.getExtension());
|
||||
var noExt = target.getFileName().equals(target.getExtension());
|
||||
return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (noExt ? "" : "." + target.getExtension()));
|
||||
}
|
||||
|
||||
private void handleSingleAcrossFileSystems(FileEntry source) throws Exception {
|
||||
@@ -248,7 +245,7 @@ public class BrowserFileTransferOperation {
|
||||
|
||||
// Prevent dropping directory into itself
|
||||
if (source.getFileSystem().equals(target.getFileSystem())
|
||||
&& FileNames.startsWith(source.getPath(), target.getPath())) {
|
||||
&& source.getPath().startsWith(target.getPath())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -260,21 +257,22 @@ public class BrowserFileTransferOperation {
|
||||
return;
|
||||
}
|
||||
|
||||
var directoryName = FileNames.getFileName(source.getPath());
|
||||
var directoryName = source.getPath().getFileName();
|
||||
flatFiles.put(source, directoryName);
|
||||
|
||||
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
|
||||
var baseRelative = source.getPath().getParent().toDirectory();
|
||||
List<FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
|
||||
for (FileEntry fileEntry : list) {
|
||||
if (cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
|
||||
var rel = fileEntry.getPath().relativize(baseRelative).toUnix().toString();
|
||||
flatFiles.put(fileEntry, rel);
|
||||
if (fileEntry.getKind() == FileKind.FILE) {
|
||||
// This one is up-to-date and does not need to be recalculated
|
||||
totalSize.addAndGet(fileEntry.getSize());
|
||||
// If we don't have a size, it doesn't matter that much as the total size is only for display
|
||||
totalSize.addAndGet(fileEntry.getFileSizeLong().orElse(0));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -284,9 +282,9 @@ public class BrowserFileTransferOperation {
|
||||
return;
|
||||
}
|
||||
|
||||
flatFiles.put(source, FileNames.getFileName(source.getPath()));
|
||||
// Recalculate as it could have been changed meanwhile
|
||||
totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath()));
|
||||
flatFiles.put(source, source.getPath().getFileName());
|
||||
// If we don't have a size, it doesn't matter that much as the total size is only for display
|
||||
totalSize.addAndGet(source.getFileSizeLong().orElse(0));
|
||||
}
|
||||
|
||||
var start = Instant.now();
|
||||
@@ -297,10 +295,10 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
|
||||
var sourceFile = e.getKey();
|
||||
var fixedRelPath = new FilePath(e.getValue())
|
||||
var fixedRelPath = FilePath.of(e.getValue())
|
||||
.fileSystemCompatible(
|
||||
target.getFileSystem().getShell().orElseThrow().getOsType());
|
||||
var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString());
|
||||
var targetFile = target.getPath().join(fixedRelPath.toString());
|
||||
if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
@@ -328,7 +326,7 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
|
||||
private void transfer(
|
||||
FileEntry sourceFile, String targetFile, AtomicLong transferred, AtomicLong totalSize, Instant start)
|
||||
FileEntry sourceFile, FilePath targetFile, AtomicLong transferred, AtomicLong totalSize, Instant start)
|
||||
throws Exception {
|
||||
if (cancelled()) {
|
||||
return;
|
||||
@@ -353,7 +351,8 @@ public class BrowserFileTransferOperation {
|
||||
}
|
||||
|
||||
outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
|
||||
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start);
|
||||
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start, fileSize);
|
||||
outputStream.flush();
|
||||
inputStream.transferTo(OutputStream.nullOutputStream());
|
||||
} catch (Exception ex) {
|
||||
// Mark progress as finished to reset any progress display
|
||||
@@ -412,7 +411,8 @@ public class BrowserFileTransferOperation {
|
||||
OutputStream outputStream,
|
||||
AtomicLong transferred,
|
||||
AtomicLong total,
|
||||
Instant start)
|
||||
Instant start,
|
||||
long expectedFileSize)
|
||||
throws Exception {
|
||||
// Initialize progress immediately prior to reading anything
|
||||
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
|
||||
@@ -421,7 +421,8 @@ public class BrowserFileTransferOperation {
|
||||
var exception = new AtomicReference<Exception>();
|
||||
var thread = ThreadHelper.createPlatformThread("transfer", true, () -> {
|
||||
try {
|
||||
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
|
||||
long readCount = 0;
|
||||
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, expectedFileSize);
|
||||
byte[] buffer = new byte[bs];
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
|
||||
@@ -437,10 +438,18 @@ public class BrowserFileTransferOperation {
|
||||
|
||||
outputStream.write(buffer, 0, read);
|
||||
transferred.addAndGet(read);
|
||||
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
|
||||
readCount += read;
|
||||
updateProgress(
|
||||
new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
|
||||
}
|
||||
|
||||
var incomplete = readCount < expectedFileSize;
|
||||
if (incomplete) {
|
||||
throw new IOException("Source file " + sourceFile.getPath() + " input did end prematurely");
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
exception.set(ex);
|
||||
killStreams.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package io.xpipe.app.browser.file;
|
||||
|
||||
import io.xpipe.core.store.FilePath;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -24,6 +26,6 @@ public interface BrowserHistorySavedState {
|
||||
class Entry {
|
||||
|
||||
UUID uuid;
|
||||
String path;
|
||||
FilePath path;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,16 +40,19 @@ public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {
|
||||
|
||||
private static BrowserHistorySavedStateImpl load() {
|
||||
return AppCache.getNonNull("browser-state", BrowserHistorySavedStateImpl.class, () -> {
|
||||
return new BrowserHistorySavedStateImpl(FXCollections.observableArrayList());
|
||||
return new BrowserHistorySavedStateImpl(
|
||||
FXCollections.synchronizedObservableList(FXCollections.observableArrayList()));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void add(BrowserHistorySavedState.Entry entry) {
|
||||
lastSystems.removeIf(s -> s.getUuid().equals(entry.getUuid()));
|
||||
lastSystems.addFirst(entry);
|
||||
if (lastSystems.size() > 15) {
|
||||
lastSystems.removeLast();
|
||||
synchronized (lastSystems) {
|
||||
lastSystems.removeIf(e -> e == null || e.getUuid().equals(entry.getUuid()));
|
||||
lastSystems.addFirst(entry);
|
||||
if (lastSystems.size() > 15) {
|
||||
lastSystems.removeLast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.util.DocumentationLink;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
@@ -36,7 +37,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var state = BrowserHistorySavedStateImpl.get();
|
||||
var list = new DerivedObservableList<>(state.getEntries(), true)
|
||||
var list = DerivedObservableList.wrap(state.getEntries(), true)
|
||||
.filtered(e -> {
|
||||
if (DataStorage.get() == null) {
|
||||
return false;
|
||||
@@ -101,7 +102,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
||||
.grow(true, false)
|
||||
.accessibleTextKey("restoreAllSessions");
|
||||
|
||||
var layout = new VerticalComp(List.of(vbox, Comp.vspacer(5), listBox, Comp.separator(), tile));
|
||||
var layout = new VerticalComp(List.of(vbox, Comp.vspacer(5), listBox, Comp.hseparator(), tile));
|
||||
layout.styleClass("welcome");
|
||||
layout.spacing(14);
|
||||
layout.maxWidth(1000);
|
||||
@@ -113,15 +114,24 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
||||
}
|
||||
|
||||
private Comp<?> createEmptyDisplay() {
|
||||
var intro = new IntroComp(
|
||||
var docs = new IntroComp("browserWelcomeDocs", new LabelGraphic.IconGraphic("mdi2b-book-open-variant"));
|
||||
docs.setButtonAction(() -> {
|
||||
DocumentationLink.GETTING_STARTED.open();
|
||||
});
|
||||
docs.setButtonDefault(true);
|
||||
|
||||
var open = new IntroComp(
|
||||
"browserWelcomeEmpty",
|
||||
new LabelGraphic.CompGraphic(PrettyImageHelper.ofSpecificFixedSize("graphics/Hips.svg", 100, 122)));
|
||||
intro.setButtonAction(() -> {
|
||||
open.setButtonAction(() -> {
|
||||
BrowserFullSessionModel.DEFAULT.openFileSystemAsync(
|
||||
DataStorage.get().local().ref(), null, null);
|
||||
});
|
||||
intro.setButtonDefault(true);
|
||||
return intro;
|
||||
|
||||
var v = new VerticalComp(List.of(docs, open));
|
||||
v.spacing(70);
|
||||
v.apply(struc -> struc.get().setAlignment(Pos.CENTER));
|
||||
return v;
|
||||
}
|
||||
|
||||
private Comp<?> entryButton(BrowserHistorySavedState.Entry e, BooleanProperty disable) {
|
||||
@@ -154,7 +164,9 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
||||
var name = Bindings.createStringBinding(
|
||||
() -> {
|
||||
var n = e.getPath();
|
||||
return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
|
||||
return AppPrefs.get().censorMode().get()
|
||||
? "*".repeat(n.toString().length())
|
||||
: n.toString();
|
||||
},
|
||||
AppPrefs.get().censorMode());
|
||||
return new ButtonComp(name, () -> {
|
||||
@@ -162,7 +174,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
|
||||
model.restoreStateAsync(e, disable);
|
||||
});
|
||||
})
|
||||
.accessibleText(e.getPath())
|
||||
.accessibleText(e.getPath().toString())
|
||||
.disable(disable)
|
||||
.styleClass("directory-button")
|
||||
.apply(struc -> struc.get().setMaxWidth(20000))
|
||||
|
||||
@@ -5,7 +5,7 @@ import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.browser.BrowserSessionTab;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
@@ -42,7 +42,7 @@ public final class BrowserHistoryTabModel extends BrowserSessionTab {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataColor getColor() {
|
||||
public DataStoreColor getColor() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.xpipe.app.browser.file;
|
||||
import io.xpipe.app.ext.LocalStore;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
|
||||
import java.nio.file.Files;
|
||||
@@ -33,9 +34,9 @@ public class BrowserLocalFileSystem {
|
||||
|
||||
return new FileEntry(
|
||||
localFileSystem.open(),
|
||||
file.toString(),
|
||||
FilePath.of(file),
|
||||
Files.getLastModifiedTime(file).toInstant(),
|
||||
Files.size(file),
|
||||
"" + Files.size(file),
|
||||
null,
|
||||
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.comp.augment.ContextMenuAugment;
|
||||
import io.xpipe.app.comp.base.PrettyImageHelper;
|
||||
import io.xpipe.app.comp.base.TextFieldComp;
|
||||
import io.xpipe.app.comp.base.TooltipAugment;
|
||||
import io.xpipe.app.comp.base.TooltipHelper;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.ContextMenuHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
@@ -46,7 +47,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
||||
? BrowserIconManager.getFileIcon(model.getCurrentDirectory())
|
||||
: null;
|
||||
},
|
||||
model.getCurrentPath());
|
||||
PlatformThread.sync(model.getCurrentPath()));
|
||||
var breadcrumbsGraphic = PrettyImageHelper.ofFixedSize(graphic, 24, 24)
|
||||
.styleClass("path-graphic")
|
||||
.createRegion();
|
||||
@@ -65,8 +66,10 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
||||
historyButton.getStyleClass().add(Styles.RIGHT_PILL);
|
||||
new ContextMenuAugment<>(event -> event.getButton() == MouseButton.PRIMARY, null, this::createContextMenu)
|
||||
.augment(new SimpleCompStructure<>(historyButton));
|
||||
new TooltipAugment<>("history", new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN))
|
||||
.augment(historyButton);
|
||||
Tooltip.install(
|
||||
historyButton,
|
||||
TooltipHelper.create(
|
||||
AppI18n.observable("history"), new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN)));
|
||||
|
||||
var breadcrumbs = new BrowserBreadcrumbBar(model);
|
||||
|
||||
@@ -85,7 +88,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
||||
&& !model.getInOverview().get();
|
||||
},
|
||||
pathRegion.focusedProperty(),
|
||||
model.getInOverview()));
|
||||
PlatformThread.sync(model.getInOverview())));
|
||||
var stack = new StackPane(pathRegion, breadcrumbsRegion);
|
||||
stack.setAlignment(Pos.CENTER_LEFT);
|
||||
pathRegion.prefHeightProperty().bind(stack.heightProperty());
|
||||
@@ -133,9 +136,11 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
||||
}
|
||||
|
||||
private Comp<CompStructure<TextField>> createPathBar() {
|
||||
var path = new SimpleStringProperty(model.getCurrentPath().get());
|
||||
var path = new SimpleStringProperty();
|
||||
model.getCurrentPath().subscribe((newValue) -> {
|
||||
path.set(newValue);
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
path.set(newValue != null ? newValue.toString() : null);
|
||||
});
|
||||
});
|
||||
path.addListener((observable, oldValue, newValue) -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
@@ -202,7 +207,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
||||
continue;
|
||||
}
|
||||
|
||||
var mi = new MenuItem(f.get(i));
|
||||
var mi = new MenuItem(f.get(i).toString());
|
||||
int target = i + 1;
|
||||
mi.setOnAction(event -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
@@ -219,7 +224,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
||||
}
|
||||
|
||||
if (model.getHistory().getCurrent() != null) {
|
||||
var current = new MenuItem(model.getHistory().getCurrent());
|
||||
var current = new MenuItem(model.getHistory().getCurrent().toString());
|
||||
current.setDisable(true);
|
||||
cm.getItems().add(current);
|
||||
}
|
||||
@@ -234,7 +239,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
|
||||
continue;
|
||||
}
|
||||
|
||||
var mi = new MenuItem(b.get(i));
|
||||
var mi = new MenuItem(b.get(i).toString());
|
||||
int target = i + 1;
|
||||
mi.setOnAction(event -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
|
||||
@@ -41,13 +41,10 @@ public class BrowserOverviewComp extends SimpleComp {
|
||||
var commonPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var common = sc.getOsType().determineInterestingPaths(sc).stream()
|
||||
.filter(s -> !s.isBlank())
|
||||
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
|
||||
.filter(entry -> {
|
||||
try {
|
||||
return sc.getShellDialect()
|
||||
.directoryExists(sc, entry.getPath())
|
||||
.executeAndCheck();
|
||||
return model.getFileSystem().directoryExists(entry.getPath());
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
return false;
|
||||
@@ -68,7 +65,7 @@ public class BrowserOverviewComp extends SimpleComp {
|
||||
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
|
||||
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview, false);
|
||||
|
||||
var recent = new DerivedObservableList<>(model.getSavedState().getRecentDirectories(), true)
|
||||
var recent = DerivedObservableList.wrap(model.getSavedState().getRecentDirectories(), true)
|
||||
.mapped(s -> FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()))
|
||||
.getList();
|
||||
var recentOverview = new BrowserFileOverviewComp(model, recent, true);
|
||||
|
||||
@@ -88,7 +88,7 @@ public class BrowserStatusBarComp extends SimpleComp {
|
||||
} else {
|
||||
var expected = p.expectedTimeRemaining();
|
||||
var show = p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0
|
||||
&& (p.getTotal() > 50_000_000 || expected.toMillis() > 5000);
|
||||
&& (!p.hasKnownTotalSize() || p.getTotal() > 50_000_000 || expected.toMillis() > 5000);
|
||||
var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "";
|
||||
return time;
|
||||
}
|
||||
@@ -106,6 +106,10 @@ public class BrowserStatusBarComp extends SimpleComp {
|
||||
return null;
|
||||
} else {
|
||||
var transferred = HumanReadableFormat.progressByteCount(p.getTransferred());
|
||||
if (!p.hasKnownTotalSize()) {
|
||||
return transferred;
|
||||
}
|
||||
|
||||
var all = HumanReadableFormat.byteCount(p.getTotal());
|
||||
return transferred + " / " + all;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ import io.xpipe.app.browser.BrowserAbstractSessionModel;
|
||||
import io.xpipe.app.browser.BrowserFullSessionModel;
|
||||
import io.xpipe.app.browser.BrowserSessionTab;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.base.AppMainWindowContentComp;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
import io.xpipe.app.terminal.TerminalDockComp;
|
||||
import io.xpipe.app.terminal.TerminalDockModel;
|
||||
import io.xpipe.app.terminal.TerminalView;
|
||||
@@ -19,6 +18,8 @@ import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.ListChangeListener;
|
||||
@@ -26,13 +27,13 @@ import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
|
||||
private final BrowserSessionTab origin;
|
||||
private final ObservableList<UUID> terminalRequests;
|
||||
private final TerminalDockModel dockModel = new TerminalDockModel();
|
||||
private final BooleanProperty opened = new SimpleBooleanProperty();
|
||||
private TerminalView.Listener listener;
|
||||
private ObservableBooleanValue viewActive;
|
||||
|
||||
@@ -47,7 +48,7 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
|
||||
@Override
|
||||
public Comp<?> comp() {
|
||||
return new TerminalDockComp(dockModel);
|
||||
return new TerminalDockComp(dockModel, opened);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -57,7 +58,6 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
|
||||
@Override
|
||||
public void init() throws Exception {
|
||||
var hasOpened = new AtomicBoolean();
|
||||
listener = new TerminalView.Listener() {
|
||||
@Override
|
||||
public void onSessionOpened(TerminalView.ShellSession session) {
|
||||
@@ -65,7 +65,7 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
return;
|
||||
}
|
||||
|
||||
hasOpened.set(true);
|
||||
opened.set(true);
|
||||
var sessions = TerminalView.get().getSessions();
|
||||
var tv = sessions.stream()
|
||||
.filter(s -> terminalRequests.contains(s.getRequest())
|
||||
@@ -116,7 +116,7 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
// If the terminal launch fails
|
||||
ThreadHelper.runAsync(() -> {
|
||||
ThreadHelper.sleep(5000);
|
||||
if (!hasOpened.get()) {
|
||||
if (!opened.get()) {
|
||||
refreshShowingState();
|
||||
}
|
||||
});
|
||||
@@ -138,7 +138,7 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
dockModel.toggleView(aBoolean);
|
||||
});
|
||||
});
|
||||
AppDialog.getModalOverlay().addListener((ListChangeListener<? super ModalOverlay>) c -> {
|
||||
AppDialog.getModalOverlays().addListener((ListChangeListener<? super ModalOverlay>) c -> {
|
||||
if (c.getList().size() > 0) {
|
||||
dockModel.toggleView(false);
|
||||
} else {
|
||||
@@ -177,7 +177,7 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataColor getColor() {
|
||||
public DataStoreColor getColor() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public class BrowserTransferComp extends SimpleComp {
|
||||
.styleClass("gray")
|
||||
.styleClass("download-background");
|
||||
|
||||
var binding = new DerivedObservableList<>(model.getItems(), true)
|
||||
var binding = DerivedObservableList.wrap(model.getItems(), true)
|
||||
.mapped(item -> item.getBrowserEntry())
|
||||
.getList();
|
||||
var list = new BrowserFileSelectionListComp(binding, entry -> {
|
||||
@@ -65,14 +65,14 @@ public class BrowserTransferComp extends SimpleComp {
|
||||
return Bindings.createStringBinding(
|
||||
() -> {
|
||||
var p = sourceItem.get().getProgress().getValue();
|
||||
var hideProgress = sourceItem
|
||||
.get()
|
||||
.downloadFinished()
|
||||
.get();
|
||||
if (p == null || !p.hasKnownTotalSize()) {
|
||||
return entry.getFileName();
|
||||
}
|
||||
|
||||
var hideProgress =
|
||||
sourceItem.get().downloadFinished().get();
|
||||
var share = p != null ? (p.getTransferred() * 100 / p.getTotal()) : 0;
|
||||
var progressSuffix = hideProgress
|
||||
? ""
|
||||
: " " + share + "%";
|
||||
var progressSuffix = hideProgress ? "" : " " + share + "%";
|
||||
return entry.getFileName() + progressSuffix;
|
||||
},
|
||||
sourceItem.get().getProgress());
|
||||
|
||||
@@ -22,6 +22,10 @@ public class BrowserTransferProgress {
|
||||
return transferred >= total;
|
||||
}
|
||||
|
||||
public boolean hasKnownTotalSize() {
|
||||
return total > 0;
|
||||
}
|
||||
|
||||
public Duration elapsedTime() {
|
||||
var now = Instant.now();
|
||||
var elapsed = Duration.between(start, now);
|
||||
|
||||
@@ -3,7 +3,6 @@ package io.xpipe.app.browser.icon;
|
||||
import io.xpipe.app.resources.AppResources;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -38,7 +37,8 @@ public abstract class BrowserIconDirectoryType {
|
||||
|
||||
@Override
|
||||
public boolean matches(FileEntry entry) {
|
||||
return entry.getPath().equals("/") || entry.getPath().matches("\\w:\\\\");
|
||||
return entry.getPath().toString().equals("/")
|
||||
|| entry.getPath().toString().matches("\\w:\\\\");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -99,7 +99,7 @@ public abstract class BrowserIconDirectoryType {
|
||||
return false;
|
||||
}
|
||||
|
||||
var name = FileNames.getFileName(entry.getPath());
|
||||
var name = entry.getPath().getFileName();
|
||||
return names.contains(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package io.xpipe.app.browser.icon;
|
||||
import io.xpipe.app.resources.AppResources;
|
||||
import io.xpipe.core.store.FileEntry;
|
||||
import io.xpipe.core.store.FileKind;
|
||||
import io.xpipe.core.store.FileNames;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -84,8 +83,8 @@ public abstract class BrowserIconFileType {
|
||||
return false;
|
||||
}
|
||||
|
||||
var name = FileNames.getFileName(entry.getPath());
|
||||
var ext = FileNames.getExtension(entry.getPath());
|
||||
var name = entry.getPath().getFileName();
|
||||
var ext = entry.getPath().getExtension();
|
||||
return (ext != null && endings.contains("." + ext.toLowerCase(Locale.ROOT))) || endings.contains(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ package io.xpipe.app.comp;
|
||||
|
||||
import io.xpipe.app.comp.augment.Augment;
|
||||
import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.comp.base.TooltipAugment;
|
||||
import io.xpipe.app.comp.base.TooltipHelper;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
@@ -12,6 +12,7 @@ import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
@@ -30,7 +31,11 @@ public abstract class Comp<S extends CompStructure<?>> {
|
||||
private List<Augment<S>> augments;
|
||||
|
||||
public static Comp<CompStructure<Region>> empty() {
|
||||
return of(() -> new Region());
|
||||
return of(() -> {
|
||||
var r = new Region();
|
||||
r.getStyleClass().add("empty");
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
public static Comp<CompStructure<Spacer>> hspacer(double size) {
|
||||
@@ -58,10 +63,14 @@ public abstract class Comp<S extends CompStructure<?>> {
|
||||
};
|
||||
}
|
||||
|
||||
public static Comp<CompStructure<Separator>> separator() {
|
||||
public static Comp<CompStructure<Separator>> hseparator() {
|
||||
return of(() -> new Separator(Orientation.HORIZONTAL));
|
||||
}
|
||||
|
||||
public static Comp<CompStructure<Separator>> vseparator() {
|
||||
return of(() -> new Separator(Orientation.VERTICAL));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <IR extends Region, SIN extends CompStructure<IR>, OR extends Region> Comp<CompStructure<OR>> derive(
|
||||
Comp<SIN> comp, Function<IR, OR> r) {
|
||||
@@ -208,15 +217,24 @@ public abstract class Comp<S extends CompStructure<?>> {
|
||||
}
|
||||
|
||||
public Comp<S> tooltip(ObservableValue<String> text) {
|
||||
return apply(new TooltipAugment<>(text, null));
|
||||
return apply(struc -> {
|
||||
var tt = TooltipHelper.create(text, null);
|
||||
Tooltip.install(struc.get(), tt);
|
||||
});
|
||||
}
|
||||
|
||||
public Comp<S> tooltipKey(String key) {
|
||||
return apply(new TooltipAugment<>(key, null));
|
||||
return apply(struc -> {
|
||||
var tt = TooltipHelper.create(AppI18n.observable(key), null);
|
||||
Tooltip.install(struc.get(), tt);
|
||||
});
|
||||
}
|
||||
|
||||
public Comp<S> tooltipKey(String key, KeyCombination shortcut) {
|
||||
return apply(new TooltipAugment<>(key, shortcut));
|
||||
return apply(struc -> {
|
||||
var tt = TooltipHelper.create(AppI18n.observable(key), shortcut);
|
||||
Tooltip.install(struc.get(), tt);
|
||||
});
|
||||
}
|
||||
|
||||
public Region createRegion() {
|
||||
|
||||
@@ -88,7 +88,7 @@ public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S
|
||||
if (!hide.get()) {
|
||||
var cm = contextMenu.get();
|
||||
if (cm != null) {
|
||||
cm.show(r, Side.BOTTOM, 0, 0);
|
||||
cm.show(r, Side.TOP, 0, 0);
|
||||
currentContextMenu.set(cm);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +26,9 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
|
||||
|
||||
private final AppLayoutModel model = AppLayoutModel.get();
|
||||
|
||||
@Override
|
||||
public Structure createBase() {
|
||||
var model = AppLayoutModel.get();
|
||||
Map<Comp<?>, ObservableValue<Boolean>> map = model.getEntries().stream()
|
||||
.filter(entry -> entry.comp() != null)
|
||||
.collect(Collectors.toMap(
|
||||
@@ -43,7 +42,7 @@ public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
|
||||
multi.styleClass("background");
|
||||
|
||||
var pane = new BorderPane();
|
||||
var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries());
|
||||
var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries(), model.getQueueEntries());
|
||||
StackPane multiR = (StackPane) multi.createRegion();
|
||||
pane.setCenter(multiR);
|
||||
var sidebarR = sidebar.createRegion();
|
||||
@@ -55,7 +54,7 @@ public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
|
||||
}
|
||||
|
||||
if (o != null && o.equals(model.getEntries().get(0))) {
|
||||
StoreViewState.get().updateDisplay();
|
||||
StoreViewState.get().triggerStoreListUpdate();
|
||||
}
|
||||
});
|
||||
pane.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
|
||||
@@ -13,7 +13,6 @@ import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.animation.Animation;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.image.ImageView;
|
||||
@@ -35,7 +34,7 @@ public class AppMainWindowContentComp extends SimpleComp {
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var overlay = AppDialog.getModalOverlay();
|
||||
var overlay = AppDialog.getModalOverlays();
|
||||
var loaded = AppMainWindow.getLoadedContent();
|
||||
var bg = Comp.of(() -> {
|
||||
var loadingIcon = new ImageView();
|
||||
|
||||
@@ -14,20 +14,23 @@ import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
@AllArgsConstructor
|
||||
public class ChoicePaneComp extends Comp<CompStructure<VBox>> {
|
||||
|
||||
List<Entry> entries;
|
||||
Property<Entry> selected;
|
||||
Function<ComboBox<Entry>, Region> transformer = c -> c;
|
||||
private final List<Entry> entries;
|
||||
private final Property<Entry> selected;
|
||||
|
||||
@Setter
|
||||
private Function<ComboBox<Entry>, Region> transformer = c -> c;
|
||||
|
||||
public ChoicePaneComp(List<Entry> entries, Property<Entry> selected) {
|
||||
this.entries = entries;
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<VBox> createBase() {
|
||||
|
||||
@@ -11,12 +11,15 @@ import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.ContextualFileReference;
|
||||
import io.xpipe.app.storage.DataStorageSyncHandler;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
@@ -40,13 +43,13 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
||||
}
|
||||
|
||||
private final Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem;
|
||||
private final Property<String> filePath;
|
||||
private final Property<FilePath> filePath;
|
||||
private final ContextualFileReferenceSync sync;
|
||||
private final List<PreviousFileReference> previousFileReferences;
|
||||
|
||||
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(
|
||||
Property<DataStoreEntryRef<T>> fileSystem,
|
||||
Property<String> filePath,
|
||||
Property<FilePath> filePath,
|
||||
ContextualFileReferenceSync sync,
|
||||
List<PreviousFileReference> previousFileReferences) {
|
||||
this.sync = sync;
|
||||
@@ -86,7 +89,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
||||
}
|
||||
|
||||
var currentPath = filePath.getValue();
|
||||
if (currentPath == null || currentPath.isBlank()) {
|
||||
if (currentPath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -95,22 +98,34 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
||||
}
|
||||
|
||||
try {
|
||||
var source = Path.of(currentPath.trim());
|
||||
var target = sync.getTargetLocation().apply(source);
|
||||
if (Files.exists(source)) {
|
||||
var shouldCopy = AppWindowHelper.showConfirmationAlert(
|
||||
"confirmGitShareTitle", "confirmGitShareHeader", "confirmGitShareContent");
|
||||
if (!shouldCopy) {
|
||||
return;
|
||||
}
|
||||
|
||||
var handler = DataStorageSyncHandler.getInstance();
|
||||
var syncedTarget = handler.addDataFile(
|
||||
source, target, sync.getPerUser().test(source));
|
||||
Platform.runLater(() -> {
|
||||
filePath.setValue(syncedTarget.toString());
|
||||
});
|
||||
var source = currentPath.asLocalPath();
|
||||
if (!Files.exists(source)) {
|
||||
ErrorEvent.fromMessage("Unable to resolve local file path " + source).expected().handle();
|
||||
return;
|
||||
}
|
||||
|
||||
var target = sync.getTargetLocation().apply(source);
|
||||
var shouldCopy = AppWindowHelper.showConfirmationAlert(
|
||||
"confirmGitShareTitle", "confirmGitShareHeader", "confirmGitShareContent");
|
||||
if (!shouldCopy) {
|
||||
return;
|
||||
}
|
||||
|
||||
var handler = DataStorageSyncHandler.getInstance();
|
||||
var syncedTarget = handler.addDataFile(
|
||||
source, target, sync.getPerUser().test(source));
|
||||
|
||||
var pubSource = Path.of(source + ".pub");
|
||||
if (Files.exists(pubSource)) {
|
||||
var pubTarget = sync.getTargetLocation().apply(pubSource);
|
||||
DataStorageSyncHandler.getInstance()
|
||||
.addDataFile(
|
||||
pubSource, pubTarget, sync.getPerUser().test(pubSource));
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
filePath.setValue(FilePath.of(syncedTarget));
|
||||
});
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
}
|
||||
@@ -147,7 +162,14 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
||||
var items = allFiles.stream()
|
||||
.map(previousFileReference -> previousFileReference.getPath().toString())
|
||||
.toList();
|
||||
var combo = new ComboTextFieldComp(filePath, items, param -> {
|
||||
var prop = new SimpleStringProperty();
|
||||
filePath.subscribe(s -> PlatformThread.runLaterIfNeeded(() -> {
|
||||
prop.set(s != null ? s.toString() : null);
|
||||
}));
|
||||
prop.addListener((observable, oldValue, newValue) -> {
|
||||
filePath.setValue(newValue != null ? FilePath.of(newValue) : null);
|
||||
});
|
||||
var combo = new ComboTextFieldComp(prop, items, param -> {
|
||||
return new ListCell<>() {
|
||||
@Override
|
||||
protected void updateItem(String item, boolean empty) {
|
||||
@@ -172,7 +194,14 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
||||
}
|
||||
|
||||
private Comp<?> createTextField() {
|
||||
var fileNameComp = new TextFieldComp(filePath)
|
||||
var prop = new SimpleStringProperty();
|
||||
filePath.subscribe(s -> PlatformThread.runLaterIfNeeded(() -> {
|
||||
prop.set(s != null ? s.toString() : null);
|
||||
}));
|
||||
prop.addListener((observable, oldValue, newValue) -> {
|
||||
filePath.setValue(newValue != null ? FilePath.of(newValue) : null);
|
||||
});
|
||||
var fileNameComp = new TextFieldComp(prop)
|
||||
.apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS))
|
||||
.styleClass(Styles.LEFT_PILL)
|
||||
.grow(false, true);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.util.GlobalTimer;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class DelayedInitComp extends SimpleComp {
|
||||
|
||||
private final Comp<?> comp;
|
||||
private final Supplier<Boolean> condition;
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var stack = new StackPane();
|
||||
GlobalTimer.scheduleUntil(Duration.ofMillis(10), () -> {
|
||||
if (!condition.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
var r = comp.createRegion();
|
||||
stack.getChildren().add(r);
|
||||
});
|
||||
return true;
|
||||
});
|
||||
return stack;
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@ import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -15,39 +13,13 @@ import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Function;
|
||||
|
||||
public abstract class DialogComp extends Comp<CompStructure<Region>> {
|
||||
|
||||
public static void showWindow(String titleKey, Function<Stage, DialogComp> f) {
|
||||
var loading = new SimpleBooleanProperty();
|
||||
var dialog = new AtomicReference<DialogComp>();
|
||||
Platform.runLater(() -> {
|
||||
var stage = AppWindowHelper.sideWindow(
|
||||
AppI18n.get(titleKey),
|
||||
window -> {
|
||||
var c = f.apply(window);
|
||||
dialog.set(c);
|
||||
loading.bind(c.busy());
|
||||
return c;
|
||||
},
|
||||
false,
|
||||
loading);
|
||||
stage.setOnCloseRequest(event -> {
|
||||
if (dialog.get() != null) {
|
||||
dialog.get().discard();
|
||||
}
|
||||
});
|
||||
stage.show();
|
||||
});
|
||||
}
|
||||
|
||||
protected Region createNavigation() {
|
||||
HBox buttons = new HBox();
|
||||
buttons.setFillHeight(true);
|
||||
|
||||
@@ -3,7 +3,11 @@ package io.xpipe.app.comp.base;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
@@ -11,10 +15,14 @@ import java.util.List;
|
||||
|
||||
public class HorizontalComp extends Comp<CompStructure<HBox>> {
|
||||
|
||||
private final List<Comp<?>> entries;
|
||||
private final ObservableList<Comp<?>> entries;
|
||||
|
||||
public HorizontalComp(List<Comp<?>> comps) {
|
||||
entries = List.copyOf(comps);
|
||||
entries = FXCollections.observableArrayList(List.copyOf(comps));
|
||||
}
|
||||
|
||||
public HorizontalComp(ObservableList<Comp<?>> entries) {
|
||||
this.entries = PlatformThread.sync(entries);
|
||||
}
|
||||
|
||||
public Comp<CompStructure<HBox>> spacing(double spacing) {
|
||||
@@ -23,8 +31,11 @@ public class HorizontalComp extends Comp<CompStructure<HBox>> {
|
||||
|
||||
@Override
|
||||
public CompStructure<HBox> createBase() {
|
||||
HBox b = new HBox();
|
||||
var b = new HBox();
|
||||
b.getStyleClass().add("horizontal-comp");
|
||||
entries.addListener((ListChangeListener<? super Comp<?>>) c -> {
|
||||
b.getChildren().setAll(c.getList().stream().map(Comp::createRegion).toList());
|
||||
});
|
||||
for (var entry : entries) {
|
||||
b.getChildren().add(entry.createRegion());
|
||||
}
|
||||
|
||||
@@ -29,8 +29,10 @@ public class IntegratedTextAreaComp extends Comp<IntegratedTextAreaComp.Structur
|
||||
|
||||
public static IntegratedTextAreaComp script(
|
||||
ObservableValue<DataStoreEntryRef<ShellStore>> host, Property<ShellScript> value) {
|
||||
var string = new SimpleStringProperty(
|
||||
value.getValue() != null ? value.getValue().getValue() : null);
|
||||
var string = new SimpleStringProperty();
|
||||
value.subscribe(shellScript -> {
|
||||
string.set(shellScript != null ? shellScript.getValue() : null);
|
||||
});
|
||||
string.addListener((observable, oldValue, newValue) -> {
|
||||
value.setValue(newValue != null ? new ShellScript(newValue) : null);
|
||||
});
|
||||
|
||||
@@ -4,8 +4,8 @@ import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
|
||||
@@ -3,6 +3,8 @@ package io.xpipe.app.comp.base;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.scene.control.SplitPane;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
@@ -30,32 +32,58 @@ public class LeftSplitPaneComp extends Comp<LeftSplitPaneComp.Structure> {
|
||||
if (initialWidth != null) {
|
||||
sidebar.setPrefWidth(initialWidth);
|
||||
}
|
||||
var r = new SplitPane(sidebar, c);
|
||||
var r = new SplitPane(c);
|
||||
|
||||
AtomicBoolean setInitial = new AtomicBoolean(false);
|
||||
r.widthProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue.doubleValue() <= 0) {
|
||||
if (newValue.doubleValue() <= 0 || !r.getItems().contains(sidebar)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!setInitial.get() && initialWidth != null) {
|
||||
if (!setInitial.get() && initialWidth != null && r.getDividers().size() > 0) {
|
||||
r.getDividers().getFirst().setPosition(initialWidth / newValue.doubleValue());
|
||||
setInitial.set(true);
|
||||
}
|
||||
});
|
||||
|
||||
SplitPane.setResizableWithParent(sidebar, false);
|
||||
r.getDividers().getFirst().positionProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (r.getWidth() <= 0) {
|
||||
var dividerPosition = new SimpleDoubleProperty();
|
||||
ChangeListener<Number> changeListener = (observable, oldValue, newValue) -> {
|
||||
if (r.getWidth() <= 0 || !r.getItems().contains(sidebar)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (onDividerChange != null) {
|
||||
onDividerChange.accept(newValue.doubleValue() * r.getWidth());
|
||||
}
|
||||
|
||||
dividerPosition.set(newValue.doubleValue());
|
||||
};
|
||||
|
||||
sidebar.managedProperty().subscribe(m -> {
|
||||
var divs = r.getDividers();
|
||||
if (!m) {
|
||||
if (!divs.isEmpty()) {
|
||||
divs.getFirst().positionProperty().removeListener(changeListener);
|
||||
}
|
||||
r.getItems().remove(sidebar);
|
||||
if (onDividerChange != null) {
|
||||
onDividerChange.accept(0.0);
|
||||
}
|
||||
} else if (!r.getItems().contains(sidebar)) {
|
||||
r.getItems().addFirst(sidebar);
|
||||
var d = dividerPosition.get();
|
||||
divs.getFirst().setPosition(d);
|
||||
r.layout();
|
||||
if (onDividerChange != null) {
|
||||
onDividerChange.accept(d);
|
||||
}
|
||||
divs.getFirst().positionProperty().addListener(changeListener);
|
||||
}
|
||||
});
|
||||
|
||||
SplitPane.setResizableWithParent(sidebar, false);
|
||||
r.getStyleClass().add("side-split-pane-comp");
|
||||
return new Structure(sidebar, c, r, r.getDividers().getFirst());
|
||||
return new Structure(sidebar, c, r);
|
||||
}
|
||||
|
||||
public LeftSplitPaneComp withInitialWidth(double val) {
|
||||
@@ -74,7 +102,6 @@ public class LeftSplitPaneComp extends Comp<LeftSplitPaneComp.Structure> {
|
||||
Region left;
|
||||
Region center;
|
||||
SplitPane pane;
|
||||
SplitPane.Divider divider;
|
||||
|
||||
@Override
|
||||
public SplitPane get() {
|
||||
|
||||
@@ -7,14 +7,11 @@ import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.comp.store.StoreViewState;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.util.PlatformState;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.animation.AnimationTimer;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
@@ -39,6 +36,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||
|
||||
private final ObservableList<T> shown;
|
||||
private final ObservableList<T> all;
|
||||
|
||||
private final Function<T, Comp<?>> compFunction;
|
||||
private final boolean scrollBar;
|
||||
|
||||
@@ -172,10 +170,10 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||
|
||||
Node c = vbox;
|
||||
do {
|
||||
c.boundsInParentProperty().addListener((change, oldBounds,newBounds) -> {
|
||||
c.boundsInParentProperty().addListener((change, oldBounds, newBounds) -> {
|
||||
dirty.set(true);
|
||||
});
|
||||
// Don't listen to root node changes, that seemingly can cause exceptions
|
||||
// Don't listen to root node changes, we don't need that
|
||||
} while ((c = c.getParent()) != null && c.getParent() != null);
|
||||
|
||||
if (newValue != null) {
|
||||
@@ -226,6 +224,10 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||
}
|
||||
|
||||
private void updateVisibilities(ScrollPane scroll, VBox vbox) {
|
||||
if (!Platform.isFxApplicationThread()) {
|
||||
throw new IllegalStateException("Not in FxApplication thread");
|
||||
}
|
||||
|
||||
if (!visibilityControl) {
|
||||
return;
|
||||
}
|
||||
@@ -251,40 +253,45 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||
Map<T, Region> cache,
|
||||
boolean refreshVisibilities) {
|
||||
Runnable update = () -> {
|
||||
synchronized (cache) {
|
||||
var set = new HashSet<T>();
|
||||
// These lists might diverge on updates, so add both
|
||||
synchronized (shown) {
|
||||
set.addAll(shown);
|
||||
}
|
||||
synchronized (all) {
|
||||
set.addAll(all);
|
||||
}
|
||||
// Clear cache of unused values
|
||||
cache.keySet().removeIf(t -> !set.contains(t));
|
||||
if (!Platform.isFxApplicationThread()) {
|
||||
throw new IllegalStateException("Not in FxApplication thread");
|
||||
}
|
||||
|
||||
var set = new HashSet<T>();
|
||||
// These lists might diverge on updates, so add both
|
||||
synchronized (shown) {
|
||||
set.addAll(shown);
|
||||
}
|
||||
synchronized (all) {
|
||||
set.addAll(all);
|
||||
}
|
||||
// Clear cache of unused values
|
||||
cache.keySet().retainAll(set);
|
||||
|
||||
// Use copy to prevent concurrent modifications and to not synchronize to long
|
||||
List<T> shownCopy;
|
||||
synchronized (shown) {
|
||||
shownCopy = new ArrayList<>(shown);
|
||||
}
|
||||
List<Region> newShown = shownCopy.stream().map(v -> {
|
||||
if (!cache.containsKey(v)) {
|
||||
var comp = compFunction.apply(v);
|
||||
if (comp != null) {
|
||||
var r = comp.createRegion();
|
||||
if (visibilityControl) {
|
||||
r.setVisible(false);
|
||||
List<Region> newShown = shownCopy.stream()
|
||||
.map(v -> {
|
||||
if (!cache.containsKey(v)) {
|
||||
var comp = compFunction.apply(v);
|
||||
if (comp != null) {
|
||||
var r = comp.createRegion();
|
||||
if (visibilityControl) {
|
||||
r.setVisible(false);
|
||||
}
|
||||
cache.put(v, r);
|
||||
} else {
|
||||
cache.put(v, null);
|
||||
}
|
||||
}
|
||||
cache.put(v, r);
|
||||
} else {
|
||||
cache.put(v, null);
|
||||
}
|
||||
}
|
||||
|
||||
return cache.get(v);
|
||||
}).filter(region -> region != null).toList();
|
||||
return cache.get(v);
|
||||
})
|
||||
.filter(region -> region != null)
|
||||
.toList();
|
||||
|
||||
if (listView.getChildren().equals(newShown)) {
|
||||
return;
|
||||
@@ -298,7 +305,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||
r.pseudoClassStateChanged(LAST, i == newShown.size() - 1);
|
||||
}
|
||||
|
||||
var d = new DerivedObservableList<>(listView.getChildren(), true);
|
||||
var d = DerivedObservableList.wrap(listView.getChildren(), true);
|
||||
d.setContent(newShown);
|
||||
if (refreshVisibilities) {
|
||||
updateVisibilities(scroll, listView);
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.control.ScrollBar;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class ListVirtualViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||
|
||||
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd");
|
||||
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even");
|
||||
private static final PseudoClass FIRST = PseudoClass.getPseudoClass("first");
|
||||
private static final PseudoClass LAST = PseudoClass.getPseudoClass("last");
|
||||
|
||||
private final ObservableList<T> shown;
|
||||
private final ObservableList<T> all;
|
||||
private final Function<T, Comp<?>> compFunction;
|
||||
private final int limit = Integer.MAX_VALUE;
|
||||
private final boolean scrollBar;
|
||||
|
||||
@Setter
|
||||
private int platformPauseInterval = -1;
|
||||
|
||||
public ListVirtualViewComp(
|
||||
ObservableList<T> shown, ObservableList<T> all, Function<T, Comp<?>> compFunction, boolean scrollBar) {
|
||||
this.shown = shown;
|
||||
this.all = all;
|
||||
this.compFunction = compFunction;
|
||||
this.scrollBar = scrollBar;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<ScrollPane> createBase() {
|
||||
Map<T, Region> cache = new IdentityHashMap<>();
|
||||
|
||||
var vbox = new VirtualFlow<>();
|
||||
vbox.getStyleClass().add("list-box-content");
|
||||
vbox.setFocusTraversable(false);
|
||||
|
||||
var scroll = new ScrollPane(vbox);
|
||||
if (scrollBar) {
|
||||
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
|
||||
scroll.skinProperty().subscribe(newValue -> {
|
||||
if (newValue != null) {
|
||||
ScrollBar bar = (ScrollBar) scroll.lookup(".scroll-bar:vertical");
|
||||
bar.opacityProperty()
|
||||
.bind(Bindings.createDoubleBinding(
|
||||
() -> {
|
||||
var v = bar.getVisibleAmount();
|
||||
// Check for rounding and accuracy issues
|
||||
// It might not be exactly equal to 1.0
|
||||
return v < 0.99 ? 1.0 : 0.0;
|
||||
},
|
||||
bar.visibleAmountProperty()));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scroll.setFitToHeight(true);
|
||||
}
|
||||
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
|
||||
scroll.setFitToWidth(true);
|
||||
scroll.getStyleClass().add("list-box-view-comp");
|
||||
return new SimpleCompStructure<>(scroll);
|
||||
}
|
||||
|
||||
private void refresh(
|
||||
VBox listView, List<? extends T> shown, List<? extends T> all, Map<T, Region> cache, boolean asynchronous) {
|
||||
Runnable update = () -> {
|
||||
synchronized (cache) {
|
||||
// Clear cache of unused values
|
||||
cache.keySet().removeIf(t -> !all.contains(t));
|
||||
}
|
||||
|
||||
final long[] lastPause = {System.currentTimeMillis()};
|
||||
// Create copy to reduce chances of concurrent modification
|
||||
var shownCopy = new ArrayList<>(shown);
|
||||
var newShown = shownCopy.stream()
|
||||
.map(v -> {
|
||||
var elapsed = System.currentTimeMillis() - lastPause[0];
|
||||
if (platformPauseInterval != -1 && elapsed > platformPauseInterval) {
|
||||
PlatformThread.runNestedLoopIteration();
|
||||
lastPause[0] = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
if (!cache.containsKey(v)) {
|
||||
var comp = compFunction.apply(v);
|
||||
cache.put(v, comp != null ? comp.createRegion() : null);
|
||||
}
|
||||
|
||||
return cache.get(v);
|
||||
})
|
||||
.filter(region -> region != null)
|
||||
.limit(limit)
|
||||
.toList();
|
||||
|
||||
if (listView.getChildren().equals(newShown)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < newShown.size(); i++) {
|
||||
var r = newShown.get(i);
|
||||
r.pseudoClassStateChanged(ODD, i % 2 != 0);
|
||||
r.pseudoClassStateChanged(EVEN, i % 2 == 0);
|
||||
r.pseudoClassStateChanged(FIRST, i == 0);
|
||||
r.pseudoClassStateChanged(LAST, i == newShown.size() - 1);
|
||||
}
|
||||
|
||||
var d = new DerivedObservableList<>(listView.getChildren(), true);
|
||||
d.setContent(newShown);
|
||||
};
|
||||
|
||||
if (asynchronous) {
|
||||
Platform.runLater(update);
|
||||
} else {
|
||||
PlatformThread.runLaterIfNeeded(update);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
|
||||
|
||||
private Path getHtmlFile(String markdown) {
|
||||
if (TEMP == null) {
|
||||
TEMP = ShellTemp.getLocalTempDataDirectory("wv");
|
||||
TEMP = ShellTemp.getLocalTempDataDirectory("webview");
|
||||
}
|
||||
|
||||
if (markdown == null) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.*;
|
||||
import lombok.experimental.NonFinal;
|
||||
|
||||
@@ -16,7 +20,7 @@ import java.util.List;
|
||||
public class ModalOverlay {
|
||||
|
||||
public static ModalOverlay of(Comp<?> content) {
|
||||
return of(null, content, null);
|
||||
return of((ObservableValue<String>) null, content, null);
|
||||
}
|
||||
|
||||
public static ModalOverlay of(String titleKey, Comp<?> content) {
|
||||
@@ -24,7 +28,11 @@ public class ModalOverlay {
|
||||
}
|
||||
|
||||
public static ModalOverlay of(String titleKey, Comp<?> content, LabelGraphic graphic) {
|
||||
return new ModalOverlay(titleKey, content, graphic, new ArrayList<>(), false);
|
||||
return of(titleKey != null ? AppI18n.observable(titleKey) : null, content, graphic);
|
||||
}
|
||||
|
||||
public static ModalOverlay of(ObservableValue<String> title, Comp<?> content, LabelGraphic graphic) {
|
||||
return new ModalOverlay(title, content, graphic, new ArrayList<>(), true, false, null);
|
||||
}
|
||||
|
||||
public ModalOverlay withDefaultButtons(Runnable action) {
|
||||
@@ -37,7 +45,7 @@ public class ModalOverlay {
|
||||
return withDefaultButtons(() -> {});
|
||||
}
|
||||
|
||||
String titleKey;
|
||||
ObservableValue<String> title;
|
||||
Comp<?> content;
|
||||
LabelGraphic graphic;
|
||||
|
||||
@@ -45,27 +53,47 @@ public class ModalOverlay {
|
||||
List<Object> buttons;
|
||||
|
||||
@NonFinal
|
||||
boolean persistent;
|
||||
@Setter
|
||||
boolean hasCloseButton;
|
||||
|
||||
@NonFinal
|
||||
@Setter
|
||||
boolean requireCloseButtonForClose;
|
||||
|
||||
@NonFinal
|
||||
@Setter
|
||||
Runnable hideAction;
|
||||
|
||||
public ModalButton addButton(ModalButton button) {
|
||||
buttons.add(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
public void hideable(ObservableValue<String> name, LabelGraphic icon, Runnable action) {
|
||||
setHideAction(() -> {
|
||||
AppLayoutModel.get().getQueueEntries().add(new AppLayoutModel.QueueEntry(name, icon, action));
|
||||
});
|
||||
}
|
||||
|
||||
public void addButtonBarComp(Comp<?> comp) {
|
||||
buttons.add(comp);
|
||||
}
|
||||
|
||||
public void persist() {
|
||||
persistent = true;
|
||||
this.hasCloseButton = false;
|
||||
this.requireCloseButtonForClose = true;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
AppDialog.show(this, false);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
AppDialog.hide(this);
|
||||
}
|
||||
|
||||
public boolean isShowing() {
|
||||
return AppDialog.getModalOverlay().contains(this);
|
||||
return AppDialog.getModalOverlays().contains(this);
|
||||
}
|
||||
|
||||
public void showAndWait() {
|
||||
|
||||
@@ -12,15 +12,14 @@ import javafx.animation.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.value.ObservableDoubleValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -45,7 +44,8 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
protected Region createSimple() {
|
||||
var bgRegion = background.createRegion();
|
||||
var modal = new ModalPane();
|
||||
modal.setInTransitionFactory(null);
|
||||
modal.setInTransitionFactory(
|
||||
OsType.getLocal() == OsType.LINUX ? null : node -> Animations.fadeIn(node, Duration.millis(150)));
|
||||
modal.setOutTransitionFactory(
|
||||
OsType.getLocal() == OsType.LINUX ? null : node -> Animations.fadeOut(node, Duration.millis(50)));
|
||||
modal.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -86,7 +86,7 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
}
|
||||
});
|
||||
|
||||
modal.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
modal.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (event.getCode() == KeyCode.ENTER) {
|
||||
var ov = overlayContent.getValue();
|
||||
if (ov != null) {
|
||||
@@ -150,9 +150,9 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
|
||||
private void showModalBox(ModalPane modal, ModalOverlay overlay) {
|
||||
var modalBox = toBox(modal, overlay);
|
||||
modal.setPersistent(overlay.isPersistent());
|
||||
modal.setPersistent(overlay.isRequireCloseButtonForClose());
|
||||
modal.show(modalBox);
|
||||
if (overlay.isPersistent() || overlay.getTitleKey() == null) {
|
||||
if (!overlay.isHasCloseButton() || overlay.getTitle() == null) {
|
||||
var closeButton = modalBox.lookup(".close-button");
|
||||
if (closeButton != null) {
|
||||
closeButton.setVisible(false);
|
||||
@@ -164,17 +164,17 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
Region r = newValue.getContent().createRegion();
|
||||
|
||||
var content = new VBox(r);
|
||||
content.getStyleClass().add("content");
|
||||
content.focusedProperty().addListener((o, old, n) -> {
|
||||
if (n) {
|
||||
r.requestFocus();
|
||||
}
|
||||
});
|
||||
content.setSpacing(25);
|
||||
content.setPadding(new Insets(13, 27, 20, 27));
|
||||
content.setSpacing(20);
|
||||
|
||||
if (newValue.getTitleKey() != null) {
|
||||
if (newValue.getTitle() != null) {
|
||||
var l = new Label(
|
||||
AppI18n.get(newValue.getTitleKey()),
|
||||
newValue.getTitle().getValue(),
|
||||
newValue.getGraphic() != null ? newValue.getGraphic().createGraphicNode() : null);
|
||||
l.setGraphicTextGap(8);
|
||||
AppFontSizes.xl(l);
|
||||
@@ -184,20 +184,38 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
}
|
||||
|
||||
if (newValue.getButtons().size() > 0) {
|
||||
var buttonBar = new ButtonBar();
|
||||
var max = new SimpleDoubleProperty();
|
||||
var buttonBar = new HBox();
|
||||
buttonBar.getStyleClass().add("button-bar");
|
||||
buttonBar.setSpacing(10);
|
||||
buttonBar.setAlignment(Pos.CENTER_RIGHT);
|
||||
for (var o : newValue.getButtons()) {
|
||||
var node = o instanceof ModalButton mb ? toButton(mb) : ((Comp<?>) o).createRegion();
|
||||
buttonBar.getButtons().add(node);
|
||||
ButtonBar.setButtonUniformSize(node, o instanceof ModalButton);
|
||||
if (o instanceof ModalButton) {
|
||||
node.widthProperty().addListener((observable, oldValue, n) -> {
|
||||
var d = Math.min(Math.max(n.doubleValue(), 70.0), 200.0);
|
||||
if (d > max.get()) {
|
||||
max.set(d);
|
||||
}
|
||||
});
|
||||
}
|
||||
node.minWidthProperty().bind(max);
|
||||
buttonBar.getChildren().add(node);
|
||||
if (o instanceof ModalButton) {
|
||||
node.prefHeightProperty().bind(buttonBar.heightProperty());
|
||||
}
|
||||
}
|
||||
content.getChildren().add(buttonBar);
|
||||
AppFontSizes.xs(buttonBar);
|
||||
AppFontSizes.apply(buttonBar, sizes -> {
|
||||
if (sizes.getBase().equals("10.5")) {
|
||||
return sizes.getBase();
|
||||
} else {
|
||||
return sizes.getSm();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var modalBox = new ModalBox(content) {
|
||||
var modalBox = new ModalBox(pane, content) {
|
||||
|
||||
@Override
|
||||
protected void setCloseButtonPosition() {
|
||||
@@ -205,18 +223,23 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
setRightAnchor(closeButton, 19d);
|
||||
}
|
||||
};
|
||||
if (newValue.getHideAction() != null) {
|
||||
modalBox.setOnMinimize(event -> {
|
||||
newValue.getHideAction().run();
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
modalBox.setOnClose(event -> {
|
||||
overlayContent.setValue(null);
|
||||
event.consume();
|
||||
});
|
||||
r.maxHeightProperty().bind(pane.heightProperty().subtract(200));
|
||||
content.maxHeightProperty().bind(pane.heightProperty().subtract(40));
|
||||
modalBox.minHeightProperty().bind(content.heightProperty());
|
||||
|
||||
content.prefWidthProperty().bind(modalBox.widthProperty());
|
||||
modalBox.setMinWidth(100);
|
||||
modalBox.setMinHeight(100);
|
||||
modalBox.prefWidthProperty().bind(modalBoxWidth(pane, r));
|
||||
modalBox.maxWidthProperty().bind(modalBox.prefWidthProperty());
|
||||
modalBox.prefHeightProperty().bind(modalBoxHeight(pane, content));
|
||||
modalBox.setMaxHeight(Region.USE_PREF_SIZE);
|
||||
modalBox.focusedProperty().addListener((o, old, n) -> {
|
||||
if (n) {
|
||||
@@ -228,14 +251,6 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
var busy = mocc.busy();
|
||||
if (busy != null) {
|
||||
var loading = LoadingOverlayComp.noProgress(Comp.of(() -> modalBox), busy);
|
||||
// loading.apply(struc -> {
|
||||
// var bg = struc.get().getChildren().getFirst();
|
||||
// struc.get().getChildren().get(1).addEventFilter(MouseEvent.MOUSE_PRESSED, event ->
|
||||
// {
|
||||
// bg.fireEvent(event);
|
||||
// event.consume();
|
||||
// });
|
||||
// });
|
||||
return loading.createRegion();
|
||||
}
|
||||
}
|
||||
@@ -256,23 +271,6 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
r.prefWidthProperty());
|
||||
}
|
||||
|
||||
private ObservableDoubleValue modalBoxHeight(ModalPane pane, Region content) {
|
||||
return Bindings.createDoubleBinding(
|
||||
() -> {
|
||||
var max = pane.getHeight() - 20;
|
||||
if (content.getPrefHeight() != Region.USE_COMPUTED_SIZE) {
|
||||
return Math.min(max, content.getPrefHeight());
|
||||
}
|
||||
|
||||
return Math.min(max, content.getHeight());
|
||||
},
|
||||
pane.heightProperty(),
|
||||
pane.prefHeightProperty(),
|
||||
content.prefHeightProperty(),
|
||||
content.heightProperty(),
|
||||
content.maxHeightProperty());
|
||||
}
|
||||
|
||||
private Button toButton(ModalButton mb) {
|
||||
var button = new Button(mb.getKey() != null ? AppI18n.get(mb.getKey()) : null);
|
||||
if (mb.isDefaultButton()) {
|
||||
@@ -281,6 +279,7 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
if (mb.getAugment() != null) {
|
||||
mb.getAugment().accept(button);
|
||||
}
|
||||
button.managedProperty().bind(button.visibleProperty());
|
||||
button.setOnAction(event -> {
|
||||
if (mb.getAction() != null) {
|
||||
mb.getAction().run();
|
||||
@@ -292,19 +291,4 @@ public class ModalOverlayComp extends SimpleComp {
|
||||
});
|
||||
return button;
|
||||
}
|
||||
|
||||
private Timeline fadeInDelyed(Node node) {
|
||||
var t = new Timeline(
|
||||
new KeyFrame(Duration.ZERO, new KeyValue(node.opacityProperty(), 0.01)),
|
||||
new KeyFrame(Duration.millis(50), new KeyValue(node.opacityProperty(), 0.01, Animations.EASE)),
|
||||
new KeyFrame(Duration.millis(1250), new KeyValue(node.opacityProperty(), 1, Animations.EASE)));
|
||||
|
||||
t.statusProperty().addListener((obs, old, val) -> {
|
||||
if (val == Animation.Status.STOPPED) {
|
||||
node.setOpacity(1);
|
||||
}
|
||||
});
|
||||
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,21 @@ import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Orientation;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import atlantafx.base.controls.Popover;
|
||||
import atlantafx.base.controls.Spacer;
|
||||
@@ -24,20 +29,17 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Getter
|
||||
public class OptionsComp extends Comp<CompStructure<Pane>> {
|
||||
public class OptionsComp extends Comp<CompStructure<VBox>> {
|
||||
|
||||
private final List<OptionsComp.Entry> entries;
|
||||
private final List<Entry> entries;
|
||||
|
||||
public OptionsComp(List<OptionsComp.Entry> entries) {
|
||||
public OptionsComp(List<Entry> entries) {
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<Pane> createBase() {
|
||||
Pane pane;
|
||||
var content = new VBox();
|
||||
content.setSpacing(7);
|
||||
pane = content;
|
||||
public CompStructure<VBox> createBase() {
|
||||
VBox pane = new VBox();
|
||||
pane.getStyleClass().add("options-comp");
|
||||
|
||||
var nameRegions = new ArrayList<Region>();
|
||||
@@ -58,7 +60,6 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
|
||||
if (showVertical) {
|
||||
var line = new VBox();
|
||||
line.prefWidthProperty().bind(pane.widthProperty());
|
||||
line.setSpacing(2);
|
||||
|
||||
var name = new Label();
|
||||
name.getStyleClass().add("name");
|
||||
@@ -67,10 +68,18 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
|
||||
name.setMinHeight(Region.USE_PREF_SIZE);
|
||||
name.setAlignment(Pos.CENTER_LEFT);
|
||||
if (compRegion != null) {
|
||||
VBox.setVgrow(line, VBox.getVgrow(compRegion));
|
||||
line.spacingProperty()
|
||||
.bind(PlatformThread.sync(Bindings.createDoubleBinding(
|
||||
() -> {
|
||||
return name.isManaged() ? 2.0 : 0.0;
|
||||
},
|
||||
name.managedProperty())));
|
||||
name.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty()));
|
||||
name.managedProperty().bind(PlatformThread.sync(compRegion.managedProperty()));
|
||||
}
|
||||
line.getChildren().add(name);
|
||||
VBox.setMargin(name, new Insets(0, 0, 0, 1));
|
||||
|
||||
if (entry.description() != null) {
|
||||
var description = new Label();
|
||||
@@ -84,15 +93,20 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
|
||||
description.managedProperty().bind(PlatformThread.sync(compRegion.managedProperty()));
|
||||
}
|
||||
|
||||
if (entry.longDescriptionSource() != null) {
|
||||
var markDown = new MarkdownComp(entry.longDescriptionSource(), s -> s, true)
|
||||
.apply(struc -> struc.get().setMaxWidth(500))
|
||||
.apply(struc -> struc.get().setMaxHeight(400));
|
||||
var popover = new Popover(markDown.createRegion());
|
||||
popover.setCloseButtonEnabled(false);
|
||||
popover.setHeaderAlwaysVisible(false);
|
||||
popover.setDetachable(true);
|
||||
AppFontSizes.xs(popover.getContentNode());
|
||||
if (entry.longDescription() != null) {
|
||||
Popover popover;
|
||||
if (!entry.longDescription().startsWith("http")) {
|
||||
var markDown = new MarkdownComp(entry.longDescription(), s -> s, true)
|
||||
.apply(struc -> struc.get().setMaxWidth(500))
|
||||
.apply(struc -> struc.get().setMaxHeight(400));
|
||||
popover = new Popover(markDown.createRegion());
|
||||
popover.setCloseButtonEnabled(false);
|
||||
popover.setHeaderAlwaysVisible(false);
|
||||
popover.setDetachable(true);
|
||||
AppFontSizes.xs(popover.getContentNode());
|
||||
} else {
|
||||
popover = null;
|
||||
}
|
||||
|
||||
var extendedDescription = new Button("... ?");
|
||||
extendedDescription.setMinWidth(Region.USE_PREF_SIZE);
|
||||
@@ -102,16 +116,27 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
|
||||
extendedDescription.setAccessibleText("Help");
|
||||
AppFontSizes.xl(extendedDescription);
|
||||
extendedDescription.setOnAction(e -> {
|
||||
popover.show(extendedDescription);
|
||||
if (entry.longDescription().startsWith("http")) {
|
||||
Hyperlinks.open(entry.longDescription());
|
||||
} else if (popover != null) {
|
||||
popover.show(extendedDescription);
|
||||
}
|
||||
e.consume();
|
||||
});
|
||||
|
||||
if (entry.longDescription().startsWith("http")) {
|
||||
var tt = TooltipHelper.create(new SimpleStringProperty(entry.longDescription()), null);
|
||||
tt.setShowDelay(Duration.millis(1));
|
||||
Tooltip.install(extendedDescription, tt);
|
||||
}
|
||||
|
||||
var descriptionBox =
|
||||
new HBox(description, new Spacer(Orientation.HORIZONTAL), extendedDescription);
|
||||
descriptionBox.setSpacing(5);
|
||||
HBox.setHgrow(descriptionBox, Priority.ALWAYS);
|
||||
descriptionBox.setAlignment(Pos.CENTER_LEFT);
|
||||
line.getChildren().add(descriptionBox);
|
||||
VBox.setMargin(descriptionBox, new Insets(0, 0, 0, 1));
|
||||
|
||||
if (compRegion != null) {
|
||||
descriptionBox.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty()));
|
||||
@@ -120,6 +145,7 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
|
||||
} else {
|
||||
line.getChildren().add(description);
|
||||
line.getChildren().add(new Spacer(2, Orientation.VERTICAL));
|
||||
VBox.setMargin(description, new Insets(0, 0, 0, 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +189,16 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
|
||||
pane.getChildren().add(compRegion);
|
||||
}
|
||||
}
|
||||
|
||||
var last = entry.equals(entries.getLast());
|
||||
if (!last) {
|
||||
Spacer spacer = new Spacer(7, Orientation.VERTICAL);
|
||||
pane.getChildren().add(spacer);
|
||||
if (compRegion != null) {
|
||||
spacer.visibleProperty().bind(PlatformThread.sync(compRegion.visibleProperty()));
|
||||
spacer.managedProperty().bind(PlatformThread.sync(compRegion.managedProperty()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entries.size() == 1 && firstComp != null) {
|
||||
@@ -196,7 +232,7 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
|
||||
public record Entry(
|
||||
String key,
|
||||
ObservableValue<String> description,
|
||||
String longDescriptionSource,
|
||||
String longDescription,
|
||||
ObservableValue<String> name,
|
||||
Comp<?> comp) {}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ public class ScrollComp extends Comp<CompStructure<ScrollPane>> {
|
||||
|
||||
@Override
|
||||
public CompStructure<ScrollPane> createBase() {
|
||||
var stack = new StackPane(content.createRegion());
|
||||
var r = content.createRegion();
|
||||
var stack = new StackPane(r);
|
||||
stack.getStyleClass().add("scroll-comp-content");
|
||||
|
||||
var sp = new ScrollPane(stack);
|
||||
|
||||
@@ -6,12 +6,16 @@ import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.core.AppDistributionType;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.AppProperties;
|
||||
import io.xpipe.app.update.UpdateAvailableDialog;
|
||||
import io.xpipe.app.util.Hyperlinks;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -19,51 +23,22 @@ import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||
|
||||
private final Property<AppLayoutModel.Entry> value;
|
||||
private final List<AppLayoutModel.Entry> entries;
|
||||
|
||||
public SideMenuBarComp(Property<AppLayoutModel.Entry> value, List<AppLayoutModel.Entry> entries) {
|
||||
this.value = value;
|
||||
this.entries = entries;
|
||||
}
|
||||
private final ObservableList<AppLayoutModel.QueueEntry> queueEntries;
|
||||
|
||||
@Override
|
||||
public CompStructure<VBox> createBase() {
|
||||
var vbox = new VBox();
|
||||
vbox.setFillWidth(true);
|
||||
|
||||
var selectedBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
var c = Platform.getPreferences()
|
||||
.getAccentColor()
|
||||
.desaturate()
|
||||
.desaturate();
|
||||
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(14, 1, 14, 2)));
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
|
||||
var hoverBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
var c = Platform.getPreferences()
|
||||
.getAccentColor()
|
||||
.darker()
|
||||
.desaturate()
|
||||
.desaturate();
|
||||
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(14, 1, 14, 2)));
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
|
||||
var noneBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
return Background.fill(Color.TRANSPARENT);
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
|
||||
var selected = PseudoClass.getPseudoClass("selected");
|
||||
for (AppLayoutModel.Entry e : entries) {
|
||||
var b = new IconButtonComp(e.icon(), () -> {
|
||||
if (e.action() != null) {
|
||||
@@ -73,47 +48,11 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||
|
||||
value.setValue(e);
|
||||
});
|
||||
var shortcut = e.combination();
|
||||
b.apply(new TooltipAugment<>(e.name(), shortcut));
|
||||
b.apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
struc.get().setAlignment(Pos.CENTER);
|
||||
|
||||
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
struc.get().pseudoClassStateChanged(selected, n.equals(e));
|
||||
});
|
||||
});
|
||||
});
|
||||
b.tooltip(e.name());
|
||||
b.accessibleText(e.name());
|
||||
|
||||
var indicator = Comp.empty().styleClass("indicator");
|
||||
var stack = new StackComp(List.of(indicator, b))
|
||||
.apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
|
||||
stack.apply(struc -> {
|
||||
var indicatorRegion = (Region) struc.get().getChildren().getFirst();
|
||||
indicatorRegion.setMaxWidth(7);
|
||||
indicatorRegion
|
||||
.backgroundProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (value.getValue().equals(e)) {
|
||||
return selectedBorder.get();
|
||||
}
|
||||
|
||||
if (struc.get().isHover()) {
|
||||
return hoverBorder.get();
|
||||
}
|
||||
|
||||
return noneBorder.get();
|
||||
},
|
||||
struc.get().hoverProperty(),
|
||||
value,
|
||||
hoverBorder,
|
||||
selectedBorder,
|
||||
noneBorder));
|
||||
});
|
||||
var stack = createStyle(e, b);
|
||||
var shortcut = e.combination();
|
||||
if (shortcut != null) {
|
||||
stack.apply(struc -> struc.get().getProperties().put("shortcut", shortcut));
|
||||
}
|
||||
@@ -121,13 +60,10 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||
}
|
||||
|
||||
{
|
||||
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableDialog.showIfNeeded())
|
||||
.tooltipKey("updateAvailableTooltip")
|
||||
.accessibleTextKey("updateAvailableTooltip");
|
||||
b.apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
});
|
||||
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
|
||||
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableDialog.showIfNeeded(false));
|
||||
b.tooltipKey("updateAvailableTooltip").accessibleTextKey("updateAvailableTooltip");
|
||||
var stack = createStyle(null, b);
|
||||
stack.hide(PlatformThread.sync(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return AppDistributionType.get()
|
||||
.getUpdateHandler()
|
||||
@@ -136,7 +72,16 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||
== null;
|
||||
},
|
||||
AppDistributionType.get().getUpdateHandler().getPreparedUpdate())));
|
||||
vbox.getChildren().add(b.createRegion());
|
||||
vbox.getChildren().add(stack.createRegion());
|
||||
}
|
||||
|
||||
if (!AppProperties.get().isStaging()) {
|
||||
var b = new IconButtonComp("mdi2t-test-tube", () -> Hyperlinks.open(Hyperlinks.GITHUB_PTB));
|
||||
b.tooltipKey("ptbAvailableTooltip");
|
||||
b.accessibleTextKey("ptbAvailableTooltip");
|
||||
var stack = createStyle(null, b);
|
||||
stack.hide(AppLayoutModel.get().getPtbAvailable().not());
|
||||
vbox.getChildren().add(stack.createRegion());
|
||||
}
|
||||
|
||||
var filler = new Button();
|
||||
@@ -145,6 +90,99 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||
vbox.getChildren().add(filler);
|
||||
VBox.setVgrow(filler, Priority.ALWAYS);
|
||||
vbox.getStyleClass().add("sidebar-comp");
|
||||
|
||||
var queueButtons = new VBox();
|
||||
queueEntries.addListener((ListChangeListener<? super AppLayoutModel.QueueEntry>) c -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
queueButtons.getChildren().clear();
|
||||
for (int i = c.getList().size() - 1; i >= 0; i--) {
|
||||
var item = c.getList().get(i);
|
||||
var b = new IconButtonComp(item.getIcon(), () -> {
|
||||
item.getAction().run();
|
||||
queueEntries.remove(item);
|
||||
});
|
||||
b.tooltip(item.getName());
|
||||
b.accessibleText(item.getName());
|
||||
var stack = createStyle(null, b);
|
||||
queueButtons.getChildren().add(stack.createRegion());
|
||||
}
|
||||
});
|
||||
});
|
||||
vbox.getChildren().add(queueButtons);
|
||||
vbox.setMinHeight(0);
|
||||
vbox.setPrefHeight(0);
|
||||
|
||||
return new SimpleCompStructure<>(vbox);
|
||||
}
|
||||
|
||||
private Comp<?> createStyle(AppLayoutModel.Entry e, IconButtonComp b) {
|
||||
var selected = PseudoClass.getPseudoClass("selected");
|
||||
|
||||
b.apply(struc -> {
|
||||
AppFontSizes.lg(struc.get());
|
||||
struc.get().setAlignment(Pos.CENTER);
|
||||
|
||||
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
struc.get().pseudoClassStateChanged(selected, n.equals(e));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var selectedBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
var c = Platform.getPreferences()
|
||||
.getAccentColor()
|
||||
.desaturate()
|
||||
.desaturate();
|
||||
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(17, 1, 15, 2)));
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
var hoverBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
var c = Platform.getPreferences()
|
||||
.getAccentColor()
|
||||
.darker()
|
||||
.desaturate()
|
||||
.desaturate();
|
||||
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(17, 1, 15, 2)));
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
var noneBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
return Background.fill(Color.TRANSPARENT);
|
||||
},
|
||||
Platform.getPreferences().accentColorProperty());
|
||||
|
||||
var indicator = Comp.empty().styleClass("indicator");
|
||||
var stack =
|
||||
new StackComp(List.of(indicator, b)).apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
|
||||
stack.apply(struc -> {
|
||||
var indicatorRegion = (Region) struc.get().getChildren().getFirst();
|
||||
var buttonRegion = (Region) struc.get().getChildren().get(1);
|
||||
indicatorRegion.setMaxWidth(7);
|
||||
indicatorRegion.prefHeightProperty().bind(buttonRegion.heightProperty());
|
||||
indicatorRegion
|
||||
.backgroundProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (value.getValue().equals(e)) {
|
||||
return selectedBorder.get();
|
||||
}
|
||||
|
||||
if (struc.get().isHover()) {
|
||||
return hoverBorder.get();
|
||||
}
|
||||
|
||||
return noneBorder.get();
|
||||
},
|
||||
struc.get().hoverProperty(),
|
||||
value,
|
||||
hoverBorder,
|
||||
selectedBorder,
|
||||
noneBorder));
|
||||
});
|
||||
return stack;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,20 +39,17 @@ public class ToggleGroupComp<T> extends Comp<CompStructure<HBox>> {
|
||||
for (var entry : val.entrySet()) {
|
||||
var b = new ToggleButton(entry.getValue().getValue());
|
||||
b.setOnAction(e -> {
|
||||
value.setValue(entry.getKey());
|
||||
if (entry.getKey().equals(value.getValue())) {
|
||||
value.setValue(null);
|
||||
} else {
|
||||
value.setValue(entry.getKey());
|
||||
}
|
||||
e.consume();
|
||||
});
|
||||
group.getToggles().add(b);
|
||||
box.getChildren().add(b);
|
||||
b.setToggleGroup(group);
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
if (entry.getKey().equals(n)) {
|
||||
group.selectToggle(b);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (entry.getKey().equals(value.getValue())) {
|
||||
group.selectToggle(b);
|
||||
b.setSelected(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,12 +62,6 @@ public class ToggleGroupComp<T> extends Comp<CompStructure<HBox>> {
|
||||
}
|
||||
});
|
||||
|
||||
group.selectedToggleProperty().addListener((obsVal, oldVal, newVal) -> {
|
||||
if (newVal == null) {
|
||||
oldVal.setSelected(true);
|
||||
}
|
||||
});
|
||||
|
||||
return new SimpleCompStructure<>(box);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class ToggleSwitchComp extends Comp<CompStructure<ToggleSwitch>> {
|
||||
});
|
||||
s.setAlignment(Pos.CENTER);
|
||||
s.getStyleClass().add("toggle-switch-comp");
|
||||
s.setSelected(selected.getValue());
|
||||
s.setSelected(selected.getValue() != null ? selected.getValue() : false);
|
||||
s.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
selected.setValue(newValue);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.SimpleCompStructure;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.ToolBar;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ToolbarComp extends Comp<CompStructure<ToolBar>> {
|
||||
|
||||
private final ObservableList<Comp<?>> entries;
|
||||
|
||||
public ToolbarComp(List<Comp<?>> comps) {
|
||||
entries = FXCollections.observableArrayList(List.copyOf(comps));
|
||||
}
|
||||
|
||||
public ToolbarComp(ObservableList<Comp<?>> entries) {
|
||||
this.entries = PlatformThread.sync(entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompStructure<ToolBar> createBase() {
|
||||
var b = new ToolBar();
|
||||
b.getStyleClass().add("horizontal-comp");
|
||||
entries.addListener((ListChangeListener<? super Comp<?>>) c -> {
|
||||
b.getItems().setAll(c.getList().stream().map(Comp::createRegion).toList());
|
||||
});
|
||||
for (var entry : entries) {
|
||||
b.getItems().add(entry.createRegion());
|
||||
}
|
||||
b.visibleProperty().bind(Bindings.isNotEmpty(entries));
|
||||
return new SimpleCompStructure<>(b);
|
||||
}
|
||||
}
|
||||
+6
-17
@@ -1,34 +1,23 @@
|
||||
package io.xpipe.app.comp.base;
|
||||
|
||||
import io.xpipe.app.comp.CompStructure;
|
||||
import io.xpipe.app.comp.augment.Augment;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.stage.Window;
|
||||
|
||||
public class TooltipAugment<S extends CompStructure<?>> implements Augment<S> {
|
||||
public class TooltipHelper {
|
||||
|
||||
private final ObservableValue<String> text;
|
||||
private final KeyCombination shortcut;
|
||||
|
||||
public TooltipAugment(ObservableValue<String> text, KeyCombination shortcut) {
|
||||
this.text = text;
|
||||
this.shortcut = shortcut;
|
||||
public static Tooltip create(String text) {
|
||||
return create(new SimpleStringProperty(text), null);
|
||||
}
|
||||
|
||||
public TooltipAugment(String key, KeyCombination shortcut) {
|
||||
this.text = AppI18n.observable(key);
|
||||
this.shortcut = shortcut;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void augment(S struc) {
|
||||
public static Tooltip create(ObservableValue<String> text, KeyCombination shortcut) {
|
||||
var tt = new FixedTooltip();
|
||||
if (shortcut != null) {
|
||||
var s = AppI18n.observable("shortcut");
|
||||
@@ -46,7 +35,7 @@ public class TooltipAugment<S extends CompStructure<?>> implements Augment<S> {
|
||||
tt.setWrapText(true);
|
||||
tt.setMaxWidth(400);
|
||||
tt.getStyleClass().add("fancy-tooltip");
|
||||
Tooltip.install(struc.get(), tt);
|
||||
return tt;
|
||||
}
|
||||
|
||||
private static class FixedTooltip extends Tooltip {
|
||||
@@ -3,8 +3,8 @@ package io.xpipe.app.comp.store;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
@@ -77,9 +77,20 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
||||
var notes = new StoreNotesComp(getWrapper()).createRegion();
|
||||
var userIcon = createUserIcon().createRegion();
|
||||
|
||||
var selection = createBatchSelection().createRegion();
|
||||
grid.add(selection, 0, 0, 1, 2);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(25));
|
||||
StoreViewState.get().getBatchMode().subscribe(batch -> {
|
||||
if (batch) {
|
||||
grid.getColumnConstraints().set(0, new ColumnConstraints(25));
|
||||
} else {
|
||||
grid.getColumnConstraints().set(0, new ColumnConstraints(-8));
|
||||
}
|
||||
});
|
||||
|
||||
var storeIcon = createIcon(28, 24);
|
||||
GridPane.setHalignment(storeIcon, HPos.CENTER);
|
||||
grid.add(storeIcon, 0, 0);
|
||||
grid.add(storeIcon, 1, 0);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(34));
|
||||
|
||||
var customSize = content != null ? 100 : 0;
|
||||
|
||||
@@ -82,8 +82,8 @@ public class OsLogoComp extends SimpleComp {
|
||||
}
|
||||
|
||||
return ICONS.entrySet().stream()
|
||||
.filter(e -> name.toLowerCase().contains(e.getKey()) ||
|
||||
name.toLowerCase().replaceAll("\\s+", "").contains(e.getKey()))
|
||||
.filter(e -> name.toLowerCase().contains(e.getKey())
|
||||
|| name.toLowerCase().replaceAll("\\s+", "").contains(e.getKey()))
|
||||
.findAny()
|
||||
.map(e -> e.getValue())
|
||||
.orElse("os/linux.svg");
|
||||
|
||||
@@ -43,15 +43,26 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
||||
grid.setHgap(6);
|
||||
grid.setVgap(OsType.getLocal() == OsType.MACOS ? 2 : 0);
|
||||
|
||||
var selection = createBatchSelection();
|
||||
grid.add(selection.createRegion(), 0, 0, 1, 2);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(25));
|
||||
StoreViewState.get().getBatchMode().subscribe(batch -> {
|
||||
if (batch) {
|
||||
grid.getColumnConstraints().set(0, new ColumnConstraints(25));
|
||||
} else {
|
||||
grid.getColumnConstraints().set(0, new ColumnConstraints(-6));
|
||||
}
|
||||
});
|
||||
|
||||
var storeIcon = createIcon(46, 40);
|
||||
grid.add(storeIcon, 0, 0, 1, 2);
|
||||
grid.add(storeIcon, 1, 0, 1, 2);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(52));
|
||||
|
||||
var active = new StoreActiveComp(getWrapper()).createRegion();
|
||||
var nameBox = new HBox(name, userIcon, notes);
|
||||
nameBox.setSpacing(6);
|
||||
nameBox.setAlignment(Pos.CENTER_LEFT);
|
||||
grid.add(nameBox, 1, 0);
|
||||
grid.add(nameBox, 2, 0);
|
||||
GridPane.setVgrow(nameBox, Priority.ALWAYS);
|
||||
getWrapper().getSessionActive().subscribe(aBoolean -> {
|
||||
if (!aBoolean) {
|
||||
@@ -64,7 +75,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
||||
var summaryBox = new HBox(createSummary());
|
||||
summaryBox.setAlignment(Pos.TOP_LEFT);
|
||||
GridPane.setVgrow(summaryBox, Priority.ALWAYS);
|
||||
grid.add(summaryBox, 1, 1);
|
||||
grid.add(summaryBox, 2, 1);
|
||||
|
||||
var nameCC = new ColumnConstraints();
|
||||
nameCC.setMinWidth(100);
|
||||
@@ -72,7 +83,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
||||
nameCC.setPrefWidth(100);
|
||||
grid.getColumnConstraints().addAll(nameCC);
|
||||
|
||||
grid.add(createInformation(), 2, 0, 1, 2);
|
||||
grid.add(createInformation(), 3, 0, 1, 2);
|
||||
var info = new ColumnConstraints();
|
||||
info.prefWidthProperty().bind(content != null ? INFO_WITH_CONTENT_WIDTH : INFO_NO_CONTENT_WIDTH);
|
||||
info.setHalignment(HPos.LEFT);
|
||||
@@ -89,7 +100,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
||||
controls.setAlignment(Pos.CENTER_RIGHT);
|
||||
controls.setSpacing(10);
|
||||
controls.setPadding(new Insets(0, 0, 0, 10));
|
||||
grid.add(controls, 3, 0, 1, 2);
|
||||
grid.add(controls, 4, 0, 1, 2);
|
||||
grid.getColumnConstraints().add(custom);
|
||||
|
||||
grid.getStyleClass().add("store-entry-grid");
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.TooltipAugment;
|
||||
import io.xpipe.app.comp.base.TooltipHelper;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.shape.Circle;
|
||||
@@ -32,7 +33,7 @@ public class StoreActiveComp extends SimpleComp {
|
||||
pane.setAlignment(Pos.CENTER);
|
||||
pane.visibleProperty().bind(wrapper.getSessionActive());
|
||||
pane.getStyleClass().add("store-active-comp");
|
||||
new TooltipAugment<>("sessionActive", null).augment(pane);
|
||||
Tooltip.install(pane, TooltipHelper.create(AppI18n.observable("sessionActive"), null));
|
||||
return pane;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,15 @@ import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreCategory;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
import io.xpipe.app.util.ClipboardHelper;
|
||||
import io.xpipe.app.util.ContextMenuHelper;
|
||||
import io.xpipe.app.util.DesktopHelper;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
@@ -23,7 +24,6 @@ import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
import javafx.scene.input.KeyCode;
|
||||
@@ -37,7 +37,6 @@ import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -70,7 +69,7 @@ public class StoreCategoryComp extends SimpleComp {
|
||||
() -> {
|
||||
var exp = category.getExpanded().get()
|
||||
&& category.getChildren().getList().size() > 0;
|
||||
return new LabelGraphic.IconGraphic(exp ? "mdi2m-menu-down-outline" : "mdi2m-menu-right-outline");
|
||||
return new LabelGraphic.IconGraphic(exp ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right");
|
||||
},
|
||||
category.getExpanded(),
|
||||
category.getChildren().getList());
|
||||
@@ -97,7 +96,7 @@ public class StoreCategoryComp extends SimpleComp {
|
||||
return new LabelGraphic.IconGraphic("mdomz-settings");
|
||||
}
|
||||
|
||||
if (!DataStorage.get().supportsSharing()
|
||||
if (!DataStorage.get().supportsSync()
|
||||
|| (!category.getCategory().canShare())) {
|
||||
return new LabelGraphic.IconGraphic("mdi2g-git");
|
||||
}
|
||||
@@ -128,7 +127,7 @@ public class StoreCategoryComp extends SimpleComp {
|
||||
string -> "(" + string + ")");
|
||||
count.visible(Bindings.notEqual(0, category.getShownContainedEntriesCount()));
|
||||
|
||||
var showStatus = hover.or(new SimpleBooleanProperty(DataStorage.get().supportsSharing()))
|
||||
var showStatus = hover.or(new SimpleBooleanProperty(DataStorage.get().supportsSync()))
|
||||
.or(showing);
|
||||
var focus = new SimpleBooleanProperty();
|
||||
var h = new HorizontalComp(List.of(
|
||||
@@ -187,7 +186,7 @@ public class StoreCategoryComp extends SimpleComp {
|
||||
});
|
||||
|
||||
category.getColor().subscribe((c) -> {
|
||||
DataColor.applyStyleClasses(c, struc.get());
|
||||
DataStoreColor.applyStyleClasses(c, struc.get());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -204,62 +203,28 @@ public class StoreCategoryComp extends SimpleComp {
|
||||
contextMenu.getItems().add(copyId);
|
||||
}
|
||||
|
||||
var newCategory = new MenuItem(AppI18n.get("newCategory"), new FontIcon("mdi2p-plus-thick"));
|
||||
if (AppPrefs.get().developerMode().getValue()) {
|
||||
var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline"));
|
||||
browse.setOnAction(event ->
|
||||
DesktopHelper.browsePathLocal(category.getCategory().getDirectory()));
|
||||
contextMenu.getItems().add(browse);
|
||||
}
|
||||
|
||||
var newCategory = new MenuItem(AppI18n.get("createNewCategory"), new FontIcon("mdi2p-plus-thick"));
|
||||
newCategory.setOnAction(event -> {
|
||||
DataStorage.get()
|
||||
.addStoreCategory(
|
||||
DataStoreCategory.createNew(category.getCategory().getUuid(), "New category"));
|
||||
DataStoreCategory.createNew(category.getCategory().getUuid(), AppI18n.get("newCategory")));
|
||||
});
|
||||
contextMenu.getItems().add(newCategory);
|
||||
|
||||
contextMenu.getItems().add(new SeparatorMenuItem());
|
||||
|
||||
var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill"));
|
||||
var none = new MenuItem();
|
||||
none.textProperty().bind(AppI18n.observable("none"));
|
||||
none.setOnAction(event -> {
|
||||
category.getCategory().setColor(null);
|
||||
event.consume();
|
||||
var configure = new MenuItem(AppI18n.get("configure"), new FontIcon("mdi2w-wrench"));
|
||||
configure.setOnAction(event -> {
|
||||
StoreCategoryConfigComp.show(category);
|
||||
});
|
||||
color.getItems().add(none);
|
||||
Arrays.stream(DataColor.values()).forEach(dataStoreColor -> {
|
||||
MenuItem m = new MenuItem();
|
||||
m.textProperty().bind(AppI18n.observable(dataStoreColor.getId()));
|
||||
m.setOnAction(event -> {
|
||||
category.getCategory().setColor(dataStoreColor);
|
||||
event.consume();
|
||||
});
|
||||
color.getItems().add(m);
|
||||
});
|
||||
contextMenu.getItems().add(color);
|
||||
|
||||
if (DataStorage.get().supportsSharing() && category.getCategory().canShare()) {
|
||||
var share = new MenuItem();
|
||||
share.textProperty()
|
||||
.bind(Bindings.createStringBinding(
|
||||
() -> {
|
||||
if (category.getSync().getValue()) {
|
||||
return AppI18n.get("unshare");
|
||||
} else {
|
||||
return AppI18n.get("share");
|
||||
}
|
||||
},
|
||||
category.getSync()));
|
||||
share.graphicProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (category.getSync().getValue()) {
|
||||
return new FontIcon("mdi2b-block-helper");
|
||||
} else {
|
||||
return new FontIcon("mdi2g-git");
|
||||
}
|
||||
},
|
||||
category.getSync()));
|
||||
share.setOnAction(event -> {
|
||||
category.getSync().setValue(!category.getSync().getValue());
|
||||
});
|
||||
contextMenu.getItems().add(share);
|
||||
}
|
||||
contextMenu.getItems().add(configure);
|
||||
|
||||
var rename = new MenuItem(AppI18n.get("rename"), new FontIcon("mdal-edit"));
|
||||
rename.setOnAction(event -> {
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.ModalButton;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreCategoryConfig;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.OptionsBuilder;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
@AllArgsConstructor
|
||||
public class StoreCategoryConfigComp extends SimpleComp {
|
||||
|
||||
public static void show(StoreCategoryWrapper wrapper) {
|
||||
var config = new SimpleObjectProperty<>(wrapper.getCategory().getConfig());
|
||||
var comp = new StoreCategoryConfigComp(wrapper, config);
|
||||
comp.prefWidth(600);
|
||||
var modal = ModalOverlay.of(
|
||||
AppI18n.observable("categoryConfigTitle", wrapper.getName().getValue()), comp, null);
|
||||
modal.addButton(ModalButton.cancel());
|
||||
modal.addButton(ModalButton.ok(() -> {
|
||||
DataStorage.get().updateCategoryConfig(wrapper.getCategory(), config.getValue());
|
||||
}));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
private final StoreCategoryWrapper wrapper;
|
||||
private final Property<DataStoreCategoryConfig> config;
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var colors = new LinkedHashMap<ObservableValue<String>, OptionsBuilder>();
|
||||
colors.put(AppI18n.observable("none"), new OptionsBuilder());
|
||||
for (DataStoreColor value : DataStoreColor.values()) {
|
||||
colors.put(AppI18n.observable(value.getId()), new OptionsBuilder());
|
||||
}
|
||||
|
||||
var c = config.getValue();
|
||||
var color = new SimpleIntegerProperty(
|
||||
c.getColor() != null ? Arrays.asList(DataStoreColor.values()).indexOf(c.getColor()) + 1 : 0);
|
||||
var scripts = new SimpleObjectProperty<>(c.getDontAllowScripts());
|
||||
var confirm = new SimpleObjectProperty<>(c.getConfirmAllModifications());
|
||||
var sync = new SimpleObjectProperty<>(c.getSync());
|
||||
var ref = new SimpleObjectProperty<>(
|
||||
c.getDefaultIdentityStore() != null
|
||||
? DataStorage.get()
|
||||
.getStoreEntryIfPresent(c.getDefaultIdentityStore())
|
||||
.map(DataStoreEntry::ref)
|
||||
.orElse(null)
|
||||
: null);
|
||||
var connectionsCategory = wrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory());
|
||||
var options = new OptionsBuilder()
|
||||
.nameAndDescription("categorySync")
|
||||
.addYesNoToggle(sync)
|
||||
.hide(!DataStorage.get().supportsSync()
|
||||
|| !wrapper.getCategory().canShare())
|
||||
.nameAndDescription("categoryDontAllowScripts")
|
||||
.addYesNoToggle(scripts)
|
||||
.hide(!connectionsCategory)
|
||||
// .nameAndDescription("categoryConfirmAllModifications")
|
||||
// .addYesNoToggle(confirm)
|
||||
// .hide(!connectionsCategory)
|
||||
.nameAndDescription("categoryDefaultIdentity")
|
||||
.addComp(
|
||||
StoreChoiceComp.other(
|
||||
ref,
|
||||
DataStore.class,
|
||||
s -> true,
|
||||
StoreViewState.get().getAllIdentitiesCategory()),
|
||||
ref)
|
||||
.hide(!connectionsCategory)
|
||||
.nameAndDescription("categoryColor")
|
||||
.choice(color, colors)
|
||||
.bind(
|
||||
() -> {
|
||||
return new DataStoreCategoryConfig(
|
||||
color.get() > 0 ? DataStoreColor.values()[color.get() - 1] : null,
|
||||
scripts.get(),
|
||||
confirm.get(),
|
||||
sync.get(),
|
||||
ref.get() != null ? ref.get().get().getUuid() : null);
|
||||
},
|
||||
config)
|
||||
.buildComp();
|
||||
var r = options.createRegion();
|
||||
var sp = new ScrollPane(r);
|
||||
sp.setFitToWidth(true);
|
||||
sp.prefHeightProperty().bind(r.heightProperty());
|
||||
return sp;
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,15 @@ package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreCategory;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
import io.xpipe.app.util.DerivedObservableList;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableStringValue;
|
||||
import javafx.collections.FXCollections;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -29,13 +28,13 @@ public class StoreCategoryWrapper {
|
||||
private final DataStoreCategory category;
|
||||
private final Property<Instant> lastAccess;
|
||||
private final Property<StoreSortMode> sortMode;
|
||||
private final Property<Boolean> sync;
|
||||
private final BooleanProperty sync;
|
||||
private final DerivedObservableList<StoreCategoryWrapper> children;
|
||||
private final DerivedObservableList<StoreEntryWrapper> directContainedEntries;
|
||||
private final IntegerProperty shownContainedEntriesCount = new SimpleIntegerProperty();
|
||||
private final IntegerProperty allContainedEntriesCount = new SimpleIntegerProperty();
|
||||
private final BooleanProperty expanded = new SimpleBooleanProperty();
|
||||
private final Property<DataColor> color = new SimpleObjectProperty<>();
|
||||
private final Property<DataStoreColor> color = new SimpleObjectProperty<>();
|
||||
private final BooleanProperty largeCategoryOptimizations = new SimpleBooleanProperty();
|
||||
private StoreCategoryWrapper cachedParent;
|
||||
|
||||
@@ -57,10 +56,12 @@ public class StoreCategoryWrapper {
|
||||
this.name = new SimpleStringProperty(category.getName());
|
||||
this.lastAccess = new SimpleObjectProperty<>(category.getLastAccess());
|
||||
this.sortMode = new SimpleObjectProperty<>(category.getSortMode());
|
||||
this.sync = new SimpleObjectProperty<>(category.isSync());
|
||||
this.children = new DerivedObservableList<>(FXCollections.observableArrayList(), true);
|
||||
this.directContainedEntries = new DerivedObservableList<>(FXCollections.observableArrayList(), true);
|
||||
this.color.setValue(category.getColor());
|
||||
this.sync = new SimpleBooleanProperty(Boolean.TRUE.equals(
|
||||
DataStorage.get().getEffectiveCategoryConfig(category).getSync()));
|
||||
this.children = DerivedObservableList.arrayList(true);
|
||||
this.directContainedEntries = DerivedObservableList.arrayList(true);
|
||||
this.color.setValue(
|
||||
DataStorage.get().getEffectiveCategoryConfig(category).getColor());
|
||||
setupListeners();
|
||||
}
|
||||
|
||||
@@ -144,10 +145,6 @@ public class StoreCategoryWrapper {
|
||||
sortMode.addListener((observable, oldValue, newValue) -> {
|
||||
category.setSortMode(newValue);
|
||||
});
|
||||
|
||||
sync.addListener((observable, oldValue, newValue) -> {
|
||||
DataStorage.get().syncCategory(category, newValue);
|
||||
});
|
||||
}
|
||||
|
||||
public void toggleExpanded() {
|
||||
@@ -168,9 +165,10 @@ public class StoreCategoryWrapper {
|
||||
|
||||
lastAccess.setValue(category.getLastAccess().minus(Duration.ofMillis(500)));
|
||||
sortMode.setValue(category.getSortMode());
|
||||
sync.setValue(category.isSync());
|
||||
sync.setValue(Boolean.TRUE.equals(
|
||||
DataStorage.get().getEffectiveCategoryConfig(category).getSync()));
|
||||
expanded.setValue(category.isExpanded());
|
||||
color.setValue(category.getColor());
|
||||
color.setValue(DataStorage.get().getEffectiveCategoryConfig(category).getColor());
|
||||
|
||||
var allEntries = new ArrayList<>(StoreViewState.get().getAllEntries().getList());
|
||||
directContainedEntries.setContent(allEntries.stream()
|
||||
|
||||
@@ -2,18 +2,18 @@ package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.comp.base.FilterComp;
|
||||
import io.xpipe.app.comp.base.HorizontalComp;
|
||||
import io.xpipe.app.comp.base.PrettyImageHelper;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppFontSizes;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.ext.LocalStore;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.BindingsHelper;
|
||||
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
@@ -21,6 +21,7 @@ import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.MenuButton;
|
||||
@@ -35,6 +36,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@@ -94,12 +96,26 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
||||
&& e.getValidity().isUsable()
|
||||
&& (applicableCheck == null || applicableCheck.test(e.ref()));
|
||||
};
|
||||
|
||||
var applicableMatch =
|
||||
StoreViewState.get().getCurrentTopLevelSection().anyMatches(applicable);
|
||||
if (!applicableMatch) {
|
||||
selectedCategory.set(initialCategory);
|
||||
}
|
||||
|
||||
var applicableCount = StoreViewState.get().getAllEntries().getList().stream()
|
||||
.filter(applicable)
|
||||
.count();
|
||||
var initialExpanded = applicableCount < 20;
|
||||
|
||||
var section = new StoreSectionMiniComp(
|
||||
StoreSection.createTopLevel(
|
||||
StoreViewState.get().getAllEntries(),
|
||||
Set.of(),
|
||||
applicable,
|
||||
filterText,
|
||||
selectedCategory,
|
||||
StoreViewState.get().getEntriesListVisibilityObservable(),
|
||||
StoreViewState.get().getEntriesListUpdateObservable()),
|
||||
(s, comp) -> {
|
||||
if (!applicable.test(s.getWrapper())) {
|
||||
@@ -111,7 +127,9 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
||||
this.selected.setValue(sec.getWrapper().getEntry().ref());
|
||||
popover.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
initialExpanded);
|
||||
|
||||
var category = new DataStoreCategoryChoiceComp(
|
||||
initialCategory != null ? initialCategory.getRoot() : null,
|
||||
StoreViewState.get().getActiveCategory(),
|
||||
@@ -124,7 +142,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
||||
MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline"));
|
||||
m.setMaxHeight(100);
|
||||
m.setMinHeight(0);
|
||||
StoreCreationMenu.addButtons(m);
|
||||
StoreCreationMenu.addButtons(m, false);
|
||||
return m;
|
||||
})
|
||||
.accessibleTextKey("addConnection")
|
||||
@@ -158,11 +176,29 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
||||
})
|
||||
.createStructure()
|
||||
.get();
|
||||
var r = section.vgrow().createRegion();
|
||||
|
||||
var emptyText = Bindings.createStringBinding(
|
||||
() -> {
|
||||
var count = StoreViewState.get().getAllEntries().getList().stream()
|
||||
.filter(applicable)
|
||||
.count();
|
||||
return count == 0 ? AppI18n.get("noCompatibleConnection") : null;
|
||||
},
|
||||
StoreViewState.get().getAllEntries().getList());
|
||||
var emptyLabel =
|
||||
new LabelComp(emptyText, new SimpleObjectProperty<>(new LabelGraphic.IconGraphic("mdi2f-filter")));
|
||||
emptyLabel.apply(struc -> AppFontSizes.sm(struc.get()));
|
||||
emptyLabel.hide(BindingsHelper.map(emptyText, s -> s == null));
|
||||
emptyLabel.minHeight(80);
|
||||
|
||||
var listStack = new StackComp(List.of(emptyLabel, section));
|
||||
listStack.vgrow();
|
||||
|
||||
var r = listStack.createRegion();
|
||||
var content = new VBox(top, r);
|
||||
content.setFillWidth(true);
|
||||
content.getStyleClass().add("choice-comp-content");
|
||||
content.setPrefWidth(500);
|
||||
content.setPrefWidth(480);
|
||||
content.setMaxHeight(550);
|
||||
|
||||
popover.setContentNode(content);
|
||||
@@ -172,6 +208,11 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
||||
popover.setDetachable(true);
|
||||
popover.setTitle(AppI18n.get("selectConnection"));
|
||||
AppFontSizes.xs(popover.getContentNode());
|
||||
|
||||
// Hide on connection creation dialog
|
||||
AppDialog.getModalOverlays().addListener((ListChangeListener<? super ModalOverlay>) c -> {
|
||||
popover.hide();
|
||||
});
|
||||
}
|
||||
|
||||
return popover;
|
||||
@@ -230,7 +271,8 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
|
||||
return;
|
||||
}
|
||||
|
||||
selected.setValue(mode == Mode.PROXY ? DataStorage.get().local().ref() : null);
|
||||
selected.setValue(
|
||||
mode == Mode.PROXY ? DataStorage.get().local().ref() : null);
|
||||
event.consume();
|
||||
});
|
||||
})
|
||||
|
||||
@@ -3,505 +3,63 @@ package io.xpipe.app.comp.store;
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.window.AppWindowHelper;
|
||||
import io.xpipe.app.ext.DataStoreCreationCategory;
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.DataStoreProviders;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.core.store.ValidatableStore;
|
||||
import io.xpipe.core.util.ValidationException;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.skin.ScrollPaneSkin;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import net.synedra.validatorfx.GraphicDecorationStackPane;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
public class StoreCreationComp extends DialogComp {
|
||||
public class StoreCreationComp extends ModalOverlayContentComp {
|
||||
|
||||
Stage window;
|
||||
CreationConsumer consumer;
|
||||
Property<DataStoreProvider> provider;
|
||||
ObjectProperty<DataStore> store;
|
||||
Predicate<DataStoreProvider> filter;
|
||||
BooleanProperty busy = new SimpleBooleanProperty();
|
||||
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
|
||||
Property<ModalOverlay> messageProp = new SimpleObjectProperty<>();
|
||||
BooleanProperty finished = new SimpleBooleanProperty();
|
||||
ObservableValue<DataStoreEntry> entry;
|
||||
BooleanProperty changedSinceError = new SimpleBooleanProperty();
|
||||
BooleanProperty skippable = new SimpleBooleanProperty();
|
||||
BooleanProperty connectable = new SimpleBooleanProperty();
|
||||
StringProperty name;
|
||||
DataStoreEntry existingEntry;
|
||||
boolean staticDisplay;
|
||||
private final StoreCreationModel model;
|
||||
|
||||
public StoreCreationComp(
|
||||
Stage window,
|
||||
CreationConsumer consumer,
|
||||
Property<DataStoreProvider> provider,
|
||||
ObjectProperty<DataStore> store,
|
||||
Predicate<DataStoreProvider> filter,
|
||||
String initialName,
|
||||
DataStoreEntry existingEntry,
|
||||
boolean staticDisplay) {
|
||||
this.window = window;
|
||||
this.consumer = consumer;
|
||||
this.provider = provider;
|
||||
this.store = store;
|
||||
this.filter = filter;
|
||||
this.name = new SimpleStringProperty(initialName != null && !initialName.isEmpty() ? initialName : null);
|
||||
this.existingEntry = existingEntry;
|
||||
this.staticDisplay = staticDisplay;
|
||||
this.store.addListener((c, o, n) -> {
|
||||
changedSinceError.setValue(true);
|
||||
});
|
||||
this.name.addListener((c, o, n) -> {
|
||||
changedSinceError.setValue(true);
|
||||
});
|
||||
|
||||
this.provider.addListener((c, o, n) -> {
|
||||
store.unbind();
|
||||
store.setValue(null);
|
||||
if (n != null) {
|
||||
store.setValue(n.defaultStore());
|
||||
}
|
||||
});
|
||||
|
||||
this.provider.subscribe((n) -> {
|
||||
if (n != null) {
|
||||
connectable.setValue(n.canConnectDuringCreation());
|
||||
}
|
||||
});
|
||||
|
||||
this.apply(r -> {
|
||||
r.get().setPrefWidth(650);
|
||||
r.get().setPrefHeight(750);
|
||||
});
|
||||
|
||||
this.validator.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> {
|
||||
newValue.validate();
|
||||
});
|
||||
});
|
||||
this.entry = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (name.getValue() == null || store.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var testE = DataStoreEntry.createNew(
|
||||
UUID.randomUUID(),
|
||||
DataStorage.get().getSelectedCategory().getUuid(),
|
||||
name.getValue(),
|
||||
store.getValue());
|
||||
var p = DataStorage.get().getDefaultDisplayParent(testE).orElse(null);
|
||||
|
||||
var targetCategory = p != null
|
||||
? p.getCategoryUuid()
|
||||
: DataStorage.get().getSelectedCategory().getUuid();
|
||||
var rootCategory = DataStorage.get()
|
||||
.getRootCategory(DataStorage.get()
|
||||
.getStoreCategoryIfPresent(targetCategory)
|
||||
.orElseThrow());
|
||||
|
||||
// Don't put it in the wrong root category
|
||||
if ((provider.getValue().getCreationCategory() == null
|
||||
|| !provider.getValue()
|
||||
.getCreationCategory()
|
||||
.getCategory()
|
||||
.equals(rootCategory.getUuid()))) {
|
||||
targetCategory = provider.getValue().getCreationCategory() != null
|
||||
? provider.getValue().getCreationCategory().getCategory()
|
||||
: DataStorage.ALL_CONNECTIONS_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Don't use the all connections category
|
||||
if (targetCategory.equals(
|
||||
DataStorage.get().getAllConnectionsCategory().getUuid())) {
|
||||
targetCategory = DataStorage.get()
|
||||
.getDefaultConnectionsCategory()
|
||||
.getUuid();
|
||||
}
|
||||
|
||||
// Don't use the all scripts category
|
||||
if (targetCategory.equals(
|
||||
DataStorage.get().getAllScriptsCategory().getUuid())) {
|
||||
targetCategory = DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Don't use the all identities category
|
||||
if (targetCategory.equals(
|
||||
DataStorage.get().getAllIdentitiesCategory().getUuid())) {
|
||||
targetCategory = DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Custom category stuff
|
||||
targetCategory = provider.getValue().getTargetCategory(store.getValue(), targetCategory);
|
||||
|
||||
return DataStoreEntry.createNew(
|
||||
UUID.randomUUID(), targetCategory, name.getValue(), store.getValue());
|
||||
},
|
||||
name,
|
||||
store);
|
||||
|
||||
skippable.bind(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
if (name.get() != null && store.get().isComplete() && store.get() instanceof ValidatableStore) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
store,
|
||||
name));
|
||||
}
|
||||
|
||||
public static void showEdit(DataStoreEntry e) {
|
||||
showEdit(e, dataStoreEntry -> {});
|
||||
}
|
||||
|
||||
public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> consumer) {
|
||||
show(
|
||||
e.getName(),
|
||||
e.getProvider(),
|
||||
e.getStore(),
|
||||
v -> true,
|
||||
(newE, validated) -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
if (!DataStorage.get().getStoreEntries().contains(e)) {
|
||||
DataStorage.get().addStoreEntryIfNotPresent(newE);
|
||||
} else {
|
||||
// We didn't change anything
|
||||
if (e.getStore().equals(newE.getStore())) {
|
||||
e.setName(newE.getName());
|
||||
} else {
|
||||
var madeValid = !e.getValidity().isUsable() && newE.getValidity().isUsable();
|
||||
DataStorage.get().updateEntry(e, newE);
|
||||
if (madeValid) {
|
||||
StoreViewState.get().toggleStoreListUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
consumer.accept(e);
|
||||
});
|
||||
},
|
||||
true,
|
||||
e);
|
||||
}
|
||||
|
||||
public static void showCreation(DataStoreProvider selected, DataStoreCreationCategory category) {
|
||||
showCreation(selected != null ? selected.defaultStore() : null, category, dataStoreEntry -> {}, true);
|
||||
}
|
||||
|
||||
public static void showCreation(
|
||||
DataStore base,
|
||||
DataStoreCreationCategory category,
|
||||
Consumer<DataStoreEntry> listener,
|
||||
boolean selectCategory) {
|
||||
var prov = base != null ? DataStoreProviders.byStore(base) : null;
|
||||
show(
|
||||
null,
|
||||
prov,
|
||||
base,
|
||||
dataStoreProvider -> (category != null && category.equals(dataStoreProvider.getCreationCategory()))
|
||||
|| dataStoreProvider.equals(prov),
|
||||
(e, validated) -> {
|
||||
try {
|
||||
var returned = DataStorage.get().addStoreEntryIfNotPresent(e);
|
||||
listener.accept(returned);
|
||||
if (validated
|
||||
&& e.getProvider().shouldShowScan()
|
||||
&& AppPrefs.get()
|
||||
.openConnectionSearchWindowOnConnectionCreation()
|
||||
.get()) {
|
||||
ScanDialog.showAsync(e);
|
||||
}
|
||||
|
||||
if (selectCategory) {
|
||||
// Select new category if needed
|
||||
var cat = DataStorage.get()
|
||||
.getStoreCategoryIfPresent(e.getCategoryUuid())
|
||||
.orElseThrow();
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
StoreViewState.get()
|
||||
.getActiveCategory()
|
||||
.setValue(StoreViewState.get().getCategoryWrapper(cat));
|
||||
});
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
}
|
||||
},
|
||||
false,
|
||||
null);
|
||||
}
|
||||
|
||||
public interface CreationConsumer {
|
||||
|
||||
void consume(DataStoreEntry entry, boolean validated);
|
||||
}
|
||||
|
||||
private static void show(
|
||||
String initialName,
|
||||
DataStoreProvider provider,
|
||||
DataStore s,
|
||||
Predicate<DataStoreProvider> filter,
|
||||
CreationConsumer con,
|
||||
boolean staticDisplay,
|
||||
DataStoreEntry existingEntry) {
|
||||
var prop = new SimpleObjectProperty<>(provider);
|
||||
var store = new SimpleObjectProperty<>(s);
|
||||
DialogComp.showWindow(
|
||||
"addConnection",
|
||||
stage -> new StoreCreationComp(
|
||||
stage, con, prop, store, filter, initialName, existingEntry, staticDisplay));
|
||||
}
|
||||
|
||||
private static boolean showInvalidConfirmAlert() {
|
||||
return AppWindowHelper.showBlockingAlert(alert -> {
|
||||
alert.setTitle(AppI18n.get("confirmInvalidStoreTitle"));
|
||||
alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader"));
|
||||
alert.getDialogPane()
|
||||
.setContent(AppWindowHelper.alertContentText(AppI18n.get("confirmInvalidStoreContent")));
|
||||
alert.setAlertType(Alert.AlertType.CONFIRMATION);
|
||||
alert.getButtonTypes().clear();
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("retry"), ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||
alert.getButtonTypes().add(new ButtonType(AppI18n.get("skip"), ButtonBar.ButtonData.OK_DONE));
|
||||
})
|
||||
.map(b -> b.getButtonData().isDefaultButton())
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Comp<?>> customButtons() {
|
||||
return List.of(
|
||||
new ButtonComp(AppI18n.observable("skipValidation"), () -> {
|
||||
if (showInvalidConfirmAlert()) {
|
||||
commit(false);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.visible(skippable),
|
||||
new ButtonComp(AppI18n.observable("connect"), () -> {
|
||||
var temp = DataStoreEntry.createTempWrapper(store.getValue());
|
||||
var action = provider.getValue().launchAction(temp);
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
action.execute();
|
||||
});
|
||||
})
|
||||
.hide(connectable
|
||||
.not()
|
||||
.or(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return store.getValue() == null
|
||||
|| !store.getValue().isComplete();
|
||||
},
|
||||
store))));
|
||||
public StoreCreationComp(StoreCreationModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObservableValue<Boolean> busy() {
|
||||
return busy;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void discard() {}
|
||||
|
||||
@Override
|
||||
protected void finish() {
|
||||
if (finished.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (store.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We didn't change anything
|
||||
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
|
||||
commit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator.getValue().validate()) {
|
||||
var msg = validator
|
||||
.getValue()
|
||||
.getValidationResult()
|
||||
.getMessages()
|
||||
.getFirst()
|
||||
.getText();
|
||||
TrackEvent.info(msg);
|
||||
messageProp.setValue(createErrorOverlay(msg));
|
||||
changedSinceError.setValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runAsync(() -> {
|
||||
// Might have changed since last time
|
||||
if (entry.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var ignored = new BooleanScope(busy).start()) {
|
||||
DataStorage.get().addStoreEntryInProgress(entry.getValue());
|
||||
entry.getValue().validateOrThrow();
|
||||
commit(true);
|
||||
} catch (Throwable ex) {
|
||||
String message;
|
||||
if (ex instanceof ValidationException) {
|
||||
ErrorEvent.expected(ex);
|
||||
message = ex.getMessage();
|
||||
} else if (ex instanceof StackOverflowError) {
|
||||
// Cycles in connection graphs can fail hard but are expected
|
||||
ErrorEvent.expected(ex);
|
||||
message = "StackOverflowError";
|
||||
} else {
|
||||
message = ex.getMessage();
|
||||
}
|
||||
|
||||
messageProp.setValue(createErrorOverlay(message));
|
||||
changedSinceError.setValue(false);
|
||||
|
||||
ErrorEvent.fromThrowable(ex).omit().handle();
|
||||
} finally {
|
||||
DataStorage.get().removeStoreEntryInProgress(entry.getValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comp<?> content() {
|
||||
return Comp.of(this::createLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Comp<?> pane(Comp<?> content) {
|
||||
var back = super.pane(content);
|
||||
return new ModalOverlayComp(back, messageProp);
|
||||
}
|
||||
|
||||
private ModalOverlay createErrorOverlay(String message) {
|
||||
var comp = Comp.of(() -> {
|
||||
var l = new TextArea();
|
||||
l.setText(message);
|
||||
l.setWrapText(true);
|
||||
l.getStyleClass().add("error-overlay-comp");
|
||||
l.setEditable(false);
|
||||
return l;
|
||||
});
|
||||
var overlay = ModalOverlay.of("error", comp, new LabelGraphic.NodeGraphic(() -> {
|
||||
var graphic = new FontIcon("mdomz-warning");
|
||||
graphic.setIconColor(Color.RED);
|
||||
return new StackPane(graphic);
|
||||
}));
|
||||
return overlay;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comp<?> bottom() {
|
||||
var disable = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return provider.getValue() == null
|
||||
|| store.getValue() == null
|
||||
|| !store.getValue().isComplete()
|
||||
// When switching providers, both observables change one after another.
|
||||
// So temporarily there might be a store class mismatch
|
||||
|| provider.getValue().getStoreClasses().stream()
|
||||
.noneMatch(aClass -> aClass.isAssignableFrom(
|
||||
store.getValue().getClass()))
|
||||
|| provider.getValue().createInsightsMarkdown(store.getValue()) == null;
|
||||
},
|
||||
provider,
|
||||
store);
|
||||
return new PopupMenuButtonComp(
|
||||
new SimpleStringProperty("Insights >"),
|
||||
Comp.of(() -> {
|
||||
return provider.getValue() != null
|
||||
? provider.getValue()
|
||||
.createInsightsComp(store)
|
||||
.createRegion()
|
||||
: null;
|
||||
}),
|
||||
true)
|
||||
.hide(disable)
|
||||
.styleClass("button-comp");
|
||||
return model.getBusy();
|
||||
}
|
||||
|
||||
private Region createStoreProperties(Comp<?> comp, Validator propVal) {
|
||||
var p = provider.getValue();
|
||||
var nameKey = p == null
|
||||
|| p.getCreationCategory() == null
|
||||
|| p.getCreationCategory().getCategory().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)
|
||||
? "connection"
|
||||
: p.getCreationCategory().getCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)
|
||||
? "script"
|
||||
: "identity";
|
||||
var nameKey = model.storeTypeNameKey();
|
||||
return new OptionsBuilder()
|
||||
.addComp(comp, store)
|
||||
.addComp(comp, model.getStore())
|
||||
.name(nameKey + "Name")
|
||||
.description(nameKey + "NameDescription")
|
||||
.addString(name, false)
|
||||
.addString(model.getName(), false)
|
||||
.nonNull(propVal)
|
||||
.buildComp()
|
||||
.onSceneAssign(struc -> {
|
||||
if (staticDisplay) {
|
||||
if (model.isStaticDisplay()) {
|
||||
struc.get().requestFocus();
|
||||
}
|
||||
})
|
||||
.styleClass("store-creator-options")
|
||||
.createRegion();
|
||||
}
|
||||
|
||||
private void commit(boolean validated) {
|
||||
if (finished.get()) {
|
||||
return;
|
||||
}
|
||||
finished.setValue(true);
|
||||
|
||||
if (entry.getValue() != null) {
|
||||
consumer.consume(entry.getValue(), validated);
|
||||
}
|
||||
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
|
||||
private Region createLayout() {
|
||||
var layout = new BorderPane();
|
||||
layout.getStyleClass().add("store-creator");
|
||||
var providerChoice = new StoreProviderChoiceComp(filter, provider);
|
||||
var showProviders = (!staticDisplay
|
||||
&& (providerChoice.getProviders().size() > 1
|
||||
|| providerChoice.getProviders().getFirst().showProviderChoice()))
|
||||
|| (staticDisplay && provider.getValue().showProviderChoice());
|
||||
if (staticDisplay) {
|
||||
var providerChoice = new StoreProviderChoiceComp(model.getFilter(), model.getProvider());
|
||||
var provider = model.getProvider().getValue() != null
|
||||
? model.getProvider().getValue()
|
||||
: providerChoice.getProviders().getFirst();
|
||||
var showProviders = (!model.isStaticDisplay() && provider.showProviderChoice())
|
||||
|| (model.isStaticDisplay() && provider.showProviderChoice());
|
||||
if (model.isStaticDisplay()) {
|
||||
providerChoice.apply(struc -> struc.get().setDisable(true));
|
||||
}
|
||||
if (showProviders) {
|
||||
@@ -509,37 +67,51 @@ public class StoreCreationComp extends DialogComp {
|
||||
}
|
||||
providerChoice.apply(GrowAugment.create(true, false));
|
||||
|
||||
provider.subscribe(n -> {
|
||||
model.getProvider().subscribe(n -> {
|
||||
if (n != null) {
|
||||
var d = n.guiDialog(existingEntry, store);
|
||||
var d = n.guiDialog(model.getExistingEntry(), model.getStore());
|
||||
var propVal = new SimpleValidator();
|
||||
var propR = createStoreProperties(d == null || d.getComp() == null ? null : d.getComp(), propVal);
|
||||
|
||||
var sp = new ScrollPane(propR);
|
||||
sp.setFitToWidth(true);
|
||||
layout.setCenter(sp);
|
||||
var valSp = new GraphicDecorationStackPane();
|
||||
valSp.getChildren().add(propR);
|
||||
|
||||
validator.setValue(new ChainedValidator(List.of(
|
||||
d != null && d.getValidator() != null ? d.getValidator() : new SimpleValidator(), propVal)));
|
||||
var sp = new ScrollPane(valSp);
|
||||
sp.setSkin(new ScrollPaneSkin(sp));
|
||||
sp.setFitToWidth(true);
|
||||
var vbar = (ScrollBar) sp.lookup(".scroll-bar:vertical");
|
||||
|
||||
var topSep = new Separator();
|
||||
topSep.setPadding(new Insets(10, 0, 0, 0));
|
||||
topSep.visibleProperty().bind(vbar.visibleProperty());
|
||||
|
||||
var bottomSep = new Separator();
|
||||
bottomSep.setPadding(new Insets(0, 0, 0, 0));
|
||||
bottomSep.visibleProperty().bind(vbar.visibleProperty());
|
||||
|
||||
var vbox = new VBox(topSep, sp, bottomSep);
|
||||
VBox.setVgrow(sp, Priority.ALWAYS);
|
||||
|
||||
layout.setCenter(vbox);
|
||||
|
||||
model.getValidator()
|
||||
.setValue(new ChainedValidator(List.of(
|
||||
d != null && d.getValidator() != null ? d.getValidator() : new SimpleValidator(),
|
||||
propVal)));
|
||||
} else {
|
||||
layout.setCenter(null);
|
||||
validator.setValue(new SimpleValidator());
|
||||
model.getValidator().setValue(new SimpleValidator());
|
||||
}
|
||||
});
|
||||
|
||||
var sep = new Separator();
|
||||
sep.getStyleClass().add("spacer");
|
||||
var top = new VBox(providerChoice.createRegion(), sep);
|
||||
top.getStyleClass().add("top");
|
||||
if (showProviders) {
|
||||
layout.setTop(top);
|
||||
layout.setPadding(new Insets(15, 20, 20, 20));
|
||||
} else {
|
||||
layout.setPadding(new Insets(5, 20, 20, 20));
|
||||
layout.setTop(providerChoice.createRegion());
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
var valSp = new GraphicDecorationStackPane();
|
||||
valSp.getChildren().add(layout);
|
||||
return valSp;
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
return createLayout();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
|
||||
public interface StoreCreationConsumer {
|
||||
|
||||
void consume(DataStoreEntry entry, boolean validated);
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.base.ModalButton;
|
||||
import io.xpipe.app.comp.base.ModalOverlay;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.core.window.AppDialog;
|
||||
import io.xpipe.app.ext.DataStoreCreationCategory;
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.DataStoreProviders;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class StoreCreationDialog {
|
||||
|
||||
public static void showEdit(DataStoreEntry e) {
|
||||
showEdit(e, dataStoreEntry -> {});
|
||||
}
|
||||
|
||||
public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> c) {
|
||||
StoreCreationConsumer consumer = (newE, validated) -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
if (!DataStorage.get().getStoreEntries().contains(e)) {
|
||||
DataStorage.get().addStoreEntryIfNotPresent(newE);
|
||||
} else {
|
||||
// We didn't change anything
|
||||
if (e.getStore().equals(newE.getStore())) {
|
||||
e.setName(newE.getName());
|
||||
} else {
|
||||
var madeValid = !e.getValidity().isUsable()
|
||||
&& newE.getValidity().isUsable();
|
||||
DataStorage.get().updateEntry(e, newE);
|
||||
if (madeValid) {
|
||||
StoreViewState.get().triggerStoreListUpdate();
|
||||
if (validated
|
||||
&& e.getProvider().shouldShowScan()
|
||||
&& AppPrefs.get()
|
||||
.openConnectionSearchWindowOnConnectionCreation()
|
||||
.get()) {
|
||||
ScanDialog.showSingleAsync(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c.accept(e);
|
||||
});
|
||||
};
|
||||
show(e.getName(), e.getProvider(), e.getStore(), v -> true, consumer, true, e);
|
||||
}
|
||||
|
||||
public static void showCreation(DataStoreProvider selected, DataStoreCreationCategory category) {
|
||||
showCreation(
|
||||
selected != null ? selected.defaultStore(DataStorage.get().getSelectedCategory()) : null,
|
||||
category,
|
||||
dataStoreEntry -> {},
|
||||
true);
|
||||
}
|
||||
|
||||
public static void showCreation(
|
||||
DataStore base,
|
||||
DataStoreCreationCategory category,
|
||||
Consumer<DataStoreEntry> listener,
|
||||
boolean selectCategory) {
|
||||
var prov = base != null ? DataStoreProviders.byStore(base) : null;
|
||||
StoreCreationConsumer consumer = (e, validated) -> {
|
||||
try {
|
||||
var returned = DataStorage.get().addStoreEntryIfNotPresent(e);
|
||||
listener.accept(returned);
|
||||
if (validated
|
||||
&& e.getProvider().shouldShowScan()
|
||||
&& AppPrefs.get()
|
||||
.openConnectionSearchWindowOnConnectionCreation()
|
||||
.get()) {
|
||||
ScanDialog.showSingleAsync(e);
|
||||
}
|
||||
|
||||
if (selectCategory) {
|
||||
// Select new category if needed
|
||||
var cat = DataStorage.get()
|
||||
.getStoreCategoryIfPresent(e.getCategoryUuid())
|
||||
.orElseThrow();
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
StoreViewState.get()
|
||||
.getActiveCategory()
|
||||
.setValue(StoreViewState.get().getCategoryWrapper(cat));
|
||||
});
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
}
|
||||
};
|
||||
show(
|
||||
null,
|
||||
prov,
|
||||
base,
|
||||
dataStoreProvider -> (category != null && category.equals(dataStoreProvider.getCreationCategory()))
|
||||
|| dataStoreProvider.equals(prov),
|
||||
consumer,
|
||||
false,
|
||||
null);
|
||||
}
|
||||
|
||||
private static void show(
|
||||
String initialName,
|
||||
DataStoreProvider provider,
|
||||
DataStore s,
|
||||
Predicate<DataStoreProvider> filter,
|
||||
StoreCreationConsumer con,
|
||||
boolean staticDisplay,
|
||||
DataStoreEntry existingEntry) {
|
||||
var prop = new SimpleObjectProperty<>(provider);
|
||||
var store = new SimpleObjectProperty<>(s);
|
||||
var model = new StoreCreationModel(prop, store, filter, initialName, existingEntry, staticDisplay, con);
|
||||
var modal = createModalOverlay(model);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
private static boolean showInvalidConfirmAlert() {
|
||||
var skipped = new SimpleBooleanProperty();
|
||||
var modal = ModalOverlay.of("confirmInvalidStoreTitle", AppDialog.dialogTextKey("confirmInvalidStoreContent"));
|
||||
modal.addButton(new ModalButton("retry", null, true, false));
|
||||
modal.addButton(new ModalButton("skip", () -> skipped.set(true), true, true));
|
||||
modal.showAndWait();
|
||||
return skipped.get();
|
||||
}
|
||||
|
||||
private static ModalOverlay createModalOverlay(StoreCreationModel model) {
|
||||
var comp = new StoreCreationComp(model);
|
||||
comp.prefWidth(650);
|
||||
var nameKey = model.storeTypeNameKey() + "Add";
|
||||
var modal = ModalOverlay.of(nameKey, comp);
|
||||
var provider = model.getProvider().getValue();
|
||||
var graphic = provider != null
|
||||
&& provider.getDisplayIconFileName(model.getStore().get()) != null
|
||||
? new LabelGraphic.ImageGraphic(
|
||||
provider.getDisplayIconFileName(model.getStore().get()), 20)
|
||||
: new LabelGraphic.IconGraphic("mdi2b-beaker-plus-outline");
|
||||
modal.hideable(AppI18n.observable(model.storeTypeNameKey() + "Add"), graphic, () -> {
|
||||
modal.show();
|
||||
});
|
||||
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
|
||||
if (model.getFinished().get() || !modal.isShowing()) {
|
||||
return;
|
||||
}
|
||||
|
||||
modal.hide();
|
||||
AppLayoutModel.get()
|
||||
.getQueueEntries()
|
||||
.add(new AppLayoutModel.QueueEntry(
|
||||
AppI18n.observable(model.storeTypeNameKey() + "Add"), graphic, () -> {
|
||||
modal.show();
|
||||
}));
|
||||
});
|
||||
modal.setRequireCloseButtonForClose(true);
|
||||
modal.addButton(new ModalButton(
|
||||
"docs",
|
||||
() -> {
|
||||
model.showDocs();
|
||||
},
|
||||
false,
|
||||
false)
|
||||
.augment(button -> {
|
||||
button.visibleProperty().bind(Bindings.not(model.canShowDocs()));
|
||||
}));
|
||||
modal.addButton(new ModalButton(
|
||||
"connect",
|
||||
() -> {
|
||||
model.connect();
|
||||
},
|
||||
false,
|
||||
false)
|
||||
.augment(button -> {
|
||||
button.visibleProperty().bind(Bindings.not(model.canConnect()));
|
||||
}));
|
||||
modal.addButton(new ModalButton(
|
||||
"skip",
|
||||
() -> {
|
||||
if (showInvalidConfirmAlert()) {
|
||||
model.commit(false);
|
||||
modal.close();
|
||||
} else {
|
||||
model.finish();
|
||||
}
|
||||
},
|
||||
false,
|
||||
false))
|
||||
.augment(button -> {
|
||||
button.visibleProperty().bind(model.getSkippable());
|
||||
});
|
||||
modal.addButton(new ModalButton(
|
||||
"finish",
|
||||
() -> {
|
||||
model.finish();
|
||||
},
|
||||
false,
|
||||
true));
|
||||
model.getFinished().addListener((obs, oldValue, newValue) -> {
|
||||
modal.close();
|
||||
});
|
||||
return modal;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import io.xpipe.app.ext.DataStoreCreationCategory;
|
||||
import io.xpipe.app.ext.DataStoreProviders;
|
||||
import io.xpipe.app.util.ScanDialog;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuButton;
|
||||
import javafx.scene.control.MenuItem;
|
||||
@@ -17,16 +18,18 @@ import java.util.Comparator;
|
||||
|
||||
public class StoreCreationMenu {
|
||||
|
||||
public static void addButtons(MenuButton menu) {
|
||||
var automatically = new MenuItem();
|
||||
automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline"));
|
||||
automatically.textProperty().bind(AppI18n.observable("addAutomatically"));
|
||||
automatically.setOnAction(event -> {
|
||||
ScanDialog.showAsync(null);
|
||||
event.consume();
|
||||
});
|
||||
menu.getItems().add(automatically);
|
||||
menu.getItems().add(new SeparatorMenuItem());
|
||||
public static void addButtons(MenuButton menu, boolean allowSearch) {
|
||||
if (allowSearch) {
|
||||
var automatically = new MenuItem();
|
||||
automatically.setGraphic(new FontIcon("mdi2e-eye-plus-outline"));
|
||||
automatically.textProperty().bind(AppI18n.observable("addAutomatically"));
|
||||
automatically.setOnAction(event -> {
|
||||
ScanDialog.showSingleAsync(null);
|
||||
event.consume();
|
||||
});
|
||||
menu.getItems().add(automatically);
|
||||
menu.getItems().add(new SeparatorMenuItem());
|
||||
}
|
||||
|
||||
menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreCreationCategory.HOST, "ssh"));
|
||||
|
||||
@@ -67,7 +70,7 @@ public class StoreCreationMenu {
|
||||
item.setGraphic(new FontIcon(graphic));
|
||||
item.textProperty().bind(AppI18n.observable(name));
|
||||
item.setOnAction(event -> {
|
||||
StoreCreationComp.showCreation(
|
||||
StoreCreationDialog.showCreation(
|
||||
defaultProvider != null
|
||||
? DataStoreProviders.byId(defaultProvider).orElseThrow()
|
||||
: null,
|
||||
@@ -85,12 +88,16 @@ public class StoreCreationMenu {
|
||||
return;
|
||||
}
|
||||
|
||||
StoreCreationComp.showCreation(
|
||||
defaultProvider != null
|
||||
? DataStoreProviders.byId(defaultProvider).orElseThrow()
|
||||
: null,
|
||||
category);
|
||||
event.consume();
|
||||
Platform.runLater(() -> {
|
||||
StoreCreationDialog.showCreation(
|
||||
defaultProvider != null
|
||||
? DataStoreProviders.byId(defaultProvider).orElseThrow()
|
||||
: null,
|
||||
category);
|
||||
});
|
||||
|
||||
// Fix weird JavaFX NPE
|
||||
menu.getParentPopup().hide();
|
||||
});
|
||||
|
||||
var providers = sub.stream()
|
||||
@@ -108,7 +115,7 @@ public class StoreCreationMenu {
|
||||
item.setGraphic(PrettyImageHelper.ofFixedSizeSquare(dataStoreProvider.getDisplayIconFileName(null), 16)
|
||||
.createRegion());
|
||||
item.setOnAction(event -> {
|
||||
StoreCreationComp.showCreation(dataStoreProvider, category);
|
||||
StoreCreationDialog.showCreation(dataStoreProvider, category);
|
||||
event.consume();
|
||||
});
|
||||
menu.getItems().add(item);
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.ext.DataStoreProvider;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreCategory;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.process.ShellTtyState;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.core.store.ValidatableStore;
|
||||
import io.xpipe.core.util.ValidationException;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
|
||||
@Getter
|
||||
public class StoreCreationModel {
|
||||
|
||||
Property<DataStoreProvider> provider;
|
||||
ObjectProperty<DataStore> store;
|
||||
Predicate<DataStoreProvider> filter;
|
||||
BooleanProperty busy = new SimpleBooleanProperty();
|
||||
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
|
||||
BooleanProperty finished = new SimpleBooleanProperty();
|
||||
ObservableValue<DataStoreEntry> entry;
|
||||
BooleanProperty changedSinceError = new SimpleBooleanProperty();
|
||||
BooleanProperty skippable = new SimpleBooleanProperty();
|
||||
BooleanProperty connectable = new SimpleBooleanProperty();
|
||||
StringProperty name;
|
||||
DataStoreEntry existingEntry;
|
||||
boolean staticDisplay;
|
||||
StoreCreationConsumer consumer;
|
||||
|
||||
public StoreCreationModel(
|
||||
Property<DataStoreProvider> provider,
|
||||
ObjectProperty<DataStore> store,
|
||||
Predicate<DataStoreProvider> filter,
|
||||
String initialName,
|
||||
DataStoreEntry existingEntry,
|
||||
boolean staticDisplay,
|
||||
StoreCreationConsumer consumer) {
|
||||
this.provider = provider;
|
||||
this.store = store;
|
||||
this.filter = filter;
|
||||
this.name = new SimpleStringProperty(initialName != null && !initialName.isEmpty() ? initialName : null);
|
||||
this.existingEntry = existingEntry;
|
||||
this.staticDisplay = staticDisplay;
|
||||
this.consumer = consumer;
|
||||
this.store.addListener((c, o, n) -> {
|
||||
changedSinceError.setValue(true);
|
||||
});
|
||||
this.name.addListener((c, o, n) -> {
|
||||
changedSinceError.setValue(true);
|
||||
});
|
||||
|
||||
this.provider.addListener((c, o, n) -> {
|
||||
store.unbind();
|
||||
store.setValue(null);
|
||||
if (n != null) {
|
||||
store.setValue(n.defaultStore(getTargetCategory(existingEntry)));
|
||||
}
|
||||
});
|
||||
|
||||
this.provider.subscribe((n) -> {
|
||||
if (n != null) {
|
||||
connectable.setValue(n.canConnectDuringCreation());
|
||||
}
|
||||
});
|
||||
|
||||
this.validator.addListener((observable, oldValue, newValue) -> {
|
||||
Platform.runLater(() -> {
|
||||
newValue.validate();
|
||||
});
|
||||
});
|
||||
this.entry = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
if (name.getValue() == null || store.getValue() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var testE = DataStoreEntry.createNew(
|
||||
UUID.randomUUID(),
|
||||
DataStorage.get().getSelectedCategory().getUuid(),
|
||||
name.getValue(),
|
||||
store.getValue());
|
||||
var p = DataStorage.get().getDefaultDisplayParent(testE).orElse(null);
|
||||
var targetCategory = getTargetCategory(p);
|
||||
return DataStoreEntry.createNew(
|
||||
UUID.randomUUID(), targetCategory.getUuid(), name.getValue(), store.getValue());
|
||||
},
|
||||
name,
|
||||
store);
|
||||
|
||||
skippable.bind(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
if (name.get() != null && store.get().isComplete() && store.get() instanceof ValidatableStore) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
store,
|
||||
name));
|
||||
}
|
||||
|
||||
private DataStoreCategory getTargetCategory(DataStoreEntry base) {
|
||||
var targetCategory = base != null
|
||||
? base.getCategoryUuid()
|
||||
: DataStorage.get().getSelectedCategory().getUuid();
|
||||
var rootCategory = DataStorage.get()
|
||||
.getRootCategory(DataStorage.get()
|
||||
.getStoreCategoryIfPresent(targetCategory)
|
||||
.orElseThrow());
|
||||
|
||||
// Don't put it in the wrong root category
|
||||
if ((provider.getValue().getCreationCategory() == null
|
||||
|| !provider.getValue().getCreationCategory().getCategory().equals(rootCategory.getUuid()))) {
|
||||
targetCategory = provider.getValue().getCreationCategory() != null
|
||||
? provider.getValue().getCreationCategory().getCategory()
|
||||
: DataStorage.ALL_CONNECTIONS_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Don't use the all connections category
|
||||
if (targetCategory.equals(DataStorage.get().getAllConnectionsCategory().getUuid())) {
|
||||
targetCategory = DataStorage.get().getDefaultConnectionsCategory().getUuid();
|
||||
}
|
||||
|
||||
// Don't use the all scripts category
|
||||
if (targetCategory.equals(DataStorage.get().getAllScriptsCategory().getUuid())) {
|
||||
targetCategory = DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Don't use the all identities category
|
||||
if (targetCategory.equals(DataStorage.get().getAllIdentitiesCategory().getUuid())) {
|
||||
targetCategory = DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID;
|
||||
}
|
||||
|
||||
// Custom category stuff
|
||||
targetCategory = provider.getValue().getTargetCategory(store.getValue(), targetCategory);
|
||||
return DataStorage.get().getStoreCategoryIfPresent(targetCategory).orElseThrow();
|
||||
}
|
||||
|
||||
ObservableBooleanValue canConnect() {
|
||||
return connectable
|
||||
.not()
|
||||
.or(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return store.getValue() == null || !store.getValue().isComplete();
|
||||
},
|
||||
store));
|
||||
}
|
||||
|
||||
void connect() {
|
||||
var temp = DataStoreEntry.createTempWrapper(store.getValue());
|
||||
var action = provider.getValue().launchAction(temp);
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
action.execute();
|
||||
});
|
||||
}
|
||||
|
||||
void finish() {
|
||||
if (finished.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (store.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We didn't change anything
|
||||
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
|
||||
commit(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator.getValue().validate()) {
|
||||
var msg = validator
|
||||
.getValue()
|
||||
.getValidationResult()
|
||||
.getMessages()
|
||||
.getFirst()
|
||||
.getText();
|
||||
ErrorEvent.fromMessage(msg).expected().handle();
|
||||
changedSinceError.setValue(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ThreadHelper.runAsync(() -> {
|
||||
// Might have changed since last time
|
||||
if (entry.getValue() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var ignored = new BooleanScope(busy).start()) {
|
||||
DataStorage.get().addStoreEntryInProgress(entry.getValue());
|
||||
validate();
|
||||
commit(true);
|
||||
} catch (Throwable ex) {
|
||||
if (ex instanceof ValidationException) {
|
||||
ErrorEvent.expected(ex);
|
||||
} else if (ex instanceof StackOverflowError) {
|
||||
// Cycles in connection graphs can fail hard but are expected
|
||||
ErrorEvent.expected(ex);
|
||||
}
|
||||
|
||||
changedSinceError.setValue(false);
|
||||
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
} finally {
|
||||
DataStorage.get().removeStoreEntryInProgress(entry.getValue());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void validate() throws Throwable {
|
||||
var s = entry.getValue().getStore();
|
||||
if (s == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
s.checkComplete();
|
||||
|
||||
// Start session for later
|
||||
if (s instanceof ShellStore ss) {
|
||||
var sc = ss.getOrStartSession();
|
||||
var unsupported = !sc.getShellDialect().getDumbMode().supportsAnyPossibleInteraction()
|
||||
|| sc.getTtyState() != ShellTtyState.NONE;
|
||||
if (unsupported) {
|
||||
ss.stopSessionIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void showDocs() {
|
||||
Hyperlinks.open(provider.getValue().getHelpLink().getLink());
|
||||
}
|
||||
|
||||
ObservableBooleanValue canShowDocs() {
|
||||
var disable = Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return provider.getValue() == null || provider.getValue().getHelpLink() == null;
|
||||
},
|
||||
provider);
|
||||
return disable;
|
||||
}
|
||||
|
||||
void commit(boolean validated) {
|
||||
if (finished.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
finished.setValue(true);
|
||||
consumer.consume(entry.getValue(), validated);
|
||||
}
|
||||
|
||||
public String storeTypeNameKey() {
|
||||
var p = provider.getValue();
|
||||
var nameKey = p == null
|
||||
|| p.getCreationCategory() == null
|
||||
|| p.getCreationCategory().getCategory().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)
|
||||
? "connection"
|
||||
: p.getCreationCategory().getCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)
|
||||
? "script"
|
||||
: "identity";
|
||||
return nameKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
public class StoreEntryBatchSelectComp extends SimpleComp {
|
||||
|
||||
private final StoreSection section;
|
||||
|
||||
public StoreEntryBatchSelectComp(StoreSection section) {
|
||||
this.section = section;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var selfUpdate = new SimpleBooleanProperty(false);
|
||||
var cb = new CheckBox();
|
||||
cb.setAllowIndeterminate(true);
|
||||
cb.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
BooleanScope.executeExclusive(selfUpdate, () -> {
|
||||
if (newValue) {
|
||||
StoreViewState.get().selectBatchMode(section);
|
||||
} else {
|
||||
StoreViewState.get().unselectBatchMode(section);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
StoreViewState.get().getBatchModeSelection().getList().addListener((ListChangeListener<
|
||||
? super StoreEntryWrapper>)
|
||||
c -> {
|
||||
if (selfUpdate.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
externalUpdate(cb);
|
||||
});
|
||||
});
|
||||
section.getShownChildren().getList().addListener((ListChangeListener<? super StoreSection>) c -> {
|
||||
BooleanScope.executeExclusive(selfUpdate, () -> {
|
||||
if (cb.isSelected()) {
|
||||
StoreViewState.get().selectBatchMode(section);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
cb.getStyleClass().add("batch-mode-selector");
|
||||
cb.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
cb.setSelected(!cb.isSelected());
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
return cb;
|
||||
}
|
||||
|
||||
private void externalUpdate(CheckBox checkBox) {
|
||||
var isSelected = section.getWrapper() == null
|
||||
? checkBox.isSelected()
|
||||
: StoreViewState.get().isBatchModeSelected(section.getWrapper());
|
||||
checkBox.setSelected(isSelected);
|
||||
if (section.getShownChildren().getList().size() == 0) {
|
||||
checkBox.setIndeterminate(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var count = section.getShownChildren().getList().stream()
|
||||
.filter(c -> StoreViewState.get().isBatchModeSelected(c.getWrapper()))
|
||||
.count();
|
||||
checkBox.setIndeterminate(
|
||||
count > 0 && count != section.getShownChildren().getList().size());
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,12 @@ import io.xpipe.app.comp.augment.GrowAugment;
|
||||
import io.xpipe.app.comp.base.IconButtonComp;
|
||||
import io.xpipe.app.comp.base.LabelComp;
|
||||
import io.xpipe.app.comp.base.LoadingOverlayComp;
|
||||
import io.xpipe.app.comp.base.TooltipAugment;
|
||||
import io.xpipe.app.core.*;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.resources.AppResources;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.process.OsType;
|
||||
@@ -42,10 +41,26 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||
|
||||
public static final PseudoClass FAILED = PseudoClass.getPseudoClass("failed");
|
||||
public static final PseudoClass INCOMPLETE = PseudoClass.getPseudoClass("incomplete");
|
||||
public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH =
|
||||
App.getApp().getStage().widthProperty().divide(2.1).add(-100);
|
||||
public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH =
|
||||
App.getApp().getStage().widthProperty().divide(2.1).add(-200);
|
||||
public static final ObservableDoubleValue INFO_NO_CONTENT_WIDTH = Bindings.createDoubleBinding(
|
||||
() -> {
|
||||
var w = App.getApp().getStage().getWidth();
|
||||
if (w >= 1000) {
|
||||
return (w / 2.1) - 100;
|
||||
} else {
|
||||
return (w / 1.7) - 50;
|
||||
}
|
||||
},
|
||||
App.getApp().getStage().widthProperty());
|
||||
public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH = Bindings.createDoubleBinding(
|
||||
() -> {
|
||||
var w = App.getApp().getStage().getWidth();
|
||||
if (w >= 1000) {
|
||||
return (w / 2.1) - 200;
|
||||
} else {
|
||||
return (w / 1.7) - 150;
|
||||
}
|
||||
},
|
||||
App.getApp().getStage().widthProperty());
|
||||
protected final StoreSection section;
|
||||
protected final Comp<?> content;
|
||||
|
||||
@@ -88,6 +103,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||
var r = createContent();
|
||||
var buttonBar = r.lookup(".button-bar");
|
||||
var iconChooser = r.lookup(".icon");
|
||||
var batchMode = r.lookup(".batch-mode-selector");
|
||||
|
||||
var button = new Button();
|
||||
button.setGraphic(r);
|
||||
@@ -105,6 +121,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||
});
|
||||
button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
var notOnButton = NodeHelper.isParent(iconChooser, event.getTarget())
|
||||
|| NodeHelper.isParent(batchMode, event.getTarget())
|
||||
|| NodeHelper.isParent(buttonBar, event.getTarget());
|
||||
if (AppPrefs.get().requireDoubleClickForConnections().get() && !notOnButton) {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
|
||||
@@ -118,6 +135,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||
});
|
||||
button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
var notOnButton = NodeHelper.isParent(iconChooser, event.getTarget())
|
||||
|| NodeHelper.isParent(batchMode, event.getTarget())
|
||||
|| NodeHelper.isParent(buttonBar, event.getTarget());
|
||||
if (AppPrefs.get().requireDoubleClickForConnections().get() && !notOnButton) {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
|
||||
@@ -198,7 +216,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||
}
|
||||
|
||||
protected Region createButtonBar() {
|
||||
var list = new DerivedObservableList<>(getWrapper().getActionProviders(), false);
|
||||
var list = DerivedObservableList.wrap(getWrapper().getActionProviders(), false);
|
||||
var buttons = list.mapped(actionProvider -> {
|
||||
var button = buildButton(actionProvider);
|
||||
return button != null ? button.createRegion() : null;
|
||||
@@ -260,7 +278,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||
}));
|
||||
}
|
||||
button.accessibleText(cs.getName(getWrapper().getEntry().ref()).getValue());
|
||||
button.apply(new TooltipAugment<>(cs.getName(getWrapper().getEntry().ref()), null));
|
||||
button.tooltip(cs.getName(getWrapper().getEntry().ref()));
|
||||
return button;
|
||||
}
|
||||
|
||||
@@ -276,6 +294,12 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||
return settingsButton;
|
||||
}
|
||||
|
||||
protected Comp<?> createBatchSelection() {
|
||||
var c = new StoreEntryBatchSelectComp(section);
|
||||
c.hide(StoreViewState.get().getBatchMode().not());
|
||||
return c;
|
||||
}
|
||||
|
||||
protected ContextMenu createContextMenu() {
|
||||
var contextMenu = ContextMenuHelper.create();
|
||||
|
||||
@@ -332,7 +356,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||
event.consume();
|
||||
});
|
||||
color.getItems().add(none);
|
||||
Arrays.stream(DataColor.values()).forEach(dataStoreColor -> {
|
||||
Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> {
|
||||
MenuItem m = new MenuItem();
|
||||
m.textProperty().bind(AppI18n.observable(dataStoreColor.getId()));
|
||||
m.setOnAction(event -> {
|
||||
@@ -429,8 +453,8 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||
var name = cs.getName(getWrapper().getEntry().ref());
|
||||
var icon = cs.getIcon(getWrapper().getEntry().ref());
|
||||
var item = (leaf != null && leaf.canLinkTo()) || branch != null
|
||||
? new Menu(null, new FontIcon(icon))
|
||||
: new MenuItem(null, new FontIcon(icon));
|
||||
? new Menu(null, icon.createGraphicNode())
|
||||
: new MenuItem(null, icon.createGraphicNode());
|
||||
|
||||
var proRequired = p.getProFeatureId() != null
|
||||
&& !LicenseProvider.get().getFeature(p.getProFeatureId()).isSupported();
|
||||
|
||||
@@ -4,28 +4,34 @@ import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||
import io.xpipe.app.comp.base.MultiContentComp;
|
||||
import io.xpipe.app.comp.base.VerticalComp;
|
||||
import io.xpipe.app.core.AppCache;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class StoreEntryListComp extends SimpleComp {
|
||||
|
||||
private Comp<?> createList() {
|
||||
var shown = StoreViewState.get()
|
||||
.getCurrentTopLevelSection()
|
||||
.getShownChildren()
|
||||
.getList();
|
||||
var all = StoreViewState.get()
|
||||
.getCurrentTopLevelSection()
|
||||
.getAllChildren()
|
||||
.getList();
|
||||
var content = new ListBoxViewComp<>(
|
||||
StoreViewState.get()
|
||||
.getCurrentTopLevelSection()
|
||||
.getShownChildren()
|
||||
.getList(),
|
||||
StoreViewState.get()
|
||||
.getCurrentTopLevelSection()
|
||||
.getAllChildren()
|
||||
.getList(),
|
||||
shown,
|
||||
all,
|
||||
(StoreSection e) -> {
|
||||
var custom = StoreSection.customSection(e).hgrow();
|
||||
return custom;
|
||||
@@ -48,7 +54,15 @@ public class StoreEntryListComp extends SimpleComp {
|
||||
struc.get().setVvalue(0);
|
||||
});
|
||||
});
|
||||
return content.styleClass("store-list-comp");
|
||||
content.styleClass("store-list-comp");
|
||||
content.vgrow();
|
||||
|
||||
var statusBar = new StoreEntryListStatusBarComp();
|
||||
statusBar.apply(struc -> {
|
||||
VBox.setMargin(struc.get(), new Insets(3, 6, 4, 2));
|
||||
});
|
||||
statusBar.hide(StoreViewState.get().getBatchMode().not());
|
||||
return new VerticalComp(List.of(content, statusBar));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -13,10 +13,8 @@ import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.MenuButton;
|
||||
@@ -26,6 +24,7 @@ import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.function.Function;
|
||||
@@ -91,39 +90,53 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
||||
StoreViewState.get().getFilterString().setValue(newValue);
|
||||
});
|
||||
});
|
||||
var filter = new FilterComp(StoreViewState.get().getFilterString());
|
||||
var f = filter.createRegion();
|
||||
var button = createAddButton();
|
||||
var hbox = new HBox(button, f);
|
||||
f.minHeightProperty().bind(button.heightProperty());
|
||||
f.prefHeightProperty().bind(button.heightProperty());
|
||||
f.maxHeightProperty().bind(button.heightProperty());
|
||||
var filter = new FilterComp(StoreViewState.get().getFilterString()).createRegion();
|
||||
var add = createAddButton();
|
||||
var batchMode = createBatchModeButton().createRegion();
|
||||
var hbox = new HBox(add, filter, batchMode);
|
||||
filter.minHeightProperty().bind(add.heightProperty());
|
||||
filter.prefHeightProperty().bind(add.heightProperty());
|
||||
filter.maxHeightProperty().bind(add.heightProperty());
|
||||
batchMode.minHeightProperty().bind(add.heightProperty());
|
||||
batchMode.prefHeightProperty().bind(add.heightProperty());
|
||||
batchMode.maxHeightProperty().bind(add.heightProperty());
|
||||
batchMode.minWidthProperty().bind(add.heightProperty());
|
||||
batchMode.prefWidthProperty().bind(add.heightProperty());
|
||||
batchMode.maxWidthProperty().bind(add.heightProperty());
|
||||
hbox.setSpacing(8);
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
HBox.setHgrow(f, Priority.ALWAYS);
|
||||
HBox.setHgrow(filter, Priority.ALWAYS);
|
||||
|
||||
f.getStyleClass().add("filter-bar");
|
||||
filter.getStyleClass().add("filter-bar");
|
||||
return hbox;
|
||||
}
|
||||
|
||||
private Region createAddButton() {
|
||||
var menu = new MenuButton(null, new FontIcon("mdi2p-plus-thick"));
|
||||
menu.textProperty().bind(AppI18n.observable("addConnections"));
|
||||
menu.textProperty().bind(AppI18n.observable("new"));
|
||||
menu.setAlignment(Pos.CENTER);
|
||||
menu.setTextAlignment(TextAlignment.CENTER);
|
||||
StoreCreationMenu.addButtons(menu);
|
||||
StoreCreationMenu.addButtons(menu, true);
|
||||
menu.setOpacity(0.85);
|
||||
menu.setMinWidth(Region.USE_PREF_SIZE);
|
||||
|
||||
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||
menu.setPadding(new Insets(-2, 0, -2, 0));
|
||||
} else {
|
||||
menu.setPadding(new Insets(-5, -2, -5, -2));
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
private Comp<?> createBatchModeButton() {
|
||||
var batchMode = StoreViewState.get().getBatchMode();
|
||||
var b = new IconButtonComp("mdi2l-layers", () -> {
|
||||
batchMode.setValue(!batchMode.getValue());
|
||||
});
|
||||
b.styleClass("batch-mode-button");
|
||||
b.apply(struc -> {
|
||||
batchMode.subscribe(a -> {
|
||||
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), a);
|
||||
});
|
||||
struc.get().getStyleClass().remove(Styles.FLAT);
|
||||
});
|
||||
return b;
|
||||
}
|
||||
|
||||
private Comp<?> createAlphabeticalSortButton() {
|
||||
var sortMode = StoreViewState.get().getSortMode();
|
||||
var icon = Bindings.createObjectBinding(
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.Comp;
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.augment.ContextMenuAugment;
|
||||
import io.xpipe.app.comp.base.*;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||
import io.xpipe.app.util.*;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class StoreEntryListStatusBarComp extends SimpleComp {
|
||||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
var checkbox = new StoreEntryBatchSelectComp(StoreViewState.get().getCurrentTopLevelSection());
|
||||
var l = new LabelComp(Bindings.createStringBinding(
|
||||
() -> {
|
||||
return AppI18n.get(
|
||||
"connectionsSelected",
|
||||
StoreViewState.get()
|
||||
.getEffectiveBatchModeSelection()
|
||||
.getList()
|
||||
.size());
|
||||
},
|
||||
StoreViewState.get().getEffectiveBatchModeSelection().getList(),
|
||||
AppI18n.activeLanguage()));
|
||||
l.minWidth(Region.USE_PREF_SIZE);
|
||||
l.apply(struc -> {
|
||||
struc.get().setAlignment(Pos.CENTER);
|
||||
});
|
||||
var busy = new SimpleBooleanProperty();
|
||||
var actions = new ToolbarComp(createActions(busy));
|
||||
var close = new IconButtonComp("mdi2c-close", () -> {
|
||||
StoreViewState.get().getBatchMode().setValue(false);
|
||||
});
|
||||
close.apply(struc -> {
|
||||
struc.get().getStyleClass().remove(Styles.FLAT);
|
||||
struc.get().minWidthProperty().bind(struc.get().heightProperty());
|
||||
struc.get().prefWidthProperty().bind(struc.get().heightProperty());
|
||||
struc.get().maxWidthProperty().bind(struc.get().heightProperty());
|
||||
});
|
||||
var bar = new HorizontalComp(List.of(
|
||||
checkbox, Comp.hspacer(12), l, Comp.hspacer(20), actions, Comp.hspacer(), Comp.hspacer(20), close));
|
||||
bar.apply(struc -> {
|
||||
struc.get().setFillHeight(true);
|
||||
struc.get().setAlignment(Pos.CENTER_LEFT);
|
||||
});
|
||||
bar.minHeight(40);
|
||||
bar.prefHeight(40);
|
||||
bar.styleClass("bar");
|
||||
bar.styleClass("store-entry-list-status-bar");
|
||||
bar.disable(busy);
|
||||
return bar.createRegion();
|
||||
}
|
||||
|
||||
private ObservableList<Comp<?>> createActions(BooleanProperty busy) {
|
||||
var l = DerivedObservableList.<ActionProvider>arrayList(true);
|
||||
StoreViewState.get().getEffectiveBatchModeSelection().getList().addListener((ListChangeListener<
|
||||
? super StoreEntryWrapper>)
|
||||
c -> {
|
||||
l.setContent(getCompatibleActionProviders());
|
||||
});
|
||||
return l.<Comp<?>>mapped(actionProvider -> {
|
||||
return buildButton(actionProvider, busy);
|
||||
})
|
||||
.getList();
|
||||
}
|
||||
|
||||
private List<ActionProvider> getCompatibleActionProviders() {
|
||||
var l = StoreViewState.get().getEffectiveBatchModeSelection().getList();
|
||||
if (l.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
var all = new ArrayList<>(ActionProvider.ALL);
|
||||
for (StoreEntryWrapper w : l) {
|
||||
var actions = ActionProvider.ALL.stream()
|
||||
.filter(actionProvider -> {
|
||||
var s = actionProvider.getBatchDataStoreCallSite();
|
||||
if (s == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!s.getApplicableClass()
|
||||
.isAssignableFrom(w.getStore().getValue().getClass())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!s.isApplicable(w.getEntry().ref())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.toList();
|
||||
all.removeIf(actionProvider -> !actions.contains(actionProvider));
|
||||
}
|
||||
return all;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends DataStore> Comp<?> buildButton(ActionProvider p, BooleanProperty busy) {
|
||||
ActionProvider.BatchDataStoreCallSite<T> s =
|
||||
(ActionProvider.BatchDataStoreCallSite<T>) p.getBatchDataStoreCallSite();
|
||||
if (s == null) {
|
||||
return Comp.empty();
|
||||
}
|
||||
|
||||
var childrenRefs = StoreViewState.get()
|
||||
.getEffectiveBatchModeSelection()
|
||||
.mapped(storeEntryWrapper -> storeEntryWrapper.getEntry().<T>ref());
|
||||
var batchActions = s.getChildren(childrenRefs.getList());
|
||||
var button = new ButtonComp(s.getName(), new SimpleObjectProperty<>(s.getIcon()), () -> {
|
||||
if (batchActions.size() > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
runActions(s, busy);
|
||||
});
|
||||
if (batchActions.size() > 0) {
|
||||
button.apply(new ContextMenuAugment<>(
|
||||
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {
|
||||
var cm = ContextMenuHelper.create();
|
||||
s.getChildren(childrenRefs.getList()).forEach(childProvider -> {
|
||||
var menu = buildMenuItemForAction(childrenRefs.getList(), childProvider, busy);
|
||||
cm.getItems().add(menu);
|
||||
});
|
||||
return cm;
|
||||
}));
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends DataStore> MenuItem buildMenuItemForAction(
|
||||
List<DataStoreEntryRef<T>> batch, ActionProvider a, BooleanProperty busy) {
|
||||
ActionProvider.BatchDataStoreCallSite<T> s =
|
||||
(ActionProvider.BatchDataStoreCallSite<T>) a.getBatchDataStoreCallSite();
|
||||
var name = s.getName();
|
||||
var icon = s.getIcon();
|
||||
var children = s.getChildren(batch);
|
||||
if (children.size() > 0) {
|
||||
var menu = new Menu();
|
||||
menu.textProperty().bind(name);
|
||||
menu.setGraphic(icon.createGraphicNode());
|
||||
var items = children.stream()
|
||||
.filter(actionProvider -> actionProvider.getBatchDataStoreCallSite() != null)
|
||||
.map(c -> buildMenuItemForAction(batch, c, busy))
|
||||
.toList();
|
||||
menu.getItems().addAll(items);
|
||||
return menu;
|
||||
} else {
|
||||
var item = new MenuItem();
|
||||
item.textProperty().bind(name);
|
||||
item.setGraphic(icon.createGraphicNode());
|
||||
item.setOnAction(event -> {
|
||||
runActions(s, busy);
|
||||
event.consume();
|
||||
if (event.getTarget() instanceof Menu m) {
|
||||
m.getParentPopup().hide();
|
||||
}
|
||||
});
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends DataStore> void runActions(ActionProvider.BatchDataStoreCallSite<?> s, BooleanProperty busy) {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
var l = new ArrayList<>(
|
||||
StoreViewState.get().getEffectiveBatchModeSelection().getList());
|
||||
var mapped = l.stream().map(w -> w.getEntry().<T>ref()).toList();
|
||||
var action = ((ActionProvider.BatchDataStoreCallSite<T>) s).createAction(mapped);
|
||||
if (action != null) {
|
||||
BooleanScope.executeExclusive(busy, () -> {
|
||||
action.execute();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.ext.ActionProvider;
|
||||
import io.xpipe.app.ext.LocalStore;
|
||||
import io.xpipe.app.ext.ShellStore;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataColor;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreCategory;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.app.util.PlatformThread;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
@@ -42,7 +43,7 @@ public class StoreEntryWrapper {
|
||||
private final BooleanProperty expanded = new SimpleBooleanProperty();
|
||||
private final Property<Object> persistentState = new SimpleObjectProperty<>();
|
||||
private final Property<Map<String, Object>> cache = new SimpleObjectProperty<>(Map.of());
|
||||
private final Property<DataColor> color = new SimpleObjectProperty<>();
|
||||
private final Property<DataStoreColor> color = new SimpleObjectProperty<>();
|
||||
private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();
|
||||
private final Property<String> summary = new SimpleObjectProperty<>();
|
||||
private final Property<StoreNotes> notes;
|
||||
@@ -120,7 +121,7 @@ public class StoreEntryWrapper {
|
||||
}
|
||||
|
||||
public void editDialog() {
|
||||
StoreCreationComp.showEdit(entry);
|
||||
StoreCreationDialog.showEdit(entry);
|
||||
}
|
||||
|
||||
public void delete() {
|
||||
@@ -201,7 +202,7 @@ public class StoreEntryWrapper {
|
||||
customIcon.setValue(entry.getIcon());
|
||||
iconFile.setValue(entry.getEffectiveIconFile());
|
||||
busy.setValue(entry.getBusyCounter().get() != 0);
|
||||
deletable.setValue(entry.getConfiguration().isDeletable());
|
||||
deletable.setValue(!(entry.getStore() instanceof LocalStore));
|
||||
sessionActive.setValue(entry.getStore() instanceof SingletonSessionStore<?> ss
|
||||
&& entry.getStore() instanceof ShellStore
|
||||
&& ss.isSessionRunning());
|
||||
|
||||
@@ -11,7 +11,6 @@ import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.LabelGraphic;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
@@ -109,13 +108,16 @@ public class StoreIconChoiceComp extends SimpleComp {
|
||||
|
||||
private void updateData(TableView<List<SystemIcon>> table, String filterString) {
|
||||
var available = icons.stream()
|
||||
.filter(systemIcon -> AppImages.hasNormalImage("icons/" + systemIcon.getSource().getId() + "/" + systemIcon.getId() + "-40.png"))
|
||||
.filter(systemIcon -> AppImages.hasNormalImage(
|
||||
"icons/" + systemIcon.getSource().getId() + "/" + systemIcon.getId() + "-40.png"))
|
||||
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))
|
||||
.toList();
|
||||
table.getPlaceholder().setVisible(available.size() == 0);
|
||||
var filtered = available;
|
||||
if (filterString != null && !filterString.isBlank() && filterString.length() >= 2) {
|
||||
filtered = available.stream().filter(icon -> containsString(icon.getId(), filterString)).toList();
|
||||
filtered = available.stream()
|
||||
.filter(icon -> containsString(icon.getId(), filterString))
|
||||
.toList();
|
||||
}
|
||||
var data = partitionList(filtered, columns);
|
||||
table.getItems().setAll(data);
|
||||
|
||||
@@ -34,7 +34,7 @@ public class StoreIconChoiceDialog {
|
||||
|
||||
private ModalOverlay createOverlay() {
|
||||
var filterText = new SimpleStringProperty();
|
||||
var filter = new FilterComp(filterText).grow(true, false);
|
||||
var filter = new FilterComp(filterText).hgrow();
|
||||
filter.focusOnShow();
|
||||
var github = new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
|
||||
overlay.close();
|
||||
|
||||
@@ -2,12 +2,13 @@ package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.comp.SimpleComp;
|
||||
import io.xpipe.app.comp.base.PrettyImageHelper;
|
||||
import io.xpipe.app.comp.base.TooltipAugment;
|
||||
import io.xpipe.app.comp.base.TooltipHelper;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
@@ -26,7 +27,9 @@ public class StoreIconComp extends SimpleComp {
|
||||
var imageComp = PrettyImageHelper.ofFixedSize(wrapper.getIconFile(), w, h);
|
||||
var storeIcon = imageComp.createRegion();
|
||||
if (wrapper.getValidity().getValue().isUsable()) {
|
||||
new TooltipAugment<>(wrapper.getEntry().getProvider().displayName(), null).augment(storeIcon);
|
||||
Tooltip.install(
|
||||
storeIcon,
|
||||
TooltipHelper.create(wrapper.getEntry().getProvider().displayName(), null));
|
||||
}
|
||||
|
||||
var background = new Region();
|
||||
@@ -54,6 +57,10 @@ public class StoreIconComp extends SimpleComp {
|
||||
|
||||
stack.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (event.getButton() == MouseButton.PRIMARY) {
|
||||
if (wrapper.getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {
|
||||
return;
|
||||
}
|
||||
|
||||
StoreIconChoiceDialog.show(wrapper.getEntry());
|
||||
event.consume();
|
||||
}
|
||||
|
||||
@@ -48,11 +48,11 @@ public class StoreIdentitiesIntroComp extends SimpleComp {
|
||||
var addButton = new Button(null, new FontIcon("mdi2p-play-circle"));
|
||||
addButton.textProperty().bind(AppI18n.observable("createIdentity"));
|
||||
addButton.setOnAction(event -> {
|
||||
var canSync = DataStorage.get().supportsSharing();
|
||||
var canSync = DataStorage.get().supportsSync();
|
||||
var prov = canSync
|
||||
? DataStoreProviders.byId("syncedIdentity").orElseThrow()
|
||||
: DataStoreProviders.byId("localIdentity").orElseThrow();
|
||||
StoreCreationComp.showCreation(prov, DataStoreCreationCategory.IDENTITY);
|
||||
StoreCreationDialog.showCreation(prov, DataStoreCreationCategory.IDENTITY);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
@@ -97,7 +97,7 @@ public class StoreIdentitiesIntroComp extends SimpleComp {
|
||||
fi.setIconSize(80);
|
||||
var img = new StackPane(fi);
|
||||
img.setPrefWidth(100);
|
||||
img.setPrefHeight(150);
|
||||
img.setPrefHeight(120);
|
||||
var text = new VBox(title, importDesc);
|
||||
text.setSpacing(5);
|
||||
text.setAlignment(Pos.CENTER_LEFT);
|
||||
|
||||
@@ -38,7 +38,8 @@ public class StoreIntroComp extends SimpleComp {
|
||||
|
||||
var scanButton = new Button(null, new FontIcon("mdi2m-magnify"));
|
||||
scanButton.textProperty().bind(AppI18n.observable("detectConnections"));
|
||||
scanButton.setOnAction(event -> ScanDialog.showAsync(DataStorage.get().local()));
|
||||
scanButton.setOnAction(
|
||||
event -> ScanDialog.showSingleAsync(DataStorage.get().local()));
|
||||
scanButton.setDefaultButton(true);
|
||||
var scanPane = new StackPane(scanButton);
|
||||
scanPane.setAlignment(Pos.CENTER);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user