mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 23:20:23 +00:00
Fixes
This commit is contained in:
parent
70ba263ec4
commit
79d09c021e
344 changed files with 5671 additions and 2610 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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
|
||||||
|
|
11
README.md
11
README.md
|
@ -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 XPipe’s features via an HTTP interface
|
- The XPipe API provides programmatic access to XPipe’s 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
|
||||||
|
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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));
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +109,7 @@ public class BeaconRequestHandler<T> implements HttpHandler {
|
||||||
// Make deserialization error message more readable
|
// Make deserialization error message more readable
|
||||||
var message = ex.getMessage()
|
var message = ex.getMessage()
|
||||||
.replace("$RequestBuilder", "")
|
.replace("$RequestBuilder", "")
|
||||||
.replace("Exchange$Request","Request")
|
.replace("Exchange$Request", "Request")
|
||||||
.replace("at [Source: UNKNOWN; byte offset: #UNKNOWN]", "")
|
.replace("at [Source: UNKNOWN; byte offset: #UNKNOWN]", "")
|
||||||
.replaceAll("(\\w+) is marked non-null but is null", "field $1 is missing from object")
|
.replaceAll("(\\w+) is marked non-null but is null", "field $1 is missing from object")
|
||||||
.trim();
|
.trim();
|
||||||
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
@ -18,7 +19,7 @@ public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
|
||||||
throw new BeaconClientException("Not a shell connection");
|
throw new BeaconClientException("Not a shell connection");
|
||||||
}
|
}
|
||||||
try (var sc = shellStore.control().start()) {
|
try (var sc = shellStore.control().start()) {
|
||||||
TerminalLauncher.open(e,e.getName(),msg.getDirectory(),sc);
|
TerminalLauncher.open(e, e.getName(), msg.getDirectory(), sc);
|
||||||
}
|
}
|
||||||
return Response.builder().build();
|
return Response.builder().build();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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());
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,172 +39,151 @@ 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(
|
.filter(item -> item.getBrowserEntry() == entry)
|
||||||
|
.findAny();
|
||||||
|
if (sourceItem.isEmpty()) {
|
||||||
|
return new SimpleStringProperty("?");
|
||||||
|
}
|
||||||
|
synchronized (sourceItem.get().getProgress()) {
|
||||||
|
return Bindings.createStringBinding(
|
||||||
() -> {
|
() -> {
|
||||||
var sourceItem = syncItems.stream()
|
var p = sourceItem.get().getProgress().getValue();
|
||||||
.filter(item -> item.getBrowserEntry() == entry)
|
var progressSuffix = p == null
|
||||||
.findAny();
|
|
||||||
if (sourceItem.isEmpty()) {
|
|
||||||
return "?";
|
|
||||||
}
|
|
||||||
var name = entry.getModel() == 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", () -> {
|
||||||
model.clear(true);
|
ThreadHelper.runAsync(() -> {
|
||||||
|
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 -> {
|
// Accept drops from inside the app window
|
||||||
// Accept drops from inside the app window
|
if (event.getGestureSource() != null && event.getGestureSource() != struc.get()) {
|
||||||
if (event.getGestureSource() != null && event.getGestureSource() != struc.get()) {
|
event.acceptTransferModes(TransferMode.ANY);
|
||||||
event.acceptTransferModes(TransferMode.ANY);
|
event.consume();
|
||||||
event.consume();
|
}
|
||||||
}
|
});
|
||||||
|
struc.get().setOnDragDropped(event -> {
|
||||||
|
// Accept drops from inside the app window
|
||||||
|
if (event.getGestureSource() != null) {
|
||||||
|
var drag = BrowserClipboard.retrieveDrag(event.getDragboard());
|
||||||
|
if (drag == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Accept drops from outside the app window
|
if (!(model.getBrowserSessionModel()
|
||||||
if (event.getGestureSource() == null
|
.getSelectedEntry()
|
||||||
&& !event.getDragboard().getFiles().isEmpty()) {
|
.getValue()
|
||||||
event.acceptTransferModes(TransferMode.ANY);
|
instanceof OpenFileSystemModel fileSystemModel)) {
|
||||||
event.consume();
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
struc.get().setOnDragDropped(event -> {
|
var files = drag.getEntries();
|
||||||
// Accept drops from inside the app window
|
model.drop(fileSystemModel, files);
|
||||||
if (event.getGestureSource() != null) {
|
event.setDropCompleted(true);
|
||||||
var drag = BrowserClipboard.retrieveDrag(event.getDragboard());
|
event.consume();
|
||||||
if (drag == null) {
|
}
|
||||||
return;
|
});
|
||||||
|
struc.get().setOnDragDetected(event -> {
|
||||||
|
var items = model.getCurrentItems();
|
||||||
|
var selected = items.stream()
|
||||||
|
.map(item -> item.getBrowserEntry())
|
||||||
|
.toList();
|
||||||
|
var files = items.stream()
|
||||||
|
.filter(item -> item.downloadFinished().get())
|
||||||
|
.map(item -> {
|
||||||
|
try {
|
||||||
|
var file = item.getLocalFile();
|
||||||
|
if (!Files.exists(file)) {
|
||||||
|
return Optional.<File>empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.of(file.toRealPath().toFile());
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.flatMap(Optional::stream)
|
||||||
|
.toList();
|
||||||
|
if (files.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!(model.getBrowserSessionModel()
|
var cc = new ClipboardContent();
|
||||||
.getSelectedEntry()
|
cc.putFiles(files);
|
||||||
.getValue()
|
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
|
||||||
instanceof OpenFileSystemModel fileSystemModel)) {
|
db.setContent(cc);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var files = drag.getEntries();
|
Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
|
||||||
model.drop(fileSystemModel, files);
|
db.setDragView(image, -20, 15);
|
||||||
event.setDropCompleted(true);
|
|
||||||
event.consume();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accept drops from outside the app window
|
event.setDragDetect(true);
|
||||||
if (event.getGestureSource() == null) {
|
event.consume();
|
||||||
model.dropLocal(event.getDragboard().getFiles());
|
});
|
||||||
event.setDropCompleted(true);
|
struc.get().setOnDragDone(event -> {
|
||||||
event.consume();
|
if (!event.isAccepted()) {
|
||||||
}
|
return;
|
||||||
});
|
}
|
||||||
struc.get().setOnDragDetected(event -> {
|
|
||||||
if (syncDownloaded.getValue()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selected = syncItems.stream()
|
// The files might not have been transferred yet
|
||||||
.map(item -> item.getBrowserEntry())
|
// We can't listen to this, so just don't delete them
|
||||||
.toList();
|
model.clear(false);
|
||||||
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
|
event.consume();
|
||||||
|
});
|
||||||
var cc = BrowserClipboard.startDrag(null, selected, BrowserFileTransferMode.NORMAL);
|
});
|
||||||
if (cc == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var files = syncItems.stream()
|
|
||||||
.filter(item -> item.downloadFinished().get())
|
|
||||||
.map(item -> {
|
|
||||||
try {
|
|
||||||
var file = item.getLocalFile();
|
|
||||||
if (!Files.exists(file)) {
|
|
||||||
return Optional.<File>empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Optional.of(
|
|
||||||
file.toRealPath().toFile());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.flatMap(Optional::stream)
|
|
||||||
.toList();
|
|
||||||
cc.putFiles(files);
|
|
||||||
db.setContent(cc);
|
|
||||||
|
|
||||||
Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
|
|
||||||
db.setDragView(image, -20, 15);
|
|
||||||
|
|
||||||
event.setDragDetect(true);
|
|
||||||
event.consume();
|
|
||||||
});
|
|
||||||
struc.get().setOnDragDone(event -> {
|
|
||||||
if (!event.isAccepted()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The files might not have been transferred yet
|
|
||||||
// We can't listen to this, so just don't delete them
|
|
||||||
model.clear(false);
|
|
||||||
event.consume();
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
syncDownloaded);
|
|
||||||
|
|
||||||
stack.apply(struc -> {
|
stack.apply(struc -> {
|
||||||
model.getBrowserSessionModel().getDraggingFiles().addListener((observable, oldValue, newValue) -> {
|
model.getBrowserSessionModel().getDraggingFiles().addListener((observable, oldValue, newValue) -> {
|
||||||
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("highlighted"),newValue);
|
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("highlighted"), newValue);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,136 +21,156 @@ 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) {
|
||||||
entries.forEach(entry -> {
|
synchronized (items) {
|
||||||
var name = entry.getFileName();
|
entries.forEach(entry -> {
|
||||||
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Path file = TEMP.resolve(name);
|
|
||||||
var item = new Item(model, name, entry, file);
|
|
||||||
items.add(item);
|
|
||||||
allDownloaded.set(false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dropLocal(List<File> entries) {
|
|
||||||
if (entries.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var empty = items.isEmpty();
|
|
||||||
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();
|
var name = entry.getFileName();
|
||||||
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
|
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var item = new Item(null, name, entry, path);
|
Path file = TEMP.resolve(name);
|
||||||
item.progress.setValue(BrowserTransferProgress.finished(
|
var item = new Item(model, name, entry, file);
|
||||||
entry.getFileName(), entry.getRawFileEntry().getSize()));
|
|
||||||
items.add(item);
|
items.add(item);
|
||||||
}
|
});
|
||||||
} catch (Exception ex) {
|
|
||||||
ErrorEvent.fromThrowable(ex).handle();
|
|
||||||
}
|
|
||||||
if (empty) {
|
|
||||||
allDownloaded.set(true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void download() {
|
public void downloadSingle(Item item) {
|
||||||
executor.submit(() -> {
|
try {
|
||||||
try {
|
FileUtils.forceMkdir(TEMP.toFile());
|
||||||
FileUtils.forceMkdir(TEMP.toFile());
|
} catch (IOException e) {
|
||||||
} catch (IOException e) {
|
ErrorEvent.fromThrowable(e).handle();
|
||||||
ErrorEvent.fromThrowable(e).handle();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.downloadFinished().get()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.getOpenFileSystemModel() != null
|
||||||
|
&& item.getOpenFileSystemModel().isClosed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var op = new BrowserFileTransferOperation(
|
||||||
|
LocalFileSystem.getLocalFileEntry(TEMP),
|
||||||
|
List.of(item.getBrowserEntry().getRawFileEntry()),
|
||||||
|
BrowserFileTransferMode.COPY,
|
||||||
|
false,
|
||||||
|
progress -> {
|
||||||
|
synchronized (item.getProgress()) {
|
||||||
|
item.getProgress().setValue(progress);
|
||||||
|
}
|
||||||
|
item.getOpenFileSystemModel().getProgress().setValue(progress);
|
||||||
|
});
|
||||||
|
op.execute();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
ErrorEvent.fromThrowable(t).handle();
|
||||||
|
synchronized (items) {
|
||||||
|
items.remove(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void transferToDownloads() throws Exception {
|
||||||
|
List<Item> toMove;
|
||||||
|
synchronized (items) {
|
||||||
|
toMove =
|
||||||
|
items.stream().filter(item -> item.downloadFinished().get()).toList();
|
||||||
|
if (toMove.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
items.removeAll(toMove);
|
||||||
|
}
|
||||||
|
|
||||||
for (Item item : new ArrayList<>(items)) {
|
var files = toMove.stream().map(item -> item.getLocalFile()).toList();
|
||||||
if (item.downloadFinished().get()) {
|
var downloads = DesktopHelper.getDownloadsDirectory();
|
||||||
continue;
|
for (Path file : files) {
|
||||||
}
|
var target = downloads.resolve(file.getFileName());
|
||||||
|
// Prevent DirectoryNotEmptyException
|
||||||
if (item.getOpenFileSystemModel() != null
|
if (Files.exists(target) && Files.isDirectory(target)) {
|
||||||
&& item.getOpenFileSystemModel().isClosed()) {
|
Files.delete(target);
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
try (var ignored = new BooleanScope(downloading).start()) {
|
|
||||||
var op = new BrowserFileTransferOperation(
|
|
||||||
LocalFileSystem.getLocalFileEntry(TEMP),
|
|
||||||
List.of(item.getBrowserEntry().getRawFileEntry()),
|
|
||||||
BrowserFileTransferMode.COPY,
|
|
||||||
false,
|
|
||||||
progress -> {
|
|
||||||
item.getProgress().setValue(progress);
|
|
||||||
item.getOpenFileSystemModel().getProgress().setValue(progress);
|
|
||||||
});
|
|
||||||
op.execute();
|
|
||||||
}
|
|
||||||
} catch (Throwable t) {
|
|
||||||
ErrorEvent.fromThrowable(t).handle();
|
|
||||||
items.remove(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
allDownloaded.set(true);
|
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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) + " ...";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,62 +218,85 @@ public class BrowserFileTransferOperation {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputStream inputStream = null;
|
transfer(sourceFile, targetFile, transferred, totalSize, start);
|
||||||
OutputStream outputStream = null;
|
|
||||||
try {
|
|
||||||
var fileSize = sourceFile.getFileSystem().getFileSize(sourceFile.getPath());
|
|
||||||
inputStream = sourceFile.getFileSystem().openInput(sourceFile.getPath());
|
|
||||||
outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
|
|
||||||
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start);
|
|
||||||
inputStream.transferTo(OutputStream.nullOutputStream());
|
|
||||||
} catch (Exception ex) {
|
|
||||||
// Mark progress as finished to reset any progress display
|
|
||||||
updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
|
|
||||||
|
|
||||||
if (inputStream != null) {
|
|
||||||
try {
|
|
||||||
inputStream.close();
|
|
||||||
} catch (Exception om) {
|
|
||||||
// This is expected as the process control has to be killed
|
|
||||||
// When calling close, it will throw an exception when it has to kill
|
|
||||||
// ErrorEvent.fromThrowable(om).handle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (outputStream != null) {
|
|
||||||
try {
|
|
||||||
outputStream.close();
|
|
||||||
} catch (Exception om) {
|
|
||||||
// This is expected as the process control has to be killed
|
|
||||||
// When calling close, it will throw an exception when it has to kill
|
|
||||||
// ErrorEvent.fromThrowable(om).handle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw ex;
|
|
||||||
}
|
|
||||||
|
|
||||||
Exception exception = null;
|
|
||||||
try {
|
|
||||||
inputStream.close();
|
|
||||||
} catch (Exception om) {
|
|
||||||
exception = om;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
outputStream.close();
|
|
||||||
} catch (Exception om) {
|
|
||||||
if (exception != null) {
|
|
||||||
ErrorEvent.fromThrowable(om).handle();
|
|
||||||
} else {
|
|
||||||
exception = om;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (exception != null) {
|
|
||||||
throw exception;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
|
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;
|
||||||
|
OutputStream outputStream = null;
|
||||||
|
try {
|
||||||
|
var fileSize = sourceFile.getFileSystem().getFileSize(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);
|
||||||
|
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start);
|
||||||
|
inputStream.transferTo(OutputStream.nullOutputStream());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Mark progress as finished to reset any progress display
|
||||||
|
updateProgress(BrowserTransferProgress.finished(sourceFile.getName(), transferred.get()));
|
||||||
|
|
||||||
|
if (inputStream != null) {
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (Exception om) {
|
||||||
|
// This is expected as the process control has to be killed
|
||||||
|
// When calling close, it will throw an exception when it has to kill
|
||||||
|
// ErrorEvent.fromThrowable(om).handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (outputStream != null) {
|
||||||
|
try {
|
||||||
|
outputStream.close();
|
||||||
|
} catch (Exception om) {
|
||||||
|
// This is expected as the process control has to be killed
|
||||||
|
// When calling close, it will throw an exception when it has to kill
|
||||||
|
// ErrorEvent.fromThrowable(om).handle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
|
||||||
|
Exception exception = null;
|
||||||
|
try {
|
||||||
|
inputStream.close();
|
||||||
|
} catch (Exception om) {
|
||||||
|
exception = om;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
outputStream.close();
|
||||||
|
} catch (Exception om) {
|
||||||
|
if (exception != null) {
|
||||||
|
ErrorEvent.fromThrowable(om).handle();
|
||||||
|
} else {
|
||||||
|
exception = om;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exception != null) {
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,32 +72,38 @@ 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;
|
||||||
// After 10 seconds
|
|
||||||
TIMEOUT_TIMER.schedule(
|
|
||||||
new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Synchronize with platform thread
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
if (model.isClosed()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Objects.equals(lastDirectory, dir)) {
|
if (delay) {
|
||||||
updateRecent(dir);
|
// After 10 seconds
|
||||||
save();
|
TIMEOUT_TIMER.schedule(
|
||||||
}
|
new TimerTask() {
|
||||||
});
|
@Override
|
||||||
}
|
public void run() {
|
||||||
},
|
// Synchronize with platform thread
|
||||||
10000);
|
Platform.runLater(() -> {
|
||||||
|
if (model.isClosed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Objects.equals(lastDirectory, dir)) {
|
||||||
|
updateRecent(dir);
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
10000);
|
||||||
|
} else {
|
||||||
|
updateRecent(dir);
|
||||||
|
save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateRecent(String dir) {
|
private void updateRecent(String dir) {
|
||||||
|
|
|
@ -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(() -> {
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,20 +80,23 @@ 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()) {
|
||||||
model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL);
|
try (var sessionBusy = new BooleanScope(busy).exclusive().start()) {
|
||||||
model.init();
|
model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL);
|
||||||
// Prevent multiple calls from interfering with each other
|
model.init();
|
||||||
synchronized (BrowserSessionModel.this) {
|
// Prevent multiple calls from interfering with each other
|
||||||
sessionEntries.add(model);
|
synchronized (BrowserSessionModel.this) {
|
||||||
// The tab pane doesn't automatically select new tabs
|
sessionEntries.add(model);
|
||||||
selectedEntry.setValue(model);
|
// The tab pane doesn't automatically select new tabs
|
||||||
|
selectedEntry.setValue(model);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (path != null) {
|
if (path != null) {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
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();
|
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);
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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()));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -41,14 +42,14 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||||
var selectedBorder = Bindings.createObjectBinding(
|
var selectedBorder = Bindings.createObjectBinding(
|
||||||
() -> {
|
() -> {
|
||||||
var c = Platform.getPreferences().getAccentColor().desaturate();
|
var c = Platform.getPreferences().getAccentColor().desaturate();
|
||||||
return new Background(new BackgroundFill(c,new CornerRadii(8), new Insets(10, 1, 10, 2)));
|
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(10, 1, 10, 2)));
|
||||||
},
|
},
|
||||||
Platform.getPreferences().accentColorProperty());
|
Platform.getPreferences().accentColorProperty());
|
||||||
|
|
||||||
var hoverBorder = Bindings.createObjectBinding(
|
var hoverBorder = Bindings.createObjectBinding(
|
||||||
() -> {
|
() -> {
|
||||||
var c = Platform.getPreferences().getAccentColor().darker().desaturate();
|
var c = Platform.getPreferences().getAccentColor().darker().desaturate();
|
||||||
return new Background(new BackgroundFill(c,new CornerRadii(8), new Insets(10, 1, 10, 2)));
|
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(10, 1, 10, 2)));
|
||||||
},
|
},
|
||||||
Platform.getPreferences().accentColorProperty());
|
Platform.getPreferences().accentColorProperty());
|
||||||
|
|
||||||
|
@ -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(
|
||||||
() -> {
|
() -> {
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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())) {
|
||||||
|
|
|
@ -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);
|
||||||
|
@ -163,7 +168,12 @@ public class StoreCreationComp extends DialogComp {
|
||||||
if (!DataStorage.get().getStoreEntries().contains(e)) {
|
if (!DataStorage.get().getStoreEntries().contains(e)) {
|
||||||
DataStorage.get().addStoreEntryIfNotPresent(newE);
|
DataStorage.get().addStoreEntryIfNotPresent(newE);
|
||||||
} else {
|
} else {
|
||||||
DataStorage.get().updateEntry(e, newE);
|
// We didn't change anything
|
||||||
|
if (e.getStore().equals(newE.getStore())) {
|
||||||
|
e.setName(newE.getName());
|
||||||
|
} else {
|
||||||
|
DataStorage.get().updateEntry(e, newE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -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");
|
||||||
layout.setTop(top);
|
if (showProviders) {
|
||||||
|
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);
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,18 +171,18 @@ 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()
|
||||||
.getApplicableClass()
|
.getApplicableClass()
|
||||||
.isAssignableFrom(entry.getStore().getClass())
|
.isAssignableFrom(entry.getStore().getClass())
|
||||||
&& e.getDefaultDataStoreCallSite().isApplicable(entry.ref()))
|
&& e.getDefaultDataStoreCallSite().isApplicable(entry.ref()))
|
||||||
.findFirst()
|
.findFirst()
|
||||||
.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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -163,10 +163,10 @@ public class StoreSection {
|
||||||
var allChildren = all.filtered(
|
var allChildren = all.filtered(
|
||||||
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);
|
||||||
|
|
||||||
// is children. This check is fast as the children are cached in the storage
|
// is children. This check is fast as the children are cached in the storage
|
||||||
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry())
|
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry())
|
||||||
|
|
|
@ -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()),
|
||||||
() -> {
|
() -> {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
+ ". Please make sure that you have the appropriate permissions and no Antivirus program is blocking the access. "
|
||||||
"Unable to access directory " + dataDirectory
|
+ "In case you use cloud storage, verify that your cloud storage is working and you are logged in.", e)
|
||||||
+ ". 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."))
|
|
||||||
.term()
|
.term()
|
||||||
.expected()
|
.expected()
|
||||||
.handle();
|
.handle();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,24 +57,50 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
var ctrl = new NativeWinWindowControl(stage);
|
switch (OsType.getLocal()) {
|
||||||
ctrl.setWindowAttribute(
|
case OsType.Linux linux -> {}
|
||||||
NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
|
case OsType.MacOs macOs -> {
|
||||||
AppPrefs.get().theme.getValue().isDark());
|
var ctrl = new NativeMacOsWindowControl(stage);
|
||||||
boolean seamlessFrame;
|
var seamlessFrame = !AppPrefs.get().performanceMode().get() && mergeFrame();
|
||||||
if (AppPrefs.get().performanceMode().get() || !mergeFrame()) {
|
var seamlessFrameApplied = ctrl.setAppearance(
|
||||||
seamlessFrame = false;
|
seamlessFrame, AppPrefs.get().theme.getValue().isDark())
|
||||||
} else {
|
&& seamlessFrame;
|
||||||
seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT);
|
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);
|
||||||
|
ctrl.setWindowAttribute(
|
||||||
|
NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
|
||||||
|
AppPrefs.get().theme.getValue().isDark());
|
||||||
|
boolean seamlessFrame;
|
||||||
|
if (AppPrefs.get().performanceMode().get() || !mergeFrame()) {
|
||||||
|
seamlessFrame = false;
|
||||||
|
} else {
|
||||||
|
seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT);
|
||||||
|
}
|
||||||
|
stage.getScene()
|
||||||
|
.getRoot()
|
||||||
|
.pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrame);
|
||||||
|
stage.getScene()
|
||||||
|
.getRoot()
|
||||||
|
.pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrame);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
stage.getScene().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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,5 +9,6 @@ public enum DataStoreCreationCategory {
|
||||||
TUNNEL,
|
TUNNEL,
|
||||||
SCRIPT,
|
SCRIPT,
|
||||||
CLUSTER,
|
CLUSTER,
|
||||||
DESKTOP
|
DESKTOP,
|
||||||
|
SERIAL
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -16,5 +16,7 @@ public enum DataStoreUsageCategory {
|
||||||
@JsonProperty("desktop")
|
@JsonProperty("desktop")
|
||||||
DESKTOP,
|
DESKTOP,
|
||||||
@JsonProperty("group")
|
@JsonProperty("group")
|
||||||
GROUP;
|
GROUP,
|
||||||
|
@JsonProperty("serial")
|
||||||
|
SERIAL;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
28
app/src/main/java/io/xpipe/app/fxcomps/impl/AnchorComp.java
Normal file
28
app/src/main/java/io/xpipe/app/fxcomps/impl/AnchorComp.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue