Merge branch 16-relase into master

This commit is contained in:
crschnick
2025-04-23 15:34:47 +00:00
parent a49d1baf87
commit cc33a86ded
1887 changed files with 16056 additions and 16114 deletions
+9 -19
View File
@@ -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
View File
@@ -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);
}
+24 -6
View File
@@ -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);
}
}
@@ -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