This commit is contained in:
crschnick 2024-08-11 10:41:03 +00:00
parent 70ba263ec4
commit 79d09c021e
344 changed files with 5671 additions and 2610 deletions

3
.gitignore vendored
View file

@ -19,3 +19,6 @@ ComponentsGenerated.wxs
!dist/javafx/**/lib !dist/javafx/**/lib
!dist/javafx/**/bin !dist/javafx/**/bin
dev.properties dev.properties
xcuserdata/
*.dylib
project.xcworkspace

View file

@ -29,7 +29,7 @@ It currently supports:
- Quickly perform various commonly used actions like starting/stopping containers, establishing tunnels, and more - Quickly perform various commonly used actions like starting/stopping containers, establishing tunnels, and more
- Create desktop shortcuts that automatically open remote connections in your terminal without having to open any GUI - Create desktop shortcuts that automatically open remote connections in your terminal without having to open any GUI
![connections](https://github.com/xpipe-io/xpipe/assets/72509152/5df3169a-4150-4478-a3de-ae1f9748c3c8) ![connections](https://github.com/user-attachments/assets/07312929-1792-4589-b139-aa10bbcdc649)
## Powerful file management ## Powerful file management
@ -40,7 +40,7 @@ It currently supports:
- Seamlessly transfer files from and to your system desktop environment - Seamlessly transfer files from and to your system desktop environment
- Work and perform transfers on multiple systems at the same time with the built-in tabbed multitasking - Work and perform transfers on multiple systems at the same time with the built-in tabbed multitasking
![browser](https://github.com/xpipe-io/xpipe/assets/72509152/4d4e4e54-17c1-4ebe-acf8-f615cfce8b3f) ![browser](https://github.com/user-attachments/assets/7e5d8b3b-8cd7-4b71-ad79-9afb385de3fd)
## Terminal launcher ## Terminal launcher
@ -52,7 +52,7 @@ It currently supports:
<br> <br>
<p align="center"> <p align="center">
<img src="https://github.com/xpipe-io/xpipe/assets/72509152/02351317-f25d-4af3-8116-bc3b4fb92312" alt="Terminal launcher"/> <img src="https://github.com/user-attachments/assets/6d369688-1c33-4b27-8de6-f7f2c5977410" alt="Terminal launcher"/>
</p> </p>
<br> <br>
@ -63,7 +63,7 @@ It currently supports:
- Setup shell init environments for connections to fully customize your work environment for every purpose - Setup shell init environments for connections to fully customize your work environment for every purpose
- Open custom shells and custom remote connections by providing your own commands - Open custom shells and custom remote connections by providing your own commands
![scripts](https://github.com/xpipe-io/xpipe/assets/72509152/56533f22-b689-4201-b58a-eebe0a6d517a) ![scripts](https://github.com/user-attachments/assets/cf39afaf-638d-48fc-9247-4c8d847d4ed4)
## Secure vault ## Secure vault
@ -72,7 +72,7 @@ It currently supports:
- There are no servers involved, all your information stays on your systems. The XPipe application does not send any personal or sensitive information to outside services. - There are no servers involved, all your information stays on your systems. The XPipe application does not send any personal or sensitive information to outside services.
- Vault changes can be pushed and pulled from your own remote git repository by multiple team members across many systems - Vault changes can be pushed and pulled from your own remote git repository by multiple team members across many systems
## API ## Programmatic connection control via the API
- The XPipe API provides programmatic access to XPipes features via an HTTP interface - The XPipe API provides programmatic access to XPipes features via an HTTP interface
- Manage all your remote systems and access their file systems in your own favorite programming language - Manage all your remote systems and access their file systems in your own favorite programming language
@ -197,6 +197,7 @@ The distributed XPipe application consists out of two parts:
- The closed-source extensions, mostly for professional edition features, which are not included in this repository - The closed-source extensions, mostly for professional edition features, which are not included in this repository
Additional features are available in the professional edition. For more details see https://xpipe.io/pricing. Additional features are available in the professional edition. For more details see https://xpipe.io/pricing.
If your enterprise puts great emphasis on having access to the full source code, there are also full source-available enterprise options available.
## More links ## More links

View file

@ -23,8 +23,8 @@ dependencies {
api project(':beacon') api project(':beacon')
compileOnly 'org.hamcrest:hamcrest:2.2' compileOnly 'org.hamcrest:hamcrest:2.2'
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.2' compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.3'
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.2' compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.3'
api 'com.vladsch.flexmark:flexmark:0.64.8' api 'com.vladsch.flexmark:flexmark:0.64.8'
api 'com.vladsch.flexmark:flexmark-util:0.64.8' api 'com.vladsch.flexmark:flexmark-util:0.64.8'
@ -50,22 +50,23 @@ dependencies {
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar") api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar") api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
api 'org.bouncycastle:bcprov-jdk18on:1.78.1'
api 'info.picocli:picocli:4.7.6' api 'info.picocli:picocli:4.7.6'
api ('org.kohsuke:github-api:1.322') { api ('org.kohsuke:github-api:1.323') {
exclude group: 'org.apache.commons', module: 'commons-lang3' exclude group: 'org.apache.commons', module: 'commons-lang3'
} }
api 'org.apache.commons:commons-lang3:3.14.0' api 'org.apache.commons:commons-lang3:3.16.0'
api 'io.sentry:sentry:7.10.0' api 'io.sentry:sentry:7.13.0'
api 'commons-io:commons-io:2.16.1' api 'commons-io:commons-io:2.16.1'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.1" api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.2"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.1" api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.2"
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0" 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-materialdesign2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0" api group: 'org.kordamp.ikonli', name: 'ikonli-feather-pack', version: "12.2.0"
api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.13' api group: 'org.slf4j', name: 'slf4j-api', version: '2.0.15'
api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.13' api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', version: '2.0.15'
api 'io.xpipe:modulefs:0.1.5' api 'io.xpipe:modulefs:0.1.5'
api 'net.synedra:validatorfx:0.4.2' api 'net.synedra:validatorfx:0.4.2'
api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar") api files("$rootDir/gradle/gradle_scripts/atlantafx-base-2.0.2.jar")
@ -93,6 +94,7 @@ run {
systemProperty 'io.xpipe.app.logLevel', "trace" systemProperty 'io.xpipe.app.logLevel', "trace"
systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion systemProperty 'io.xpipe.app.fullVersion', rootProject.fullVersion
systemProperty 'io.xpipe.app.staging', isStage systemProperty 'io.xpipe.app.staging', isStage
// systemProperty 'io.xpipe.beacon.port', "30000"
// Apply passed xpipe properties // Apply passed xpipe properties
for (final def e in System.getProperties().entrySet()) { for (final def e in System.getProperties().entrySet()) {

View file

@ -1,22 +1,23 @@
package io.xpipe.app.beacon; package io.xpipe.app.beacon;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import io.xpipe.app.core.AppResources; import io.xpipe.app.core.AppResources;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.util.MarkdownHelper; import io.xpipe.app.util.MarkdownHelper;
import io.xpipe.beacon.BeaconConfig; import io.xpipe.beacon.BeaconConfig;
import io.xpipe.beacon.BeaconInterface; import io.xpipe.beacon.BeaconInterface;
import io.xpipe.core.process.OsType;
import io.xpipe.core.util.XPipeInstallation; import io.xpipe.core.util.XPipeInstallation;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import lombok.Getter; import lombok.Getter;
import java.io.IOException; import java.io.IOException;
import java.net.InetAddress; import java.net.Inet4Address;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.*; import java.util.*;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -84,6 +85,7 @@ public class AppBeaconServer {
public static void reset() { public static void reset() {
if (INSTANCE != null) { if (INSTANCE != null) {
INSTANCE.stop(); INSTANCE.stop();
INSTANCE.deleteAuthSecret();
INSTANCE = null; INSTANCE = null;
} }
} }
@ -109,11 +111,22 @@ public class AppBeaconServer {
var file = XPipeInstallation.getLocalBeaconAuthFile(); var file = XPipeInstallation.getLocalBeaconAuthFile();
var id = UUID.randomUUID().toString(); var id = UUID.randomUUID().toString();
Files.writeString(file, id); Files.writeString(file, id);
if (OsType.getLocal() != OsType.WINDOWS) {
Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----"));
}
localAuthSecret = id; localAuthSecret = id;
} }
private void deleteAuthSecret() {
var file = XPipeInstallation.getLocalBeaconAuthFile();
try {
Files.delete(file);
} catch (IOException ignored) {
}
}
private void start() throws IOException { private void start() throws IOException {
server = HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(), port), 10); server = HttpServer.create(new InetSocketAddress(Inet4Address.getByAddress(new byte[]{ 0x7f,0x00,0x00,0x01 }), port), 10);
BeaconInterface.getAll().forEach(beaconInterface -> { BeaconInterface.getAll().forEach(beaconInterface -> {
server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface)); server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface));
}); });

View file

@ -28,7 +28,8 @@ public class BeaconRequestHandler<T> implements HttpHandler {
@Override @Override
public void handle(HttpExchange exchange) { public void handle(HttpExchange exchange) {
if (OperationMode.isInShutdown()) { if (OperationMode.isInShutdown() && !beaconInterface.acceptInShutdown()) {
writeError(exchange, new BeaconClientErrorResponse("Daemon is currently in shutdown"), 400);
return; return;
} }
@ -124,10 +125,13 @@ public class BeaconRequestHandler<T> implements HttpHandler {
try { try {
var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0; var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0;
if (!emptyResponseClass && response != null) { if (!emptyResponseClass && response != null) {
TrackEvent.trace("Sending response:\n" + object); TrackEvent.trace("Sending response:\n" + response);
var tree = JacksonMapper.getDefault().valueToTree(response); TrackEvent.trace("Sending raw response:\n"
TrackEvent.trace("Sending raw response:\n" + tree.toPrettyString()); + JacksonMapper.getCensored().valueToTree(response).toPrettyString());
var bytes = tree.toPrettyString().getBytes(StandardCharsets.UTF_8); var bytes = JacksonMapper.getDefault()
.valueToTree(response)
.toPrettyString()
.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, bytes.length); exchange.sendResponseHeaders(200, bytes.length);
try (OutputStream os = exchange.getResponseBody()) { try (OutputStream os = exchange.getResponseBody()) {
os.write(bytes); os.write(bytes);

View file

@ -50,7 +50,7 @@ public class BlobManager {
public Path newBlobFile() throws IOException { public Path newBlobFile() throws IOException {
var file = TEMP.resolve(UUID.randomUUID().toString()); var file = TEMP.resolve(UUID.randomUUID().toString());
Files.createDirectories(file.getParent()); FileUtils.forceMkdir(file.getParent().toFile());
return file; return file;
} }

View file

@ -1,12 +1,13 @@
package io.xpipe.app.beacon.impl; package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.api.ConnectionAddExchange; import io.xpipe.beacon.api.ConnectionAddExchange;
import io.xpipe.core.util.ValidationException; import io.xpipe.core.util.ValidationException;
import com.sun.net.httpserver.HttpExchange;
public class ConnectionAddExchangeImpl extends ConnectionAddExchange { public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
@Override @Override

View file

@ -1,6 +1,5 @@
package io.xpipe.app.beacon.impl; package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
@ -8,6 +7,8 @@ import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionBrowseExchange; import io.xpipe.beacon.api.ConnectionBrowseExchange;
import io.xpipe.core.store.FileSystemStore; import io.xpipe.core.store.FileSystemStore;
import com.sun.net.httpserver.HttpExchange;
public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange { public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
@Override @Override
@ -18,7 +19,8 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
if (!(e.getStore() instanceof FileSystemStore)) { if (!(e.getStore() instanceof FileSystemStore)) {
throw new BeaconClientException("Not a file system connection"); throw new BeaconClientException("Not a file system connection");
} }
BrowserSessionModel.DEFAULT.openFileSystemSync(e.ref(),msg.getDirectory() != null ? ignored -> msg.getDirectory() : null,null); BrowserSessionModel.DEFAULT.openFileSystemSync(
e.ref(), msg.getDirectory() != null ? ignored -> msg.getDirectory() : null, null);
AppLayoutModel.get().selectBrowser(); AppLayoutModel.get().selectBrowser();
return Response.builder().build(); return Response.builder().build();
} }

View file

@ -28,9 +28,16 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange {
.orElseThrow()) .orElseThrow())
.getNames(); .getNames();
var cat = new StorePath(names.subList(1, names.size())); var cat = new StorePath(names.subList(1, names.size()));
var cache = e.getStoreCache().entrySet().stream().filter(stringObjectEntry -> { var cache = e.getStoreCache().entrySet().stream()
return stringObjectEntry.getValue() != null && (ClassUtils.isPrimitiveOrWrapper(stringObjectEntry.getValue().getClass()) || stringObjectEntry.getValue() instanceof String); .filter(stringObjectEntry -> {
}).collect(Collectors.toMap(stringObjectEntry -> stringObjectEntry.getKey(),stringObjectEntry -> stringObjectEntry.getValue())); return stringObjectEntry.getValue() != null
&& (ClassUtils.isPrimitiveOrWrapper(
stringObjectEntry.getValue().getClass())
|| stringObjectEntry.getValue() instanceof String);
})
.collect(Collectors.toMap(
stringObjectEntry -> stringObjectEntry.getKey(),
stringObjectEntry -> stringObjectEntry.getValue()));
var apply = InfoResponse.builder() var apply = InfoResponse.builder()
.lastModified(e.getLastModified()) .lastModified(e.getLastModified())
@ -50,27 +57,17 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange {
} }
private Class<?> toWrapper(Class<?> clazz) { private Class<?> toWrapper(Class<?> clazz) {
if (!clazz.isPrimitive()) if (!clazz.isPrimitive()) return clazz;
return clazz;
if (clazz == Integer.TYPE) if (clazz == Integer.TYPE) return Integer.class;
return Integer.class; if (clazz == Long.TYPE) return Long.class;
if (clazz == Long.TYPE) if (clazz == Boolean.TYPE) return Boolean.class;
return Long.class; if (clazz == Byte.TYPE) return Byte.class;
if (clazz == Boolean.TYPE) if (clazz == Character.TYPE) return Character.class;
return Boolean.class; if (clazz == Float.TYPE) return Float.class;
if (clazz == Byte.TYPE) if (clazz == Double.TYPE) return Double.class;
return Byte.class; if (clazz == Short.TYPE) return Short.class;
if (clazz == Character.TYPE) if (clazz == Void.TYPE) return Void.class;
return Character.class;
if (clazz == Float.TYPE)
return Float.class;
if (clazz == Double.TYPE)
return Double.class;
if (clazz == Short.TYPE)
return Short.class;
if (clazz == Void.TYPE)
return Void.class;
return clazz; return clazz;
} }

View file

@ -1,11 +1,12 @@
package io.xpipe.app.beacon.impl; package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.FixedHierarchyStore; import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionRefreshExchange; import io.xpipe.beacon.api.ConnectionRefreshExchange;
import com.sun.net.httpserver.HttpExchange;
public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange { public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {
@Override @Override

View file

@ -1,11 +1,12 @@
package io.xpipe.app.beacon.impl; package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionRemoveExchange; import io.xpipe.beacon.api.ConnectionRemoveExchange;
import com.sun.net.httpserver.HttpExchange;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.UUID; import java.util.UUID;

View file

@ -1,12 +1,13 @@
package io.xpipe.app.beacon.impl; package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.TerminalLauncher; import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionTerminalExchange; import io.xpipe.beacon.api.ConnectionTerminalExchange;
import io.xpipe.core.store.ShellStore; import io.xpipe.core.store.ShellStore;
import com.sun.net.httpserver.HttpExchange;
public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange { public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
@Override @Override

View file

@ -1,11 +1,12 @@
package io.xpipe.app.beacon.impl; package io.xpipe.app.beacon.impl;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.BeaconClientException; import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.api.ConnectionToggleExchange; import io.xpipe.beacon.api.ConnectionToggleExchange;
import io.xpipe.core.store.SingletonSessionStore; import io.xpipe.core.store.SingletonSessionStore;
import com.sun.net.httpserver.HttpExchange;
public class ConnectionToggleExchangeImpl extends ConnectionToggleExchange { public class ConnectionToggleExchangeImpl extends ConnectionToggleExchange {
@Override @Override

View file

@ -36,6 +36,7 @@ public class ShellStartExchangeImpl extends ShellStartExchange {
.osType(control.getOsType()) .osType(control.getOsType())
.osName(control.getOsName()) .osName(control.getOsName())
.temp(control.getSystemTemporaryDirectory()) .temp(control.getSystemTemporaryDirectory())
.ttyState(control.getTtyState())
.build(); .build();
} }
} }

View file

@ -10,10 +10,12 @@ import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import java.util.HashSet;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Predicate; import java.util.function.Predicate;
@ -41,13 +43,13 @@ public final class BrowserBookmarkComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
BooleanProperty busy = new SimpleBooleanProperty(false); var busyEntries = FXCollections.<StoreSection>observableSet(new HashSet<>());
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (s, comp) -> { BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (s, comp) -> {
comp.disable(Bindings.createBooleanBinding( comp.disable(Bindings.createBooleanBinding(
() -> { () -> {
return busy.get() || !applicable.test(s.getWrapper()); return busyEntries.contains(s) || !applicable.test(s.getWrapper());
}, },
busy)); busyEntries));
comp.apply(struc -> { comp.apply(struc -> {
selected.addListener((observable, oldValue, newValue) -> { selected.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
@ -70,7 +72,17 @@ public final class BrowserBookmarkComp extends SimpleComp {
category, category,
StoreViewState.get().getEntriesListUpdateObservable()), StoreViewState.get().getEntriesListUpdateObservable()),
augment, augment,
entryWrapper -> action.accept(entryWrapper, busy)); selectedAction -> {
BooleanProperty busy = new SimpleBooleanProperty(false);
action.accept(selectedAction.getWrapper(), busy);
busy.addListener((observable, oldValue, newValue) -> {
if (newValue) {
busyEntries.add(selectedAction);
} else {
busyEntries.remove(selectedAction);
}
});
});
var r = section.vgrow().createRegion(); var r = section.vgrow().createRegion();
r.getStyleClass().add("bookmark-list"); r.getStyleClass().add("bookmark-list");

View file

@ -2,6 +2,7 @@ package io.xpipe.app.browser;
import io.xpipe.app.comp.store.StoreCategoryWrapper; import io.xpipe.app.comp.store.StoreCategoryWrapper;
import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
@ -30,18 +31,26 @@ public final class BrowserBookmarkHeaderComp extends SimpleComp {
StoreViewState.get().getAllConnectionsCategory(), StoreViewState.get().getAllConnectionsCategory(),
StoreViewState.get().getActiveCategory(), StoreViewState.get().getActiveCategory(),
this.category) this.category)
.styleClass(Styles.LEFT_PILL); .styleClass(Styles.LEFT_PILL)
var filter = new FilterComp(this.filter).styleClass(Styles.RIGHT_PILL).minWidth(0).hgrow(); .apply(struc -> {
AppFont.medium(struc.get());
});
var filter = new FilterComp(this.filter)
.styleClass(Styles.RIGHT_PILL)
.minWidth(0)
.hgrow()
.apply(struc -> {
AppFont.medium(struc.get());
});
var top = new HorizontalComp(List.of(category, filter)) var top = new HorizontalComp(List.of(category, filter))
.apply(struc -> struc.get().setFillHeight(true)) .apply(struc -> struc.get().setFillHeight(true))
.apply(struc -> { .apply(struc -> {
((Region) struc.get().getChildren().get(0)) var first = ((Region) struc.get().getChildren().get(0));
.prefHeightProperty() var second = ((Region) struc.get().getChildren().get(1));
.bind(((Region) struc.get().getChildren().get(1)).heightProperty()); first.prefHeightProperty().bind(second.heightProperty());
((Region) struc.get().getChildren().get(0)) first.minHeightProperty().bind(second.heightProperty());
.minWidthProperty() first.maxHeightProperty().bind(second.heightProperty());
.bind(struc.get().widthProperty().divide(2.0));
}) })
.styleClass("bookmarks-header") .styleClass("bookmarks-header")
.createRegion(); .createRegion();

View file

@ -109,7 +109,7 @@ public class BrowserNavBar extends Comp<BrowserNavBar.Structure> {
new TooltipAugment<>("history", new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN)) new TooltipAugment<>("history", new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN))
.augment(historyButton); .augment(historyButton);
var breadcrumbs = new BrowserBreadcrumbBar(model).grow(false, true); var breadcrumbs = new BrowserBreadcrumbBar(model);
var pathRegion = pathBar.createStructure().get(); var pathRegion = pathBar.createStructure().get();
var breadcrumbsRegion = breadcrumbs.createRegion(); var breadcrumbsRegion = breadcrumbs.createRegion();
@ -143,7 +143,7 @@ public class BrowserNavBar extends Comp<BrowserNavBar.Structure> {
topBox.setFillHeight(true); topBox.setFillHeight(true);
topBox.setAlignment(Pos.CENTER); topBox.setAlignment(Pos.CENTER);
homeButton.minWidthProperty().bind(pathRegion.heightProperty()); homeButton.minWidthProperty().bind(pathRegion.heightProperty());
homeButton.maxWidthProperty().bind(pathRegion.heightProperty().multiply(1.3)); homeButton.maxWidthProperty().bind(pathRegion.heightProperty());
homeButton.minHeightProperty().bind(pathRegion.heightProperty()); homeButton.minHeightProperty().bind(pathRegion.heightProperty());
homeButton.maxHeightProperty().bind(pathRegion.heightProperty()); homeButton.maxHeightProperty().bind(pathRegion.heightProperty());
historyButton.minHeightProperty().bind(pathRegion.heightProperty()); historyButton.minHeightProperty().bind(pathRegion.heightProperty());

View file

@ -29,7 +29,16 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
this.lastSystems = FXCollections.observableArrayList(lastSystems); this.lastSystems = FXCollections.observableArrayList(lastSystems);
} }
public static BrowserSavedStateImpl load() { private static BrowserSavedStateImpl INSTANCE;
public static BrowserSavedState get() {
if (INSTANCE == null) {
INSTANCE = load();
}
return INSTANCE;
}
private static BrowserSavedStateImpl load() {
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> { return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
return new BrowserSavedStateImpl(FXCollections.observableArrayList()); return new BrowserSavedStateImpl(FXCollections.observableArrayList());
}); });

View file

@ -7,6 +7,7 @@ import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -58,9 +59,15 @@ public class BrowserSelectionListComp extends SimpleComp {
return Comp.of(() -> { return Comp.of(() -> {
var image = PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 24) var image = PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 24)
.createRegion(); .createRegion();
var l = new Label(null, image); var t = nameTransformation.apply(entry);
var l = new Label(t.getValue(), image);
l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
l.textProperty().bind(PlatformThread.sync(nameTransformation.apply(entry))); t.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
l.setText(newValue);
});
});
BindingsHelper.preserve(l, t);
return l; return l;
}); });
}, },

View file

@ -12,10 +12,12 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.app.util.HumanReadableFormat;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
@ -37,8 +39,7 @@ public class BrowserStatusBarComp extends SimpleComp {
createProgressEstimateStatus(), createProgressEstimateStatus(),
Comp.hspacer(), Comp.hspacer(),
createClipboardStatus(), createClipboardStatus(),
createSelectionStatus() createSelectionStatus()));
));
bar.spacing(15); bar.spacing(15);
bar.styleClass("status-bar"); bar.styleClass("status-bar");
@ -58,12 +59,16 @@ public class BrowserStatusBarComp extends SimpleComp {
return null; return null;
} else { } else {
var expected = p.expectedTimeRemaining(); var expected = p.expectedTimeRemaining();
var show = (p.getTotal() > 50_000_000 && p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0) || expected.toMillis() > 5000; var show = p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0
var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "..."; && (p.getTotal() > 50_000_000 || expected.toMillis() > 5000);
var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "";
return time; return time;
} }
}); });
var progressComp = new LabelComp(text).styleClass("progress").apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)).prefWidth(90); var progressComp = new LabelComp(text)
.styleClass("progress")
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.prefWidth(90);
return progressComp; return progressComp;
} }
@ -77,7 +82,10 @@ public class BrowserStatusBarComp extends SimpleComp {
return transferred + " / " + all; return transferred + " / " + all;
} }
}); });
var progressComp = new LabelComp(text).styleClass("progress").apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)).prefWidth(150); var progressComp = new LabelComp(text)
.styleClass("progress")
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.prefWidth(150);
return progressComp; return progressComp;
} }
@ -89,7 +97,10 @@ public class BrowserStatusBarComp extends SimpleComp {
return p.getName(); return p.getName();
} }
}); });
var progressComp = new LabelComp(text).styleClass("progress").apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT)).prefWidth(180); var progressComp = new LabelComp(text)
.styleClass("progress")
.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT))
.prefWidth(180);
return progressComp; return progressComp;
} }
@ -160,7 +171,6 @@ public class BrowserStatusBarComp extends SimpleComp {
emptyEntry.onDragDone(event); emptyEntry.onDragDone(event);
}); });
// Use status bar as an extension of file list // Use status bar as an extension of file list
new ContextMenuAugment<>( new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY, mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,

View file

@ -1,8 +1,6 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserFileTransferMode;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
@ -10,15 +8,19 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment; import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard; import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode; import javafx.scene.input.TransferMode;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.io.File; import java.io.File;
@ -37,71 +39,70 @@ public class BrowserTransferComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var syncItems = PlatformThread.sync(model.getItems());
var syncDownloaded = PlatformThread.sync(model.getDownloading());
var syncAllDownloaded = PlatformThread.sync(model.getAllDownloaded());
var background = new LabelComp(AppI18n.observable("transferDescription")) var background = new LabelComp(AppI18n.observable("transferDescription"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.apply(struc -> struc.get().setWrapText(true)) .apply(struc -> struc.get().setWrapText(true))
.visible(Bindings.isEmpty(syncItems)); .visible(model.getEmpty());
var backgroundStack = var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
var binding = new DerivedObservableList<>(syncItems, true) var binding = new DerivedObservableList<>(model.getItems(), true)
.mapped(item -> item.getBrowserEntry()) .mapped(item -> item.getBrowserEntry())
.getList(); .getList();
var list = new BrowserSelectionListComp( var list = new BrowserSelectionListComp(binding, entry -> {
binding, var sourceItem = model.getCurrentItems().stream()
entry -> Bindings.createStringBinding(
() -> {
var sourceItem = syncItems.stream()
.filter(item -> item.getBrowserEntry() == entry) .filter(item -> item.getBrowserEntry() == entry)
.findAny(); .findAny();
if (sourceItem.isEmpty()) { if (sourceItem.isEmpty()) {
return "?"; return new SimpleStringProperty("?");
} }
var name = entry.getModel() == null synchronized (sourceItem.get().getProgress()) {
return Bindings.createStringBinding(
() -> {
var p = sourceItem.get().getProgress().getValue();
var progressSuffix = p == null
|| sourceItem || sourceItem
.get() .get()
.downloadFinished() .downloadFinished()
.get() .get()
? "Local" ? ""
: entry.getModel() : " " + (p.getTransferred() * 100 / p.getTotal()) + "%";
.getFileSystemModel() return entry.getFileName() + progressSuffix;
.getName();
return entry.getFileName() + " (" + name + ")";
}, },
syncAllDownloaded)) sourceItem.get().getProgress());
}
})
.grow(false, true); .grow(false, true);
var dragNotice = new LabelComp(syncAllDownloaded.flatMap( var dragNotice = new LabelComp(AppI18n.observable("dragLocalFiles"))
aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
.apply(struc -> AppFont.medium(struc.get())) .apply(struc -> AppFont.medium(struc.get()))
.apply(struc -> struc.get().setWrapText(true)) .apply(struc -> struc.get().setWrapText(true))
.hide(Bindings.isEmpty(syncItems)); .hide(model.getEmpty());
var downloadButton = new IconButtonComp("mdi2d-download", () -> {
model.download();
})
.hide(Bindings.isEmpty(syncItems))
.disable(syncAllDownloaded)
.tooltipKey("downloadStageDescription");
var clearButton = new IconButtonComp("mdi2c-close", () -> { var clearButton = new IconButtonComp("mdi2c-close", () -> {
ThreadHelper.runAsync(() -> {
model.clear(true); model.clear(true);
});
}) })
.hide(Bindings.isEmpty(syncItems)) .hide(model.getEmpty())
.tooltipKey("clearTransferDescription"); .tooltipKey("clearTransferDescription");
var bottom = var downloadButton = new IconButtonComp("mdi2f-folder-move-outline", () -> {
new HorizontalComp(List.of(Comp.hspacer(), dragNotice, Comp.hspacer(), downloadButton, Comp.hspacer(4), clearButton)); ThreadHelper.runFailableAsync(() -> {
model.transferToDownloads();
});
})
.hide(model.getEmpty())
.tooltipKey("downloadStageDescription");
var bottom = new HorizontalComp(
List.of(Comp.hspacer(), dragNotice, Comp.hspacer(), downloadButton, Comp.hspacer(4), clearButton));
var listBox = new VerticalComp(List.of(list, bottom)) var listBox = new VerticalComp(List.of(list, bottom))
.spacing(5) .spacing(5)
.padding(new Insets(10, 10, 5, 10)) .padding(new Insets(10, 10, 5, 10))
.apply(struc -> struc.get().setMinHeight(200)) .apply(struc -> struc.get().setMinHeight(200))
.apply(struc -> struc.get().setMaxHeight(200)); .apply(struc -> struc.get().setMaxHeight(200));
var stack = LoadingOverlayComp.noProgress( var stack = new StackComp(List.of(backgroundStack, listBox))
new StackComp(List.of(backgroundStack, listBox))
.apply(DragOverPseudoClassAugment.create()) .apply(DragOverPseudoClassAugment.create())
.apply(struc -> { .apply(struc -> {
struc.get().setOnDragOver(event -> { struc.get().setOnDragOver(event -> {
@ -110,13 +111,6 @@ public class BrowserTransferComp extends SimpleComp {
event.acceptTransferModes(TransferMode.ANY); event.acceptTransferModes(TransferMode.ANY);
event.consume(); event.consume();
} }
// Accept drops from outside the app window
if (event.getGestureSource() == null
&& !event.getDragboard().getFiles().isEmpty()) {
event.acceptTransferModes(TransferMode.ANY);
event.consume();
}
}); });
struc.get().setOnDragDropped(event -> { struc.get().setOnDragDropped(event -> {
// Accept drops from inside the app window // Accept drops from inside the app window
@ -138,30 +132,13 @@ public class BrowserTransferComp extends SimpleComp {
event.setDropCompleted(true); event.setDropCompleted(true);
event.consume(); event.consume();
} }
// Accept drops from outside the app window
if (event.getGestureSource() == null) {
model.dropLocal(event.getDragboard().getFiles());
event.setDropCompleted(true);
event.consume();
}
}); });
struc.get().setOnDragDetected(event -> { struc.get().setOnDragDetected(event -> {
if (syncDownloaded.getValue()) { var items = model.getCurrentItems();
return; var selected = items.stream()
}
var selected = syncItems.stream()
.map(item -> item.getBrowserEntry()) .map(item -> item.getBrowserEntry())
.toList(); .toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY); var files = items.stream()
var cc = BrowserClipboard.startDrag(null, selected, BrowserFileTransferMode.NORMAL);
if (cc == null) {
return;
}
var files = syncItems.stream()
.filter(item -> item.downloadFinished().get()) .filter(item -> item.downloadFinished().get())
.map(item -> { .map(item -> {
try { try {
@ -170,15 +147,20 @@ public class BrowserTransferComp extends SimpleComp {
return Optional.<File>empty(); return Optional.<File>empty();
} }
return Optional.of( return Optional.of(file.toRealPath().toFile());
file.toRealPath().toFile());
} catch (IOException e) { } catch (IOException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
}) })
.flatMap(Optional::stream) .flatMap(Optional::stream)
.toList(); .toList();
if (files.isEmpty()) {
return;
}
var cc = new ClipboardContent();
cc.putFiles(files); cc.putFiles(files);
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
db.setContent(cc); db.setContent(cc);
Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected)); Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
@ -197,8 +179,7 @@ public class BrowserTransferComp extends SimpleComp {
model.clear(false); model.clear(false);
event.consume(); event.consume();
}); });
}), });
syncDownloaded);
stack.apply(struc -> { stack.apply(struc -> {
model.getBrowserSessionModel().getDraggingFiles().addListener((observable, oldValue, newValue) -> { model.getBrowserSessionModel().getDraggingFiles().addListener((observable, oldValue, newValue) -> {

View file

@ -7,13 +7,12 @@ import io.xpipe.app.browser.file.LocalFileSystem;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.ShellTemp; import io.xpipe.app.util.ShellTemp;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableBooleanValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@ -22,56 +21,83 @@ import javafx.collections.ObservableList;
import lombok.Value; import lombok.Value;
import org.apache.commons.io.FileUtils; import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutorService; import java.util.Optional;
import java.util.concurrent.Executors;
@Value @Value
public class BrowserTransferModel { public class BrowserTransferModel {
private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("download"); private static final Path TEMP = ShellTemp.getLocalTempDataDirectory("download");
ExecutorService executor = Executors.newSingleThreadExecutor(r -> {
Thread t = Executors.defaultThreadFactory().newThread(r);
t.setDaemon(true);
t.setName("file downloader");
return t;
});
BrowserSessionModel browserSessionModel; BrowserSessionModel browserSessionModel;
ObservableList<Item> items = FXCollections.observableArrayList(); ObservableList<Item> items = FXCollections.observableArrayList();
BooleanProperty downloading = new SimpleBooleanProperty(); ObservableBooleanValue empty = Bindings.createBooleanBinding(() -> items.isEmpty(), items);
BooleanProperty allDownloaded = new SimpleBooleanProperty();
private void cleanDirectory() { public BrowserTransferModel(BrowserSessionModel browserSessionModel) {
this.browserSessionModel = browserSessionModel;
var thread = ThreadHelper.createPlatformThread("file downloader", true, () -> {
while (true) {
Optional<Item> toDownload;
synchronized (items) {
toDownload = items.stream()
.filter(item -> !item.downloadFinished().get())
.findFirst();
}
if (toDownload.isPresent()) {
downloadSingle(toDownload.get());
}
ThreadHelper.sleep(20);
}
});
thread.start();
}
public List<Item> getCurrentItems() {
synchronized (items) {
return new ArrayList<>(items);
}
}
private void cleanItem(Item item) {
if (!Files.isDirectory(TEMP)) { if (!Files.isDirectory(TEMP)) {
return; return;
} }
try (var ls = Files.list(TEMP)) { if (!Files.exists(item.getLocalFile())) {
var list = ls.toList(); return;
for (Path path : list) {
FileUtils.forceDelete(path.toFile());
} }
try {
FileUtils.forceDelete(item.getLocalFile().toFile());
} catch (IOException e) { } catch (IOException e) {
ErrorEvent.fromThrowable(e).handle(); ErrorEvent.fromThrowable(e).handle();
} }
} }
public void clear(boolean delete) { public void clear(boolean delete) {
items.clear(); List<Item> toClear;
synchronized (items) {
toClear =
items.stream().filter(item -> item.downloadFinished().get()).toList();
if (toClear.isEmpty()) {
return;
}
items.removeAll(toClear);
}
if (delete) { if (delete) {
executor.submit(() -> { for (Item item : toClear) {
cleanDirectory(); cleanItem(item);
}); }
} }
} }
public void drop(OpenFileSystemModel model, List<BrowserEntry> entries) { public void drop(OpenFileSystemModel model, List<BrowserEntry> entries) {
synchronized (items) {
entries.forEach(entry -> { entries.forEach(entry -> {
var name = entry.getFileName(); var name = entry.getFileName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) { if (items.stream().anyMatch(item -> item.getName().equals(name))) {
@ -81,40 +107,11 @@ public class BrowserTransferModel {
Path file = TEMP.resolve(name); Path file = TEMP.resolve(name);
var item = new Item(model, name, entry, file); var item = new Item(model, name, entry, file);
items.add(item); items.add(item);
allDownloaded.set(false);
}); });
} }
public void dropLocal(List<File> entries) {
if (entries.isEmpty()) {
return;
} }
var empty = items.isEmpty(); public void downloadSingle(Item item) {
try {
var paths = entries.stream().map(File::toPath).filter(Files::exists).toList();
for (Path path : paths) {
var entry = LocalFileSystem.getLocalBrowserEntry(path);
var name = entry.getFileName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
return;
}
var item = new Item(null, name, entry, path);
item.progress.setValue(BrowserTransferProgress.finished(
entry.getFileName(), entry.getRawFileEntry().getSize()));
items.add(item);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
if (empty) {
allDownloaded.set(true);
}
}
public void download() {
executor.submit(() -> {
try { try {
FileUtils.forceMkdir(TEMP.toFile()); FileUtils.forceMkdir(TEMP.toFile());
} catch (IOException e) { } catch (IOException e) {
@ -122,36 +119,58 @@ public class BrowserTransferModel {
return; return;
} }
for (Item item : new ArrayList<>(items)) {
if (item.downloadFinished().get()) { if (item.downloadFinished().get()) {
continue; return;
} }
if (item.getOpenFileSystemModel() != null if (item.getOpenFileSystemModel() != null
&& item.getOpenFileSystemModel().isClosed()) { && item.getOpenFileSystemModel().isClosed()) {
continue; return;
} }
try { try {
try (var ignored = new BooleanScope(downloading).start()) {
var op = new BrowserFileTransferOperation( var op = new BrowserFileTransferOperation(
LocalFileSystem.getLocalFileEntry(TEMP), LocalFileSystem.getLocalFileEntry(TEMP),
List.of(item.getBrowserEntry().getRawFileEntry()), List.of(item.getBrowserEntry().getRawFileEntry()),
BrowserFileTransferMode.COPY, BrowserFileTransferMode.COPY,
false, false,
progress -> { progress -> {
synchronized (item.getProgress()) {
item.getProgress().setValue(progress); item.getProgress().setValue(progress);
}
item.getOpenFileSystemModel().getProgress().setValue(progress); item.getOpenFileSystemModel().getProgress().setValue(progress);
}); });
op.execute(); op.execute();
}
} catch (Throwable t) { } catch (Throwable t) {
ErrorEvent.fromThrowable(t).handle(); ErrorEvent.fromThrowable(t).handle();
synchronized (items) {
items.remove(item); items.remove(item);
} }
} }
allDownloaded.set(true); }
});
public void transferToDownloads() throws Exception {
List<Item> toMove;
synchronized (items) {
toMove =
items.stream().filter(item -> item.downloadFinished().get()).toList();
if (toMove.isEmpty()) {
return;
}
items.removeAll(toMove);
}
var files = toMove.stream().map(item -> item.getLocalFile()).toList();
var downloads = DesktopHelper.getDownloadsDirectory();
for (Path file : files) {
var target = downloads.resolve(file.getFileName());
// Prevent DirectoryNotEmptyException
if (Files.exists(target) && Files.isDirectory(target)) {
Files.delete(target);
}
Files.move(file, target, StandardCopyOption.REPLACE_EXISTING);
}
DesktopHelper.browseFileInDirectory(downloads.resolve(files.getFirst().getFileName()));
} }
@Value @Value
@ -171,12 +190,11 @@ public class BrowserTransferModel {
} }
public ObservableBooleanValue downloadFinished() { public ObservableBooleanValue downloadFinished() {
return Bindings.createBooleanBinding( synchronized (progress) {
() -> { return Bindings.createBooleanBinding(() -> {
return progress.getValue() != null return progress.getValue() != null && progress.getValue().done();
&& progress.getValue().done(); }, progress);
}, }
progress);
} }
} }
} }

View file

@ -41,6 +41,7 @@ public class BrowserTransferProgress {
var share = (double) transferred / total; var share = (double) transferred / total;
var rest = (1.0 - share) / share; var rest = (1.0 - share) / share;
var restMillis = (long) (elapsed.toMillis() * rest); var restMillis = (long) (elapsed.toMillis() * rest);
return Duration.of(restMillis, ChronoUnit.MILLIS); var startupAdjustment = (long) (restMillis / (1.0 + Math.max(10000 - elapsed.toMillis(), 0) / 10000.0));
return Duration.of(restMillis + startupAdjustment, ChronoUnit.MILLIS);
} }
} }

View file

@ -45,7 +45,7 @@ public class BrowserWelcomeComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var state = model.getSavedState(); var state = BrowserSavedStateImpl.get();
var welcome = new BrowserGreetingComp().createSimple(); var welcome = new BrowserGreetingComp().createSimple();
@ -55,6 +55,7 @@ public class BrowserWelcomeComp extends SimpleComp {
var img = new PrettySvgComp(new SimpleStringProperty("Hips.svg"), 50, 75) var img = new PrettySvgComp(new SimpleStringProperty("Hips.svg"), 50, 75)
.padding(new Insets(5, 0, 0, 0)) .padding(new Insets(5, 0, 0, 0))
.createRegion(); .createRegion();
var hbox = new HBox(img, vbox); var hbox = new HBox(img, vbox);
hbox.setAlignment(Pos.CENTER_LEFT); hbox.setAlignment(Pos.CENTER_LEFT);
hbox.setSpacing(15); hbox.setSpacing(15);
@ -139,7 +140,6 @@ public class BrowserWelcomeComp extends SimpleComp {
.hide(empty) .hide(empty)
.accessibleTextKey("restoreAllSessions"); .accessibleTextKey("restoreAllSessions");
layout.getChildren().add(tile.createRegion()); layout.getChildren().add(tile.createRegion());
return layout; return layout;
} }
@ -149,7 +149,7 @@ public class BrowserWelcomeComp extends SimpleComp {
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore()); entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24); var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24);
return new ButtonComp( return new ButtonComp(
new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())), new SimpleStringProperty(DataStorage.get().getStoreEntryDisplayName(entry.get())),
view.createRegion(), view.createRegion(),
() -> { () -> {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
@ -160,7 +160,7 @@ public class BrowserWelcomeComp extends SimpleComp {
}); });
}) })
.minWidth(250) .minWidth(250)
.accessibleText(DataStorage.get().getStoreDisplayName(entry.get())) .accessibleText(DataStorage.get().getStoreEntryDisplayName(entry.get()))
.disable(disable) .disable(disable)
.styleClass("entry-button") .styleClass("entry-button")
.styleClass(Styles.LEFT_PILL) .styleClass(Styles.LEFT_PILL)

View file

@ -2,10 +2,40 @@ package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.util.LicenseProvider;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List; import java.util.List;
public interface BranchAction extends BrowserAction { public interface BranchAction extends BrowserAction {
List<LeafAction> getBranchingActions(OpenFileSystemModel model, List<BrowserEntry> entries); default MenuItem toMenuItem(OpenFileSystemModel model, List<BrowserEntry> selected) {
var m = new Menu(getName(model, selected).getValue() + " ...");
for (var sub : getBranchingActions(model, selected)) {
var subselected = resolveFilesIfNeeded(selected);
if (!sub.isApplicable(model, subselected)) {
continue;
}
m.getItems().add(sub.toMenuItem(model, subselected));
}
var graphic = getIcon(model, selected);
if (graphic != null) {
m.setGraphic(graphic);
}
m.setDisable(!isActive(model, selected));
if (getProFeatureId() != null
&& !LicenseProvider.get()
.getFeature(getProFeatureId())
.isSupported()) {
m.setDisable(true);
m.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
}
return m;
}
List<? extends BrowserAction> getBranchingActions(OpenFileSystemModel model, List<BrowserEntry> entries);
} }

View file

@ -7,6 +7,7 @@ import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyCombination;
import java.util.ArrayList; import java.util.ArrayList;
@ -19,13 +20,17 @@ public interface BrowserAction {
static List<LeafAction> getFlattened(OpenFileSystemModel model, List<BrowserEntry> entries) { static List<LeafAction> getFlattened(OpenFileSystemModel model, List<BrowserEntry> entries) {
return ALL.stream() return ALL.stream()
.map(browserAction -> browserAction instanceof LeafAction .map(browserAction -> getFlattened(browserAction, model, entries))
? List.of((LeafAction) browserAction)
: ((BranchAction) browserAction).getBranchingActions(model, entries))
.flatMap(List::stream) .flatMap(List::stream)
.toList(); .toList();
} }
static List<LeafAction> getFlattened(BrowserAction browserAction, OpenFileSystemModel model, List<BrowserEntry> entries) {
return browserAction instanceof LeafAction
? List.of((LeafAction) browserAction)
: ((BranchAction) browserAction).getBranchingActions(model, entries).stream().map(action -> getFlattened(action, model, entries)).flatMap(List::stream).toList();
}
static LeafAction byId(String id, OpenFileSystemModel model, List<BrowserEntry> entries) { static LeafAction byId(String id, OpenFileSystemModel model, List<BrowserEntry> entries) {
return getFlattened(model, entries).stream() return getFlattened(model, entries).stream()
.filter(browserAction -> id.equals(browserAction.getId())) .filter(browserAction -> id.equals(browserAction.getId()))
@ -33,6 +38,17 @@ public interface BrowserAction {
.orElseThrow(); .orElseThrow();
} }
default List<BrowserEntry> resolveFilesIfNeeded(List<BrowserEntry> selected) {
return automaticallyResolveLinks()
? selected.stream()
.map(browserEntry ->
new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel()))
.toList()
: selected;
}
MenuItem toMenuItem(OpenFileSystemModel model, List<BrowserEntry> selected);
default void init(OpenFileSystemModel model) throws Exception {} default void init(OpenFileSystemModel model) throws Exception {}
default String getProFeatureId() { default String getProFeatureId() {

View file

@ -80,7 +80,10 @@ public class BrowserAlerts {
private static String getSelectedElementsString(List<FileSystem.FileEntry> source) { private static String getSelectedElementsString(List<FileSystem.FileEntry> source) {
var namesHeader = AppI18n.get("selectedElements"); var namesHeader = AppI18n.get("selectedElements");
var names = namesHeader + "\n" var names = namesHeader + "\n"
+ source.stream().limit(10).map(entry -> "- " + new FilePath(entry.getPath()).getFileName()).collect(Collectors.joining("\n")); + source.stream()
.limit(10)
.map(entry -> "- " + new FilePath(entry.getPath()).getFileName())
.collect(Collectors.joining("\n"));
if (source.size() > 10) { if (source.size() > 10) {
names += "\n+ " + (source.size() - 10) + " ..."; names += "\n+ " + (source.size() - 10) + " ...";
} }

View file

@ -1,19 +1,12 @@
package io.xpipe.app.browser.file; package io.xpipe.app.browser.file;
import io.xpipe.app.browser.action.BranchAction;
import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.fs.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.util.InputHelper; import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.LicenseProvider;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
import javafx.scene.control.Menu;
import javafx.scene.control.SeparatorMenuItem; import javafx.scene.control.SeparatorMenuItem;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -30,15 +23,6 @@ public final class BrowserContextMenu extends ContextMenu {
createMenu(); createMenu();
} }
private static List<BrowserEntry> resolveIfNeeded(BrowserAction action, List<BrowserEntry> selected) {
return action.automaticallyResolveLinks()
? selected.stream()
.map(browserEntry ->
new BrowserEntry(browserEntry.getRawFileEntry().resolved(), browserEntry.getModel()))
.toList()
: selected;
}
private void createMenu() { private void createMenu() {
InputHelper.onLeft(this, false, e -> { InputHelper.onLeft(this, false, e -> {
hide(); hide();
@ -60,7 +44,7 @@ public final class BrowserContextMenu extends ContextMenu {
var all = BrowserAction.ALL.stream() var all = BrowserAction.ALL.stream()
.filter(browserAction -> browserAction.getCategory() == cat) .filter(browserAction -> browserAction.getCategory() == cat)
.filter(browserAction -> { .filter(browserAction -> {
var used = resolveIfNeeded(browserAction, selected); var used = browserAction.resolveFilesIfNeeded(selected);
if (!browserAction.isApplicable(model, used)) { if (!browserAction.isApplicable(model, used)) {
return false; return false;
} }
@ -81,36 +65,8 @@ public final class BrowserContextMenu extends ContextMenu {
} }
for (BrowserAction a : all) { for (BrowserAction a : all) {
var used = resolveIfNeeded(a, selected); var used = a.resolveFilesIfNeeded(selected);
if (a instanceof LeafAction la) { getItems().add(a.toMenuItem(model, used));
getItems().add(la.toMenuItem(model, used));
}
if (a instanceof BranchAction la) {
var m = new Menu(a.getName(model, used).getValue() + " ...");
for (LeafAction sub : la.getBranchingActions(model, used)) {
var subUsed = resolveIfNeeded(sub, selected);
if (!sub.isApplicable(model, subUsed)) {
continue;
}
m.getItems().add(sub.toMenuItem(model, subUsed));
}
var graphic = a.getIcon(model, used);
if (graphic != null) {
m.setGraphic(graphic);
}
m.setDisable(!a.isActive(model, used));
if (la.getProFeatureId() != null
&& !LicenseProvider.get()
.getFeature(la.getProFeatureId())
.isSupported()) {
m.setDisable(true);
m.setGraphic(new FontIcon("mdi2p-professional-hexagon"));
}
getItems().add(m);
}
} }
} }
} }

View file

@ -4,8 +4,6 @@ import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.comp.base.LazyTextFieldComp; import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.*; import io.xpipe.app.util.*;
@ -29,10 +27,7 @@ import javafx.scene.Node;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.control.skin.TableViewSkin; import javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.skin.VirtualFlow; import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.input.DragEvent; import javafx.scene.input.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
@ -40,11 +35,13 @@ import javafx.scene.layout.Region;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import java.time.Duration;
import java.time.Instant; import java.time.Instant;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import static io.xpipe.app.util.HumanReadableFormat.byteCount; import static io.xpipe.app.util.HumanReadableFormat.byteCount;
import static javafx.scene.control.TableColumn.SortType.ASCENDING; import static javafx.scene.control.TableColumn.SortType.ASCENDING;
@ -60,6 +57,7 @@ public final class BrowserFileListComp extends SimpleComp {
private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current"); private static final PseudoClass DRAG_INTO_CURRENT = PseudoClass.getPseudoClass("drag-into-current");
private final BrowserFileListModel fileList; private final BrowserFileListModel fileList;
private final StringProperty typedSelection = new SimpleStringProperty("");
public BrowserFileListComp(BrowserFileListModel fileList) { public BrowserFileListComp(BrowserFileListModel fileList) {
this.fileList = fileList; this.fileList = fileList;
@ -124,16 +122,80 @@ public final class BrowserFileListComp extends SimpleComp {
return true; return true;
}); });
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN); table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
table.setFixedCellSize(34.0); table.setFixedCellSize(32.0);
prepareTableSelectionModel(table); prepareTableSelectionModel(table);
prepareTableShortcuts(table); prepareTableShortcuts(table);
prepareTableEntries(table); prepareTableEntries(table);
prepareTableChanges(table, mtimeCol, modeCol); prepareTableChanges(table, mtimeCol, modeCol);
prepareTypedSelectionModel(table);
return table; return table;
} }
private void prepareTypedSelectionModel(TableView<BrowserEntry> table) {
AtomicReference<Instant> lastFail = new AtomicReference<>();
table.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
updateTypedSelection(table, lastFail, event, false);
});
table.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
typedSelection.set("");
lastFail.set(null);
});
fileList.getFileSystemModel().getCurrentPath().addListener((observable, oldValue, newValue) -> {
typedSelection.set("");
lastFail.set(null);
});
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ESCAPE) {
typedSelection.set("");
lastFail.set(null);
}
});
}
private void updateTypedSelection(TableView<BrowserEntry> table, AtomicReference<Instant> lastType, KeyEvent event, boolean recursive) {
var typed = event.getText();
if (typed.isEmpty()) {
return;
}
var updated = typedSelection.get() + typed;
var found = fileList.getShown().getValue().stream()
.filter(browserEntry ->
browserEntry.getFileName().toLowerCase().startsWith(updated.toLowerCase()))
.findFirst();
if (found.isEmpty()) {
if (typedSelection.get().isEmpty()) {
return;
}
var inCooldown = lastType.get() != null && Duration.between(lastType.get(), Instant.now()).toMillis() < 1000;
if (inCooldown) {
lastType.set(Instant.now());
event.consume();
return;
} else {
lastType.set(null);
typedSelection.set("");
table.getSelectionModel().clearSelection();
if (!recursive) {
updateTypedSelection(table, lastType, event, true);
}
return;
}
}
lastType.set(Instant.now());
typedSelection.set(updated);
table.scrollTo(found.get());
table.getSelectionModel().clearAndSelect(fileList.getShown().getValue().indexOf(found.get()));
event.consume();
}
private void prepareTableSelectionModel(TableView<BrowserEntry> table) { private void prepareTableSelectionModel(TableView<BrowserEntry> table) {
if (!fileList.getSelectionMode().isMultiple()) { if (!fileList.getSelectionMode().isMultiple()) {
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
@ -167,7 +229,7 @@ public final class BrowserFileListComp extends SimpleComp {
} }
private void prepareTableShortcuts(TableView<BrowserEntry> table) { private void prepareTableShortcuts(TableView<BrowserEntry> table) {
table.setOnKeyPressed(event -> { table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
var selected = fileList.getSelection(); var selected = fileList.getSelection();
var action = BrowserAction.getFlattened(fileList.getFileSystemModel(), selected).stream() var action = BrowserAction.getFlattened(fileList.getFileSystemModel(), selected).stream()
.filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected) .filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected)
@ -219,7 +281,6 @@ public final class BrowserFileListComp extends SimpleComp {
emptyEntry.onDragDone(event); emptyEntry.onDragDone(event);
}); });
// Don't let the list view see this event // Don't let the list view see this event
// otherwise it unselects everything as it doesn't understand shift clicks // otherwise it unselects everything as it doesn't understand shift clicks
table.addEventFilter(MouseEvent.MOUSE_CLICKED, t -> { table.addEventFilter(MouseEvent.MOUSE_CLICKED, t -> {
@ -242,38 +303,6 @@ public final class BrowserFileListComp extends SimpleComp {
return row.getItem() != null; return row.getItem() != null;
}, },
row.itemProperty())); row.itemProperty()));
new ContextMenuAugment<>(
event -> {
if (row.getItem() == null) {
return event.getButton() == MouseButton.SECONDARY;
}
if (row.getItem() != null
&& row.getItem()
.getRawFileEntry()
.resolved()
.getKind()
== FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY;
}
if (row.getItem() != null
&& row.getItem()
.getRawFileEntry()
.resolved()
.getKind()
!= FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY
|| event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2;
}
return false;
},
null,
() -> {
return new BrowserContextMenu(fileList.getFileSystemModel(), row.getItem(), false);
})
.augment(new SimpleCompStructure<>(row));
var listEntry = Bindings.createObjectBinding( var listEntry = Bindings.createObjectBinding(
() -> new BrowserFileListCompEntry(table, row, row.getItem(), fileList), row.itemProperty()); () -> new BrowserFileListCompEntry(table, row, row.getItem(), fileList), row.itemProperty());
@ -332,7 +361,6 @@ public final class BrowserFileListComp extends SimpleComp {
listEntry.get().onDragDone(event); listEntry.get().onDragDone(event);
}); });
return row; return row;
}); });
} }
@ -564,7 +592,18 @@ public final class BrowserFileListComp extends SimpleComp {
event.consume(); event.consume();
} }
}); });
InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, false, event -> { InputHelper.onExactKeyCode(tableView, KeyCode.SPACE, true, event -> {
var selection = typedSelection.get() + " ";
var found = fileList.getShown().getValue().stream()
.filter(browserEntry ->
browserEntry.getFileName().toLowerCase().startsWith(selection))
.findFirst();
// Ugly fix to prevent space from showing the menu when there is a file matching
// Due to the table view input map, these events always get sent and consumed, not allowing us to differentiate between these cases
if (found.isPresent()) {
return;
}
var selected = fileList.getSelection(); var selected = fileList.getSelection();
// Only show one menu across all selected entries // Only show one menu across all selected entries
if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) { if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) {

View file

@ -7,6 +7,7 @@ import io.xpipe.core.store.FileKind;
import javafx.geometry.Point2D; import javafx.geometry.Point2D;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.TableView; import javafx.scene.control.TableView;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.input.*; import javafx.scene.input.*;
@ -31,6 +32,7 @@ public class BrowserFileListCompEntry {
private Point2D lastOver = new Point2D(-1, -1); private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask; private TimerTask activeTask;
private ContextMenu lastContextMenu;
public BrowserFileListCompEntry( public BrowserFileListCompEntry(
TableView<BrowserEntry> tv, Node row, BrowserEntry item, BrowserFileListModel model) { TableView<BrowserEntry> tv, Node row, BrowserEntry item, BrowserFileListModel model) {
@ -41,6 +43,19 @@ public class BrowserFileListCompEntry {
} }
public void onMouseClick(MouseEvent t) { public void onMouseClick(MouseEvent t) {
if (lastContextMenu != null) {
lastContextMenu.hide();
lastContextMenu = null;
}
if (showContextMenu(t)) {
var cm = new BrowserContextMenu(model.getFileSystemModel(), item, false);
cm.show(row, t.getScreenX(), t.getScreenY());
lastContextMenu = cm;
t.consume();
return;
}
if (item == null) { if (item == null) {
// Only clear for normal clicks // Only clear for normal clicks
if (t.isStillSincePress()) { if (t.isStillSincePress()) {
@ -62,6 +77,23 @@ public class BrowserFileListCompEntry {
t.consume(); t.consume();
} }
private boolean showContextMenu(MouseEvent event) {
if (item == null) {
return event.getButton() == MouseButton.SECONDARY;
}
if (item.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY;
}
if (item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
return event.getButton() == MouseButton.SECONDARY
|| event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 2;
}
return false;
}
public void onMouseShiftClick(MouseEvent t) { public void onMouseShiftClick(MouseEvent t) {
if (t.getButton() != MouseButton.PRIMARY) { if (t.getButton() != MouseButton.PRIMARY) {
return; return;

View file

@ -99,24 +99,32 @@ public final class BrowserFileListModel {
} }
public BrowserEntry rename(BrowserEntry old, String newName) { public BrowserEntry rename(BrowserEntry old, String newName) {
if (fileSystemModel == null || fileSystemModel.isClosed() || fileSystemModel.getCurrentPath().get() == null) {
return old;
}
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), old.getFileName()); var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), old.getFileName());
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName); var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
// This check will fail on case-insensitive file systems when changing the case of the file // This check will fail on case-insensitive file systems when changing the case of the file
// So skip it in this case // So skip it in this case
var skipExistCheck = fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS && old.getFileName() var skipExistCheck =
.equalsIgnoreCase(newName); fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS
&& old.getFileName().equalsIgnoreCase(newName);
if (!skipExistCheck) { if (!skipExistCheck) {
boolean exists; boolean exists;
try { try {
exists = fileSystemModel.getFileSystem().fileExists(newFullPath) || fileSystemModel.getFileSystem().directoryExists(newFullPath); exists = fileSystemModel.getFileSystem().fileExists(newFullPath)
|| fileSystemModel.getFileSystem().directoryExists(newFullPath);
} catch (Exception e) { } catch (Exception e) {
ErrorEvent.fromThrowable(e).handle(); ErrorEvent.fromThrowable(e).handle();
return old; return old;
} }
if (exists) { if (exists) {
ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").expected().handle(); ErrorEvent.fromMessage("Target " + newFullPath + " does already exist")
.expected()
.handle();
fileSystemModel.refresh(); fileSystemModel.refresh();
return old; return old;
} }

View file

@ -7,9 +7,7 @@ import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath; import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem; import io.xpipe.core.store.FileSystem;
import java.io.IOException; import java.io.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant; import java.time.Instant;
@ -220,11 +218,37 @@ public class BrowserFileTransferOperation {
continue; continue;
} }
transfer(sourceFile, targetFile, transferred, totalSize, start);
}
}
updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
}
private void transfer(
FileSystem.FileEntry sourceFile,
String targetFile,
AtomicLong transferred,
AtomicLong totalSize,
Instant start)
throws Exception {
InputStream inputStream = null; InputStream inputStream = null;
OutputStream outputStream = null; OutputStream outputStream = null;
try { try {
var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath()); var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath());
inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath());
// Read the first few bytes to figure out possible command failure early
// before creating the output stream
inputStream = new BufferedInputStream(sourceFile.getFileSystem().openInput(sourceFile.getPath()), 1024);
inputStream.mark(1024);
var streamStart = new byte[1024];
var streamStartLength = inputStream.read(streamStart, 0, 1024);
if (streamStartLength < 1024) {
inputStream.close();
inputStream = new ByteArrayInputStream(streamStart);
} else {
inputStream.reset();
}
outputStream = target.getFileSystem().openOutput(targetFile, fileSize); outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start); transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start);
inputStream.transferTo(OutputStream.nullOutputStream()); inputStream.transferTo(OutputStream.nullOutputStream());
@ -272,9 +296,6 @@ public class BrowserFileTransferOperation {
throw exception; throw exception;
} }
} }
}
updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
}
private void deleteSingle(FileSystem.FileEntry source) throws Exception { private void deleteSingle(FileSystem.FileEntry source) throws Exception {
source.getFileSystem().delete(source.getPath()); source.getFileSystem().delete(source.getPath());

View file

@ -79,7 +79,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
getItems().addAll(r.getItems()); getItems().addAll(r.getItems());
// Prevent NPE in show() // Prevent NPE in show()
if (getScene() == null) { if (getScene() == null || anchor == null) {
return; return;
} }
show(anchor, Side.RIGHT, 0, 0); show(anchor, Side.RIGHT, 0, 0);

View file

@ -9,6 +9,7 @@ import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.file.BrowserFileListComp; import io.xpipe.app.browser.file.BrowserFileListComp;
import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
@ -84,10 +85,11 @@ public class OpenFileSystemComp extends SimpleComp {
var filter = new BrowserFilterComp(model, model.getFilter()).createStructure(); var filter = new BrowserFilterComp(model, model.getFilter()).createStructure();
var topBar = new HBox(); var topBar = new HBox();
filter.textField().prefHeightProperty().bind(topBar.heightProperty());
topBar.setAlignment(Pos.CENTER); topBar.setAlignment(Pos.CENTER);
topBar.getStyleClass().add("top-bar"); topBar.getStyleClass().add("top-bar");
var navBar = new BrowserNavBar(model).createStructure(); var navBar = new BrowserNavBar(model).createStructure();
filter.textField().prefHeightProperty().bind(navBar.get().heightProperty());
AppFont.medium(navBar.get());
topBar.getChildren() topBar.getChildren()
.setAll( .setAll(
overview, overview,
@ -117,13 +119,13 @@ public class OpenFileSystemComp extends SimpleComp {
}); });
InputHelper.onKeyCombination( InputHelper.onKeyCombination(
root, new KeyCodeCombination(KeyCode.F, KeyCombination.CONTROL_DOWN), true, keyEvent -> { root, new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> {
filter.toggleButton().fire(); filter.toggleButton().fire();
filter.textField().requestFocus(); filter.textField().requestFocus();
keyEvent.consume(); keyEvent.consume();
}); });
InputHelper.onKeyCombination( InputHelper.onKeyCombination(
root, new KeyCodeCombination(KeyCode.L, KeyCombination.CONTROL_DOWN), true, keyEvent -> { root, new KeyCodeCombination(KeyCode.L, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> {
navBar.textField().requestFocus(); navBar.textField().requestFocus();
keyEvent.consume(); keyEvent.consume();
}); });
@ -140,6 +142,14 @@ public class OpenFileSystemComp extends SimpleComp {
} }
keyEvent.consume(); keyEvent.consume();
}); });
InputHelper.onKeyCombination(
root, new KeyCodeCombination(KeyCode.BACK_SPACE), true, keyEvent -> {
var p = model.getCurrentParentDirectory();
if (p != null) {
model.cdAsync(p.getPath());
}
keyEvent.consume();
});
return root; return root;
} }

View file

@ -1,6 +1,7 @@
package io.xpipe.app.browser.fs; package io.xpipe.app.browser.fs;
import io.xpipe.app.browser.BrowserSavedState; import io.xpipe.app.browser.BrowserSavedState;
import io.xpipe.app.browser.BrowserSavedStateImpl;
import io.xpipe.app.browser.BrowserTransferProgress; import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.file.BrowserFileListModel; import io.xpipe.app.browser.file.BrowserFileListModel;
@ -8,7 +9,6 @@ import io.xpipe.app.browser.file.BrowserFileTransferMode;
import io.xpipe.app.browser.file.BrowserFileTransferOperation; import io.xpipe.app.browser.file.BrowserFileTransferOperation;
import io.xpipe.app.browser.file.FileSystemHelper; import io.xpipe.app.browser.file.FileSystemHelper;
import io.xpipe.app.browser.session.BrowserAbstractSessionModel; import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.browser.session.BrowserSessionTab; import io.xpipe.app.browser.session.BrowserSessionTab;
import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
@ -110,14 +110,13 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
return; return;
} }
var current = getCurrentDirectory();
if (DataStorage.get().getStoreEntries().contains(getEntry().get()) if (DataStorage.get().getStoreEntries().contains(getEntry().get())
&& savedState != null && savedState != null
&& getCurrentPath().get() != null) { && current != null) {
if (getBrowserModel() instanceof BrowserSessionModel bm) { savedState.cd(current.getPath(), false);
bm.getSavedState() BrowserSavedStateImpl.get()
.add(new BrowserSavedState.Entry( .add(new BrowserSavedState.Entry(getEntry().get().getUuid(), current.getPath()));
getEntry().get().getUuid(), getCurrentPath().get()));
}
} }
try { try {
fileSystem.close(); fileSystem.close();
@ -300,7 +299,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
// path = FileSystemHelper.normalizeDirectoryPath(this, path); // path = FileSystemHelper.normalizeDirectoryPath(this, path);
filter.setValue(null); filter.setValue(null);
savedState.cd(path); savedState.cd(path, true);
history.updateCurrent(path); history.updateCurrent(path);
currentPath.set(path); currentPath.set(path);
loadFilesSync(path); loadFilesSync(path);
@ -461,7 +460,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
} }
public void initWithDefaultDirectory() { public void initWithDefaultDirectory() {
savedState.cd(null); savedState.cd(null, false);
history.updateCurrent(null); history.updateCurrent(null);
} }

View file

@ -72,13 +72,15 @@ public class OpenFileSystemSavedState {
AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this); AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
} }
public void cd(String dir) { public void cd(String dir, boolean delay) {
if (dir == null) { if (dir == null) {
lastDirectory = null; lastDirectory = null;
return; return;
} }
lastDirectory = dir; lastDirectory = dir;
if (delay) {
// After 10 seconds // After 10 seconds
TIMEOUT_TIMER.schedule( TIMEOUT_TIMER.schedule(
new TimerTask() { new TimerTask() {
@ -98,6 +100,10 @@ public class OpenFileSystemSavedState {
} }
}, },
10000); 10000);
} else {
updateRecent(dir);
save();
}
} }
private void updateRecent(String dir) { private void updateRecent(String dir) {

View file

@ -17,6 +17,7 @@ public class BrowserAbstractSessionModel<T extends BrowserSessionTab<?>> {
protected final ObservableList<T> sessionEntries = FXCollections.observableArrayList(); protected final ObservableList<T> sessionEntries = FXCollections.observableArrayList();
protected final Property<T> selectedEntry = new SimpleObjectProperty<>(); protected final Property<T> selectedEntry = new SimpleObjectProperty<>();
protected final BooleanProperty busy = new SimpleBooleanProperty();
public void closeAsync(BrowserSessionTab<?> e) { public void closeAsync(BrowserSessionTab<?> e) {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {

View file

@ -67,6 +67,10 @@ public class BrowserChooserComp extends SimpleComp {
window.close(); window.close();
}); });
window.show(); window.show();
window.setOnHidden(event -> {
model.finishWithoutChoice();
event.consume();
});
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
model.openFileSystemAsync(store.get(), null, null); model.openFileSystemAsync(store.get(), null, null);
}); });

View file

@ -65,6 +65,17 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel<OpenFil
onFinish.accept(stores); onFinish.accept(stores);
} }
public void finishWithoutChoice() {
synchronized (BrowserFileChooserModel.this) {
var open = selectedEntry.getValue();
if (open != null) {
ThreadHelper.runAsync(() -> {
open.close();
});
}
}
}
public void openFileSystemAsync( public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store, DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path, FailableFunction<OpenFileSystemModel, String, Exception> path,

View file

@ -3,10 +3,13 @@ package io.xpipe.app.browser.session;
import io.xpipe.app.browser.BrowserBookmarkComp; import io.xpipe.app.browser.BrowserBookmarkComp;
import io.xpipe.app.browser.BrowserBookmarkHeaderComp; import io.xpipe.app.browser.BrowserBookmarkHeaderComp;
import io.xpipe.app.browser.BrowserTransferComp; import io.xpipe.app.browser.BrowserTransferComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.base.SideSplitPaneComp; import io.xpipe.app.comp.base.SideSplitPaneComp;
import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.AnchorComp;
import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
@ -18,6 +21,7 @@ import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleDoubleProperty;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
@ -100,10 +104,22 @@ public class BrowserSessionComp extends SimpleComp {
new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left"); new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
var split = new SimpleDoubleProperty(); var split = new SimpleDoubleProperty();
var tabs = new BrowserSessionTabsComp(model, split) var tabs = new BrowserSessionTabsComp(model, split).apply(struc -> {
.apply(struc -> struc.get().setViewOrder(1)) struc.get().setViewOrder(1);
.apply(struc -> struc.get().setPickOnBounds(false)); struc.get().setPickOnBounds(false);
var splitPane = new SideSplitPaneComp(vertical, tabs) AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setBottomAnchor(struc.get(), 0.0);
AnchorPane.setLeftAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
});
var loadingIndicator = LoadingOverlayComp.noProgress(Comp.empty(), model.getBusy())
.apply(struc -> {
AnchorPane.setTopAnchor(struc.get(), 0.0);
AnchorPane.setRightAnchor(struc.get(), 0.0);
})
.styleClass("tab-loading-indicator");
var loadingStack = new AnchorComp(List.of(tabs, loadingIndicator));
var splitPane = new SideSplitPaneComp(vertical, loadingStack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth()) .withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(d -> { .withOnDividerChange(d -> {
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d); AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);

View file

@ -23,16 +23,11 @@ import java.util.ArrayList;
@Getter @Getter
public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab<?>> { public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab<?>> {
public static final BrowserSessionModel DEFAULT = new BrowserSessionModel(BrowserSavedStateImpl.load()); public static final BrowserSessionModel DEFAULT = new BrowserSessionModel();
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this); private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final BrowserSavedState savedState;
private final Property<Boolean> draggingFiles = new SimpleBooleanProperty(); private final Property<Boolean> draggingFiles = new SimpleBooleanProperty();
public BrowserSessionModel(BrowserSavedState savedState) {
this.savedState = savedState;
}
public void restoreState(BrowserSavedState state) { public void restoreState(BrowserSavedState state) {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
var l = new ArrayList<>(state.getEntries()); var l = new ArrayList<>(state.getEntries());
@ -62,9 +57,7 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
closeSync(o); closeSync(o);
} }
if (savedState != null) { BrowserSavedStateImpl.get().save();
savedState.save();
}
} }
// Delete all files // Delete all files
@ -87,13 +80,15 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
public void openFileSystemSync( public void openFileSystemSync(
DataStoreEntryRef<? extends FileSystemStore> store, DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path, FailableFunction<OpenFileSystemModel, String, Exception> path,
BooleanProperty externalBusy) throws Exception { BooleanProperty externalBusy)
throws Exception {
if (store == null) { if (store == null) {
return; return;
} }
OpenFileSystemModel model; OpenFileSystemModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) { try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
try (var sessionBusy = new BooleanScope(busy).exclusive().start()) {
model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL); model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL);
model.init(); model.init();
// Prevent multiple calls from interfering with each other // Prevent multiple calls from interfering with each other
@ -103,6 +98,7 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
selectedEntry.setValue(model); selectedEntry.setValue(model);
} }
} }
}
if (path != null) { if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model))); model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
} else { } else {

View file

@ -22,7 +22,7 @@ public abstract class BrowserSessionTab<T extends DataStore> {
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) { public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) {
this.browserModel = browserModel; this.browserModel = browserModel;
this.entry = entry; this.entry = entry;
this.name = DataStorage.get().getStoreDisplayName(entry.get()); this.name = DataStorage.get().getStoreEntryDisplayName(entry.get());
this.tooltip = DataStorage.get().getStorePath(entry.getEntry()).toString(); this.tooltip = DataStorage.get().getStorePath(entry.getEntry()).toString();
} }

View file

@ -1,16 +1,19 @@
package io.xpipe.app.browser.session; package io.xpipe.app.browser.session;
import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.BrowserWelcomeComp; import io.xpipe.app.browser.BrowserWelcomeComp;
import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.InputHelper; import io.xpipe.app.util.ContextMenuHelper;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -20,20 +23,12 @@ import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label; import javafx.scene.control.*;
import javafx.scene.control.Tab; import javafx.scene.input.*;
import javafx.scene.control.TabPane;
import javafx.scene.input.DragEvent;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import atlantafx.base.controls.RingProgressIndicator; import java.util.*;
import atlantafx.base.theme.Styles;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import static atlantafx.base.theme.Styles.DENSE; import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass; import static atlantafx.base.theme.Styles.toggleStyleClass;
@ -50,17 +45,17 @@ public class BrowserSessionTabsComp extends SimpleComp {
} }
public Region createSimple() { public Region createSimple() {
var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of( var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
Comp.of(() -> createTabPane()), map.put(Comp.hspacer().styleClass("top-spacer"), new SimpleBooleanProperty(true));
Bindings.isNotEmpty(model.getSessionEntries()), map.put(Comp.of(() -> createTabPane()), Bindings.isNotEmpty(model.getSessionEntries()));
map.put(
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)), new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
Bindings.createBooleanBinding( Bindings.createBooleanBinding(
() -> { () -> {
return model.getSessionEntries().size() == 0; return model.getSessionEntries().size() == 0;
}, },
model.getSessionEntries()), model.getSessionEntries()));
Comp.hspacer().styleClass("top-spacer"), var multi = new MultiContentComp(map);
new SimpleBooleanProperty(true)));
multi.apply(struc -> ((StackPane) struc.get()).setAlignment(Pos.TOP_CENTER)); multi.apply(struc -> ((StackPane) struc.get()).setAlignment(Pos.TOP_CENTER));
return multi.createRegion(); return multi.createRegion();
} }
@ -198,28 +193,132 @@ public class BrowserSessionTabsComp extends SimpleComp {
} }
}); });
InputHelper.onInput(tabs, true, keyEvent -> { tabs.addEventHandler(KeyEvent.KEY_PRESSED, keyEvent -> {
var current = tabs.getSelectionModel().getSelectedItem(); var current = tabs.getSelectionModel().getSelectedItem();
if (current == null) { if (current == null) {
return; return;
} }
if (keyEvent.getCode() == KeyCode.W && keyEvent.isShortcutDown()) { if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(keyEvent)) {
tabs.getTabs().remove(current); tabs.getTabs().remove(current);
keyEvent.consume(); keyEvent.consume();
return;
}
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN).match(keyEvent)) {
tabs.getTabs().clear();
keyEvent.consume();
} }
if (keyEvent.getCode() == KeyCode.W && keyEvent.isShortcutDown() && keyEvent.isShiftDown()) { if (keyEvent.getCode().isFunctionKey()) {
tabs.getTabs().clear(); var start = KeyCode.F1.getCode();
var index = keyEvent.getCode().getCode() - start;
if (index < tabs.getTabs().size()) {
tabs.getSelectionModel().select(index);
keyEvent.consume(); keyEvent.consume();
return;
}
}
var forward = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN);
if (forward.match(keyEvent)) {
var next = (tabs.getSelectionModel().getSelectedIndex() + 1)
% tabs.getTabs().size();
tabs.getSelectionModel().select(next);
keyEvent.consume();
return;
}
var back = new KeyCodeCombination(KeyCode.TAB, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN);
if (back.match(keyEvent)) {
var previous = (tabs.getTabs().size() + tabs.getSelectionModel().getSelectedIndex() - 1)
% tabs.getTabs().size();
tabs.getSelectionModel().select(previous);
keyEvent.consume();
return;
} }
}); });
return tabs; return tabs;
} }
private ContextMenu createContextMenu(TabPane tabs, Tab tab) {
var cm = ContextMenuHelper.create();
var select = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("selectTab"));
select.acceleratorProperty()
.bind(Bindings.createObjectBinding(
() -> {
var start = KeyCode.F1.getCode();
var index = tabs.getTabs().indexOf(tab);
var keyCode = Arrays.stream(KeyCode.values())
.filter(code -> code.getCode() == start + index)
.findAny()
.orElse(null);
return keyCode != null ? new KeyCodeCombination(keyCode) : null;
},
tabs.getTabs()));
select.setOnAction(event -> {
tabs.getSelectionModel().select(tab);
event.consume();
});
cm.getItems().add(select);
cm.getItems().add(new SeparatorMenuItem());
var close = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeTab"));
close.setAccelerator(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN));
close.setOnAction(event -> {
tabs.getTabs().remove(tab);
event.consume();
});
cm.getItems().add(close);
var closeOthers = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeOtherTabs"));
closeOthers.setOnAction(event -> {
tabs.getTabs()
.removeAll(tabs.getTabs().stream().filter(t -> t != tab).toList());
event.consume();
});
cm.getItems().add(closeOthers);
var closeLeft = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeLeftTabs"));
closeLeft.setOnAction(event -> {
var index = tabs.getTabs().indexOf(tab);
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
.filter(t -> tabs.getTabs().indexOf(t) < index)
.toList());
event.consume();
});
cm.getItems().add(closeLeft);
var closeRight = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeRightTabs"));
closeRight.setOnAction(event -> {
var index = tabs.getTabs().indexOf(tab);
tabs.getTabs()
.removeAll(tabs.getTabs().stream()
.filter(t -> tabs.getTabs().indexOf(t) > index)
.toList());
event.consume();
});
cm.getItems().add(closeRight);
var closeAll = ContextMenuHelper.item(LabelGraphic.none(), AppI18n.get("closeAllTabs"));
closeAll.setAccelerator(
new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN, KeyCombination.SHIFT_DOWN));
closeAll.setOnAction(event -> {
tabs.getTabs().clear();
event.consume();
});
cm.getItems().add(closeAll);
return cm;
}
private Tab createTab(TabPane tabs, BrowserSessionTab<?> model) { private Tab createTab(TabPane tabs, BrowserSessionTab<?> model) {
var tab = new Tab(); var tab = new Tab();
tab.setContextMenu(createContextMenu(tabs, tab));
var ring = new RingProgressIndicator(0, false); var ring = new RingProgressIndicator(0, false);
ring.setMinSize(16, 16); ring.setMinSize(16, 16);

View file

@ -13,10 +13,9 @@ import io.xpipe.app.storage.DataStorage;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.Parent;
import javafx.scene.control.ButtonBase; import javafx.scene.control.ButtonBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
@ -63,28 +62,11 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
sidebarR.getChildrenUnmodifiable().forEach(node -> { sidebarR.getChildrenUnmodifiable().forEach(node -> {
var shortcut = (KeyCodeCombination) node.getProperties().get("shortcut"); var shortcut = (KeyCodeCombination) node.getProperties().get("shortcut");
if (shortcut != null && shortcut.match(event)) { if (shortcut != null && shortcut.match(event)) {
((ButtonBase) node).fire(); ((ButtonBase) ((Parent) node).getChildrenUnmodifiable().get(1)).fire();
event.consume(); event.consume();
return; return;
} }
}); });
if (event.isConsumed()) {
return;
}
var forward = new KeyCodeCombination(KeyCode.TAB, KeyCombination.CONTROL_DOWN);
if (forward.match(event)) {
var next = (model.getEntries().indexOf(model.getSelected().getValue()) + 1) % 3;
model.getSelected().setValue(model.getEntries().get(next));
return;
}
var back = new KeyCodeCombination(KeyCode.TAB, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN);
if (back.match(event)) {
var next = (model.getEntries().indexOf(model.getSelected().getValue()) + 2) % 3;
model.getSelected().setValue(model.getEntries().get(next));
return;
}
}); });
AppFont.normal(pane); AppFont.normal(pane);
pane.getStyleClass().add("layout"); pane.getStyleClass().add("layout");

View file

@ -70,7 +70,9 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
.bind(Bindings.createDoubleBinding( .bind(Bindings.createDoubleBinding(
() -> { () -> {
var v = bar.getVisibleAmount(); var v = bar.getVisibleAmount();
return v < 1.0 ? 1.0 : 0.0; // 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())); bar.visibleAmountProperty()));
} }

View file

@ -88,6 +88,7 @@ public class ListSelectorComp<T> extends SimpleComp {
var sp = new ScrollPane(vbox); var sp = new ScrollPane(vbox);
sp.setFitToWidth(true); sp.setFitToWidth(true);
sp.getStyleClass().add("list-selector-comp");
return sp; return sp;
} }
} }

View file

@ -22,6 +22,7 @@ import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView; import javafx.scene.web.WebView;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.apache.commons.io.FileUtils;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -63,7 +64,7 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
var html = MarkdownHelper.toHtml(markdown, s -> s, htmlTransformation, null); var html = MarkdownHelper.toHtml(markdown, s -> s, htmlTransformation, null);
try { try {
// Workaround for https://bugs.openjdk.org/browse/JDK-8199014 // Workaround for https://bugs.openjdk.org/browse/JDK-8199014
Files.createDirectories(file.getParent()); FileUtils.forceMkdir(file.getParent().toFile());
Files.writeString(file, html); Files.writeString(file, html);
return file; return file;
} catch (IOException e) { } catch (IOException e) {

View file

@ -11,6 +11,7 @@ import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.update.UpdateAvailableAlert; import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.Property; import javafx.beans.property.Property;
@ -70,12 +71,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
value.setValue(e); value.setValue(e);
}); });
var shortcut = e.combination(); var shortcut = e.combination();
if (shortcut != null) {
b.apply(struc -> struc.get().getProperties().put("shortcut", shortcut));
}
b.apply(new TooltipAugment<>(e.name(), shortcut)); b.apply(new TooltipAugment<>(e.name(), shortcut));
b.apply(struc -> { b.apply(struc -> {
AppFont.setSize(struc.get(), 2); AppFont.setSize(struc.get(), 1);
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e)); struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
value.addListener((c, o, n) -> { value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
@ -86,7 +84,8 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
b.accessibleText(e.name()); b.accessibleText(e.name());
var indicator = Comp.empty().styleClass("indicator"); var indicator = Comp.empty().styleClass("indicator");
var stack = new StackComp(List.of(indicator, b)).apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT)); var stack = new StackComp(List.of(indicator, b))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
stack.apply(struc -> { stack.apply(struc -> {
var indicatorRegion = (Region) struc.get().getChildren().getFirst(); var indicatorRegion = (Region) struc.get().getChildren().getFirst();
indicatorRegion.setMaxWidth(7); indicatorRegion.setMaxWidth(7);
@ -110,6 +109,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
selectedBorder, selectedBorder,
noneBorder)); noneBorder));
}); });
if (shortcut != null) {
stack.apply(struc -> struc.get().getProperties().put("shortcut", shortcut));
}
vbox.getChildren().add(stack.createRegion()); vbox.getChildren().add(stack.createRegion());
} }
@ -118,7 +120,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
.tooltipKey("updateAvailableTooltip") .tooltipKey("updateAvailableTooltip")
.accessibleTextKey("updateAvailableTooltip"); .accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> { b.apply(struc -> {
AppFont.setSize(struc.get(), 2); AppFont.setSize(struc.get(), 1);
}); });
b.hide(PlatformThread.sync(Bindings.createBooleanBinding( b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> { () -> {

View file

@ -95,7 +95,7 @@ public class StoreToggleComp extends SimpleComp {
v -> { v -> {
Platform.runLater(() -> { Platform.runLater(() -> {
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v); setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);
StoreViewState.get().toggleStoreListUpdate(); StoreViewState.get().triggerStoreListUpdate();
}); });
}); });
t.tooltipKey("showAllChildren"); t.tooltipKey("showAllChildren");

View file

@ -33,16 +33,27 @@ public class SystemStateComp extends SimpleComp {
PlatformThread.runLaterIfNeeded(() -> fi.setIconLiteral(i)); PlatformThread.runLaterIfNeeded(() -> fi.setIconLiteral(i));
}); });
var border = new FontIcon("mdi2c-circle-outline"); var border = new FontIcon("mdi2s-square-rounded-outline");
border.getStyleClass().add("outer-icon"); border.getStyleClass().add("outer-icon");
border.setOpacity(0.5); border.setOpacity(0.3);
var success = Styles.toDataURI( var success = Styles.toDataURI(
".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }"); """
.stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-success-emphasis; }
"""
);
var failure = var failure =
Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }"); Styles.toDataURI(
"""
.stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-danger-emphasis; }
"""
);
var other = var other =
Styles.toDataURI(".stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }"); Styles.toDataURI(
"""
.stacked-ikonli-font-icon > .outer-icon { -fx-icon-color: -color-accent-emphasis; }
"""
);
var pane = new StackedFontIcon(); var pane = new StackedFontIcon();
pane.getChildren().addAll(fi, border); pane.getChildren().addAll(fi, border);
@ -51,7 +62,7 @@ public class SystemStateComp extends SimpleComp {
var dataClass1 = var dataClass1 =
""" """
.stacked-ikonli-font-icon > .outer-icon { .stacked-ikonli-font-icon > .outer-icon {
-fx-icon-size: 22px; -fx-icon-size: 26px;
} }
.stacked-ikonli-font-icon > .inner-icon { .stacked-ikonli-font-icon > .inner-icon {
-fx-icon-size: 12px; -fx-icon-size: 12px;

View file

@ -1,10 +1,8 @@
package io.xpipe.app.comp.store; package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.HPos; import javafx.geometry.HPos;
@ -17,8 +15,8 @@ public class DenseStoreEntryComp extends StoreEntryComp {
private final boolean showIcon; private final boolean showIcon;
public DenseStoreEntryComp(StoreEntryWrapper entry, boolean showIcon, Comp<?> content) { public DenseStoreEntryComp(StoreSection section, boolean showIcon, Comp<?> content) {
super(entry, content); super(section, content);
this.showIcon = showIcon; this.showIcon = showIcon;
} }
@ -26,24 +24,27 @@ public class DenseStoreEntryComp extends StoreEntryComp {
var information = new Label(); var information = new Label();
information.setGraphicTextGap(7); information.setGraphicTextGap(7);
information.getStyleClass().add("information"); information.getStyleClass().add("information");
AppFont.header(information);
var state = wrapper.getEntry().getProvider() != null var state = getWrapper().getEntry().getProvider() != null
? wrapper.getEntry().getProvider().stateDisplay(wrapper) ? getWrapper().getEntry().getProvider().stateDisplay(getWrapper())
: Comp.empty(); : Comp.empty();
information.setGraphic(state.createRegion()); information.setGraphic(state.createRegion());
var info = wrapper.getEntry().getProvider() != null ? wrapper.getEntry().getProvider().informationString(wrapper) : new SimpleStringProperty(); var info = getWrapper().getEntry().getProvider() != null
var summary = wrapper.getSummary(); ? getWrapper().getEntry().getProvider().informationString(section)
if (wrapper.getEntry().getProvider() != null) { : new SimpleStringProperty();
var summary = getWrapper().getSummary();
if (getWrapper().getEntry().getProvider() != null) {
information information
.textProperty() .textProperty()
.bind(PlatformThread.sync(Bindings.createStringBinding( .bind(PlatformThread.sync(Bindings.createStringBinding(
() -> { () -> {
var val = summary.getValue(); var val = summary.getValue();
if (val != null var p = getWrapper().getEntry().getProvider();
&& grid.isHover() if (val != null && grid.isHover()
&& wrapper.getEntry().getProvider().alwaysShowSummary()) { && p.alwaysShowSummary()) {
return val;
} else if (info.getValue() == null && p.alwaysShowSummary()){
return val; return val;
} else { } else {
return info.getValue(); return info.getValue();
@ -73,11 +74,11 @@ public class DenseStoreEntryComp extends StoreEntryComp {
return grid.getWidth() / 2.5; return grid.getWidth() / 2.5;
}, },
grid.widthProperty())); grid.widthProperty()));
var notes = new StoreNotesComp(wrapper).createRegion(); var notes = new StoreNotesComp(getWrapper()).createRegion();
if (showIcon) { if (showIcon) {
var storeIcon = createIcon(30, 24); var storeIcon = createIcon(28, 24);
grid.getColumnConstraints().add(new ColumnConstraints(46)); grid.getColumnConstraints().add(new ColumnConstraints(38));
grid.add(storeIcon, 0, 0); grid.add(storeIcon, 0, 0);
GridPane.setHalignment(storeIcon, HPos.CENTER); GridPane.setHalignment(storeIcon, HPos.CENTER);
} }
@ -95,7 +96,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
nameCC.setHgrow(Priority.ALWAYS); nameCC.setHgrow(Priority.ALWAYS);
grid.getColumnConstraints().addAll(nameCC); grid.getColumnConstraints().addAll(nameCC);
var nameBox = new HBox(name, notes); var nameBox = new HBox(name, notes);
nameBox.setSpacing(1); nameBox.setSpacing(6);
nameBox.setAlignment(Pos.CENTER_LEFT); nameBox.setAlignment(Pos.CENTER_LEFT);
grid.addRow(0, nameBox); grid.addRow(0, nameBox);

View file

@ -1,16 +1,19 @@
package io.xpipe.app.comp.store; package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.core.process.OsType;
import javafx.geometry.HPos; import javafx.geometry.HPos;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.*; import javafx.scene.layout.*;
public class StandardStoreEntryComp extends StoreEntryComp { public class StandardStoreEntryComp extends StoreEntryComp {
public StandardStoreEntryComp(StoreEntryWrapper entry, Comp<?> content) { public StandardStoreEntryComp(StoreSection section, Comp<?> content) {
super(entry, content); super(section, content);
} }
@Override @Override
@ -18,23 +21,37 @@ public class StandardStoreEntryComp extends StoreEntryComp {
return true; return true;
} }
private Label createSummary() {
var summary = new Label();
summary.textProperty().bind(getWrapper().getSummary());
summary.getStyleClass().add("summary");
AppFont.small(summary);
return summary;
}
protected Region createContent() { protected Region createContent() {
var name = createName().createRegion(); var name = createName().createRegion();
var notes = new StoreNotesComp(wrapper).createRegion(); var notes = new StoreNotesComp(getWrapper()).createRegion();
var grid = new GridPane(); var grid = new GridPane();
grid.setHgap(7); grid.setHgap(6);
grid.setVgap(0); grid.setVgap(OsType.getLocal() == OsType.MACOS ? 2 : 0);
var storeIcon = createIcon(50, 40); var storeIcon = createIcon(46, 40);
grid.add(storeIcon, 0, 0, 1, 2); grid.add(storeIcon, 0, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(66)); grid.getColumnConstraints().add(new ColumnConstraints(56));
var nameAndNotes = new HBox(name, notes); var nameAndNotes = new HBox(name, notes);
nameAndNotes.setSpacing(1); nameAndNotes.setSpacing(6);
nameAndNotes.setAlignment(Pos.CENTER_LEFT); nameAndNotes.setAlignment(Pos.CENTER_LEFT);
grid.add(nameAndNotes, 1, 0); grid.add(nameAndNotes, 1, 0);
grid.add(createSummary(), 1, 1); GridPane.setVgrow(nameAndNotes, Priority.ALWAYS);
var summaryBox = new HBox(createSummary());
summaryBox.setAlignment(Pos.TOP_LEFT);
GridPane.setVgrow(summaryBox, Priority.ALWAYS);
grid.add(summaryBox, 1, 1);
var nameCC = new ColumnConstraints(); var nameCC = new ColumnConstraints();
nameCC.setMinWidth(100); nameCC.setMinWidth(100);
nameCC.setHgrow(Priority.ALWAYS); nameCC.setHgrow(Priority.ALWAYS);

View file

@ -112,6 +112,11 @@ public class StoreCategoryWrapper {
} }
public void update() { public void update() {
// We are probably in shutdown then
if (StoreViewState.get() == null) {
return;
}
// Avoid reupdating name when changed from the name property! // Avoid reupdating name when changed from the name property!
var catName = translatedName(category.getName()); var catName = translatedName(category.getName());
if (!catName.equals(name.getValue())) { if (!catName.equals(name.getValue())) {

View file

@ -1,5 +1,6 @@
package io.xpipe.app.comp.store; package io.xpipe.app.comp.store;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.DialogComp; import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.ErrorOverlayComp; import io.xpipe.app.comp.base.ErrorOverlayComp;
@ -21,7 +22,6 @@ import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.*; import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.ValidationException; import io.xpipe.core.util.ValidationException;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
@ -33,8 +33,6 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.stage.Stage; import javafx.stage.Stage;
import atlantafx.base.controls.Spacer;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import net.synedra.validatorfx.GraphicDecorationStackPane; import net.synedra.validatorfx.GraphicDecorationStackPane;
@ -51,7 +49,7 @@ public class StoreCreationComp extends DialogComp {
Stage window; Stage window;
BiConsumer<DataStoreEntry, Boolean> consumer; BiConsumer<DataStoreEntry, Boolean> consumer;
Property<DataStoreProvider> provider; Property<DataStoreProvider> provider;
Property<DataStore> store; ObjectProperty<DataStore> store;
Predicate<DataStoreProvider> filter; Predicate<DataStoreProvider> filter;
BooleanProperty busy = new SimpleBooleanProperty(); BooleanProperty busy = new SimpleBooleanProperty();
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator()); Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
@ -60,6 +58,7 @@ public class StoreCreationComp extends DialogComp {
ObservableValue<DataStoreEntry> entry; ObservableValue<DataStoreEntry> entry;
BooleanProperty changedSinceError = new SimpleBooleanProperty(); BooleanProperty changedSinceError = new SimpleBooleanProperty();
BooleanProperty skippable = new SimpleBooleanProperty(); BooleanProperty skippable = new SimpleBooleanProperty();
BooleanProperty connectable = new SimpleBooleanProperty();
StringProperty name; StringProperty name;
DataStoreEntry existingEntry; DataStoreEntry existingEntry;
boolean staticDisplay; boolean staticDisplay;
@ -68,7 +67,7 @@ public class StoreCreationComp extends DialogComp {
Stage window, Stage window,
BiConsumer<DataStoreEntry, Boolean> consumer, BiConsumer<DataStoreEntry, Boolean> consumer,
Property<DataStoreProvider> provider, Property<DataStoreProvider> provider,
Property<DataStore> store, ObjectProperty<DataStore> store,
Predicate<DataStoreProvider> filter, Predicate<DataStoreProvider> filter,
String initialName, String initialName,
DataStoreEntry existingEntry, DataStoreEntry existingEntry,
@ -96,6 +95,12 @@ public class StoreCreationComp extends DialogComp {
} }
}); });
this.provider.subscribe((n) -> {
if (n != null) {
connectable.setValue(n.canConnectDuringCreation());
}
});
this.apply(r -> { this.apply(r -> {
r.get().setPrefWidth(650); r.get().setPrefWidth(650);
r.get().setPrefHeight(750); r.get().setPrefHeight(750);
@ -162,9 +167,14 @@ public class StoreCreationComp extends DialogComp {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
if (!DataStorage.get().getStoreEntries().contains(e)) { if (!DataStorage.get().getStoreEntries().contains(e)) {
DataStorage.get().addStoreEntryIfNotPresent(newE); DataStorage.get().addStoreEntryIfNotPresent(newE);
} else {
// We didn't change anything
if (e.getStore().equals(newE.getStore())) {
e.setName(newE.getName());
} else { } else {
DataStorage.get().updateEntry(e, newE); DataStorage.get().updateEntry(e, newE);
} }
}
}); });
}, },
true, true,
@ -239,7 +249,16 @@ public class StoreCreationComp extends DialogComp {
finish(); finish();
} }
}) })
.visible(skippable)); .visible(skippable),
new ButtonComp(AppI18n.observable("connect"), null, () -> {
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))));
} }
@Override @Override
@ -393,11 +412,10 @@ public class StoreCreationComp extends DialogComp {
private Region createLayout() { private Region createLayout() {
var layout = new BorderPane(); var layout = new BorderPane();
layout.getStyleClass().add("store-creator"); layout.getStyleClass().add("store-creator");
layout.setPadding(new Insets(20));
var providerChoice = new StoreProviderChoiceComp(filter, provider, staticDisplay); var providerChoice = new StoreProviderChoiceComp(filter, provider, staticDisplay);
if (staticDisplay) { var showProviders = (!staticDisplay && (providerChoice.getProviders().size() > 1 || providerChoice.getProviders().getFirst().showProviderChoice())) ||
providerChoice.apply(struc -> struc.get().setDisable(true)); (staticDisplay && provider.getValue().showProviderChoice());
} else { if (showProviders) {
providerChoice.onSceneAssign(struc -> struc.get().requestFocus()); providerChoice.onSceneAssign(struc -> struc.get().requestFocus());
} }
providerChoice.apply(GrowAugment.create(true, false)); providerChoice.apply(GrowAugment.create(true, false));
@ -422,9 +440,14 @@ public class StoreCreationComp extends DialogComp {
var sep = new Separator(); var sep = new Separator();
sep.getStyleClass().add("spacer"); sep.getStyleClass().add("spacer");
var top = new VBox(providerChoice.createRegion(), new Spacer(7, Orientation.VERTICAL), sep); var top = new VBox(providerChoice.createRegion(), new Spacer(5, Orientation.VERTICAL), sep);
top.getStyleClass().add("top"); top.getStyleClass().add("top");
if (showProviders) {
layout.setTop(top); layout.setTop(top);
layout.setPadding(new Insets(15, 20, 20, 20));
} else {
layout.setPadding(new Insets(5, 20, 20, 20));
}
var valSp = new GraphicDecorationStackPane(); var valSp = new GraphicDecorationStackPane();
valSp.getChildren().add(layout); valSp.getChildren().add(layout);

View file

@ -40,8 +40,9 @@ public class StoreCreationMenu {
menu.getItems() menu.getItems()
.add(category("addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, null)); .add(category("addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, null));
menu.getItems() menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, null));
.add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, "cmd"));
menu.getItems().add(category("addSerial", "mdi2s-serial-port", DataStoreCreationCategory.SERIAL, "serial"));
menu.getItems().add(category("addDatabase", "mdi2d-database-plus", DataStoreCreationCategory.DATABASE, null)); menu.getItems().add(category("addDatabase", "mdi2d-database-plus", DataStoreCreationCategory.DATABASE, null));
} }

View file

@ -14,6 +14,7 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.TooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.DerivedObservableList;
import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
@ -23,6 +24,7 @@ import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.*; import io.xpipe.app.util.*;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableDoubleValue; import javafx.beans.value.ObservableDoubleValue;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
@ -51,21 +53,25 @@ public abstract class StoreEntryComp extends SimpleComp {
App.getApp().getStage().widthProperty().divide(2.1).add(-100); App.getApp().getStage().widthProperty().divide(2.1).add(-100);
public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH = public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH =
App.getApp().getStage().widthProperty().divide(2.1).add(-200); App.getApp().getStage().widthProperty().divide(2.1).add(-200);
protected final StoreEntryWrapper wrapper; protected final StoreSection section;
protected final Comp<?> content; protected final Comp<?> content;
public StoreEntryComp(StoreEntryWrapper wrapper, Comp<?> content) { public StoreEntryComp(StoreSection section, Comp<?> content) {
this.wrapper = wrapper; this.section = section;
this.content = content; this.content = content;
} }
public static StoreEntryComp create(StoreEntryWrapper entry, Comp<?> content, boolean preferLarge) { public StoreEntryWrapper getWrapper() {
return section.getWrapper();
}
public static StoreEntryComp create(StoreSection section, Comp<?> content, boolean preferLarge) {
var forceCondensed = AppPrefs.get() != null var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get(); && AppPrefs.get().condenseConnectionDisplay().get();
if (!preferLarge || forceCondensed) { if (!preferLarge || forceCondensed) {
return new DenseStoreEntryComp(entry, true, content); return new DenseStoreEntryComp(section, true, content);
} else { } else {
return new StandardStoreEntryComp(entry, content); return new StandardStoreEntryComp(section, content);
} }
} }
@ -76,9 +82,7 @@ public abstract class StoreEntryComp extends SimpleComp {
} else { } else {
var forceCondensed = AppPrefs.get() != null var forceCondensed = AppPrefs.get() != null
&& AppPrefs.get().condenseConnectionDisplay().get(); && AppPrefs.get().condenseConnectionDisplay().get();
return forceCondensed return forceCondensed ? new DenseStoreEntryComp(e, true, null) : new StandardStoreEntryComp(e, null);
? new DenseStoreEntryComp(e.getWrapper(), true, null)
: new StandardStoreEntryComp(e.getWrapper(), null);
} }
} }
@ -95,16 +99,16 @@ public abstract class StoreEntryComp extends SimpleComp {
button.setPadding(Insets.EMPTY); button.setPadding(Insets.EMPTY);
button.setMaxWidth(5000); button.setMaxWidth(5000);
button.setFocusTraversable(true); button.setFocusTraversable(true);
button.accessibleTextProperty().bind(wrapper.nameProperty()); button.accessibleTextProperty().bind(getWrapper().nameProperty());
button.setOnAction(event -> { button.setOnAction(event -> {
event.consume(); event.consume();
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
wrapper.executeDefaultAction(); getWrapper().executeDefaultAction();
}); });
}); });
button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> { button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
if (AppPrefs.get().requireDoubleClickForConnections().get()) { if (AppPrefs.get().requireDoubleClickForConnections().get()) {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() > 2) { if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
event.consume(); event.consume();
} }
} else { } else {
@ -115,7 +119,7 @@ public abstract class StoreEntryComp extends SimpleComp {
}); });
button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> { button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (AppPrefs.get().requireDoubleClickForConnections().get()) { if (AppPrefs.get().requireDoubleClickForConnections().get()) {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() > 2) { if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
event.consume(); event.consume();
} }
} else { } else {
@ -132,9 +136,12 @@ public abstract class StoreEntryComp extends SimpleComp {
var loading = LoadingOverlayComp.noProgress( var loading = LoadingOverlayComp.noProgress(
Comp.of(() -> button), Comp.of(() -> button),
wrapper.getEntry().getValidity().isUsable() getWrapper().getEntry().getValidity().isUsable()
? wrapper.getBusy().or(wrapper.getEntry().getProvider().busy(wrapper)) ? getWrapper()
: wrapper.getBusy()); .getBusy()
.or(getWrapper().getEntry().getProvider().busy(getWrapper()))
: getWrapper().getBusy());
AppFont.normal(button);
return loading.createRegion(); return loading.createRegion();
} }
@ -146,31 +153,22 @@ public abstract class StoreEntryComp extends SimpleComp {
information information
.textProperty() .textProperty()
.bind( .bind(
wrapper.getEntry().getProvider() != null getWrapper().getEntry().getProvider() != null
? PlatformThread.sync( ? PlatformThread.sync(
wrapper.getEntry().getProvider().informationString(wrapper)) getWrapper().getEntry().getProvider().informationString(section))
: new SimpleStringProperty()); : new SimpleStringProperty());
information.getStyleClass().add("information"); information.getStyleClass().add("information");
AppFont.header(information);
var state = wrapper.getEntry().getProvider() != null var state = getWrapper().getEntry().getProvider() != null
? wrapper.getEntry().getProvider().stateDisplay(wrapper) ? getWrapper().getEntry().getProvider().stateDisplay(getWrapper())
: Comp.empty(); : Comp.empty();
information.setGraphic(state.createRegion()); information.setGraphic(state.createRegion());
return information; return information;
} }
protected Label createSummary() {
var summary = new Label();
summary.textProperty().bind(wrapper.getSummary());
summary.getStyleClass().add("summary");
AppFont.small(summary);
return summary;
}
protected void applyState(Node node) { protected void applyState(Node node) {
PlatformThread.sync(wrapper.getValidity()).subscribe(val -> { PlatformThread.sync(getWrapper().getValidity()).subscribe(val -> {
switch (val) { switch (val) {
case LOAD_FAILED -> { case LOAD_FAILED -> {
node.pseudoClassStateChanged(FAILED, true); node.pseudoClassStateChanged(FAILED, true);
@ -189,24 +187,23 @@ public abstract class StoreEntryComp extends SimpleComp {
} }
protected Comp<?> createName() { protected Comp<?> createName() {
LabelComp name = new LabelComp(wrapper.nameProperty()); LabelComp name = new LabelComp(getWrapper().nameProperty());
name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS)) name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS));
.apply(struc -> struc.get().setPadding(new Insets(5, 5, 5, 0)));
name.apply(s -> AppFont.header(s.get()));
name.styleClass("name"); name.styleClass("name");
return name; return name;
} }
protected Node createIcon(int w, int h) { protected Node createIcon(int w, int h) {
var img = wrapper.disabledProperty().get() var img = getWrapper().disabledProperty().get()
? "disabled_icon.png" ? "disabled_icon.png"
: wrapper.getEntry() : getWrapper()
.getEntry()
.getProvider() .getProvider()
.getDisplayIconFileName(wrapper.getEntry().getStore()); .getDisplayIconFileName(getWrapper().getEntry().getStore());
var imageComp = PrettyImageHelper.ofFixedSize(img, w, h); var imageComp = PrettyImageHelper.ofFixedSize(img, w, h);
var storeIcon = imageComp.createRegion(); var storeIcon = imageComp.createRegion();
if (wrapper.getValidity().getValue().isUsable()) { if (getWrapper().getValidity().getValue().isUsable()) {
new TooltipAugment<>(wrapper.getEntry().getProvider().displayName(), null).augment(storeIcon); new TooltipAugment<>(getWrapper().getEntry().getProvider().displayName(), null).augment(storeIcon);
} }
var stack = new StackPane(storeIcon); var stack = new StackPane(storeIcon);
@ -220,7 +217,7 @@ public abstract class StoreEntryComp extends SimpleComp {
} }
protected Region createButtonBar() { protected Region createButtonBar() {
var list = new DerivedObservableList<>(wrapper.getActionProviders(), false); var list = new DerivedObservableList<>(getWrapper().getActionProviders(), false);
var buttons = list.mapped(actionProvider -> { var buttons = list.mapped(actionProvider -> {
var button = buildButton(actionProvider); var button = buildButton(actionProvider);
return button != null ? button.createRegion() : null; return button != null ? button.createRegion() : null;
@ -239,8 +236,8 @@ public abstract class StoreEntryComp extends SimpleComp {
buttons.subscribe(update); buttons.subscribe(update);
update.run(); update.run();
ig.setAlignment(Pos.CENTER_RIGHT); ig.setAlignment(Pos.CENTER_RIGHT);
ig.setPadding(new Insets(5));
ig.getStyleClass().add("button-bar"); ig.getStyleClass().add("button-bar");
AppFont.medium(ig);
return ig; return ig;
} }
@ -249,17 +246,20 @@ public abstract class StoreEntryComp extends SimpleComp {
var branch = p.getBranchDataStoreCallSite(); var branch = p.getBranchDataStoreCallSite();
var cs = leaf != null ? leaf : branch; var cs = leaf != null ? leaf : branch;
if (cs == null || !cs.isMajor(wrapper.getEntry().ref())) { if (cs == null || !cs.isMajor(getWrapper().getEntry().ref())) {
return null; return null;
} }
var icon = new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(cs.getIcon(getWrapper().getEntry().ref())));
var button = new IconButtonComp( var button = new IconButtonComp(
cs.getIcon(wrapper.getEntry().ref()), icon,
leaf != null leaf != null
? () -> { ? () -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
wrapper.runAction( getWrapper().runAction(
leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy()); leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy());
// Update icon in case action changed it
icon.set(new LabelGraphic.IconGraphic(cs.getIcon(getWrapper().getEntry().ref())));
}); });
} }
: null); : null);
@ -267,7 +267,7 @@ public abstract class StoreEntryComp extends SimpleComp {
button.apply(new ContextMenuAugment<>( button.apply(new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> { mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {
var cm = ContextMenuHelper.create(); var cm = ContextMenuHelper.create();
branch.getChildren().forEach(childProvider -> { branch.getChildren(getWrapper().getEntry().ref()).forEach(childProvider -> {
var menu = buildMenuItemForAction(childProvider); var menu = buildMenuItemForAction(childProvider);
if (menu != null) { if (menu != null) {
cm.getItems().add(menu); cm.getItems().add(menu);
@ -276,13 +276,13 @@ public abstract class StoreEntryComp extends SimpleComp {
return cm; return cm;
})); }));
} }
button.accessibleText(cs.getName(wrapper.getEntry().ref()).getValue()); button.accessibleText(cs.getName(getWrapper().getEntry().ref()).getValue());
button.apply(new TooltipAugment<>(cs.getName(wrapper.getEntry().ref()), null)); button.apply(new TooltipAugment<>(cs.getName(getWrapper().getEntry().ref()), null));
return button; return button;
} }
protected Comp<?> createSettingsButton() { protected Comp<?> createSettingsButton() {
var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline", null); var settingsButton = new IconButtonComp("mdi2d-dots-horizontal-circle-outline");
settingsButton.styleClass("settings"); settingsButton.styleClass("settings");
settingsButton.accessibleText("More"); settingsButton.accessibleText("More");
settingsButton.apply(new ContextMenuAugment<>( settingsButton.apply(new ContextMenuAugment<>(
@ -298,7 +298,7 @@ public abstract class StoreEntryComp extends SimpleComp {
AppFont.normal(contextMenu.getStyleableNode()); AppFont.normal(contextMenu.getStyleableNode());
var hasSep = false; var hasSep = false;
for (var p : wrapper.getActionProviders()) { for (var p : getWrapper().getActionProviders()) {
var item = buildMenuItemForAction(p); var item = buildMenuItemForAction(p);
if (item == null) { if (item == null) {
continue; continue;
@ -321,36 +321,36 @@ public abstract class StoreEntryComp extends SimpleComp {
var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text")); var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text"));
notes.setOnAction(event -> { notes.setOnAction(event -> {
wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes())); getWrapper().getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
event.consume(); event.consume();
}); });
notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null)); notes.visibleProperty().bind(BindingsHelper.map(getWrapper().getNotes(), s -> s.getCommited() == null));
contextMenu.getItems().add(notes); contextMenu.getItems().add(notes);
if (AppPrefs.get().developerMode().getValue()) { if (AppPrefs.get().developerMode().getValue()) {
var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline")); var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline"));
browse.setOnAction( browse.setOnAction(event ->
event -> DesktopHelper.browsePathLocal(wrapper.getEntry().getDirectory())); DesktopHelper.browsePathLocal(getWrapper().getEntry().getDirectory()));
contextMenu.getItems().add(browse); contextMenu.getItems().add(browse);
var copyId = new MenuItem(AppI18n.get("copyId"), new FontIcon("mdi2c-content-copy")); var copyId = new MenuItem(AppI18n.get("copyId"), new FontIcon("mdi2c-content-copy"));
copyId.setOnAction(event -> copyId.setOnAction(event ->
ClipboardHelper.copyText(wrapper.getEntry().getUuid().toString())); ClipboardHelper.copyText(getWrapper().getEntry().getUuid().toString()));
contextMenu.getItems().add(copyId); contextMenu.getItems().add(copyId);
} }
if (DataStorage.get().isRootEntry(wrapper.getEntry())) { if (DataStorage.get().isRootEntry(getWrapper().getEntry())) {
var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill")); var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill"));
var none = new MenuItem("None"); var none = new MenuItem("None");
none.setOnAction(event -> { none.setOnAction(event -> {
wrapper.getEntry().setColor(null); getWrapper().getEntry().setColor(null);
event.consume(); event.consume();
}); });
color.getItems().add(none); color.getItems().add(none);
Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> { Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> {
MenuItem m = new MenuItem(DataStoreFormatter.capitalize(dataStoreColor.getId())); MenuItem m = new MenuItem(DataStoreFormatter.capitalize(dataStoreColor.getId()));
m.setOnAction(event -> { m.setOnAction(event -> {
wrapper.getEntry().setColor(dataStoreColor); getWrapper().getEntry().setColor(dataStoreColor);
event.consume(); event.consume();
}); });
color.getItems().add(m); color.getItems().add(m);
@ -358,10 +358,10 @@ public abstract class StoreEntryComp extends SimpleComp {
contextMenu.getItems().add(color); contextMenu.getItems().add(color);
} }
if (wrapper.getEntry().getProvider() != null) { if (getWrapper().getEntry().getProvider() != null) {
var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline"));
StoreViewState.get() StoreViewState.get()
.getSortedCategories(wrapper.getCategory().getValue().getRoot()) .getSortedCategories(getWrapper().getCategory().getValue().getRoot())
.getList() .getList()
.forEach(storeCategoryWrapper -> { .forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem(); MenuItem m = new MenuItem();
@ -369,12 +369,12 @@ public abstract class StoreEntryComp extends SimpleComp {
.setValue(" ".repeat(storeCategoryWrapper.getDepth()) .setValue(" ".repeat(storeCategoryWrapper.getDepth())
+ storeCategoryWrapper.getName().getValue()); + storeCategoryWrapper.getName().getValue());
m.setOnAction(event -> { m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory()); getWrapper().moveTo(storeCategoryWrapper.getCategory());
event.consume(); event.consume();
}); });
if (storeCategoryWrapper.getParent() == null if (storeCategoryWrapper.getParent() == null
|| storeCategoryWrapper.equals( || storeCategoryWrapper.equals(
wrapper.getCategory().getValue())) { getWrapper().getCategory().getValue())) {
m.setDisable(true); m.setDisable(true);
} }
@ -386,10 +386,10 @@ public abstract class StoreEntryComp extends SimpleComp {
var order = new Menu(AppI18n.get("order"), new FontIcon("mdal-bookmarks")); var order = new Menu(AppI18n.get("order"), new FontIcon("mdal-bookmarks"));
var noOrder = new MenuItem(AppI18n.get("none"), new FontIcon("mdi2r-reorder-horizontal")); var noOrder = new MenuItem(AppI18n.get("none"), new FontIcon("mdi2r-reorder-horizontal"));
noOrder.setOnAction(event -> { noOrder.setOnAction(event -> {
wrapper.setOrder(null); getWrapper().setOrder(null);
event.consume(); event.consume();
}); });
if (wrapper.getEntry().getExplicitOrder() == null) { if (getWrapper().getEntry().getExplicitOrder() == null) {
noOrder.setDisable(true); noOrder.setDisable(true);
} }
order.getItems().add(noOrder); order.getItems().add(noOrder);
@ -397,20 +397,20 @@ public abstract class StoreEntryComp extends SimpleComp {
var top = new MenuItem(AppI18n.get("stickToTop"), new FontIcon("mdi2o-order-bool-descending")); var top = new MenuItem(AppI18n.get("stickToTop"), new FontIcon("mdi2o-order-bool-descending"));
top.setOnAction(event -> { top.setOnAction(event -> {
wrapper.setOrder(DataStoreEntry.Order.TOP); getWrapper().setOrder(DataStoreEntry.Order.TOP);
event.consume(); event.consume();
}); });
if (DataStoreEntry.Order.TOP.equals(wrapper.getEntry().getExplicitOrder())) { if (DataStoreEntry.Order.TOP.equals(getWrapper().getEntry().getExplicitOrder())) {
top.setDisable(true); top.setDisable(true);
} }
order.getItems().add(top); order.getItems().add(top);
var bottom = new MenuItem(AppI18n.get("stickToBottom"), new FontIcon("mdi2o-order-bool-ascending")); var bottom = new MenuItem(AppI18n.get("stickToBottom"), new FontIcon("mdi2o-order-bool-ascending"));
bottom.setOnAction(event -> { bottom.setOnAction(event -> {
wrapper.setOrder(DataStoreEntry.Order.BOTTOM); getWrapper().setOrder(DataStoreEntry.Order.BOTTOM);
event.consume(); event.consume();
}); });
if (DataStoreEntry.Order.BOTTOM.equals(wrapper.getEntry().getExplicitOrder())) { if (DataStoreEntry.Order.BOTTOM.equals(getWrapper().getEntry().getExplicitOrder())) {
bottom.setDisable(true); bottom.setDisable(true);
} }
order.getItems().add(bottom); order.getItems().add(bottom);
@ -423,14 +423,14 @@ public abstract class StoreEntryComp extends SimpleComp {
del.disableProperty() del.disableProperty()
.bind(Bindings.createBooleanBinding( .bind(Bindings.createBooleanBinding(
() -> { () -> {
return !wrapper.getDeletable().get() return !getWrapper().getDeletable().get()
&& !AppPrefs.get() && !AppPrefs.get()
.developerDisableGuiRestrictions() .developerDisableGuiRestrictions()
.get(); .get();
}, },
wrapper.getDeletable(), getWrapper().getDeletable(),
AppPrefs.get().developerDisableGuiRestrictions())); AppPrefs.get().developerDisableGuiRestrictions()));
del.setOnAction(event -> wrapper.delete()); del.setOnAction(event -> getWrapper().delete());
contextMenu.getItems().add(del); contextMenu.getItems().add(del);
return contextMenu; return contextMenu;
@ -441,12 +441,12 @@ public abstract class StoreEntryComp extends SimpleComp {
var branch = p.getBranchDataStoreCallSite(); var branch = p.getBranchDataStoreCallSite();
var cs = leaf != null ? leaf : branch; var cs = leaf != null ? leaf : branch;
if (cs == null || cs.isMajor(wrapper.getEntry().ref())) { if (cs == null || cs.isMajor(getWrapper().getEntry().ref())) {
return null; return null;
} }
var name = cs.getName(wrapper.getEntry().ref()); var name = cs.getName(getWrapper().getEntry().ref());
var icon = cs.getIcon(wrapper.getEntry().ref()); var icon = cs.getIcon(getWrapper().getEntry().ref());
var item = (leaf != null && leaf.canLinkTo()) || branch != null var item = (leaf != null && leaf.canLinkTo()) || branch != null
? new Menu(null, new FontIcon(icon)) ? new Menu(null, new FontIcon(icon))
: new MenuItem(null, new FontIcon(icon)); : new MenuItem(null, new FontIcon(icon));
@ -462,7 +462,7 @@ public abstract class StoreEntryComp extends SimpleComp {
Menu menu = item instanceof Menu m ? m : null; Menu menu = item instanceof Menu m ? m : null;
if (branch != null) { if (branch != null) {
var items = branch.getChildren().stream() var items = branch.getChildren(getWrapper().getEntry().ref()).stream()
.map(c -> buildMenuItemForAction(c)) .map(c -> buildMenuItemForAction(c))
.toList(); .toList();
menu.getItems().addAll(items); menu.getItems().addAll(items);
@ -472,21 +472,25 @@ public abstract class StoreEntryComp extends SimpleComp {
run.textProperty().bind(AppI18n.observable("base.execute")); run.textProperty().bind(AppI18n.observable("base.execute"));
run.setOnAction(event -> { run.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy()); getWrapper()
.runAction(leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy());
}); });
}); });
menu.getItems().add(run); menu.getItems().add(run);
var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than")); var sc = new MenuItem(null, new FontIcon("mdi2c-code-greater-than"));
var url = "xpipe://action/" + p.getId() + "/" + wrapper.getEntry().getUuid(); var url = "xpipe://action/" + p.getId() + "/"
+ getWrapper().getEntry().getUuid();
sc.textProperty().bind(AppI18n.observable("base.createShortcut")); sc.textProperty().bind(AppI18n.observable("base.createShortcut"));
sc.setOnAction(event -> { sc.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
DesktopShortcuts.create( DesktopShortcuts.createCliOpen(
url, url,
wrapper.nameProperty().getValue() + " (" DataStorage.get()
.getStoreEntryDisplayName(
getWrapper().getEntry()) + " ("
+ p.getLeafDataStoreCallSite() + p.getLeafDataStoreCallSite()
.getName(wrapper.getEntry().ref()) .getName(getWrapper().getEntry().ref())
.getValue() + ")"); .getValue() + ")");
}); });
}); });
@ -516,7 +520,7 @@ public abstract class StoreEntryComp extends SimpleComp {
event.consume(); event.consume();
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy()); getWrapper().runAction(leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy());
}); });
}); });

View file

@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
@ -95,8 +96,8 @@ public class StoreEntryListOverviewComp extends SimpleComp {
createDateSortButton().createRegion(), createDateSortButton().createRegion(),
Comp.hspacer(2).createRegion(), Comp.hspacer(2).createRegion(),
createAlphabeticalSortButton().createRegion()); createAlphabeticalSortButton().createRegion());
AppFont.setSize(label, 3); AppFont.setSize(label, 2);
AppFont.setSize(c, 3); AppFont.setSize(c, 2);
topBar.setAlignment(Pos.CENTER); topBar.setAlignment(Pos.CENTER);
topBar.getStyleClass().add("top"); topBar.getStyleClass().add("top");
return topBar; return topBar;
@ -111,9 +112,11 @@ public class StoreEntryListOverviewComp extends SimpleComp {
}); });
var filter = new FilterComp(StoreViewState.get().getFilterString()); var filter = new FilterComp(StoreViewState.get().getFilterString());
var f = filter.createRegion(); var f = filter.createRegion();
var buttons = createAddButton(); var button = createAddButton();
var hbox = new HBox(buttons, f); var hbox = new HBox(button, f);
f.prefHeightProperty().bind(buttons.heightProperty()); f.minHeightProperty().bind(button.heightProperty());
f.prefHeightProperty().bind(button.heightProperty());
f.maxHeightProperty().bind(button.heightProperty());
hbox.setSpacing(8); hbox.setSpacing(8);
hbox.setAlignment(Pos.CENTER); hbox.setAlignment(Pos.CENTER);
HBox.setHgrow(f, Priority.ALWAYS); HBox.setHgrow(f, Priority.ALWAYS);
@ -136,22 +139,22 @@ public class StoreEntryListOverviewComp extends SimpleComp {
if (OsType.getLocal().equals(OsType.MACOS)) { if (OsType.getLocal().equals(OsType.MACOS)) {
menu.setPadding(new Insets(-2, 0, -2, 0)); menu.setPadding(new Insets(-2, 0, -2, 0));
} else { } else {
menu.setPadding(new Insets(-3, 0, -3, 0)); menu.setPadding(new Insets(-4, 0, -4, 0));
} }
return menu; return menu;
} }
private Comp<?> createAlphabeticalSortButton() { private Comp<?> createAlphabeticalSortButton() {
var icon = Bindings.createStringBinding( var icon = Bindings.createObjectBinding(
() -> { () -> {
if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_ASC) { if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_ASC) {
return "mdi2s-sort-alphabetical-descending"; return new LabelGraphic.IconGraphic("mdi2s-sort-alphabetical-descending");
} }
if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_DESC) { if (sortMode.getValue() == StoreSortMode.ALPHABETICAL_DESC) {
return "mdi2s-sort-alphabetical-ascending"; return new LabelGraphic.IconGraphic("mdi2s-sort-alphabetical-ascending");
} }
return "mdi2s-sort-alphabetical-descending"; return new LabelGraphic.IconGraphic("mdi2s-sort-alphabetical-descending");
}, },
sortMode); sortMode);
var alphabetical = new IconButtonComp(icon, () -> { var alphabetical = new IconButtonComp(icon, () -> {
@ -164,6 +167,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
} }
}); });
alphabetical.apply(alphabeticalR -> { alphabetical.apply(alphabeticalR -> {
AppFont.medium(alphabeticalR.get());
alphabeticalR alphabeticalR
.get() .get()
.opacityProperty() .opacityProperty()
@ -183,15 +187,15 @@ public class StoreEntryListOverviewComp extends SimpleComp {
} }
private Comp<?> createDateSortButton() { private Comp<?> createDateSortButton() {
var icon = Bindings.createStringBinding( var icon = Bindings.createObjectBinding(
() -> { () -> {
if (sortMode.getValue() == StoreSortMode.DATE_ASC) { if (sortMode.getValue() == StoreSortMode.DATE_ASC) {
return "mdi2s-sort-clock-ascending-outline"; return new LabelGraphic.IconGraphic("mdi2s-sort-clock-ascending-outline");
} }
if (sortMode.getValue() == StoreSortMode.DATE_DESC) { if (sortMode.getValue() == StoreSortMode.DATE_DESC) {
return "mdi2s-sort-clock-descending-outline"; return new LabelGraphic.IconGraphic("mdi2s-sort-clock-descending-outline");
} }
return "mdi2s-sort-clock-ascending-outline"; return new LabelGraphic.IconGraphic("mdi2s-sort-clock-ascending-outline");
}, },
sortMode); sortMode);
var date = new IconButtonComp(icon, () -> { var date = new IconButtonComp(icon, () -> {
@ -204,6 +208,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
} }
}); });
date.apply(dateR -> { date.apply(dateR -> {
AppFont.medium(dateR.get());
dateR.get() dateR.get()
.opacityProperty() .opacityProperty()
.bind(Bindings.createDoubleBinding( .bind(Bindings.createDoubleBinding(

View file

@ -40,12 +40,13 @@ public class StoreEntryWrapper {
private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>(); private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();
private final Property<String> summary = new SimpleObjectProperty<>(); private final Property<String> summary = new SimpleObjectProperty<>();
private final Property<StoreNotes> notes; private final Property<StoreNotes> notes;
private final IntegerProperty childrenStateUpdateObservable = new SimpleIntegerProperty();
public StoreEntryWrapper(DataStoreEntry entry) { public StoreEntryWrapper(DataStoreEntry entry) {
this.entry = entry; this.entry = entry;
this.name = new SimpleStringProperty(entry.getName()); this.name = new SimpleStringProperty(entry.getName());
this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500))); this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500)));
ActionProvider.ALL.stream() ActionProvider.ALL_STANDALONE.stream()
.filter(dataStoreActionProvider -> { .filter(dataStoreActionProvider -> {
return !entry.isDisabled() return !entry.isDisabled()
&& dataStoreActionProvider.getLeafDataStoreCallSite() != null && dataStoreActionProvider.getLeafDataStoreCallSite() != null
@ -63,6 +64,12 @@ public class StoreEntryWrapper {
setupListeners(); setupListeners();
} }
public void triggerChildrenStateUpdate() {
PlatformThread.runLaterIfNeeded(() -> {
childrenStateUpdateObservable.set(childrenStateUpdateObservable.get() + 1);
});
}
public void applyLastAccess() { public void applyLastAccess() {
this.lastAccessApplied.setValue(lastAccess.getValue()); this.lastAccessApplied.setValue(lastAccess.getValue());
} }
@ -151,7 +158,8 @@ public class StoreEntryWrapper {
summary.setValue(null); summary.setValue(null);
} else { } else {
try { try {
summary.setValue(entry.getProvider() != null ? entry.getProvider().summaryString(this) : null); summary.setValue(
entry.getProvider() != null ? entry.getProvider().summaryString(this) : null);
} catch (Exception ex) { } catch (Exception ex) {
// Summary creation might fail or have a bug // Summary creation might fail or have a bug
ErrorEvent.fromThrowable(ex).handle(); ErrorEvent.fromThrowable(ex).handle();
@ -163,7 +171,7 @@ public class StoreEntryWrapper {
defaultActionProvider.setValue(null); defaultActionProvider.setValue(null);
} else { } else {
try { try {
var defaultProvider = ActionProvider.ALL.stream() var defaultProvider = ActionProvider.ALL_STANDALONE.stream()
.filter(e -> entry.getStore() != null .filter(e -> entry.getStore() != null
&& e.getDefaultDataStoreCallSite() != null && e.getDefaultDataStoreCallSite() != null
&& e.getDefaultDataStoreCallSite() && e.getDefaultDataStoreCallSite()
@ -174,7 +182,7 @@ public class StoreEntryWrapper {
.orElse(null); .orElse(null);
this.defaultActionProvider.setValue(defaultProvider); this.defaultActionProvider.setValue(defaultProvider);
var newProviders = ActionProvider.ALL.stream() var newProviders = ActionProvider.ALL_STANDALONE.stream()
.filter(dataStoreActionProvider -> { .filter(dataStoreActionProvider -> {
return showActionProvider(dataStoreActionProvider); return showActionProvider(dataStoreActionProvider);
}) })
@ -203,7 +211,7 @@ public class StoreEntryWrapper {
if (branch != null if (branch != null
&& entry.getStore() != null && entry.getStore() != null
&& branch.getApplicableClass().isAssignableFrom(entry.getStore().getClass())) { && branch.getApplicableClass().isAssignableFrom(entry.getStore().getClass())) {
return branch.getChildren().stream().anyMatch(child -> { return branch.getChildren(entry.ref()).stream().anyMatch(child -> {
return showActionProvider(child); return showActionProvider(child);
}); });
} }

View file

@ -39,7 +39,6 @@ public class StoreNotesComp extends Comp<StoreNotesComp.Structure> {
.focusTraversableForAccessibility() .focusTraversableForAccessibility()
.tooltipKey("notes") .tooltipKey("notes")
.styleClass("notes-button") .styleClass("notes-button")
.grow(false, true)
.hide(BindingsHelper.map(n, s -> s.getCommited() == null && s.getCurrent() == null)) .hide(BindingsHelper.map(n, s -> s.getCommited() == null && s.getCurrent() == null))
.createStructure() .createStructure()
.get(); .get();

View file

@ -29,7 +29,7 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
Property<DataStoreProvider> provider; Property<DataStoreProvider> provider;
boolean staticDisplay; boolean staticDisplay;
private List<DataStoreProvider> getProviders() { public List<DataStoreProvider> getProviders() {
return DataStoreProviders.getAll().stream() return DataStoreProviders.getAll().stream()
.filter(val -> filter == null || filter.test(val)) .filter(val -> filter == null || filter.test(val))
.toList(); .toList();

View file

@ -4,6 +4,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.util.ContextMenuHelper; import io.xpipe.app.util.ContextMenuHelper;
import javafx.geometry.Side; import javafx.geometry.Side;
@ -18,9 +19,9 @@ import java.util.function.Consumer;
public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> { public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
private final StoreSection section; private final StoreSection section;
private final Consumer<StoreEntryWrapper> action; private final Consumer<StoreSection> action;
public StoreQuickAccessButtonComp(StoreSection section, Consumer<StoreEntryWrapper> action) { public StoreQuickAccessButtonComp(StoreSection section, Consumer<StoreSection> action) {
this.section = section; this.section = section;
this.action = action; this.action = action;
} }
@ -44,10 +45,9 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore()); w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore());
if (c.getList().isEmpty()) { if (c.getList().isEmpty()) {
var item = ContextMenuHelper.item( var item = ContextMenuHelper.item(
PrettyImageHelper.ofFixedSizeSquare(graphic, 16), new LabelGraphic.ImageGraphic(graphic, 16), w.getName().getValue());
w.getName().getValue());
item.setOnAction(event -> { item.setOnAction(event -> {
action.accept(w); action.accept(section);
contextMenu.hide(); contextMenu.hide();
event.consume(); event.consume();
}); });
@ -72,7 +72,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
return; return;
} }
action.accept(w); action.accept(section);
contextMenu.hide(); contextMenu.hide();
event.consume(); event.consume();
} }

View file

@ -164,7 +164,7 @@ public class StoreSection {
other -> { other -> {
// Legacy implementation that does not use children caches. Use for testing // Legacy implementation that does not use children caches. Use for testing
// if (true) return DataStorage.get() // if (true) return DataStorage.get()
// .getDisplayParent(other.getEntry()) // .getDefaultDisplayParent(other.getEntry())
// .map(found -> found.equals(e.getEntry())) // .map(found -> found.equals(e.getEntry()))
// .orElse(false); // .orElse(false);

View file

@ -7,6 +7,7 @@ import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
@ -44,9 +45,9 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
return section.getShownChildren().getList().isEmpty(); return section.getShownChildren().getList().isEmpty();
}, },
section.getShownChildren().getList()); section.getShownChildren().getList());
Consumer<StoreEntryWrapper> quickAccessAction = w -> { Consumer<StoreSection> quickAccessAction = w -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
w.executeDefaultAction(); w.getWrapper().executeDefaultAction();
}); });
}; };
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction) var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
@ -68,11 +69,11 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
private Comp<CompStructure<Button>> createExpandButton() { private Comp<CompStructure<Button>> createExpandButton() {
var expandButton = new IconButtonComp( var expandButton = new IconButtonComp(
Bindings.createStringBinding( Bindings.createObjectBinding(
() -> section.getWrapper().getExpanded().get() () -> new LabelGraphic.IconGraphic(section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0 && section.getShownChildren().getList().size() > 0
? "mdal-keyboard_arrow_down" ? "mdal-keyboard_arrow_down"
: "mdal-keyboard_arrow_right", : "mdal-keyboard_arrow_right"),
section.getWrapper().getExpanded(), section.getWrapper().getExpanded(),
section.getShownChildren().getList()), section.getShownChildren().getList()),
() -> { () -> {

View file

@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.LabelGraphic;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -34,12 +35,12 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
private final StoreSection section; private final StoreSection section;
private final BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment; private final BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment;
private final Consumer<StoreEntryWrapper> action; private final Consumer<StoreSection> action;
public StoreSectionMiniComp( public StoreSectionMiniComp(
StoreSection section, StoreSection section,
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment, BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment,
Consumer<StoreEntryWrapper> action) { Consumer<StoreSection> action) {
this.section = section; this.section = section;
this.augment = augment; this.augment = augment;
this.action = action; this.action = action;
@ -68,7 +69,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
}) })
.apply(struc -> { .apply(struc -> {
struc.get().setOnAction(event -> { struc.get().setOnAction(event -> {
action.accept(section.getWrapper()); action.accept(section);
event.consume(); event.consume();
}); });
}) })
@ -81,8 +82,8 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
new SimpleBooleanProperty(section.getWrapper().getExpanded().get() new SimpleBooleanProperty(section.getWrapper().getExpanded().get()
&& section.getShownChildren().getList().size() > 0); && section.getShownChildren().getList().size() > 0);
var button = new IconButtonComp( var button = new IconButtonComp(
Bindings.createStringBinding( Bindings.createObjectBinding(
() -> expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right", () -> new LabelGraphic.IconGraphic(expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right"),
expanded), expanded),
() -> { () -> {
expanded.set(!expanded.get()); expanded.set(!expanded.get());
@ -105,7 +106,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
return section.getShownChildren().getList().isEmpty(); return section.getShownChildren().getList().isEmpty();
}, },
section.getShownChildren().getList()); section.getShownChildren().getList());
Consumer<StoreEntryWrapper> quickAccessAction = action; Consumer<StoreSection> quickAccessAction = action;
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction) var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
.vgrow() .vgrow()
.styleClass("quick-access-button") .styleClass("quick-access-button")

View file

@ -65,8 +65,7 @@ public interface StoreSortMode {
.isUsable()) .isUsable())
.map(this::representative), .map(this::representative),
Stream.of(s)) Stream.of(s))
.max(Comparator.comparing( .max(Comparator.comparing(section -> date(section)))
section -> date(section)))
.orElseThrow(); .orElseThrow();
} }
@ -103,8 +102,7 @@ public interface StoreSortMode {
.isUsable()) .isUsable())
.map(this::representative), .map(this::representative),
Stream.of(s)) Stream.of(s))
.max(Comparator.comparing( .max(Comparator.comparing(section -> date(section)))
section -> date(section)))
.orElseThrow(); .orElseThrow();
} }

View file

@ -124,10 +124,10 @@ public class StoreViewState {
public void updateDisplay() { public void updateDisplay() {
allEntries.getList().forEach(e -> e.applyLastAccess()); allEntries.getList().forEach(e -> e.applyLastAccess());
toggleStoreListUpdate(); triggerStoreListUpdate();
} }
public void toggleStoreListUpdate() { public void triggerStoreListUpdate() {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
entriesListUpdateObservable.set(entriesListUpdateObservable.get() + 1); entriesListUpdateObservable.set(entriesListUpdateObservable.get() + 1);
}); });
@ -152,7 +152,7 @@ public class StoreViewState {
@Override @Override
public void onStoreListUpdate() { public void onStoreListUpdate() {
Platform.runLater(() -> { Platform.runLater(() -> {
toggleStoreListUpdate(); triggerStoreListUpdate();
}); });
} }

View file

@ -7,9 +7,11 @@ import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.XPipeDistributionType; import io.xpipe.app.update.XPipeDistributionType;
import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.LicenseProvider;
import javafx.application.Application; import javafx.application.Application;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.stage.Stage; import javafx.stage.Stage;
import lombok.Getter; import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;

View file

@ -1,6 +1,7 @@
package io.xpipe.app.core; package io.xpipe.app.core;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import org.apache.commons.io.FileUtils;
import java.io.RandomAccessFile; import java.io.RandomAccessFile;
import java.nio.channels.FileChannel; import java.nio.channels.FileChannel;
@ -21,7 +22,7 @@ public class AppDataLock {
public static boolean lock() { public static boolean lock() {
try { try {
var file = getLockFile().toFile(); var file = getLockFile().toFile();
Files.createDirectories(file.toPath().getParent()); FileUtils.forceMkdir(file.getParentFile());
if (!Files.exists(file.toPath())) { if (!Files.exists(file.toPath())) {
try { try {
// It is possible that another instance creates the lock at almost the same time // It is possible that another instance creates the lock at almost the same time

View file

@ -93,7 +93,7 @@ public class AppExtensionManager {
Path p = Path.of(localInstallation); Path p = Path.of(localInstallation);
if (!Files.exists(p)) { if (!Files.exists(p)) {
throw new IllegalStateException( throw new IllegalStateException(
"Required local XPipe installation was not found but is required for development"); "Required local XPipe installation was not found but is required for development. See https://github.com/xpipe-io/xpipe/blob/master/CONTRIBUTING.md#development-setup");
} }
var iv = getLocalInstallVersion(); var iv = getLocalInstallVersion();
@ -105,8 +105,9 @@ public class AppExtensionManager {
var sourceVersion = AppVersion.parse(sv) var sourceVersion = AppVersion.parse(sv)
.orElseThrow(() -> new IllegalArgumentException("Invalid source version: " + sv)); .orElseThrow(() -> new IllegalArgumentException("Invalid source version: " + sv));
if (AppProperties.get().isLocatorVersionCheck() && !installVersion.equals(sourceVersion)) { if (AppProperties.get().isLocatorVersionCheck() && !installVersion.equals(sourceVersion)) {
throw new IllegalStateException("Incompatible development version. Source: " + iv + ", Installation: " throw new IllegalStateException(
+ sv + "\n\nPlease try to check out the matching release version in the repository."); "Incompatible development version. Source: " + iv + ", Installation: " + sv
+ "\n\nPlease try to check out the matching release version in the repository. See https://github.com/xpipe-io/xpipe/blob/master/CONTRIBUTING.md#development-setup");
} }
var extensions = XPipeInstallation.getLocalExtensionsDirectory(p); var extensions = XPipeInstallation.getLocalExtensionsDirectory(p);

View file

@ -55,6 +55,12 @@ public class AppGreetings {
if (set || AppProperties.get().isDevelopmentEnvironment()) { if (set || AppProperties.get().isDevelopmentEnvironment()) {
return; return;
} }
if (AppProperties.get().isAutoAcceptEula()) {
AppCache.update("legalAccepted", true);
return;
}
var read = new SimpleBooleanProperty(); var read = new SimpleBooleanProperty();
var accepted = new SimpleBooleanProperty(); var accepted = new SimpleBooleanProperty();
AppWindowHelper.showBlockingAlert(alert -> { AppWindowHelper.showBlockingAlert(alert -> {

View file

@ -78,19 +78,19 @@ public class AppLayoutModel {
"mdi2f-file-cabinet", "mdi2f-file-cabinet",
new BrowserSessionComp(BrowserSessionModel.DEFAULT), new BrowserSessionComp(BrowserSessionModel.DEFAULT),
null, null,
new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.CONTROL_DOWN)), new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.SHORTCUT_DOWN)),
new Entry( new Entry(
AppI18n.observable("connections"), AppI18n.observable("connections"),
"mdi2c-connection", "mdi2c-connection",
new StoreLayoutComp(), new StoreLayoutComp(),
null, null,
new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.CONTROL_DOWN)), new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.SHORTCUT_DOWN)),
new Entry( new Entry(
AppI18n.observable("settings"), AppI18n.observable("settings"),
"mdsmz-miscellaneous_services", "mdsmz-miscellaneous_services",
new AppPrefsComp(), new AppPrefsComp(),
null, null,
new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)), new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.SHORTCUT_DOWN)),
new Entry( new Entry(
AppI18n.observable("explorePlans"), AppI18n.observable("explorePlans"),
"mdi2p-professional-hexagon", "mdi2p-professional-hexagon",
@ -102,20 +102,20 @@ public class AppLayoutModel {
"mdi2g-github", "mdi2g-github",
null, null,
() -> Hyperlinks.open(Hyperlinks.GITHUB), () -> Hyperlinks.open(Hyperlinks.GITHUB),
new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)), null),
new Entry( new Entry(
AppI18n.observable("discord"), AppI18n.observable("discord"),
"mdi2d-discord", "mdi2d-discord",
null, null,
() -> Hyperlinks.open(Hyperlinks.DISCORD), () -> Hyperlinks.open(Hyperlinks.DISCORD),
new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)), null),
new Entry( new Entry(
AppI18n.observable("api"), AppI18n.observable("api"),
"mdi2c-code-json", "mdi2c-code-json",
null, null,
() -> Hyperlinks.open( () -> Hyperlinks.open(
"http://localhost:" + AppBeaconServer.get().getPort()), "http://localhost:" + AppBeaconServer.get().getPort()),
new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)))); null)));
return l; return l;
} }
@ -128,5 +128,6 @@ public class AppLayoutModel {
double browserConnectionsWidth; double browserConnectionsWidth;
} }
public record Entry(ObservableValue<String> name, String icon, Comp<?> comp, Runnable action, KeyCombination combination) {} public record Entry(
ObservableValue<String> name, String icon, Comp<?> comp, Runnable action, KeyCombination combination) {}
} }

View file

@ -138,7 +138,7 @@ public class AppLogs {
var shouldLogToFile = shouldWriteLogs(); var shouldLogToFile = shouldWriteLogs();
if (shouldLogToFile) { if (shouldLogToFile) {
try { try {
Files.createDirectories(usedLogsDir); FileUtils.forceMkdir(usedLogsDir.toFile());
var file = usedLogsDir.resolve("xpipe.log"); var file = usedLogsDir.resolve("xpipe.log");
var fos = new FileOutputStream(file.toFile(), true); var fos = new FileOutputStream(file.toFile(), true);
var buf = new BufferedOutputStream(fos); var buf = new BufferedOutputStream(fos);

View file

@ -37,11 +37,13 @@ public class AppProperties {
boolean useVirtualThreads; boolean useVirtualThreads;
boolean debugThreads; boolean debugThreads;
Path dataDir; Path dataDir;
Path defaultDataDir;
boolean showcase; boolean showcase;
AppVersion canonicalVersion; AppVersion canonicalVersion;
boolean locatePtb; boolean locatePtb;
boolean locatorVersionCheck; boolean locatorVersionCheck;
boolean isTest; boolean isTest;
boolean autoAcceptEula;
public AppProperties() { public AppProperties() {
var appDir = Path.of(System.getProperty("user.dir")).resolve("app"); var appDir = Path.of(System.getProperty("user.dir")).resolve("app");
@ -86,6 +88,7 @@ public class AppProperties {
debugThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.debugThreads")) debugThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.debugThreads"))
.map(Boolean::parseBoolean) .map(Boolean::parseBoolean)
.orElse(false); .orElse(false);
defaultDataDir = Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe-ptb" : ".xpipe");
dataDir = Optional.ofNullable(System.getProperty("io.xpipe.app.dataDir")) dataDir = Optional.ofNullable(System.getProperty("io.xpipe.app.dataDir"))
.map(s -> { .map(s -> {
var p = Path.of(s); var p = Path.of(s);
@ -94,7 +97,7 @@ public class AppProperties {
} }
return p; return p;
}) })
.orElse(Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe-ptb" : ".xpipe")); .orElse(defaultDataDir);
showcase = Optional.ofNullable(System.getProperty("io.xpipe.app.showcase")) showcase = Optional.ofNullable(System.getProperty("io.xpipe.app.showcase"))
.map(Boolean::parseBoolean) .map(Boolean::parseBoolean)
.orElse(false); .orElse(false);
@ -107,6 +110,9 @@ public class AppProperties {
.map(s -> !Boolean.parseBoolean(s)) .map(s -> !Boolean.parseBoolean(s))
.orElse(true); .orElse(true);
isTest = isJUnitTest(); isTest = isJUnitTest();
autoAcceptEula = Optional.ofNullable(System.getProperty("io.xpipe.app.acceptEula"))
.map(Boolean::parseBoolean)
.orElse(false);
} }
private static boolean isJUnitTest() { private static boolean isJUnitTest() {

View file

@ -43,6 +43,10 @@ public class AppTheme {
public static void initThemeHandlers(Stage stage) { public static void initThemeHandlers(Stage stage) {
Runnable r = () -> { Runnable r = () -> {
stage.getScene()
.getRoot()
.pseudoClassStateChanged(
PseudoClass.getPseudoClass(OsType.getLocal().getId()), true);
if (AppPrefs.get() == null) { if (AppPrefs.get() == null) {
var def = Theme.getDefaultLightTheme(); var def = Theme.getDefaultLightTheme();
stage.getScene().getRoot().getStyleClass().add(def.getCssId()); stage.getScene().getRoot().getStyleClass().add(def.getCssId());
@ -109,6 +113,9 @@ public class AppTheme {
} }
}); });
}); });
} catch (UnsupportedOperationException ex) {
// The platform preferences are sometimes not initialized yet
ErrorEvent.fromThrowable(ex).expected().omit().handle();
} catch (Throwable t) { } catch (Throwable t) {
ErrorEvent.fromThrowable(t).omit().handle(); ErrorEvent.fromThrowable(t).omit().handle();
} }
@ -132,6 +139,9 @@ public class AppTheme {
} else { } else {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme()); AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
} }
} catch (UnsupportedOperationException ex) {
// The platform preferences are sometimes not initialized yet
ErrorEvent.fromThrowable(ex).expected().omit().handle();
} catch (Exception ex) { } catch (Exception ex) {
// The color scheme query can fail if the toolkit is not initialized properly // The color scheme query can fail if the toolkit is not initialized properly
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme()); AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
@ -206,7 +216,6 @@ public class AppTheme {
Application.setUserAgentStylesheet(Styles.toDataURI(builder.toString())); Application.setUserAgentStylesheet(Styles.toDataURI(builder.toString()));
} }
public List<String> getAdditionalStylesheets() { public List<String> getAdditionalStylesheets() {
return List.of(); return List.of();
} }

View file

@ -90,7 +90,8 @@ public class AppTrayIcon {
tray.add(this.trayIcon); tray.add(this.trayIcon);
fixBackground(); fixBackground();
} catch (Exception e) { } catch (Exception e) {
ErrorEvent.fromThrowable("Unable to add TrayIcon", e).handle(); // This can sometimes fail on Linux
ErrorEvent.fromThrowable("Unable to add TrayIcon", e).expected().handle();
} }
}); });
} }

View file

@ -0,0 +1,30 @@
package io.xpipe.app.core.check;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.OsType;
public class AppRosettaCheck {
public static void check() throws Exception {
if (OsType.getLocal() != OsType.MACOS) {
return;
}
if (!AppProperties.get().getArch().equals("x86_64")) {
return;
}
var ret = LocalShell.getShell().command("sysctl -n sysctl.proc_translated").readStdoutIfPossible();
if (ret.isEmpty()) {
return;
}
if (ret.get().equals("1")) {
ErrorEvent.fromMessage("You are running the Intel version of XPipe on an Apple Silicon system."
+ " There is a native build available that comes with much better performance."
+ " Please install that one instead.");
}
}
}

View file

@ -6,6 +6,8 @@ import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.process.ProcessOutputException; import io.xpipe.core.process.ProcessOutputException;
import lombok.Value;
import java.util.Optional; import java.util.Optional;
public class AppShellCheck { public class AppShellCheck {
@ -17,15 +19,19 @@ public class AppShellCheck {
.getEffectiveLocalDialect() .getEffectiveLocalDialect()
.equals(ProcessControlProvider.get().getFallbackDialect()); .equals(ProcessControlProvider.get().getFallbackDialect());
if (err.isPresent() && canFallback) { if (err.isPresent() && canFallback) {
var msg = formatMessage(err.get()); var msg = formatMessage(err.get().getMessage());
ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle(); ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle();
enableFallback(); enableFallback();
err = selfTestErrorCheck(); err = selfTestErrorCheck();
} }
if (err.isPresent()) { if (err.isPresent()) {
var msg = formatMessage(err.get()); var msg = formatMessage(err.get().getMessage());
ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle(); var event = ErrorEvent.fromThrowable(new IllegalStateException(msg));
if (!err.get().isCanContinue()) {
event.term();
}
event.handle();
} }
} }
@ -71,17 +77,24 @@ public class AppShellCheck {
LocalShell.init(); LocalShell.init();
} }
private static Optional<String> selfTestErrorCheck() { private static Optional<FailureResult> selfTestErrorCheck() {
try (var command = LocalShell.getShell().command("echo test").complex().start()) { try (var command = LocalShell.getShell().command("echo test").complex().start()) {
var out = command.readStdoutOrThrow(); var out = command.readStdoutOrThrow();
if (!out.equals("test")) { if (!out.equals("test")) {
return Optional.of("Expected \"test\", got \"" + out + "\""); return Optional.of(new FailureResult("Expected \"test\", got \"" + out + "\"", true));
} }
} catch (ProcessOutputException ex) { } catch (ProcessOutputException ex) {
return Optional.of(ex.getOutput() != null ? ex.getOutput() : ex.toString()); return Optional.of(new FailureResult(ex.getOutput() != null ? ex.getOutput() : ex.toString(), true));
} catch (Throwable t) { } catch (Throwable t) {
return Optional.of(t.getMessage() != null ? t.getMessage() : t.toString()); return Optional.of(new FailureResult(t.getMessage() != null ? t.getMessage() : t.toString(), false));
} }
return Optional.empty(); return Optional.empty();
} }
@Value
private static class FailureResult {
String message;
boolean canContinue;
}
} }

View file

@ -2,6 +2,7 @@ package io.xpipe.app.core.check;
import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import org.apache.commons.io.FileUtils;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
@ -12,17 +13,18 @@ public class AppUserDirectoryCheck {
var dataDirectory = AppProperties.get().getDataDir(); var dataDirectory = AppProperties.get().getDataDir();
try { try {
Files.createDirectories(dataDirectory); FileUtils.forceMkdir(dataDirectory.toFile());
var testDirectory = dataDirectory.resolve("permissions_check"); var testDirectory = dataDirectory.resolve("permissions_check");
Files.createDirectories(testDirectory); FileUtils.forceMkdir(testDirectory.toFile());
if (!Files.exists(testDirectory)) {
throw new IOException("Directory creation in user home directory failed silently");
}
Files.delete(testDirectory); Files.delete(testDirectory);
// if (true) throw new IOException(); // if (true) throw new IOException();
} catch (IOException e) { } catch (IOException e) {
ErrorEvent.fromThrowable( ErrorEvent.fromThrowable("Unable to access directory " + dataDirectory
new IOException(
"Unable to access directory " + dataDirectory
+ ". Please make sure that you have the appropriate permissions and no Antivirus program is blocking the access. " + ". Please make sure that you have the appropriate permissions and no Antivirus program is blocking the access. "
+ "In case you use cloud storage, verify that your cloud storage is working and you are logged in.")) + "In case you use cloud storage, verify that your cloud storage is working and you are logged in.", e)
.term() .term()
.expected() .expected()
.handle(); .handle();

View file

@ -7,6 +7,7 @@ import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.*; import io.xpipe.app.core.*;
import io.xpipe.app.core.check.AppAvCheck; import io.xpipe.app.core.check.AppAvCheck;
import io.xpipe.app.core.check.AppCertutilCheck; import io.xpipe.app.core.check.AppCertutilCheck;
import io.xpipe.app.core.check.AppRosettaCheck;
import io.xpipe.app.core.check.AppShellCheck; import io.xpipe.app.core.check.AppShellCheck;
import io.xpipe.app.ext.ActionProvider; import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.ext.DataStoreProviders;
@ -51,6 +52,7 @@ public class BaseMode extends OperationMode {
AppSid.init(); AppSid.init();
LocalShell.init(); LocalShell.init();
AppShellCheck.check(); AppShellCheck.check();
AppRosettaCheck.check();
XPipeDistributionType.init(); XPipeDistributionType.init();
AppPrefs.setLocalDefaultsIfNeeded(); AppPrefs.setLocalDefaultsIfNeeded();
// Initialize beacon server as we should be prepared for git askpass commands // Initialize beacon server as we should be prepared for git askpass commands

View file

@ -86,6 +86,9 @@ public abstract class OperationMode {
private static void setup(String[] args) { private static void setup(String[] args) {
try { try {
// Register stage theming early to make it apply for any potential early popups
ModifiedStage.init();
// Only for handling SIGTERM // Only for handling SIGTERM
Runtime.getRuntime().addShutdownHook(new Thread(() -> { Runtime.getRuntime().addShutdownHook(new Thread(() -> {
TrackEvent.info("Received SIGTERM externally"); TrackEvent.info("Received SIGTERM externally");
@ -96,7 +99,9 @@ public abstract class OperationMode {
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> { Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
// It seems like a few exceptions are thrown in the quantum renderer // It seems like a few exceptions are thrown in the quantum renderer
// when in shutdown. We can ignore these // when in shutdown. We can ignore these
if (OperationMode.isInShutdown() && Platform.isFxApplicationThread() && ex instanceof NullPointerException) { if (OperationMode.isInShutdown()
&& Platform.isFxApplicationThread()
&& ex instanceof NullPointerException) {
return; return;
} }
@ -117,8 +122,6 @@ public abstract class OperationMode {
AppExtensionManager.init(true); AppExtensionManager.init(true);
AppI18n.init(); AppI18n.init();
AppPrefs.initLocal(); AppPrefs.initLocal();
// Register stage theming early to make it apply for any potential early popups
ModifiedStage.init();
AppBeaconServer.setupPort(); AppBeaconServer.setupPort();
TrackEvent.info("Finished initial setup"); TrackEvent.info("Finished initial setup");
} catch (Throwable ex) { } catch (Throwable ex) {
@ -224,7 +227,7 @@ public abstract class OperationMode {
CURRENT = null; CURRENT = null;
r.run(); r.run();
} catch (Throwable ex) { } catch (Throwable ex) {
ErrorEvent.fromThrowable(ex).build().handle(); ErrorEvent.fromThrowable(ex).handle();
OperationMode.halt(1); OperationMode.halt(1);
} }

View file

@ -8,6 +8,7 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.update.UpdateAvailableAlert; import io.xpipe.app.update.UpdateAvailableAlert;
import io.xpipe.app.util.PlatformState; import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.application.Application; import javafx.application.Application;
public abstract class PlatformMode extends OperationMode { public abstract class PlatformMode extends OperationMode {

View file

@ -11,7 +11,7 @@ public class TrayMode extends PlatformMode {
@Override @Override
public boolean isSupported() { public boolean isSupported() {
return !OsType.getLocal().equals(OsType.MACOS) return OsType.getLocal().equals(OsType.WINDOWS)
&& super.isSupported() && super.isSupported()
&& Desktop.isDesktopSupported() && Desktop.isDesktopSupported()
&& SystemTray.isSupported(); && SystemTray.isSupported();

View file

@ -16,6 +16,8 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Rectangle2D; import javafx.geometry.Rectangle2D;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
@ -167,8 +169,8 @@ public class AppMainWindow {
e.consume(); e.consume();
}); });
stage.addEventFilter(KeyEvent.KEY_PRESSED, event -> { stage.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode().equals(KeyCode.Q) && event.isShortcutDown()) { if (new KeyCodeCombination(KeyCode.Q, KeyCombination.SHORTCUT_DOWN).match(event)) {
stage.close(); stage.close();
AppPrefs.get().closeBehaviour().getValue().run(); AppPrefs.get().closeBehaviour().getValue().run();
event.consume(); event.consume();
@ -184,7 +186,7 @@ public class AppMainWindow {
stage.setY(state.windowY); stage.setY(state.windowY);
stage.setWidth(state.windowWidth); stage.setWidth(state.windowWidth);
stage.setHeight(state.windowHeight); stage.setHeight(state.windowHeight);
// stage.setMaximized(state.maximized); stage.setMaximized(state.maximized);
TrackEvent.debug("Window loaded saved bounds"); TrackEvent.debug("Window loaded saved bounds");
} else if (!AppProperties.get().isShowcase()) { } else if (!AppProperties.get().isShowcase()) {
@ -271,8 +273,8 @@ public class AppMainWindow {
contentR.prefHeightProperty().bind(stage.getScene().heightProperty()); contentR.prefHeightProperty().bind(stage.getScene().heightProperty());
if (OsType.getLocal().equals(OsType.LINUX) || OsType.getLocal().equals(OsType.MACOS)) { if (OsType.getLocal().equals(OsType.LINUX) || OsType.getLocal().equals(OsType.MACOS)) {
stage.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> { stage.getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) { if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
AppPrefs.get().closeBehaviour().getValue().run(); AppPrefs.get().closeBehaviour().getValue().run();
event.consume(); event.consume();
} }

View file

@ -8,6 +8,7 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.InputHelper; import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
@ -17,6 +18,8 @@ import javafx.scene.Scene;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType; import javafx.scene.control.ButtonType;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.input.KeyEvent; import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
@ -25,7 +28,6 @@ import javafx.scene.paint.Color;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.stage.Modality; import javafx.stage.Modality;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
@ -142,8 +144,8 @@ public class AppWindowHelper {
event.consume(); event.consume();
}); });
AppWindowBounds.fixInvalidStagePosition(s); AppWindowBounds.fixInvalidStagePosition(s);
a.getDialogPane().getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> { a.getDialogPane().getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) { if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
s.close(); s.close();
event.consume(); event.consume();
return; return;
@ -260,8 +262,8 @@ public class AppWindowHelper {
} }
}); });
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> { scene.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) { if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
stage.close(); stage.close();
event.consume(); event.consume();
} }

View file

@ -14,21 +14,16 @@ import javafx.stage.StageStyle;
import javafx.stage.Window; import javafx.stage.Window;
import javafx.util.Duration; import javafx.util.Duration;
import lombok.SneakyThrows;
import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.SystemUtils;
public class ModifiedStage extends Stage { public class ModifiedStage extends Stage {
public static boolean mergeFrame() { public static boolean mergeFrame() {
return SystemUtils.IS_OS_WINDOWS_11; return SystemUtils.IS_OS_WINDOWS_11 || SystemUtils.IS_OS_MAC;
} }
@SneakyThrows
@SuppressWarnings("unchecked")
public static void init() { public static void init() {
var windowsField = Window.class.getDeclaredField("windows"); ObservableList<Window> list = Window.getWindows();
windowsField.setAccessible(true);
ObservableList<Window> list = (ObservableList<Window>) windowsField.get(null);
list.addListener((ListChangeListener<Window>) c -> { list.addListener((ListChangeListener<Window>) c -> {
if (c.next() && c.wasAdded()) { if (c.next() && c.wasAdded()) {
var added = c.getAddedSubList().getFirst(); var added = c.getAddedSubList().getFirst();
@ -62,12 +57,32 @@ public class ModifiedStage extends Stage {
return; return;
} }
if (OsType.getLocal() != OsType.WINDOWS || AppPrefs.get() == null || AppPrefs.get().theme.getValue() == null) { var applyToStage = (OsType.getLocal() == OsType.WINDOWS)
|| (OsType.getLocal() == OsType.MACOS
&& AppMainWindow.getInstance() != null
&& AppMainWindow.getInstance().getStage() == stage);
if (!applyToStage || AppPrefs.get() == null || AppPrefs.get().theme.getValue() == null) {
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), false); stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), false);
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), true); stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), true);
return; return;
} }
switch (OsType.getLocal()) {
case OsType.Linux linux -> {}
case OsType.MacOs macOs -> {
var ctrl = new NativeMacOsWindowControl(stage);
var seamlessFrame = !AppPrefs.get().performanceMode().get() && mergeFrame();
var seamlessFrameApplied = ctrl.setAppearance(
seamlessFrame, AppPrefs.get().theme.getValue().isDark())
&& seamlessFrame;
stage.getScene()
.getRoot()
.pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied);
stage.getScene()
.getRoot()
.pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied);
}
case OsType.Windows windows -> {
var ctrl = new NativeWinWindowControl(stage); var ctrl = new NativeWinWindowControl(stage);
ctrl.setWindowAttribute( ctrl.setWindowAttribute(
NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(), NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
@ -78,8 +93,14 @@ public class ModifiedStage extends Stage {
} else { } else {
seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT); seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT);
} }
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame); stage.getScene()
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame); .getRoot()
.pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame);
stage.getScene()
.getRoot()
.pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame);
}
}
} }
private static void updateStage(Stage stage) { private static void updateStage(Stage stage) {

View file

@ -0,0 +1,56 @@
package io.xpipe.app.core.window;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.NativeBridge;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.util.ModuleHelper;
import javafx.stage.Window;
import com.sun.jna.NativeLong;
import lombok.Getter;
import lombok.SneakyThrows;
import java.lang.reflect.Method;
@Getter
public class NativeMacOsWindowControl {
private final long nsWindow;
@SneakyThrows
public NativeMacOsWindowControl(Window stage) {
Method tkStageGetter = Window.class.getDeclaredMethod("getPeer");
tkStageGetter.setAccessible(true);
Object tkStage = tkStageGetter.invoke(stage);
Method getPlatformWindow = tkStage.getClass().getDeclaredMethod("getPlatformWindow");
getPlatformWindow.setAccessible(true);
Object platformWindow = getPlatformWindow.invoke(tkStage);
Method getNativeHandle = platformWindow.getClass().getMethod("getNativeHandle");
getNativeHandle.setAccessible(true);
Object nativeHandle = getNativeHandle.invoke(platformWindow);
this.nsWindow = (long) nativeHandle;
}
public boolean setAppearance(boolean seamlessFrame, boolean darkMode) {
if (!ModuleHelper.isImage() || !AppProperties.get().isFullVersion()) {
return false;
}
var lib = NativeBridge.getMacOsLibrary();
if (lib.isEmpty()) {
return false;
}
try {
lib.get().setAppearance(new NativeLong(nsWindow), seamlessFrame, darkMode);
if (seamlessFrame) {
ThreadHelper.sleep(100);
}
} catch (Throwable e) {
ErrorEvent.fromThrowable(e).handle();
}
return true;
}
}

View file

@ -16,6 +16,7 @@ import java.util.ServiceLoader;
public interface ActionProvider { public interface ActionProvider {
List<ActionProvider> ALL = new ArrayList<>(); List<ActionProvider> ALL = new ArrayList<>();
List<ActionProvider> ALL_STANDALONE = new ArrayList<>();
static void initProviders() { static void initProviders() {
for (ActionProvider actionProvider : ALL) { for (ActionProvider actionProvider : ALL) {
@ -111,7 +112,7 @@ public interface ActionProvider {
String getIcon(DataStoreEntryRef<T> store); String getIcon(DataStoreEntryRef<T> store);
Class<T> getApplicableClass(); Class<?> getApplicableClass();
default boolean showBusy() { default boolean showBusy() {
return true; return true;
@ -120,9 +121,11 @@ public interface ActionProvider {
interface BranchDataStoreCallSite<T extends DataStore> extends DataStoreCallSite<T> { interface BranchDataStoreCallSite<T extends DataStore> extends DataStoreCallSite<T> {
default List<ActionProvider> getChildren() { default boolean isDynamicallyGenerated(){
return List.of(); return false;
} }
List<? extends ActionProvider> getChildren(DataStoreEntryRef<T> store);
} }
interface LeafDataStoreCallSite<T extends DataStore> extends DataStoreCallSite<T> { interface LeafDataStoreCallSite<T extends DataStore> extends DataStoreCallSite<T> {
@ -145,6 +148,18 @@ public interface ActionProvider {
ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream() ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream()
.map(actionProviderProvider -> actionProviderProvider.get()) .map(actionProviderProvider -> actionProviderProvider.get())
.toList()); .toList());
var menuProviders = ALL.stream()
.map(actionProvider -> actionProvider.getBranchDataStoreCallSite() != null &&
!actionProvider.getBranchDataStoreCallSite().isDynamicallyGenerated()
? actionProvider.getBranchDataStoreCallSite().getChildren(null)
: List.of())
.flatMap(List::stream)
.toList();
ALL_STANDALONE.addAll(ALL.stream()
.filter(actionProvider -> menuProviders.stream()
.noneMatch(menuItem -> menuItem.getClass().equals(actionProvider.getClass())))
.toList());
} }
} }
} }

View file

@ -9,5 +9,6 @@ public enum DataStoreCreationCategory {
TUNNEL, TUNNEL,
SCRIPT, SCRIPT,
CLUSTER, CLUSTER,
DESKTOP DESKTOP,
SERIAL
} }

View file

@ -10,7 +10,6 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppImages; import io.xpipe.app.core.AppImages;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonizedValue; import io.xpipe.core.util.JacksonizedValue;
@ -27,10 +26,16 @@ import java.util.List;
public interface DataStoreProvider { public interface DataStoreProvider {
default boolean showProviderChoice() {
return true;
}
default boolean shouldShow(StoreEntryWrapper w) { default boolean shouldShow(StoreEntryWrapper w) {
return true; return true;
} }
default void onParentRefresh(DataStoreEntry entry) {}
default void onChildrenRefresh(DataStoreEntry entry) {} default void onChildrenRefresh(DataStoreEntry entry) {}
default ObservableBooleanValue busy(StoreEntryWrapper wrapper) { default ObservableBooleanValue busy(StoreEntryWrapper wrapper) {
@ -71,21 +76,16 @@ public interface DataStoreProvider {
return null; return null;
} }
default String browserDisplayName(DataStore store) { default String displayName(DataStoreEntry entry) {
var e = DataStorage.get().getStoreDisplayName(store); return entry.getName();
return e.orElse("?");
} }
default List<String> getSearchableTerms(DataStore store) { default List<String> getSearchableTerms(DataStore store) {
return List.of(); return List.of();
} }
default boolean shouldEdit() {
return false;
}
default StoreEntryComp customEntryComp(StoreSection s, boolean preferLarge) { default StoreEntryComp customEntryComp(StoreSection s, boolean preferLarge) {
return StoreEntryComp.create(s.getWrapper(), null, preferLarge); return StoreEntryComp.create(s, null, preferLarge);
} }
default StoreSectionComp customSectionComp(StoreSection section, boolean topLevel) { default StoreSectionComp customSectionComp(StoreSection section, boolean topLevel) {
@ -104,6 +104,10 @@ public interface DataStoreProvider {
return Comp.empty(); return Comp.empty();
} }
default boolean canConnectDuringCreation() {
return false;
}
default Comp<?> createInsightsComp(ObservableValue<DataStore> store) { default Comp<?> createInsightsComp(ObservableValue<DataStore> store) {
var content = Bindings.createStringBinding( var content = Bindings.createStringBinding(
() -> { () -> {
@ -152,6 +156,10 @@ public interface DataStoreProvider {
return DataStoreUsageCategory.DATABASE; return DataStoreUsageCategory.DATABASE;
} }
if (cc == DataStoreCreationCategory.SERIAL) {
return DataStoreUsageCategory.SERIAL;
}
return null; return null;
} }
@ -191,7 +199,7 @@ public interface DataStoreProvider {
return null; return null;
} }
default ObservableValue<String> informationString(StoreEntryWrapper wrapper) { default ObservableValue<String> informationString(StoreSection section) {
return new SimpleStringProperty(null); return new SimpleStringProperty(null);
} }

View file

@ -68,7 +68,10 @@ public class DataStoreProviders {
throw new IllegalStateException("Not initialized"); throw new IllegalStateException("Not initialized");
} }
return (T) ALL.stream().filter(d -> d.getStoreClasses().contains(store.getClass())).findAny().orElseThrow(() -> new IllegalArgumentException("Unknown store class")); return (T) ALL.stream()
.filter(d -> d.getStoreClasses().contains(store.getClass()))
.findAny()
.orElseThrow(() -> new IllegalArgumentException("Unknown store class"));
} }
public static List<DataStoreProvider> getAll() { public static List<DataStoreProvider> getAll() {

View file

@ -16,5 +16,7 @@ public enum DataStoreUsageCategory {
@JsonProperty("desktop") @JsonProperty("desktop")
DESKTOP, DESKTOP,
@JsonProperty("group") @JsonProperty("group")
GROUP; GROUP,
@JsonProperty("serial")
SERIAL;
} }

View file

@ -14,8 +14,8 @@ public interface EnabledParentStoreProvider extends DataStoreProvider {
@Override @Override
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) { if (sec.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {
return StoreEntryComp.create(sec.getWrapper(), null, preferLarge); return StoreEntryComp.create(sec, null, preferLarge);
} }
var enabled = StoreToggleComp.<StatefulDataStore<EnabledStoreState>>enableToggle( var enabled = StoreToggleComp.<StatefulDataStore<EnabledStoreState>>enableToggle(
@ -35,6 +35,6 @@ public interface EnabledParentStoreProvider extends DataStoreProvider {
})); }));
} }
return StoreEntryComp.create(sec.getWrapper(), enabled, preferLarge); return StoreEntryComp.create(sec, enabled, preferLarge);
} }
} }

View file

@ -11,8 +11,8 @@ public interface EnabledStoreProvider extends DataStoreProvider {
@Override @Override
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) { if (sec.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {
return StoreEntryComp.create(sec.getWrapper(), null, preferLarge); return StoreEntryComp.create(sec, null, preferLarge);
} }
var enabled = StoreToggleComp.<StatefulDataStore<EnabledStoreState>>enableToggle( var enabled = StoreToggleComp.<StatefulDataStore<EnabledStoreState>>enableToggle(
@ -20,6 +20,6 @@ public interface EnabledStoreProvider extends DataStoreProvider {
var state = s.getState().toBuilder().enabled(aBoolean).build(); var state = s.getState().toBuilder().enabled(aBoolean).build();
s.setState(state); s.setState(state);
}); });
return StoreEntryComp.create(sec.getWrapper(), enabled, preferLarge); return StoreEntryComp.create(sec, enabled, preferLarge);
} }
} }

View file

@ -2,10 +2,8 @@ package io.xpipe.app.ext;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.FailableRunnable; import io.xpipe.core.util.FailableRunnable;
import io.xpipe.core.util.ModuleLayerLoader; import io.xpipe.core.util.ModuleLayerLoader;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Value; import lombok.Value;
@ -22,10 +20,6 @@ public abstract class ScanProvider {
return ALL; return ALL;
} }
public ScanOperation create(DataStore store) {
return null;
}
public ScanOperation create(DataStoreEntry entry, ShellControl sc) throws Exception { public ScanOperation create(DataStoreEntry entry, ShellControl sc) throws Exception {
return null; return null;
} }

View file

@ -30,7 +30,7 @@ public interface SingletonSessionStoreProvider extends DataStoreProvider {
@Override @Override
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) { default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
var t = createToggleComp(sec); var t = createToggleComp(sec);
return StoreEntryComp.create(sec.getWrapper(), t, preferLarge); return StoreEntryComp.create(sec, t, preferLarge);
} }
default StoreToggleComp createToggleComp(StoreSection sec) { default StoreToggleComp createToggleComp(StoreSection sec) {

View file

@ -0,0 +1,28 @@
package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import javafx.scene.layout.AnchorPane;
import java.util.List;
public class AnchorComp extends Comp<CompStructure<AnchorPane>> {
private final List<Comp<?>> comps;
public AnchorComp(List<Comp<?>> comps) {
this.comps = List.copyOf(comps);
}
@Override
public CompStructure<AnchorPane> createBase() {
var pane = new AnchorPane();
for (var c : comps) {
pane.getChildren().add(c.createRegion());
}
pane.setPickOnBounds(false);
return new SimpleCompStructure<>(pane);
}
}

View file

@ -1,5 +1,6 @@
package io.xpipe.app.fxcomps.impl; package io.xpipe.app.fxcomps.impl;
import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.session.BrowserChooserComp; import io.xpipe.app.browser.session.BrowserChooserComp;
import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
@ -15,39 +16,29 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore; import io.xpipe.core.store.FileSystemStore;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Alert; import javafx.scene.control.Alert;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardCopyOption; import java.nio.file.StandardCopyOption;
import java.util.List; import java.util.ArrayList;
public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>> { public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>> {
private final Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem; private final Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem;
private final Property<String> filePath; private final Property<String> filePath;
private final boolean allowSync;
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp( public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(
ObservableValue<DataStoreEntryRef<T>> fileSystem, Property<String> filePath) { Property<DataStoreEntryRef<T>> fileSystem, Property<String> filePath, boolean allowSync
this.fileSystem = new SimpleObjectProperty<>(); ) {
fileSystem.subscribe(val -> { this.allowSync = allowSync;
this.fileSystem.setValue(val);
});
this.filePath = filePath;
}
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(
Property<DataStoreEntryRef<T>> fileSystem, Property<String> filePath) {
this.fileSystem = new SimpleObjectProperty<>(); this.fileSystem = new SimpleObjectProperty<>();
fileSystem.subscribe(val -> { fileSystem.subscribe(val -> {
this.fileSystem.setValue(val); this.fileSystem.setValue(val);
@ -79,7 +70,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
}, },
false); false);
}) })
.styleClass(Styles.CENTER_PILL) .styleClass(allowSync ? Styles.CENTER_PILL : Styles.RIGHT_PILL)
.grow(false, true); .grow(false, true);
var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> { var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> {
@ -126,7 +117,13 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
gitShareButton.tooltipKey("gitShareFileTooltip"); gitShareButton.tooltipKey("gitShareFileTooltip");
gitShareButton.styleClass(Styles.RIGHT_PILL).grow(false, true); gitShareButton.styleClass(Styles.RIGHT_PILL).grow(false, true);
var layout = new HorizontalComp(List.of(fileNameComp, fileBrowseButton, gitShareButton)) var nodes = new ArrayList<Comp<?>>();
nodes.add(fileNameComp);
nodes.add(fileBrowseButton);
if (allowSync) {
nodes.add(gitShareButton);
}
var layout = new HorizontalComp(nodes)
.apply(struc -> struc.get().setFillHeight(true)); .apply(struc -> struc.get().setFillHeight(true));
layout.apply(struc -> { layout.apply(struc -> {

View file

@ -1,7 +1,5 @@
package io.xpipe.app.fxcomps.impl; package io.xpipe.app.fxcomps.impl;
import atlantafx.base.controls.Popover;
import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.store.*; import io.xpipe.app.comp.store.*;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
@ -15,6 +13,7 @@ import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.LocalStore; import io.xpipe.core.store.LocalStore;
import io.xpipe.core.store.ShellStore; import io.xpipe.core.store.ShellStore;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@ -26,6 +25,9 @@ import javafx.scene.control.MenuButton;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import atlantafx.base.controls.Popover;
import atlantafx.base.theme.Styles;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
@ -101,9 +103,9 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
comp.disable(new SimpleBooleanProperty(true)); comp.disable(new SimpleBooleanProperty(true));
} }
}, },
storeEntryWrapper -> { sec -> {
if (applicable.test(storeEntryWrapper)) { if (applicable.test(sec.getWrapper())) {
selected.setValue(storeEntryWrapper.getEntry().ref()); selected.setValue(sec.getWrapper().getEntry().ref());
popover.hide(); popover.hide();
} }
}); });
@ -112,22 +114,31 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
StoreViewState.get().getActiveCategory(), StoreViewState.get().getActiveCategory(),
selectedCategory) selectedCategory)
.styleClass(Styles.LEFT_PILL); .styleClass(Styles.LEFT_PILL);
var filter = var filter = new FilterComp(filterText).styleClass(Styles.CENTER_PILL).hgrow();
new FilterComp(filterText).styleClass(Styles.CENTER_PILL).hgrow();
var addButton = Comp.of(() -> { var addButton = Comp.of(() -> {
MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline")); MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline"));
m.setMaxHeight(100);
m.setMinHeight(0);
StoreCreationMenu.addButtons(m); StoreCreationMenu.addButtons(m);
return m; return m;
}) })
.accessibleTextKey("addConnection") .accessibleTextKey("addConnection")
.padding(new Insets(-2)) .padding(new Insets(-5))
.styleClass(Styles.RIGHT_PILL) .styleClass(Styles.RIGHT_PILL);
.grow(false, true);
var top = new HorizontalComp(List.of(category, filter.hgrow(), addButton)) var top = new HorizontalComp(List.of(category, filter, addButton))
.styleClass("top") .styleClass("top")
.apply(struc -> struc.get().setFillHeight(true)) .apply(struc -> struc.get().setFillHeight(true))
.apply(struc -> {
var first = ((Region) struc.get().getChildren().get(0));
var second = ((Region) struc.get().getChildren().get(1));
var third = ((Region) struc.get().getChildren().get(1));
second.prefHeightProperty().bind(first.heightProperty());
second.minHeightProperty().bind(first.heightProperty());
second.maxHeightProperty().bind(first.heightProperty());
third.prefHeightProperty().bind(first.heightProperty());
})
.apply(struc -> { .apply(struc -> {
// Ugly solution to focus the text field // Ugly solution to focus the text field
// Somehow this does not work through the normal on shown listeners // Somehow this does not work through the normal on shown listeners

View file

@ -53,7 +53,7 @@ public class DataStoreListChoiceComp<T extends DataStore> extends SimpleComp {
}); });
return new HorizontalComp(List.of(label, Comp.hspacer(), delete)).styleClass("entry"); return new HorizontalComp(List.of(label, Comp.hspacer(), delete)).styleClass("entry");
}, },
true) false)
.padding(new Insets(0)) .padding(new Insets(0))
.apply(struc -> struc.get().setMinHeight(0)) .apply(struc -> struc.get().setMinHeight(0))
.apply(struc -> ((VBox) struc.get().getContent()).setSpacing(5)); .apply(struc -> ((VBox) struc.get().getContent()).setSpacing(5));

View file

@ -1,17 +1,21 @@
package io.xpipe.app.fxcomps.impl; package io.xpipe.app.fxcomps.impl;
import atlantafx.base.controls.CustomTextField;
import io.xpipe.app.core.AppActionLinkDetector; import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.geometry.Pos;
import javafx.scene.Cursor; import javafx.scene.Cursor;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton; import javafx.scene.input.MouseButton;
import atlantafx.base.controls.CustomTextField;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.Objects; import java.util.Objects;
@ -36,16 +40,29 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
} }
}); });
var filter = new CustomTextField(); var filter = new CustomTextField();
filter.alignmentProperty().bind(Bindings.createObjectBinding(() -> { filter.setMinHeight(0);
return filter.isFocused() || (filter.getText() != null && !filter.getText().isEmpty()) ? Pos.CENTER_LEFT : Pos.CENTER;
}, filter.textProperty(), filter.focusedProperty()));
filter.setMaxHeight(2000); filter.setMaxHeight(2000);
filter.getStyleClass().add("filter-comp"); filter.getStyleClass().add("filter-comp");
filter.promptTextProperty().bind(AppI18n.observable("searchFilter")); filter.promptTextProperty().bind(AppI18n.observable("searchFilter"));
filter.setLeft(fi); filter.rightProperty()
filter.setRight(clear); .bind(Bindings.createObjectBinding(
() -> {
return filter.isFocused()
|| (filter.getText() != null
&& !filter.getText().isEmpty())
? clear
: fi;
},
filter.focusedProperty()));
filter.setAccessibleText("Filter"); filter.setAccessibleText("Filter");
filter.addEventFilter(KeyEvent.KEY_PRESSED,event -> {
if (new KeyCodeCombination(KeyCode.ESCAPE).match(event)) {
filter.getScene().getRoot().requestFocus();
event.consume();
}
});
filterText.subscribe(val -> { filterText.subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
clear.setVisible(val != null); clear.setVisible(val != null);

Some files were not shown because too many files have changed in this diff Show more