mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 15:10: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/**/bin
|
||||
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
|
||||
- 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
|
||||
|
||||
|
@ -40,7 +40,7 @@ It currently supports:
|
|||
- 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
|
||||
|
||||
![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
|
||||
|
||||
|
@ -52,7 +52,7 @@ It currently supports:
|
|||
|
||||
<br>
|
||||
<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>
|
||||
<br>
|
||||
|
||||
|
@ -63,7 +63,7 @@ It currently supports:
|
|||
- 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
|
||||
|
||||
![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
|
||||
|
||||
|
@ -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.
|
||||
- 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
|
||||
- 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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
@ -23,8 +23,8 @@ dependencies {
|
|||
api project(':beacon')
|
||||
|
||||
compileOnly 'org.hamcrest:hamcrest:2.2'
|
||||
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.2'
|
||||
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.2'
|
||||
compileOnly 'org.junit.jupiter:junit-jupiter-api:5.10.3'
|
||||
compileOnly 'org.junit.jupiter:junit-jupiter-params:5.10.3'
|
||||
|
||||
api 'com.vladsch.flexmark:flexmark: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/vernacular-1.16.jar")
|
||||
api 'org.bouncycastle:bcprov-jdk18on:1.78.1'
|
||||
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'
|
||||
}
|
||||
api 'org.apache.commons:commons-lang3:3.14.0'
|
||||
api 'io.sentry:sentry:7.10.0'
|
||||
api 'org.apache.commons:commons-lang3:3.16.0'
|
||||
api 'io.sentry:sentry:7.13.0'
|
||||
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.datatype', name: 'jackson-datatype-jsr310', 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.2"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
|
||||
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"
|
||||
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.slf4j', name: 'slf4j-api', version: '2.0.13'
|
||||
api group: 'org.slf4j', name: 'slf4j-jdk-platform-logging', 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.15'
|
||||
api 'io.xpipe:modulefs:0.1.5'
|
||||
api 'net.synedra:validatorfx:0.4.2'
|
||||
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.fullVersion', rootProject.fullVersion
|
||||
systemProperty 'io.xpipe.app.staging', isStage
|
||||
// systemProperty 'io.xpipe.beacon.port', "30000"
|
||||
|
||||
// Apply passed xpipe properties
|
||||
for (final def e in System.getProperties().entrySet()) {
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
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.issue.ErrorEvent;
|
||||
import io.xpipe.app.issue.TrackEvent;
|
||||
import io.xpipe.app.util.MarkdownHelper;
|
||||
import io.xpipe.beacon.BeaconConfig;
|
||||
import io.xpipe.beacon.BeaconInterface;
|
||||
import io.xpipe.core.process.OsType;
|
||||
import io.xpipe.core.util.XPipeInstallation;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -84,6 +85,7 @@ public class AppBeaconServer {
|
|||
public static void reset() {
|
||||
if (INSTANCE != null) {
|
||||
INSTANCE.stop();
|
||||
INSTANCE.deleteAuthSecret();
|
||||
INSTANCE = null;
|
||||
}
|
||||
}
|
||||
|
@ -109,11 +111,22 @@ public class AppBeaconServer {
|
|||
var file = XPipeInstallation.getLocalBeaconAuthFile();
|
||||
var id = UUID.randomUUID().toString();
|
||||
Files.writeString(file, id);
|
||||
if (OsType.getLocal() != OsType.WINDOWS) {
|
||||
Files.setPosixFilePermissions(file, PosixFilePermissions.fromString("rw-rw----"));
|
||||
}
|
||||
localAuthSecret = id;
|
||||
}
|
||||
|
||||
private void deleteAuthSecret() {
|
||||
var file = XPipeInstallation.getLocalBeaconAuthFile();
|
||||
try {
|
||||
Files.delete(file);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
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 -> {
|
||||
server.createContext(beaconInterface.getPath(), new BeaconRequestHandler<>(beaconInterface));
|
||||
});
|
||||
|
|
|
@ -28,7 +28,8 @@ public class BeaconRequestHandler<T> implements HttpHandler {
|
|||
|
||||
@Override
|
||||
public void handle(HttpExchange exchange) {
|
||||
if (OperationMode.isInShutdown()) {
|
||||
if (OperationMode.isInShutdown() && !beaconInterface.acceptInShutdown()) {
|
||||
writeError(exchange, new BeaconClientErrorResponse("Daemon is currently in shutdown"), 400);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -108,7 +109,7 @@ public class BeaconRequestHandler<T> implements HttpHandler {
|
|||
// Make deserialization error message more readable
|
||||
var message = ex.getMessage()
|
||||
.replace("$RequestBuilder", "")
|
||||
.replace("Exchange$Request","Request")
|
||||
.replace("Exchange$Request", "Request")
|
||||
.replace("at [Source: UNKNOWN; byte offset: #UNKNOWN]", "")
|
||||
.replaceAll("(\\w+) is marked non-null but is null", "field $1 is missing from object")
|
||||
.trim();
|
||||
|
@ -124,10 +125,13 @@ public class BeaconRequestHandler<T> implements HttpHandler {
|
|||
try {
|
||||
var emptyResponseClass = beaconInterface.getResponseClass().getDeclaredFields().length == 0;
|
||||
if (!emptyResponseClass && response != null) {
|
||||
TrackEvent.trace("Sending response:\n" + object);
|
||||
var tree = JacksonMapper.getDefault().valueToTree(response);
|
||||
TrackEvent.trace("Sending raw response:\n" + tree.toPrettyString());
|
||||
var bytes = tree.toPrettyString().getBytes(StandardCharsets.UTF_8);
|
||||
TrackEvent.trace("Sending response:\n" + response);
|
||||
TrackEvent.trace("Sending raw response:\n"
|
||||
+ JacksonMapper.getCensored().valueToTree(response).toPrettyString());
|
||||
var bytes = JacksonMapper.getDefault()
|
||||
.valueToTree(response)
|
||||
.toPrettyString()
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, bytes.length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(bytes);
|
||||
|
|
|
@ -50,7 +50,7 @@ public class BlobManager {
|
|||
|
||||
public Path newBlobFile() throws IOException {
|
||||
var file = TEMP.resolve(UUID.randomUUID().toString());
|
||||
Files.createDirectories(file.getParent());
|
||||
FileUtils.forceMkdir(file.getParent().toFile());
|
||||
return file;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.beacon.api.ConnectionAddExchange;
|
||||
import io.xpipe.core.util.ValidationException;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
|
@ -8,6 +7,8 @@ import io.xpipe.beacon.BeaconClientException;
|
|||
import io.xpipe.beacon.api.ConnectionBrowseExchange;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
|
||||
|
||||
@Override
|
||||
|
@ -18,7 +19,8 @@ public class ConnectionBrowseExchangeImpl extends ConnectionBrowseExchange {
|
|||
if (!(e.getStore() instanceof FileSystemStore)) {
|
||||
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();
|
||||
return Response.builder().build();
|
||||
}
|
||||
|
|
|
@ -28,9 +28,16 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange {
|
|||
.orElseThrow())
|
||||
.getNames();
|
||||
var cat = new StorePath(names.subList(1, names.size()));
|
||||
var cache = e.getStoreCache().entrySet().stream().filter(stringObjectEntry -> {
|
||||
return stringObjectEntry.getValue() != null && (ClassUtils.isPrimitiveOrWrapper(stringObjectEntry.getValue().getClass()) || stringObjectEntry.getValue() instanceof String);
|
||||
}).collect(Collectors.toMap(stringObjectEntry -> stringObjectEntry.getKey(),stringObjectEntry -> stringObjectEntry.getValue()));
|
||||
var cache = e.getStoreCache().entrySet().stream()
|
||||
.filter(stringObjectEntry -> {
|
||||
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()
|
||||
.lastModified(e.getLastModified())
|
||||
|
@ -50,27 +57,17 @@ public class ConnectionInfoExchangeImpl extends ConnectionInfoExchange {
|
|||
}
|
||||
|
||||
private Class<?> toWrapper(Class<?> clazz) {
|
||||
if (!clazz.isPrimitive())
|
||||
return clazz;
|
||||
if (!clazz.isPrimitive()) return clazz;
|
||||
|
||||
if (clazz == Integer.TYPE)
|
||||
return Integer.class;
|
||||
if (clazz == Long.TYPE)
|
||||
return Long.class;
|
||||
if (clazz == Boolean.TYPE)
|
||||
return Boolean.class;
|
||||
if (clazz == Byte.TYPE)
|
||||
return Byte.class;
|
||||
if (clazz == Character.TYPE)
|
||||
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;
|
||||
if (clazz == Integer.TYPE) return Integer.class;
|
||||
if (clazz == Long.TYPE) return Long.class;
|
||||
if (clazz == Boolean.TYPE) return Boolean.class;
|
||||
if (clazz == Byte.TYPE) return Byte.class;
|
||||
if (clazz == Character.TYPE) 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;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.FixedHierarchyStore;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.api.ConnectionRefreshExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public class ConnectionRefreshExchangeImpl extends ConnectionRefreshExchange {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.api.ConnectionRemoveExchange;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.util.TerminalLauncher;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.api.ConnectionTerminalExchange;
|
||||
import io.xpipe.core.store.ShellStore;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
|
||||
|
||||
@Override
|
||||
|
@ -18,7 +19,7 @@ public class ConnectionTerminalExchangeImpl extends ConnectionTerminalExchange {
|
|||
throw new BeaconClientException("Not a shell connection");
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package io.xpipe.app.beacon.impl;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.beacon.BeaconClientException;
|
||||
import io.xpipe.beacon.api.ConnectionToggleExchange;
|
||||
import io.xpipe.core.store.SingletonSessionStore;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
|
||||
public class ConnectionToggleExchangeImpl extends ConnectionToggleExchange {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -36,6 +36,7 @@ public class ShellStartExchangeImpl extends ShellStartExchange {
|
|||
.osType(control.getOsType())
|
||||
.osName(control.getOsName())
|
||||
.temp(control.getSystemTemporaryDirectory())
|
||||
.ttyState(control.getTtyState())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,12 @@ import io.xpipe.app.storage.DataStoreEntry;
|
|||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
|
@ -41,13 +43,13 @@ public final class BrowserBookmarkComp extends SimpleComp {
|
|||
|
||||
@Override
|
||||
protected Region createSimple() {
|
||||
BooleanProperty busy = new SimpleBooleanProperty(false);
|
||||
var busyEntries = FXCollections.<StoreSection>observableSet(new HashSet<>());
|
||||
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (s, comp) -> {
|
||||
comp.disable(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return busy.get() || !applicable.test(s.getWrapper());
|
||||
return busyEntries.contains(s) || !applicable.test(s.getWrapper());
|
||||
},
|
||||
busy));
|
||||
busyEntries));
|
||||
comp.apply(struc -> {
|
||||
selected.addListener((observable, oldValue, newValue) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
|
@ -70,7 +72,17 @@ public final class BrowserBookmarkComp extends SimpleComp {
|
|||
category,
|
||||
StoreViewState.get().getEntriesListUpdateObservable()),
|
||||
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();
|
||||
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.StoreViewState;
|
||||
import io.xpipe.app.core.AppFont;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.impl.FilterComp;
|
||||
import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
||||
|
@ -30,18 +31,26 @@ public final class BrowserBookmarkHeaderComp extends SimpleComp {
|
|||
StoreViewState.get().getAllConnectionsCategory(),
|
||||
StoreViewState.get().getActiveCategory(),
|
||||
this.category)
|
||||
.styleClass(Styles.LEFT_PILL);
|
||||
var filter = new FilterComp(this.filter).styleClass(Styles.RIGHT_PILL).minWidth(0).hgrow();
|
||||
.styleClass(Styles.LEFT_PILL)
|
||||
.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))
|
||||
.apply(struc -> struc.get().setFillHeight(true))
|
||||
.apply(struc -> {
|
||||
((Region) struc.get().getChildren().get(0))
|
||||
.prefHeightProperty()
|
||||
.bind(((Region) struc.get().getChildren().get(1)).heightProperty());
|
||||
((Region) struc.get().getChildren().get(0))
|
||||
.minWidthProperty()
|
||||
.bind(struc.get().widthProperty().divide(2.0));
|
||||
var first = ((Region) struc.get().getChildren().get(0));
|
||||
var second = ((Region) struc.get().getChildren().get(1));
|
||||
first.prefHeightProperty().bind(second.heightProperty());
|
||||
first.minHeightProperty().bind(second.heightProperty());
|
||||
first.maxHeightProperty().bind(second.heightProperty());
|
||||
})
|
||||
.styleClass("bookmarks-header")
|
||||
.createRegion();
|
||||
|
|
|
@ -109,7 +109,7 @@ public class BrowserNavBar extends Comp<BrowserNavBar.Structure> {
|
|||
new TooltipAugment<>("history", new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN))
|
||||
.augment(historyButton);
|
||||
|
||||
var breadcrumbs = new BrowserBreadcrumbBar(model).grow(false, true);
|
||||
var breadcrumbs = new BrowserBreadcrumbBar(model);
|
||||
|
||||
var pathRegion = pathBar.createStructure().get();
|
||||
var breadcrumbsRegion = breadcrumbs.createRegion();
|
||||
|
@ -143,7 +143,7 @@ public class BrowserNavBar extends Comp<BrowserNavBar.Structure> {
|
|||
topBox.setFillHeight(true);
|
||||
topBox.setAlignment(Pos.CENTER);
|
||||
homeButton.minWidthProperty().bind(pathRegion.heightProperty());
|
||||
homeButton.maxWidthProperty().bind(pathRegion.heightProperty().multiply(1.3));
|
||||
homeButton.maxWidthProperty().bind(pathRegion.heightProperty());
|
||||
homeButton.minHeightProperty().bind(pathRegion.heightProperty());
|
||||
homeButton.maxHeightProperty().bind(pathRegion.heightProperty());
|
||||
historyButton.minHeightProperty().bind(pathRegion.heightProperty());
|
||||
|
|
|
@ -29,7 +29,16 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
|
|||
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 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.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
|
@ -58,9 +59,15 @@ public class BrowserSelectionListComp extends SimpleComp {
|
|||
return Comp.of(() -> {
|
||||
var image = PrettyImageHelper.ofFixedSizeSquare(entry.getIcon(), 24)
|
||||
.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.textProperty().bind(PlatformThread.sync(nameTransformation.apply(entry)));
|
||||
t.addListener((observable, oldValue, newValue) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
l.setText(newValue);
|
||||
});
|
||||
});
|
||||
BindingsHelper.preserve(l, t);
|
||||
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.util.BindingsHelper;
|
||||
import io.xpipe.app.util.HumanReadableFormat;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
|
@ -37,8 +39,7 @@ public class BrowserStatusBarComp extends SimpleComp {
|
|||
createProgressEstimateStatus(),
|
||||
Comp.hspacer(),
|
||||
createClipboardStatus(),
|
||||
createSelectionStatus()
|
||||
));
|
||||
createSelectionStatus()));
|
||||
bar.spacing(15);
|
||||
bar.styleClass("status-bar");
|
||||
|
||||
|
@ -58,12 +59,16 @@ public class BrowserStatusBarComp extends SimpleComp {
|
|||
return null;
|
||||
} else {
|
||||
var expected = p.expectedTimeRemaining();
|
||||
var show = (p.getTotal() > 50_000_000 && p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0) || expected.toMillis() > 5000;
|
||||
var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "...";
|
||||
var show = p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0
|
||||
&& (p.getTotal() > 50_000_000 || expected.toMillis() > 5000);
|
||||
var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "";
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -77,7 +82,10 @@ public class BrowserStatusBarComp extends SimpleComp {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -89,7 +97,10 @@ public class BrowserStatusBarComp extends SimpleComp {
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -160,7 +171,6 @@ public class BrowserStatusBarComp extends SimpleComp {
|
|||
emptyEntry.onDragDone(event);
|
||||
});
|
||||
|
||||
|
||||
// Use status bar as an extension of file list
|
||||
new ContextMenuAugment<>(
|
||||
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package io.xpipe.app.browser;
|
||||
|
||||
import io.xpipe.app.browser.file.BrowserFileTransferMode;
|
||||
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.AppI18n;
|
||||
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.impl.*;
|
||||
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.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.Dragboard;
|
||||
import javafx.scene.input.TransferMode;
|
||||
import javafx.scene.layout.Region;
|
||||
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -37,172 +39,151 @@ public class BrowserTransferComp extends SimpleComp {
|
|||
|
||||
@Override
|
||||
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"))
|
||||
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
|
||||
.apply(struc -> struc.get().setWrapText(true))
|
||||
.visible(Bindings.isEmpty(syncItems));
|
||||
.visible(model.getEmpty());
|
||||
var backgroundStack =
|
||||
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())
|
||||
.getList();
|
||||
var list = new BrowserSelectionListComp(
|
||||
binding,
|
||||
entry -> Bindings.createStringBinding(
|
||||
var list = new BrowserSelectionListComp(binding, entry -> {
|
||||
var sourceItem = model.getCurrentItems().stream()
|
||||
.filter(item -> item.getBrowserEntry() == entry)
|
||||
.findAny();
|
||||
if (sourceItem.isEmpty()) {
|
||||
return new SimpleStringProperty("?");
|
||||
}
|
||||
synchronized (sourceItem.get().getProgress()) {
|
||||
return Bindings.createStringBinding(
|
||||
() -> {
|
||||
var sourceItem = syncItems.stream()
|
||||
.filter(item -> item.getBrowserEntry() == entry)
|
||||
.findAny();
|
||||
if (sourceItem.isEmpty()) {
|
||||
return "?";
|
||||
}
|
||||
var name = entry.getModel() == null
|
||||
var p = sourceItem.get().getProgress().getValue();
|
||||
var progressSuffix = p == null
|
||||
|| sourceItem
|
||||
.get()
|
||||
.downloadFinished()
|
||||
.get()
|
||||
? "Local"
|
||||
: entry.getModel()
|
||||
.getFileSystemModel()
|
||||
.getName();
|
||||
return entry.getFileName() + " (" + name + ")";
|
||||
? ""
|
||||
: " " + (p.getTransferred() * 100 / p.getTotal()) + "%";
|
||||
return entry.getFileName() + progressSuffix;
|
||||
},
|
||||
syncAllDownloaded))
|
||||
sourceItem.get().getProgress());
|
||||
}
|
||||
})
|
||||
.grow(false, true);
|
||||
var dragNotice = new LabelComp(syncAllDownloaded.flatMap(
|
||||
aBoolean -> aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
|
||||
var dragNotice = new LabelComp(AppI18n.observable("dragLocalFiles"))
|
||||
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
|
||||
.apply(struc -> AppFont.medium(struc.get()))
|
||||
.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", () -> {
|
||||
model.clear(true);
|
||||
ThreadHelper.runAsync(() -> {
|
||||
model.clear(true);
|
||||
});
|
||||
})
|
||||
.hide(Bindings.isEmpty(syncItems))
|
||||
.hide(model.getEmpty())
|
||||
.tooltipKey("clearTransferDescription");
|
||||
|
||||
var bottom =
|
||||
new HorizontalComp(List.of(Comp.hspacer(), dragNotice, Comp.hspacer(), downloadButton, Comp.hspacer(4), clearButton));
|
||||
var downloadButton = new IconButtonComp("mdi2f-folder-move-outline", () -> {
|
||||
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))
|
||||
.spacing(5)
|
||||
.padding(new Insets(10, 10, 5, 10))
|
||||
.apply(struc -> struc.get().setMinHeight(200))
|
||||
.apply(struc -> struc.get().setMaxHeight(200));
|
||||
var stack = LoadingOverlayComp.noProgress(
|
||||
new StackComp(List.of(backgroundStack, listBox))
|
||||
.apply(DragOverPseudoClassAugment.create())
|
||||
.apply(struc -> {
|
||||
struc.get().setOnDragOver(event -> {
|
||||
// Accept drops from inside the app window
|
||||
if (event.getGestureSource() != null && event.getGestureSource() != struc.get()) {
|
||||
event.acceptTransferModes(TransferMode.ANY);
|
||||
event.consume();
|
||||
}
|
||||
var stack = new StackComp(List.of(backgroundStack, listBox))
|
||||
.apply(DragOverPseudoClassAugment.create())
|
||||
.apply(struc -> {
|
||||
struc.get().setOnDragOver(event -> {
|
||||
// Accept drops from inside the app window
|
||||
if (event.getGestureSource() != null && event.getGestureSource() != struc.get()) {
|
||||
event.acceptTransferModes(TransferMode.ANY);
|
||||
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 (event.getGestureSource() == null
|
||||
&& !event.getDragboard().getFiles().isEmpty()) {
|
||||
event.acceptTransferModes(TransferMode.ANY);
|
||||
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;
|
||||
if (!(model.getBrowserSessionModel()
|
||||
.getSelectedEntry()
|
||||
.getValue()
|
||||
instanceof OpenFileSystemModel fileSystemModel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var files = drag.getEntries();
|
||||
model.drop(fileSystemModel, files);
|
||||
event.setDropCompleted(true);
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
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()
|
||||
.getSelectedEntry()
|
||||
.getValue()
|
||||
instanceof OpenFileSystemModel fileSystemModel)) {
|
||||
return;
|
||||
}
|
||||
var cc = new ClipboardContent();
|
||||
cc.putFiles(files);
|
||||
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
|
||||
db.setContent(cc);
|
||||
|
||||
var files = drag.getEntries();
|
||||
model.drop(fileSystemModel, files);
|
||||
event.setDropCompleted(true);
|
||||
event.consume();
|
||||
}
|
||||
Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
|
||||
db.setDragView(image, -20, 15);
|
||||
|
||||
// Accept drops from outside the app window
|
||||
if (event.getGestureSource() == null) {
|
||||
model.dropLocal(event.getDragboard().getFiles());
|
||||
event.setDropCompleted(true);
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
struc.get().setOnDragDetected(event -> {
|
||||
if (syncDownloaded.getValue()) {
|
||||
return;
|
||||
}
|
||||
event.setDragDetect(true);
|
||||
event.consume();
|
||||
});
|
||||
struc.get().setOnDragDone(event -> {
|
||||
if (!event.isAccepted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var selected = syncItems.stream()
|
||||
.map(item -> item.getBrowserEntry())
|
||||
.toList();
|
||||
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
|
||||
|
||||
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);
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
stack.apply(struc -> {
|
||||
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.session.BrowserSessionModel;
|
||||
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.ThreadHelper;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableBooleanValue;
|
||||
import javafx.collections.FXCollections;
|
||||
|
@ -22,136 +21,156 @@ import javafx.collections.ObservableList;
|
|||
import lombok.Value;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.Optional;
|
||||
|
||||
@Value
|
||||
public class BrowserTransferModel {
|
||||
|
||||
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;
|
||||
ObservableList<Item> items = FXCollections.observableArrayList();
|
||||
BooleanProperty downloading = new SimpleBooleanProperty();
|
||||
BooleanProperty allDownloaded = new SimpleBooleanProperty();
|
||||
ObservableBooleanValue empty = Bindings.createBooleanBinding(() -> items.isEmpty(), items);
|
||||
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try (var ls = Files.list(TEMP)) {
|
||||
var list = ls.toList();
|
||||
for (Path path : list) {
|
||||
FileUtils.forceDelete(path.toFile());
|
||||
}
|
||||
if (!Files.exists(item.getLocalFile())) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
FileUtils.forceDelete(item.getLocalFile().toFile());
|
||||
} catch (IOException e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
executor.submit(() -> {
|
||||
cleanDirectory();
|
||||
});
|
||||
for (Item item : toClear) {
|
||||
cleanItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void drop(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
entries.forEach(entry -> {
|
||||
var name = entry.getFileName();
|
||||
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);
|
||||
synchronized (items) {
|
||||
entries.forEach(entry -> {
|
||||
var name = entry.getFileName();
|
||||
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
|
||||
return;
|
||||
}
|
||||
|
||||
var item = new Item(null, name, entry, path);
|
||||
item.progress.setValue(BrowserTransferProgress.finished(
|
||||
entry.getFileName(), entry.getRawFileEntry().getSize()));
|
||||
Path file = TEMP.resolve(name);
|
||||
var item = new Item(model, name, entry, file);
|
||||
items.add(item);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
}
|
||||
if (empty) {
|
||||
allDownloaded.set(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void download() {
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
FileUtils.forceMkdir(TEMP.toFile());
|
||||
} catch (IOException e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
public void downloadSingle(Item item) {
|
||||
try {
|
||||
FileUtils.forceMkdir(TEMP.toFile());
|
||||
} catch (IOException e) {
|
||||
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;
|
||||
}
|
||||
items.removeAll(toMove);
|
||||
}
|
||||
|
||||
for (Item item : new ArrayList<>(items)) {
|
||||
if (item.downloadFinished().get()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item.getOpenFileSystemModel() != null
|
||||
&& item.getOpenFileSystemModel().isClosed()) {
|
||||
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);
|
||||
}
|
||||
var files = toMove.stream().map(item -> item.getLocalFile()).toList();
|
||||
var downloads = DesktopHelper.getDownloadsDirectory();
|
||||
for (Path file : files) {
|
||||
var target = downloads.resolve(file.getFileName());
|
||||
// Prevent DirectoryNotEmptyException
|
||||
if (Files.exists(target) && Files.isDirectory(target)) {
|
||||
Files.delete(target);
|
||||
}
|
||||
allDownloaded.set(true);
|
||||
});
|
||||
Files.move(file, target, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
DesktopHelper.browseFileInDirectory(downloads.resolve(files.getFirst().getFileName()));
|
||||
}
|
||||
|
||||
@Value
|
||||
|
@ -171,12 +190,11 @@ public class BrowserTransferModel {
|
|||
}
|
||||
|
||||
public ObservableBooleanValue downloadFinished() {
|
||||
return Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return progress.getValue() != null
|
||||
&& progress.getValue().done();
|
||||
},
|
||||
progress);
|
||||
synchronized (progress) {
|
||||
return Bindings.createBooleanBinding(() -> {
|
||||
return progress.getValue() != null && progress.getValue().done();
|
||||
}, progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ public class BrowserTransferProgress {
|
|||
var share = (double) transferred / total;
|
||||
var rest = (1.0 - share) / share;
|
||||
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
|
||||
protected Region createSimple() {
|
||||
var state = model.getSavedState();
|
||||
var state = BrowserSavedStateImpl.get();
|
||||
|
||||
var welcome = new BrowserGreetingComp().createSimple();
|
||||
|
||||
|
@ -55,6 +55,7 @@ public class BrowserWelcomeComp extends SimpleComp {
|
|||
var img = new PrettySvgComp(new SimpleStringProperty("Hips.svg"), 50, 75)
|
||||
.padding(new Insets(5, 0, 0, 0))
|
||||
.createRegion();
|
||||
|
||||
var hbox = new HBox(img, vbox);
|
||||
hbox.setAlignment(Pos.CENTER_LEFT);
|
||||
hbox.setSpacing(15);
|
||||
|
@ -139,7 +140,6 @@ public class BrowserWelcomeComp extends SimpleComp {
|
|||
.hide(empty)
|
||||
.accessibleTextKey("restoreAllSessions");
|
||||
layout.getChildren().add(tile.createRegion());
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
|
@ -149,7 +149,7 @@ public class BrowserWelcomeComp extends SimpleComp {
|
|||
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
|
||||
var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24);
|
||||
return new ButtonComp(
|
||||
new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())),
|
||||
new SimpleStringProperty(DataStorage.get().getStoreEntryDisplayName(entry.get())),
|
||||
view.createRegion(),
|
||||
() -> {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
|
@ -160,7 +160,7 @@ public class BrowserWelcomeComp extends SimpleComp {
|
|||
});
|
||||
})
|
||||
.minWidth(250)
|
||||
.accessibleText(DataStorage.get().getStoreDisplayName(entry.get()))
|
||||
.accessibleText(DataStorage.get().getStoreEntryDisplayName(entry.get()))
|
||||
.disable(disable)
|
||||
.styleClass("entry-button")
|
||||
.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.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;
|
||||
|
||||
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.scene.Node;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -19,13 +20,17 @@ public interface BrowserAction {
|
|||
|
||||
static List<LeafAction> getFlattened(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||
return ALL.stream()
|
||||
.map(browserAction -> browserAction instanceof LeafAction
|
||||
? List.of((LeafAction) browserAction)
|
||||
: ((BranchAction) browserAction).getBranchingActions(model, entries))
|
||||
.map(browserAction -> getFlattened(browserAction, model, entries))
|
||||
.flatMap(List::stream)
|
||||
.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) {
|
||||
return getFlattened(model, entries).stream()
|
||||
.filter(browserAction -> id.equals(browserAction.getId()))
|
||||
|
@ -33,6 +38,17 @@ public interface BrowserAction {
|
|||
.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 String getProFeatureId() {
|
||||
|
|
|
@ -80,7 +80,10 @@ public class BrowserAlerts {
|
|||
private static String getSelectedElementsString(List<FileSystem.FileEntry> source) {
|
||||
var namesHeader = AppI18n.get("selectedElements");
|
||||
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) {
|
||||
names += "\n+ " + (source.size() - 10) + " ...";
|
||||
}
|
||||
|
|
|
@ -1,19 +1,12 @@
|
|||
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.LeafAction;
|
||||
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||
import io.xpipe.app.core.AppFont;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Menu;
|
||||
import javafx.scene.control.SeparatorMenuItem;
|
||||
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -30,15 +23,6 @@ public final class BrowserContextMenu extends ContextMenu {
|
|||
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() {
|
||||
InputHelper.onLeft(this, false, e -> {
|
||||
hide();
|
||||
|
@ -60,7 +44,7 @@ public final class BrowserContextMenu extends ContextMenu {
|
|||
var all = BrowserAction.ALL.stream()
|
||||
.filter(browserAction -> browserAction.getCategory() == cat)
|
||||
.filter(browserAction -> {
|
||||
var used = resolveIfNeeded(browserAction, selected);
|
||||
var used = browserAction.resolveFilesIfNeeded(selected);
|
||||
if (!browserAction.isApplicable(model, used)) {
|
||||
return false;
|
||||
}
|
||||
|
@ -81,36 +65,8 @@ public final class BrowserContextMenu extends ContextMenu {
|
|||
}
|
||||
|
||||
for (BrowserAction a : all) {
|
||||
var used = resolveIfNeeded(a, selected);
|
||||
if (a instanceof LeafAction la) {
|
||||
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);
|
||||
}
|
||||
var used = a.resolveFilesIfNeeded(selected);
|
||||
getItems().add(a.toMenuItem(model, used));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ import io.xpipe.app.browser.action.BrowserAction;
|
|||
import io.xpipe.app.comp.base.LazyTextFieldComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
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.util.PlatformThread;
|
||||
import io.xpipe.app.util.*;
|
||||
|
@ -29,10 +27,7 @@ import javafx.scene.Node;
|
|||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.skin.TableViewSkin;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.MouseButton;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
|
@ -40,11 +35,13 @@ import javafx.scene.layout.Region;
|
|||
import atlantafx.base.controls.Spacer;
|
||||
import atlantafx.base.theme.Styles;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static io.xpipe.app.util.HumanReadableFormat.byteCount;
|
||||
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 final BrowserFileListModel fileList;
|
||||
private final StringProperty typedSelection = new SimpleStringProperty("");
|
||||
|
||||
public BrowserFileListComp(BrowserFileListModel fileList) {
|
||||
this.fileList = fileList;
|
||||
|
@ -124,16 +122,80 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
return true;
|
||||
});
|
||||
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
|
||||
table.setFixedCellSize(34.0);
|
||||
table.setFixedCellSize(32.0);
|
||||
|
||||
prepareTableSelectionModel(table);
|
||||
prepareTableShortcuts(table);
|
||||
prepareTableEntries(table);
|
||||
prepareTableChanges(table, mtimeCol, modeCol);
|
||||
prepareTypedSelectionModel(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) {
|
||||
if (!fileList.getSelectionMode().isMultiple()) {
|
||||
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||
|
@ -167,7 +229,7 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
}
|
||||
|
||||
private void prepareTableShortcuts(TableView<BrowserEntry> table) {
|
||||
table.setOnKeyPressed(event -> {
|
||||
table.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
var selected = fileList.getSelection();
|
||||
var action = BrowserAction.getFlattened(fileList.getFileSystemModel(), selected).stream()
|
||||
.filter(browserAction -> browserAction.isApplicable(fileList.getFileSystemModel(), selected)
|
||||
|
@ -219,7 +281,6 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
emptyEntry.onDragDone(event);
|
||||
});
|
||||
|
||||
|
||||
// Don't let the list view see this event
|
||||
// otherwise it unselects everything as it doesn't understand shift clicks
|
||||
table.addEventFilter(MouseEvent.MOUSE_CLICKED, t -> {
|
||||
|
@ -242,38 +303,6 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
return row.getItem() != null;
|
||||
},
|
||||
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(
|
||||
() -> new BrowserFileListCompEntry(table, row, row.getItem(), fileList), row.itemProperty());
|
||||
|
||||
|
@ -332,7 +361,6 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
listEntry.get().onDragDone(event);
|
||||
});
|
||||
|
||||
|
||||
return row;
|
||||
});
|
||||
}
|
||||
|
@ -564,7 +592,18 @@ public final class BrowserFileListComp extends SimpleComp {
|
|||
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();
|
||||
// Only show one menu across all selected entries
|
||||
if (selected.size() > 0 && selected.getLast() == getTableRow().getItem()) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.xpipe.core.store.FileKind;
|
|||
|
||||
import javafx.geometry.Point2D;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.TableView;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.input.*;
|
||||
|
@ -31,6 +32,7 @@ public class BrowserFileListCompEntry {
|
|||
|
||||
private Point2D lastOver = new Point2D(-1, -1);
|
||||
private TimerTask activeTask;
|
||||
private ContextMenu lastContextMenu;
|
||||
|
||||
public BrowserFileListCompEntry(
|
||||
TableView<BrowserEntry> tv, Node row, BrowserEntry item, BrowserFileListModel model) {
|
||||
|
@ -41,6 +43,19 @@ public class BrowserFileListCompEntry {
|
|||
}
|
||||
|
||||
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) {
|
||||
// Only clear for normal clicks
|
||||
if (t.isStillSincePress()) {
|
||||
|
@ -62,6 +77,23 @@ public class BrowserFileListCompEntry {
|
|||
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) {
|
||||
if (t.getButton() != MouseButton.PRIMARY) {
|
||||
return;
|
||||
|
|
|
@ -99,24 +99,32 @@ public final class BrowserFileListModel {
|
|||
}
|
||||
|
||||
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 newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
|
||||
|
||||
// This check will fail on case-insensitive file systems when changing the case of the file
|
||||
// So skip it in this case
|
||||
var skipExistCheck = fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS && old.getFileName()
|
||||
.equalsIgnoreCase(newName);
|
||||
var skipExistCheck =
|
||||
fileSystemModel.getFileSystem().getShell().orElseThrow().getOsType() == OsType.WINDOWS
|
||||
&& old.getFileName().equalsIgnoreCase(newName);
|
||||
if (!skipExistCheck) {
|
||||
boolean exists;
|
||||
try {
|
||||
exists = fileSystemModel.getFileSystem().fileExists(newFullPath) || fileSystemModel.getFileSystem().directoryExists(newFullPath);
|
||||
exists = fileSystemModel.getFileSystem().fileExists(newFullPath)
|
||||
|| fileSystemModel.getFileSystem().directoryExists(newFullPath);
|
||||
} catch (Exception e) {
|
||||
ErrorEvent.fromThrowable(e).handle();
|
||||
return old;
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
ErrorEvent.fromMessage("Target " + newFullPath + " does already exist").expected().handle();
|
||||
ErrorEvent.fromMessage("Target " + newFullPath + " does already exist")
|
||||
.expected()
|
||||
.handle();
|
||||
fileSystemModel.refresh();
|
||||
return old;
|
||||
}
|
||||
|
|
|
@ -7,9 +7,7 @@ import io.xpipe.core.store.FileNames;
|
|||
import io.xpipe.core.store.FilePath;
|
||||
import io.xpipe.core.store.FileSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
|
@ -220,62 +218,85 @@ public class BrowserFileTransferOperation {
|
|||
continue;
|
||||
}
|
||||
|
||||
InputStream inputStream = null;
|
||||
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;
|
||||
}
|
||||
transfer(sourceFile, targetFile, transferred, totalSize, start);
|
||||
}
|
||||
}
|
||||
updateProgress(BrowserTransferProgress.finished(source.getName(), totalSize.get()));
|
||||
}
|
||||
|
||||
private void transfer(
|
||||
FileSystem.FileEntry sourceFile,
|
||||
String targetFile,
|
||||
AtomicLong transferred,
|
||||
AtomicLong totalSize,
|
||||
Instant start)
|
||||
throws Exception {
|
||||
InputStream inputStream = null;
|
||||
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 {
|
||||
source.getFileSystem().delete(source.getPath());
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
|
|||
getItems().addAll(r.getItems());
|
||||
|
||||
// Prevent NPE in show()
|
||||
if (getScene() == null) {
|
||||
if (getScene() == null || anchor == null) {
|
||||
return;
|
||||
}
|
||||
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.comp.base.ModalOverlayComp;
|
||||
import io.xpipe.app.comp.base.MultiContentComp;
|
||||
import io.xpipe.app.core.AppFont;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||
|
@ -84,10 +85,11 @@ public class OpenFileSystemComp extends SimpleComp {
|
|||
var filter = new BrowserFilterComp(model, model.getFilter()).createStructure();
|
||||
|
||||
var topBar = new HBox();
|
||||
filter.textField().prefHeightProperty().bind(topBar.heightProperty());
|
||||
topBar.setAlignment(Pos.CENTER);
|
||||
topBar.getStyleClass().add("top-bar");
|
||||
var navBar = new BrowserNavBar(model).createStructure();
|
||||
filter.textField().prefHeightProperty().bind(navBar.get().heightProperty());
|
||||
AppFont.medium(navBar.get());
|
||||
topBar.getChildren()
|
||||
.setAll(
|
||||
overview,
|
||||
|
@ -117,13 +119,13 @@ public class OpenFileSystemComp extends SimpleComp {
|
|||
});
|
||||
|
||||
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.textField().requestFocus();
|
||||
keyEvent.consume();
|
||||
});
|
||||
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();
|
||||
keyEvent.consume();
|
||||
});
|
||||
|
@ -140,6 +142,14 @@ public class OpenFileSystemComp extends SimpleComp {
|
|||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.xpipe.app.browser.fs;
|
||||
|
||||
import io.xpipe.app.browser.BrowserSavedState;
|
||||
import io.xpipe.app.browser.BrowserSavedStateImpl;
|
||||
import io.xpipe.app.browser.BrowserTransferProgress;
|
||||
import io.xpipe.app.browser.action.BrowserAction;
|
||||
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.FileSystemHelper;
|
||||
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.comp.base.ModalOverlayComp;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
|
@ -110,14 +110,13 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
|
|||
return;
|
||||
}
|
||||
|
||||
var current = getCurrentDirectory();
|
||||
if (DataStorage.get().getStoreEntries().contains(getEntry().get())
|
||||
&& savedState != null
|
||||
&& getCurrentPath().get() != null) {
|
||||
if (getBrowserModel() instanceof BrowserSessionModel bm) {
|
||||
bm.getSavedState()
|
||||
.add(new BrowserSavedState.Entry(
|
||||
getEntry().get().getUuid(), getCurrentPath().get()));
|
||||
}
|
||||
&& current != null) {
|
||||
savedState.cd(current.getPath(), false);
|
||||
BrowserSavedStateImpl.get()
|
||||
.add(new BrowserSavedState.Entry(getEntry().get().getUuid(), current.getPath()));
|
||||
}
|
||||
try {
|
||||
fileSystem.close();
|
||||
|
@ -300,7 +299,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
|
|||
// path = FileSystemHelper.normalizeDirectoryPath(this, path);
|
||||
|
||||
filter.setValue(null);
|
||||
savedState.cd(path);
|
||||
savedState.cd(path, true);
|
||||
history.updateCurrent(path);
|
||||
currentPath.set(path);
|
||||
loadFilesSync(path);
|
||||
|
@ -461,7 +460,7 @@ public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore
|
|||
}
|
||||
|
||||
public void initWithDefaultDirectory() {
|
||||
savedState.cd(null);
|
||||
savedState.cd(null, false);
|
||||
history.updateCurrent(null);
|
||||
}
|
||||
|
||||
|
|
|
@ -72,32 +72,38 @@ public class OpenFileSystemSavedState {
|
|||
AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
|
||||
}
|
||||
|
||||
public void cd(String dir) {
|
||||
public void cd(String dir, boolean delay) {
|
||||
if (dir == null) {
|
||||
lastDirectory = null;
|
||||
return;
|
||||
}
|
||||
|
||||
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)) {
|
||||
updateRecent(dir);
|
||||
save();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
10000);
|
||||
if (delay) {
|
||||
// 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)) {
|
||||
updateRecent(dir);
|
||||
save();
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
10000);
|
||||
} else {
|
||||
updateRecent(dir);
|
||||
save();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRecent(String dir) {
|
||||
|
|
|
@ -17,6 +17,7 @@ public class BrowserAbstractSessionModel<T extends BrowserSessionTab<?>> {
|
|||
|
||||
protected final ObservableList<T> sessionEntries = FXCollections.observableArrayList();
|
||||
protected final Property<T> selectedEntry = new SimpleObjectProperty<>();
|
||||
protected final BooleanProperty busy = new SimpleBooleanProperty();
|
||||
|
||||
public void closeAsync(BrowserSessionTab<?> e) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
|
|
|
@ -67,6 +67,10 @@ public class BrowserChooserComp extends SimpleComp {
|
|||
window.close();
|
||||
});
|
||||
window.show();
|
||||
window.setOnHidden(event -> {
|
||||
model.finishWithoutChoice();
|
||||
event.consume();
|
||||
});
|
||||
ThreadHelper.runAsync(() -> {
|
||||
model.openFileSystemAsync(store.get(), null, null);
|
||||
});
|
||||
|
|
|
@ -65,6 +65,17 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel<OpenFil
|
|||
onFinish.accept(stores);
|
||||
}
|
||||
|
||||
public void finishWithoutChoice() {
|
||||
synchronized (BrowserFileChooserModel.this) {
|
||||
var open = selectedEntry.getValue();
|
||||
if (open != null) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
open.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void openFileSystemAsync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
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.BrowserBookmarkHeaderComp;
|
||||
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.store.StoreEntryWrapper;
|
||||
import io.xpipe.app.core.AppLayoutModel;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
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.VerticalComp;
|
||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||
|
@ -18,6 +21,7 @@ import javafx.application.Platform;
|
|||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.scene.layout.AnchorPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.shape.Rectangle;
|
||||
|
||||
|
@ -100,10 +104,22 @@ public class BrowserSessionComp extends SimpleComp {
|
|||
new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
|
||||
|
||||
var split = new SimpleDoubleProperty();
|
||||
var tabs = new BrowserSessionTabsComp(model, split)
|
||||
.apply(struc -> struc.get().setViewOrder(1))
|
||||
.apply(struc -> struc.get().setPickOnBounds(false));
|
||||
var splitPane = new SideSplitPaneComp(vertical, tabs)
|
||||
var tabs = new BrowserSessionTabsComp(model, split).apply(struc -> {
|
||||
struc.get().setViewOrder(1);
|
||||
struc.get().setPickOnBounds(false);
|
||||
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())
|
||||
.withOnDividerChange(d -> {
|
||||
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);
|
||||
|
|
|
@ -23,16 +23,11 @@ import java.util.ArrayList;
|
|||
@Getter
|
||||
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 BrowserSavedState savedState;
|
||||
private final Property<Boolean> draggingFiles = new SimpleBooleanProperty();
|
||||
|
||||
public BrowserSessionModel(BrowserSavedState savedState) {
|
||||
this.savedState = savedState;
|
||||
}
|
||||
|
||||
public void restoreState(BrowserSavedState state) {
|
||||
ThreadHelper.runAsync(() -> {
|
||||
var l = new ArrayList<>(state.getEntries());
|
||||
|
@ -62,9 +57,7 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
|
|||
|
||||
closeSync(o);
|
||||
}
|
||||
if (savedState != null) {
|
||||
savedState.save();
|
||||
}
|
||||
BrowserSavedStateImpl.get().save();
|
||||
}
|
||||
|
||||
// Delete all files
|
||||
|
@ -87,20 +80,23 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
|
|||
public void openFileSystemSync(
|
||||
DataStoreEntryRef<? extends FileSystemStore> store,
|
||||
FailableFunction<OpenFileSystemModel, String, Exception> path,
|
||||
BooleanProperty externalBusy) throws Exception {
|
||||
BooleanProperty externalBusy)
|
||||
throws Exception {
|
||||
if (store == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
OpenFileSystemModel model;
|
||||
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
|
||||
model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL);
|
||||
model.init();
|
||||
// Prevent multiple calls from interfering with each other
|
||||
synchronized (BrowserSessionModel.this) {
|
||||
sessionEntries.add(model);
|
||||
// The tab pane doesn't automatically select new tabs
|
||||
selectedEntry.setValue(model);
|
||||
try (var sessionBusy = new BooleanScope(busy).exclusive().start()) {
|
||||
model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL);
|
||||
model.init();
|
||||
// Prevent multiple calls from interfering with each other
|
||||
synchronized (BrowserSessionModel.this) {
|
||||
sessionEntries.add(model);
|
||||
// The tab pane doesn't automatically select new tabs
|
||||
selectedEntry.setValue(model);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (path != null) {
|
||||
|
|
|
@ -22,7 +22,7 @@ public abstract class BrowserSessionTab<T extends DataStore> {
|
|||
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) {
|
||||
this.browserModel = browserModel;
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
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.comp.base.MultiContentComp;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.SimpleComp;
|
||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||
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.storage.DataStorage;
|
||||
import io.xpipe.app.util.BooleanScope;
|
||||
import io.xpipe.app.util.InputHelper;
|
||||
|
||||
import io.xpipe.app.util.ContextMenuHelper;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -20,20 +23,12 @@ import javafx.beans.value.ObservableValue;
|
|||
import javafx.collections.ListChangeListener;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.input.DragEvent;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.*;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
import atlantafx.base.controls.RingProgressIndicator;
|
||||
import atlantafx.base.theme.Styles;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
|
||||
import static atlantafx.base.theme.Styles.DENSE;
|
||||
import static atlantafx.base.theme.Styles.toggleStyleClass;
|
||||
|
@ -50,17 +45,17 @@ public class BrowserSessionTabsComp extends SimpleComp {
|
|||
}
|
||||
|
||||
public Region createSimple() {
|
||||
var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of(
|
||||
Comp.of(() -> createTabPane()),
|
||||
Bindings.isNotEmpty(model.getSessionEntries()),
|
||||
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
|
||||
map.put(Comp.hspacer().styleClass("top-spacer"), new SimpleBooleanProperty(true));
|
||||
map.put(Comp.of(() -> createTabPane()), Bindings.isNotEmpty(model.getSessionEntries()));
|
||||
map.put(
|
||||
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
|
||||
Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return model.getSessionEntries().size() == 0;
|
||||
},
|
||||
model.getSessionEntries()),
|
||||
Comp.hspacer().styleClass("top-spacer"),
|
||||
new SimpleBooleanProperty(true)));
|
||||
model.getSessionEntries()));
|
||||
var multi = new MultiContentComp(map);
|
||||
multi.apply(struc -> ((StackPane) struc.get()).setAlignment(Pos.TOP_CENTER));
|
||||
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();
|
||||
if (current == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyEvent.getCode() == KeyCode.W && keyEvent.isShortcutDown()) {
|
||||
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(keyEvent)) {
|
||||
tabs.getTabs().remove(current);
|
||||
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()) {
|
||||
tabs.getTabs().clear();
|
||||
if (keyEvent.getCode().isFunctionKey()) {
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
var tab = new Tab();
|
||||
tab.setContextMenu(createContextMenu(tabs, tab));
|
||||
|
||||
var ring = new RingProgressIndicator(0, false);
|
||||
ring.setMinSize(16, 16);
|
||||
|
|
|
@ -13,10 +13,9 @@ import io.xpipe.app.storage.DataStorage;
|
|||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.ButtonBase;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
|
@ -63,28 +62,11 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
|
|||
sidebarR.getChildrenUnmodifiable().forEach(node -> {
|
||||
var shortcut = (KeyCodeCombination) node.getProperties().get("shortcut");
|
||||
if (shortcut != null && shortcut.match(event)) {
|
||||
((ButtonBase) node).fire();
|
||||
((ButtonBase) ((Parent) node).getChildrenUnmodifiable().get(1)).fire();
|
||||
event.consume();
|
||||
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);
|
||||
pane.getStyleClass().add("layout");
|
||||
|
|
|
@ -70,7 +70,9 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
|||
.bind(Bindings.createDoubleBinding(
|
||||
() -> {
|
||||
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()));
|
||||
}
|
||||
|
|
|
@ -88,6 +88,7 @@ public class ListSelectorComp<T> extends SimpleComp {
|
|||
|
||||
var sp = new ScrollPane(vbox);
|
||||
sp.setFitToWidth(true);
|
||||
sp.getStyleClass().add("list-selector-comp");
|
||||
return sp;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import javafx.scene.web.WebEngine;
|
|||
import javafx.scene.web.WebView;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
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);
|
||||
try {
|
||||
// Workaround for https://bugs.openjdk.org/browse/JDK-8199014
|
||||
Files.createDirectories(file.getParent());
|
||||
FileUtils.forceMkdir(file.getParent().toFile());
|
||||
Files.writeString(file, html);
|
||||
return file;
|
||||
} 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.update.UpdateAvailableAlert;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
|
@ -41,14 +42,14 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
var selectedBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
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());
|
||||
|
||||
var hoverBorder = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
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());
|
||||
|
||||
|
@ -70,12 +71,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
value.setValue(e);
|
||||
});
|
||||
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(struc -> {
|
||||
AppFont.setSize(struc.get(), 2);
|
||||
AppFont.setSize(struc.get(), 1);
|
||||
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
|
||||
value.addListener((c, o, n) -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
|
@ -86,7 +84,8 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
b.accessibleText(e.name());
|
||||
|
||||
var indicator = Comp.empty().styleClass("indicator");
|
||||
var stack = new StackComp(List.of(indicator, b)).apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
|
||||
var stack = new StackComp(List.of(indicator, b))
|
||||
.apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
|
||||
stack.apply(struc -> {
|
||||
var indicatorRegion = (Region) struc.get().getChildren().getFirst();
|
||||
indicatorRegion.setMaxWidth(7);
|
||||
|
@ -110,6 +109,9 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
selectedBorder,
|
||||
noneBorder));
|
||||
});
|
||||
if (shortcut != null) {
|
||||
stack.apply(struc -> struc.get().getProperties().put("shortcut", shortcut));
|
||||
}
|
||||
vbox.getChildren().add(stack.createRegion());
|
||||
}
|
||||
|
||||
|
@ -118,7 +120,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
|||
.tooltipKey("updateAvailableTooltip")
|
||||
.accessibleTextKey("updateAvailableTooltip");
|
||||
b.apply(struc -> {
|
||||
AppFont.setSize(struc.get(), 2);
|
||||
AppFont.setSize(struc.get(), 1);
|
||||
});
|
||||
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
|
|
|
@ -95,7 +95,7 @@ public class StoreToggleComp extends SimpleComp {
|
|||
v -> {
|
||||
Platform.runLater(() -> {
|
||||
setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v);
|
||||
StoreViewState.get().toggleStoreListUpdate();
|
||||
StoreViewState.get().triggerStoreListUpdate();
|
||||
});
|
||||
});
|
||||
t.tooltipKey("showAllChildren");
|
||||
|
|
|
@ -33,16 +33,27 @@ public class SystemStateComp extends SimpleComp {
|
|||
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.setOpacity(0.5);
|
||||
border.setOpacity(0.3);
|
||||
|
||||
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 =
|
||||
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 =
|
||||
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();
|
||||
pane.getChildren().addAll(fi, border);
|
||||
|
@ -51,7 +62,7 @@ public class SystemStateComp extends SimpleComp {
|
|||
var dataClass1 =
|
||||
"""
|
||||
.stacked-ikonli-font-icon > .outer-icon {
|
||||
-fx-icon-size: 22px;
|
||||
-fx-icon-size: 26px;
|
||||
}
|
||||
.stacked-ikonli-font-icon > .inner-icon {
|
||||
-fx-icon-size: 12px;
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.core.AppFont;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.geometry.HPos;
|
||||
|
@ -17,8 +15,8 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
|
||||
private final boolean showIcon;
|
||||
|
||||
public DenseStoreEntryComp(StoreEntryWrapper entry, boolean showIcon, Comp<?> content) {
|
||||
super(entry, content);
|
||||
public DenseStoreEntryComp(StoreSection section, boolean showIcon, Comp<?> content) {
|
||||
super(section, content);
|
||||
this.showIcon = showIcon;
|
||||
}
|
||||
|
||||
|
@ -26,24 +24,27 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
var information = new Label();
|
||||
information.setGraphicTextGap(7);
|
||||
information.getStyleClass().add("information");
|
||||
AppFont.header(information);
|
||||
|
||||
var state = wrapper.getEntry().getProvider() != null
|
||||
? wrapper.getEntry().getProvider().stateDisplay(wrapper)
|
||||
var state = getWrapper().getEntry().getProvider() != null
|
||||
? getWrapper().getEntry().getProvider().stateDisplay(getWrapper())
|
||||
: Comp.empty();
|
||||
information.setGraphic(state.createRegion());
|
||||
|
||||
var info = wrapper.getEntry().getProvider() != null ? wrapper.getEntry().getProvider().informationString(wrapper) : new SimpleStringProperty();
|
||||
var summary = wrapper.getSummary();
|
||||
if (wrapper.getEntry().getProvider() != null) {
|
||||
var info = getWrapper().getEntry().getProvider() != null
|
||||
? getWrapper().getEntry().getProvider().informationString(section)
|
||||
: new SimpleStringProperty();
|
||||
var summary = getWrapper().getSummary();
|
||||
if (getWrapper().getEntry().getProvider() != null) {
|
||||
information
|
||||
.textProperty()
|
||||
.bind(PlatformThread.sync(Bindings.createStringBinding(
|
||||
() -> {
|
||||
var val = summary.getValue();
|
||||
if (val != null
|
||||
&& grid.isHover()
|
||||
&& wrapper.getEntry().getProvider().alwaysShowSummary()) {
|
||||
var p = getWrapper().getEntry().getProvider();
|
||||
if (val != null && grid.isHover()
|
||||
&& p.alwaysShowSummary()) {
|
||||
return val;
|
||||
} else if (info.getValue() == null && p.alwaysShowSummary()){
|
||||
return val;
|
||||
} else {
|
||||
return info.getValue();
|
||||
|
@ -73,11 +74,11 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
return grid.getWidth() / 2.5;
|
||||
},
|
||||
grid.widthProperty()));
|
||||
var notes = new StoreNotesComp(wrapper).createRegion();
|
||||
var notes = new StoreNotesComp(getWrapper()).createRegion();
|
||||
|
||||
if (showIcon) {
|
||||
var storeIcon = createIcon(30, 24);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(46));
|
||||
var storeIcon = createIcon(28, 24);
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(38));
|
||||
grid.add(storeIcon, 0, 0);
|
||||
GridPane.setHalignment(storeIcon, HPos.CENTER);
|
||||
}
|
||||
|
@ -95,7 +96,7 @@ public class DenseStoreEntryComp extends StoreEntryComp {
|
|||
nameCC.setHgrow(Priority.ALWAYS);
|
||||
grid.getColumnConstraints().addAll(nameCC);
|
||||
var nameBox = new HBox(name, notes);
|
||||
nameBox.setSpacing(1);
|
||||
nameBox.setSpacing(6);
|
||||
nameBox.setAlignment(Pos.CENTER_LEFT);
|
||||
grid.addRow(0, nameBox);
|
||||
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import io.xpipe.app.core.AppFont;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.*;
|
||||
|
||||
public class StandardStoreEntryComp extends StoreEntryComp {
|
||||
|
||||
public StandardStoreEntryComp(StoreEntryWrapper entry, Comp<?> content) {
|
||||
super(entry, content);
|
||||
public StandardStoreEntryComp(StoreSection section, Comp<?> content) {
|
||||
super(section, content);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -18,23 +21,37 @@ public class StandardStoreEntryComp extends StoreEntryComp {
|
|||
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() {
|
||||
var name = createName().createRegion();
|
||||
var notes = new StoreNotesComp(wrapper).createRegion();
|
||||
var notes = new StoreNotesComp(getWrapper()).createRegion();
|
||||
|
||||
var grid = new GridPane();
|
||||
grid.setHgap(7);
|
||||
grid.setVgap(0);
|
||||
grid.setHgap(6);
|
||||
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.getColumnConstraints().add(new ColumnConstraints(66));
|
||||
grid.getColumnConstraints().add(new ColumnConstraints(56));
|
||||
|
||||
var nameAndNotes = new HBox(name, notes);
|
||||
nameAndNotes.setSpacing(1);
|
||||
nameAndNotes.setSpacing(6);
|
||||
nameAndNotes.setAlignment(Pos.CENTER_LEFT);
|
||||
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();
|
||||
nameCC.setMinWidth(100);
|
||||
nameCC.setHgrow(Priority.ALWAYS);
|
||||
|
|
|
@ -112,6 +112,11 @@ public class StoreCategoryWrapper {
|
|||
}
|
||||
|
||||
public void update() {
|
||||
// We are probably in shutdown then
|
||||
if (StoreViewState.get() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid reupdating name when changed from the name property!
|
||||
var catName = translatedName(category.getName());
|
||||
if (!catName.equals(name.getValue())) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package io.xpipe.app.comp.store;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
import io.xpipe.app.comp.base.DialogComp;
|
||||
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.core.store.DataStore;
|
||||
import io.xpipe.core.util.ValidationException;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
|
@ -33,8 +33,6 @@ import javafx.scene.layout.BorderPane;
|
|||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import atlantafx.base.controls.Spacer;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.experimental.FieldDefaults;
|
||||
import net.synedra.validatorfx.GraphicDecorationStackPane;
|
||||
|
@ -51,7 +49,7 @@ public class StoreCreationComp extends DialogComp {
|
|||
Stage window;
|
||||
BiConsumer<DataStoreEntry, Boolean> consumer;
|
||||
Property<DataStoreProvider> provider;
|
||||
Property<DataStore> store;
|
||||
ObjectProperty<DataStore> store;
|
||||
Predicate<DataStoreProvider> filter;
|
||||
BooleanProperty busy = new SimpleBooleanProperty();
|
||||
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
|
||||
|
@ -60,6 +58,7 @@ public class StoreCreationComp extends DialogComp {
|
|||
ObservableValue<DataStoreEntry> entry;
|
||||
BooleanProperty changedSinceError = new SimpleBooleanProperty();
|
||||
BooleanProperty skippable = new SimpleBooleanProperty();
|
||||
BooleanProperty connectable = new SimpleBooleanProperty();
|
||||
StringProperty name;
|
||||
DataStoreEntry existingEntry;
|
||||
boolean staticDisplay;
|
||||
|
@ -68,7 +67,7 @@ public class StoreCreationComp extends DialogComp {
|
|||
Stage window,
|
||||
BiConsumer<DataStoreEntry, Boolean> consumer,
|
||||
Property<DataStoreProvider> provider,
|
||||
Property<DataStore> store,
|
||||
ObjectProperty<DataStore> store,
|
||||
Predicate<DataStoreProvider> filter,
|
||||
String initialName,
|
||||
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 -> {
|
||||
r.get().setPrefWidth(650);
|
||||
r.get().setPrefHeight(750);
|
||||
|
@ -163,7 +168,12 @@ public class StoreCreationComp extends DialogComp {
|
|||
if (!DataStorage.get().getStoreEntries().contains(e)) {
|
||||
DataStorage.get().addStoreEntryIfNotPresent(newE);
|
||||
} 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();
|
||||
}
|
||||
})
|
||||
.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
|
||||
|
@ -393,11 +412,10 @@ public class StoreCreationComp extends DialogComp {
|
|||
private Region createLayout() {
|
||||
var layout = new BorderPane();
|
||||
layout.getStyleClass().add("store-creator");
|
||||
layout.setPadding(new Insets(20));
|
||||
var providerChoice = new StoreProviderChoiceComp(filter, provider, staticDisplay);
|
||||
if (staticDisplay) {
|
||||
providerChoice.apply(struc -> struc.get().setDisable(true));
|
||||
} else {
|
||||
var showProviders = (!staticDisplay && (providerChoice.getProviders().size() > 1 || providerChoice.getProviders().getFirst().showProviderChoice())) ||
|
||||
(staticDisplay && provider.getValue().showProviderChoice());
|
||||
if (showProviders) {
|
||||
providerChoice.onSceneAssign(struc -> struc.get().requestFocus());
|
||||
}
|
||||
providerChoice.apply(GrowAugment.create(true, false));
|
||||
|
@ -422,9 +440,14 @@ public class StoreCreationComp extends DialogComp {
|
|||
|
||||
var sep = new Separator();
|
||||
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");
|
||||
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();
|
||||
valSp.getChildren().add(layout);
|
||||
|
|
|
@ -40,8 +40,9 @@ public class StoreCreationMenu {
|
|||
menu.getItems()
|
||||
.add(category("addTunnel", "mdi2v-vector-polyline-plus", DataStoreCreationCategory.TUNNEL, null));
|
||||
|
||||
menu.getItems()
|
||||
.add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, "cmd"));
|
||||
menu.getItems().add(category("addCommand", "mdi2c-code-greater-than", DataStoreCreationCategory.COMMAND, null));
|
||||
|
||||
menu.getItems().add(category("addSerial", "mdi2s-serial-port", DataStoreCreationCategory.SERIAL, "serial"));
|
||||
|
||||
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.util.BindingsHelper;
|
||||
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.prefs.AppPrefs;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
|
@ -23,6 +24,7 @@ import io.xpipe.app.update.XPipeDistributionType;
|
|||
import io.xpipe.app.util.*;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.value.ObservableDoubleValue;
|
||||
import javafx.css.PseudoClass;
|
||||
|
@ -51,21 +53,25 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
App.getApp().getStage().widthProperty().divide(2.1).add(-100);
|
||||
public static final ObservableDoubleValue INFO_WITH_CONTENT_WIDTH =
|
||||
App.getApp().getStage().widthProperty().divide(2.1).add(-200);
|
||||
protected final StoreEntryWrapper wrapper;
|
||||
protected final StoreSection section;
|
||||
protected final Comp<?> content;
|
||||
|
||||
public StoreEntryComp(StoreEntryWrapper wrapper, Comp<?> content) {
|
||||
this.wrapper = wrapper;
|
||||
public StoreEntryComp(StoreSection section, Comp<?> content) {
|
||||
this.section = section;
|
||||
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
|
||||
&& AppPrefs.get().condenseConnectionDisplay().get();
|
||||
if (!preferLarge || forceCondensed) {
|
||||
return new DenseStoreEntryComp(entry, true, content);
|
||||
return new DenseStoreEntryComp(section, true, content);
|
||||
} else {
|
||||
return new StandardStoreEntryComp(entry, content);
|
||||
return new StandardStoreEntryComp(section, content);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,9 +82,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
} else {
|
||||
var forceCondensed = AppPrefs.get() != null
|
||||
&& AppPrefs.get().condenseConnectionDisplay().get();
|
||||
return forceCondensed
|
||||
? new DenseStoreEntryComp(e.getWrapper(), true, null)
|
||||
: new StandardStoreEntryComp(e.getWrapper(), null);
|
||||
return forceCondensed ? new DenseStoreEntryComp(e, true, null) : new StandardStoreEntryComp(e, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -95,16 +99,16 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
button.setPadding(Insets.EMPTY);
|
||||
button.setMaxWidth(5000);
|
||||
button.setFocusTraversable(true);
|
||||
button.accessibleTextProperty().bind(wrapper.nameProperty());
|
||||
button.accessibleTextProperty().bind(getWrapper().nameProperty());
|
||||
button.setOnAction(event -> {
|
||||
event.consume();
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
wrapper.executeDefaultAction();
|
||||
getWrapper().executeDefaultAction();
|
||||
});
|
||||
});
|
||||
button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
|
||||
if (AppPrefs.get().requireDoubleClickForConnections().get()) {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() > 2) {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
|
||||
event.consume();
|
||||
}
|
||||
} else {
|
||||
|
@ -115,7 +119,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
});
|
||||
button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
if (AppPrefs.get().requireDoubleClickForConnections().get()) {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() > 2) {
|
||||
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
|
||||
event.consume();
|
||||
}
|
||||
} else {
|
||||
|
@ -132,9 +136,12 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
var loading = LoadingOverlayComp.noProgress(
|
||||
Comp.of(() -> button),
|
||||
wrapper.getEntry().getValidity().isUsable()
|
||||
? wrapper.getBusy().or(wrapper.getEntry().getProvider().busy(wrapper))
|
||||
: wrapper.getBusy());
|
||||
getWrapper().getEntry().getValidity().isUsable()
|
||||
? getWrapper()
|
||||
.getBusy()
|
||||
.or(getWrapper().getEntry().getProvider().busy(getWrapper()))
|
||||
: getWrapper().getBusy());
|
||||
AppFont.normal(button);
|
||||
return loading.createRegion();
|
||||
}
|
||||
|
||||
|
@ -146,31 +153,22 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
information
|
||||
.textProperty()
|
||||
.bind(
|
||||
wrapper.getEntry().getProvider() != null
|
||||
getWrapper().getEntry().getProvider() != null
|
||||
? PlatformThread.sync(
|
||||
wrapper.getEntry().getProvider().informationString(wrapper))
|
||||
getWrapper().getEntry().getProvider().informationString(section))
|
||||
: new SimpleStringProperty());
|
||||
information.getStyleClass().add("information");
|
||||
AppFont.header(information);
|
||||
|
||||
var state = wrapper.getEntry().getProvider() != null
|
||||
? wrapper.getEntry().getProvider().stateDisplay(wrapper)
|
||||
var state = getWrapper().getEntry().getProvider() != null
|
||||
? getWrapper().getEntry().getProvider().stateDisplay(getWrapper())
|
||||
: Comp.empty();
|
||||
information.setGraphic(state.createRegion());
|
||||
|
||||
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) {
|
||||
PlatformThread.sync(wrapper.getValidity()).subscribe(val -> {
|
||||
PlatformThread.sync(getWrapper().getValidity()).subscribe(val -> {
|
||||
switch (val) {
|
||||
case LOAD_FAILED -> {
|
||||
node.pseudoClassStateChanged(FAILED, true);
|
||||
|
@ -189,24 +187,23 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
}
|
||||
|
||||
protected Comp<?> createName() {
|
||||
LabelComp name = new LabelComp(wrapper.nameProperty());
|
||||
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()));
|
||||
LabelComp name = new LabelComp(getWrapper().nameProperty());
|
||||
name.apply(struc -> struc.get().setTextOverrun(OverrunStyle.CENTER_ELLIPSIS));
|
||||
name.styleClass("name");
|
||||
return name;
|
||||
}
|
||||
|
||||
protected Node createIcon(int w, int h) {
|
||||
var img = wrapper.disabledProperty().get()
|
||||
var img = getWrapper().disabledProperty().get()
|
||||
? "disabled_icon.png"
|
||||
: wrapper.getEntry()
|
||||
: getWrapper()
|
||||
.getEntry()
|
||||
.getProvider()
|
||||
.getDisplayIconFileName(wrapper.getEntry().getStore());
|
||||
.getDisplayIconFileName(getWrapper().getEntry().getStore());
|
||||
var imageComp = PrettyImageHelper.ofFixedSize(img, w, h);
|
||||
var storeIcon = imageComp.createRegion();
|
||||
if (wrapper.getValidity().getValue().isUsable()) {
|
||||
new TooltipAugment<>(wrapper.getEntry().getProvider().displayName(), null).augment(storeIcon);
|
||||
if (getWrapper().getValidity().getValue().isUsable()) {
|
||||
new TooltipAugment<>(getWrapper().getEntry().getProvider().displayName(), null).augment(storeIcon);
|
||||
}
|
||||
|
||||
var stack = new StackPane(storeIcon);
|
||||
|
@ -220,7 +217,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
}
|
||||
|
||||
protected Region createButtonBar() {
|
||||
var list = new DerivedObservableList<>(wrapper.getActionProviders(), false);
|
||||
var list = new DerivedObservableList<>(getWrapper().getActionProviders(), false);
|
||||
var buttons = list.mapped(actionProvider -> {
|
||||
var button = buildButton(actionProvider);
|
||||
return button != null ? button.createRegion() : null;
|
||||
|
@ -239,8 +236,8 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
buttons.subscribe(update);
|
||||
update.run();
|
||||
ig.setAlignment(Pos.CENTER_RIGHT);
|
||||
ig.setPadding(new Insets(5));
|
||||
ig.getStyleClass().add("button-bar");
|
||||
AppFont.medium(ig);
|
||||
return ig;
|
||||
}
|
||||
|
||||
|
@ -249,17 +246,20 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
var branch = p.getBranchDataStoreCallSite();
|
||||
var cs = leaf != null ? leaf : branch;
|
||||
|
||||
if (cs == null || !cs.isMajor(wrapper.getEntry().ref())) {
|
||||
if (cs == null || !cs.isMajor(getWrapper().getEntry().ref())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var icon = new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(cs.getIcon(getWrapper().getEntry().ref())));
|
||||
var button = new IconButtonComp(
|
||||
cs.getIcon(wrapper.getEntry().ref()),
|
||||
icon,
|
||||
leaf != null
|
||||
? () -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
wrapper.runAction(
|
||||
leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy());
|
||||
getWrapper().runAction(
|
||||
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);
|
||||
|
@ -267,7 +267,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
button.apply(new ContextMenuAugment<>(
|
||||
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {
|
||||
var cm = ContextMenuHelper.create();
|
||||
branch.getChildren().forEach(childProvider -> {
|
||||
branch.getChildren(getWrapper().getEntry().ref()).forEach(childProvider -> {
|
||||
var menu = buildMenuItemForAction(childProvider);
|
||||
if (menu != null) {
|
||||
cm.getItems().add(menu);
|
||||
|
@ -276,13 +276,13 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
return cm;
|
||||
}));
|
||||
}
|
||||
button.accessibleText(cs.getName(wrapper.getEntry().ref()).getValue());
|
||||
button.apply(new TooltipAugment<>(cs.getName(wrapper.getEntry().ref()), null));
|
||||
button.accessibleText(cs.getName(getWrapper().getEntry().ref()).getValue());
|
||||
button.apply(new TooltipAugment<>(cs.getName(getWrapper().getEntry().ref()), null));
|
||||
return button;
|
||||
}
|
||||
|
||||
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.accessibleText("More");
|
||||
settingsButton.apply(new ContextMenuAugment<>(
|
||||
|
@ -298,7 +298,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
AppFont.normal(contextMenu.getStyleableNode());
|
||||
|
||||
var hasSep = false;
|
||||
for (var p : wrapper.getActionProviders()) {
|
||||
for (var p : getWrapper().getActionProviders()) {
|
||||
var item = buildMenuItemForAction(p);
|
||||
if (item == null) {
|
||||
continue;
|
||||
|
@ -321,36 +321,36 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text"));
|
||||
notes.setOnAction(event -> {
|
||||
wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
|
||||
getWrapper().getNotes().setValue(new StoreNotes(null, getDefaultNotes()));
|
||||
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);
|
||||
|
||||
if (AppPrefs.get().developerMode().getValue()) {
|
||||
var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline"));
|
||||
browse.setOnAction(
|
||||
event -> DesktopHelper.browsePathLocal(wrapper.getEntry().getDirectory()));
|
||||
browse.setOnAction(event ->
|
||||
DesktopHelper.browsePathLocal(getWrapper().getEntry().getDirectory()));
|
||||
contextMenu.getItems().add(browse);
|
||||
|
||||
var copyId = new MenuItem(AppI18n.get("copyId"), new FontIcon("mdi2c-content-copy"));
|
||||
copyId.setOnAction(event ->
|
||||
ClipboardHelper.copyText(wrapper.getEntry().getUuid().toString()));
|
||||
ClipboardHelper.copyText(getWrapper().getEntry().getUuid().toString()));
|
||||
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 none = new MenuItem("None");
|
||||
none.setOnAction(event -> {
|
||||
wrapper.getEntry().setColor(null);
|
||||
getWrapper().getEntry().setColor(null);
|
||||
event.consume();
|
||||
});
|
||||
color.getItems().add(none);
|
||||
Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> {
|
||||
MenuItem m = new MenuItem(DataStoreFormatter.capitalize(dataStoreColor.getId()));
|
||||
m.setOnAction(event -> {
|
||||
wrapper.getEntry().setColor(dataStoreColor);
|
||||
getWrapper().getEntry().setColor(dataStoreColor);
|
||||
event.consume();
|
||||
});
|
||||
color.getItems().add(m);
|
||||
|
@ -358,10 +358,10 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
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"));
|
||||
StoreViewState.get()
|
||||
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
|
||||
.getSortedCategories(getWrapper().getCategory().getValue().getRoot())
|
||||
.getList()
|
||||
.forEach(storeCategoryWrapper -> {
|
||||
MenuItem m = new MenuItem();
|
||||
|
@ -369,12 +369,12 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
.setValue(" ".repeat(storeCategoryWrapper.getDepth())
|
||||
+ storeCategoryWrapper.getName().getValue());
|
||||
m.setOnAction(event -> {
|
||||
wrapper.moveTo(storeCategoryWrapper.getCategory());
|
||||
getWrapper().moveTo(storeCategoryWrapper.getCategory());
|
||||
event.consume();
|
||||
});
|
||||
if (storeCategoryWrapper.getParent() == null
|
||||
|| storeCategoryWrapper.equals(
|
||||
wrapper.getCategory().getValue())) {
|
||||
getWrapper().getCategory().getValue())) {
|
||||
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 noOrder = new MenuItem(AppI18n.get("none"), new FontIcon("mdi2r-reorder-horizontal"));
|
||||
noOrder.setOnAction(event -> {
|
||||
wrapper.setOrder(null);
|
||||
getWrapper().setOrder(null);
|
||||
event.consume();
|
||||
});
|
||||
if (wrapper.getEntry().getExplicitOrder() == null) {
|
||||
if (getWrapper().getEntry().getExplicitOrder() == null) {
|
||||
noOrder.setDisable(true);
|
||||
}
|
||||
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"));
|
||||
top.setOnAction(event -> {
|
||||
wrapper.setOrder(DataStoreEntry.Order.TOP);
|
||||
getWrapper().setOrder(DataStoreEntry.Order.TOP);
|
||||
event.consume();
|
||||
});
|
||||
if (DataStoreEntry.Order.TOP.equals(wrapper.getEntry().getExplicitOrder())) {
|
||||
if (DataStoreEntry.Order.TOP.equals(getWrapper().getEntry().getExplicitOrder())) {
|
||||
top.setDisable(true);
|
||||
}
|
||||
order.getItems().add(top);
|
||||
|
||||
var bottom = new MenuItem(AppI18n.get("stickToBottom"), new FontIcon("mdi2o-order-bool-ascending"));
|
||||
bottom.setOnAction(event -> {
|
||||
wrapper.setOrder(DataStoreEntry.Order.BOTTOM);
|
||||
getWrapper().setOrder(DataStoreEntry.Order.BOTTOM);
|
||||
event.consume();
|
||||
});
|
||||
if (DataStoreEntry.Order.BOTTOM.equals(wrapper.getEntry().getExplicitOrder())) {
|
||||
if (DataStoreEntry.Order.BOTTOM.equals(getWrapper().getEntry().getExplicitOrder())) {
|
||||
bottom.setDisable(true);
|
||||
}
|
||||
order.getItems().add(bottom);
|
||||
|
@ -423,14 +423,14 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
del.disableProperty()
|
||||
.bind(Bindings.createBooleanBinding(
|
||||
() -> {
|
||||
return !wrapper.getDeletable().get()
|
||||
return !getWrapper().getDeletable().get()
|
||||
&& !AppPrefs.get()
|
||||
.developerDisableGuiRestrictions()
|
||||
.get();
|
||||
},
|
||||
wrapper.getDeletable(),
|
||||
getWrapper().getDeletable(),
|
||||
AppPrefs.get().developerDisableGuiRestrictions()));
|
||||
del.setOnAction(event -> wrapper.delete());
|
||||
del.setOnAction(event -> getWrapper().delete());
|
||||
contextMenu.getItems().add(del);
|
||||
|
||||
return contextMenu;
|
||||
|
@ -441,12 +441,12 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
var branch = p.getBranchDataStoreCallSite();
|
||||
var cs = leaf != null ? leaf : branch;
|
||||
|
||||
if (cs == null || cs.isMajor(wrapper.getEntry().ref())) {
|
||||
if (cs == null || cs.isMajor(getWrapper().getEntry().ref())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = cs.getName(wrapper.getEntry().ref());
|
||||
var icon = cs.getIcon(wrapper.getEntry().ref());
|
||||
var name = cs.getName(getWrapper().getEntry().ref());
|
||||
var icon = cs.getIcon(getWrapper().getEntry().ref());
|
||||
var item = (leaf != null && leaf.canLinkTo()) || branch != null
|
||||
? new Menu(null, new FontIcon(icon))
|
||||
: new MenuItem(null, new FontIcon(icon));
|
||||
|
@ -462,7 +462,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
Menu menu = item instanceof Menu m ? m : null;
|
||||
|
||||
if (branch != null) {
|
||||
var items = branch.getChildren().stream()
|
||||
var items = branch.getChildren(getWrapper().getEntry().ref()).stream()
|
||||
.map(c -> buildMenuItemForAction(c))
|
||||
.toList();
|
||||
menu.getItems().addAll(items);
|
||||
|
@ -472,21 +472,25 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
run.textProperty().bind(AppI18n.observable("base.execute"));
|
||||
run.setOnAction(event -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
wrapper.runAction(leaf.createAction(wrapper.getEntry().ref()), leaf.showBusy());
|
||||
getWrapper()
|
||||
.runAction(leaf.createAction(getWrapper().getEntry().ref()), leaf.showBusy());
|
||||
});
|
||||
});
|
||||
menu.getItems().add(run);
|
||||
|
||||
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.setOnAction(event -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
DesktopShortcuts.create(
|
||||
DesktopShortcuts.createCliOpen(
|
||||
url,
|
||||
wrapper.nameProperty().getValue() + " ("
|
||||
DataStorage.get()
|
||||
.getStoreEntryDisplayName(
|
||||
getWrapper().getEntry()) + " ("
|
||||
+ p.getLeafDataStoreCallSite()
|
||||
.getName(wrapper.getEntry().ref())
|
||||
.getName(getWrapper().getEntry().ref())
|
||||
.getValue() + ")");
|
||||
});
|
||||
});
|
||||
|
@ -516,7 +520,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
|||
|
||||
event.consume();
|
||||
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.IconButtonComp;
|
||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||
import io.xpipe.app.fxcomps.util.LabelGraphic;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
|
@ -95,8 +96,8 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
|||
createDateSortButton().createRegion(),
|
||||
Comp.hspacer(2).createRegion(),
|
||||
createAlphabeticalSortButton().createRegion());
|
||||
AppFont.setSize(label, 3);
|
||||
AppFont.setSize(c, 3);
|
||||
AppFont.setSize(label, 2);
|
||||
AppFont.setSize(c, 2);
|
||||
topBar.setAlignment(Pos.CENTER);
|
||||
topBar.getStyleClass().add("top");
|
||||
return topBar;
|
||||
|
@ -111,9 +112,11 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
|||
});
|
||||
var filter = new FilterComp(StoreViewState.get().getFilterString());
|
||||
var f = filter.createRegion();
|
||||
var buttons = createAddButton();
|
||||
var hbox = new HBox(buttons, f);
|
||||
f.prefHeightProperty().bind(buttons.heightProperty());
|
||||
var button = createAddButton();
|
||||
var hbox = new HBox(button, f);
|
||||
f.minHeightProperty().bind(button.heightProperty());
|
||||
f.prefHeightProperty().bind(button.heightProperty());
|
||||
f.maxHeightProperty().bind(button.heightProperty());
|
||||
hbox.setSpacing(8);
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
HBox.setHgrow(f, Priority.ALWAYS);
|
||||
|
@ -136,22 +139,22 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
|||
if (OsType.getLocal().equals(OsType.MACOS)) {
|
||||
menu.setPadding(new Insets(-2, 0, -2, 0));
|
||||
} else {
|
||||
menu.setPadding(new Insets(-3, 0, -3, 0));
|
||||
menu.setPadding(new Insets(-4, 0, -4, 0));
|
||||
}
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
private Comp<?> createAlphabeticalSortButton() {
|
||||
var icon = Bindings.createStringBinding(
|
||||
var icon = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
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) {
|
||||
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);
|
||||
var alphabetical = new IconButtonComp(icon, () -> {
|
||||
|
@ -164,6 +167,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
|||
}
|
||||
});
|
||||
alphabetical.apply(alphabeticalR -> {
|
||||
AppFont.medium(alphabeticalR.get());
|
||||
alphabeticalR
|
||||
.get()
|
||||
.opacityProperty()
|
||||
|
@ -183,15 +187,15 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
|||
}
|
||||
|
||||
private Comp<?> createDateSortButton() {
|
||||
var icon = Bindings.createStringBinding(
|
||||
var icon = Bindings.createObjectBinding(
|
||||
() -> {
|
||||
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) {
|
||||
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);
|
||||
var date = new IconButtonComp(icon, () -> {
|
||||
|
@ -204,6 +208,7 @@ public class StoreEntryListOverviewComp extends SimpleComp {
|
|||
}
|
||||
});
|
||||
date.apply(dateR -> {
|
||||
AppFont.medium(dateR.get());
|
||||
dateR.get()
|
||||
.opacityProperty()
|
||||
.bind(Bindings.createDoubleBinding(
|
||||
|
|
|
@ -40,12 +40,13 @@ public class StoreEntryWrapper {
|
|||
private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();
|
||||
private final Property<String> summary = new SimpleObjectProperty<>();
|
||||
private final Property<StoreNotes> notes;
|
||||
private final IntegerProperty childrenStateUpdateObservable = new SimpleIntegerProperty();
|
||||
|
||||
public StoreEntryWrapper(DataStoreEntry entry) {
|
||||
this.entry = entry;
|
||||
this.name = new SimpleStringProperty(entry.getName());
|
||||
this.lastAccess = new SimpleObjectProperty<>(entry.getLastAccess().minus(Duration.ofMillis(500)));
|
||||
ActionProvider.ALL.stream()
|
||||
ActionProvider.ALL_STANDALONE.stream()
|
||||
.filter(dataStoreActionProvider -> {
|
||||
return !entry.isDisabled()
|
||||
&& dataStoreActionProvider.getLeafDataStoreCallSite() != null
|
||||
|
@ -63,6 +64,12 @@ public class StoreEntryWrapper {
|
|||
setupListeners();
|
||||
}
|
||||
|
||||
public void triggerChildrenStateUpdate() {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
childrenStateUpdateObservable.set(childrenStateUpdateObservable.get() + 1);
|
||||
});
|
||||
}
|
||||
|
||||
public void applyLastAccess() {
|
||||
this.lastAccessApplied.setValue(lastAccess.getValue());
|
||||
}
|
||||
|
@ -151,7 +158,8 @@ public class StoreEntryWrapper {
|
|||
summary.setValue(null);
|
||||
} else {
|
||||
try {
|
||||
summary.setValue(entry.getProvider() != null ? entry.getProvider().summaryString(this) : null);
|
||||
summary.setValue(
|
||||
entry.getProvider() != null ? entry.getProvider().summaryString(this) : null);
|
||||
} catch (Exception ex) {
|
||||
// Summary creation might fail or have a bug
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
|
@ -163,18 +171,18 @@ public class StoreEntryWrapper {
|
|||
defaultActionProvider.setValue(null);
|
||||
} else {
|
||||
try {
|
||||
var defaultProvider = ActionProvider.ALL.stream()
|
||||
var defaultProvider = ActionProvider.ALL_STANDALONE.stream()
|
||||
.filter(e -> entry.getStore() != null
|
||||
&& e.getDefaultDataStoreCallSite() != null
|
||||
&& e.getDefaultDataStoreCallSite()
|
||||
.getApplicableClass()
|
||||
.isAssignableFrom(entry.getStore().getClass())
|
||||
.getApplicableClass()
|
||||
.isAssignableFrom(entry.getStore().getClass())
|
||||
&& e.getDefaultDataStoreCallSite().isApplicable(entry.ref()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
this.defaultActionProvider.setValue(defaultProvider);
|
||||
|
||||
var newProviders = ActionProvider.ALL.stream()
|
||||
var newProviders = ActionProvider.ALL_STANDALONE.stream()
|
||||
.filter(dataStoreActionProvider -> {
|
||||
return showActionProvider(dataStoreActionProvider);
|
||||
})
|
||||
|
@ -203,7 +211,7 @@ public class StoreEntryWrapper {
|
|||
if (branch != null
|
||||
&& entry.getStore() != null
|
||||
&& branch.getApplicableClass().isAssignableFrom(entry.getStore().getClass())) {
|
||||
return branch.getChildren().stream().anyMatch(child -> {
|
||||
return branch.getChildren(entry.ref()).stream().anyMatch(child -> {
|
||||
return showActionProvider(child);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@ public class StoreNotesComp extends Comp<StoreNotesComp.Structure> {
|
|||
.focusTraversableForAccessibility()
|
||||
.tooltipKey("notes")
|
||||
.styleClass("notes-button")
|
||||
.grow(false, true)
|
||||
.hide(BindingsHelper.map(n, s -> s.getCommited() == null && s.getCurrent() == null))
|
||||
.createStructure()
|
||||
.get();
|
||||
|
|
|
@ -29,7 +29,7 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
|
|||
Property<DataStoreProvider> provider;
|
||||
boolean staticDisplay;
|
||||
|
||||
private List<DataStoreProvider> getProviders() {
|
||||
public List<DataStoreProvider> getProviders() {
|
||||
return DataStoreProviders.getAll().stream()
|
||||
.filter(val -> filter == null || filter.test(val))
|
||||
.toList();
|
||||
|
|
|
@ -4,6 +4,7 @@ import io.xpipe.app.fxcomps.Comp;
|
|||
import io.xpipe.app.fxcomps.CompStructure;
|
||||
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||
import io.xpipe.app.fxcomps.util.LabelGraphic;
|
||||
import io.xpipe.app.util.ContextMenuHelper;
|
||||
|
||||
import javafx.geometry.Side;
|
||||
|
@ -18,9 +19,9 @@ import java.util.function.Consumer;
|
|||
public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
|
||||
|
||||
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.action = action;
|
||||
}
|
||||
|
@ -44,10 +45,9 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
|
|||
w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore());
|
||||
if (c.getList().isEmpty()) {
|
||||
var item = ContextMenuHelper.item(
|
||||
PrettyImageHelper.ofFixedSizeSquare(graphic, 16),
|
||||
w.getName().getValue());
|
||||
new LabelGraphic.ImageGraphic(graphic, 16), w.getName().getValue());
|
||||
item.setOnAction(event -> {
|
||||
action.accept(w);
|
||||
action.accept(section);
|
||||
contextMenu.hide();
|
||||
event.consume();
|
||||
});
|
||||
|
@ -72,7 +72,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
|
|||
return;
|
||||
}
|
||||
|
||||
action.accept(w);
|
||||
action.accept(section);
|
||||
contextMenu.hide();
|
||||
event.consume();
|
||||
}
|
||||
|
|
|
@ -163,10 +163,10 @@ public class StoreSection {
|
|||
var allChildren = all.filtered(
|
||||
other -> {
|
||||
// Legacy implementation that does not use children caches. Use for testing
|
||||
// if (true) return DataStorage.get()
|
||||
// .getDisplayParent(other.getEntry())
|
||||
// .map(found -> found.equals(e.getEntry()))
|
||||
// .orElse(false);
|
||||
// if (true) return DataStorage.get()
|
||||
// .getDefaultDisplayParent(other.getEntry())
|
||||
// .map(found -> found.equals(e.getEntry()))
|
||||
// .orElse(false);
|
||||
|
||||
// is children. This check is fast as the children are cached in the storage
|
||||
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.IconButtonComp;
|
||||
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
||||
import io.xpipe.app.fxcomps.util.LabelGraphic;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
|
@ -44,9 +45,9 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
|
|||
return section.getShownChildren().getList().isEmpty();
|
||||
},
|
||||
section.getShownChildren().getList());
|
||||
Consumer<StoreEntryWrapper> quickAccessAction = w -> {
|
||||
Consumer<StoreSection> quickAccessAction = w -> {
|
||||
ThreadHelper.runFailableAsync(() -> {
|
||||
w.executeDefaultAction();
|
||||
w.getWrapper().executeDefaultAction();
|
||||
});
|
||||
};
|
||||
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
|
||||
|
@ -68,11 +69,11 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
|
|||
|
||||
private Comp<CompStructure<Button>> createExpandButton() {
|
||||
var expandButton = new IconButtonComp(
|
||||
Bindings.createStringBinding(
|
||||
() -> section.getWrapper().getExpanded().get()
|
||||
Bindings.createObjectBinding(
|
||||
() -> new LabelGraphic.IconGraphic(section.getWrapper().getExpanded().get()
|
||||
&& section.getShownChildren().getList().size() > 0
|
||||
? "mdal-keyboard_arrow_down"
|
||||
: "mdal-keyboard_arrow_right",
|
||||
: "mdal-keyboard_arrow_right"),
|
||||
section.getWrapper().getExpanded(),
|
||||
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.PrettyImageHelper;
|
||||
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
||||
import io.xpipe.app.fxcomps.util.LabelGraphic;
|
||||
import io.xpipe.app.storage.DataStoreColor;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
|
@ -34,12 +35,12 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
|||
|
||||
private final StoreSection section;
|
||||
private final BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment;
|
||||
private final Consumer<StoreEntryWrapper> action;
|
||||
private final Consumer<StoreSection> action;
|
||||
|
||||
public StoreSectionMiniComp(
|
||||
StoreSection section,
|
||||
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment,
|
||||
Consumer<StoreEntryWrapper> action) {
|
||||
Consumer<StoreSection> action) {
|
||||
this.section = section;
|
||||
this.augment = augment;
|
||||
this.action = action;
|
||||
|
@ -68,7 +69,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
|||
})
|
||||
.apply(struc -> {
|
||||
struc.get().setOnAction(event -> {
|
||||
action.accept(section.getWrapper());
|
||||
action.accept(section);
|
||||
event.consume();
|
||||
});
|
||||
})
|
||||
|
@ -81,8 +82,8 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
|||
new SimpleBooleanProperty(section.getWrapper().getExpanded().get()
|
||||
&& section.getShownChildren().getList().size() > 0);
|
||||
var button = new IconButtonComp(
|
||||
Bindings.createStringBinding(
|
||||
() -> expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right",
|
||||
Bindings.createObjectBinding(
|
||||
() -> new LabelGraphic.IconGraphic(expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right"),
|
||||
expanded),
|
||||
() -> {
|
||||
expanded.set(!expanded.get());
|
||||
|
@ -105,7 +106,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
|||
return section.getShownChildren().getList().isEmpty();
|
||||
},
|
||||
section.getShownChildren().getList());
|
||||
Consumer<StoreEntryWrapper> quickAccessAction = action;
|
||||
Consumer<StoreSection> quickAccessAction = action;
|
||||
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
|
||||
.vgrow()
|
||||
.styleClass("quick-access-button")
|
||||
|
|
|
@ -65,8 +65,7 @@ public interface StoreSortMode {
|
|||
.isUsable())
|
||||
.map(this::representative),
|
||||
Stream.of(s))
|
||||
.max(Comparator.comparing(
|
||||
section -> date(section)))
|
||||
.max(Comparator.comparing(section -> date(section)))
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
|
@ -103,8 +102,7 @@ public interface StoreSortMode {
|
|||
.isUsable())
|
||||
.map(this::representative),
|
||||
Stream.of(s))
|
||||
.max(Comparator.comparing(
|
||||
section -> date(section)))
|
||||
.max(Comparator.comparing(section -> date(section)))
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
|
|
|
@ -124,10 +124,10 @@ public class StoreViewState {
|
|||
|
||||
public void updateDisplay() {
|
||||
allEntries.getList().forEach(e -> e.applyLastAccess());
|
||||
toggleStoreListUpdate();
|
||||
triggerStoreListUpdate();
|
||||
}
|
||||
|
||||
public void toggleStoreListUpdate() {
|
||||
public void triggerStoreListUpdate() {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
entriesListUpdateObservable.set(entriesListUpdateObservable.get() + 1);
|
||||
});
|
||||
|
@ -152,7 +152,7 @@ public class StoreViewState {
|
|||
@Override
|
||||
public void onStoreListUpdate() {
|
||||
Platform.runLater(() -> {
|
||||
toggleStoreListUpdate();
|
||||
triggerStoreListUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -7,9 +7,11 @@ import io.xpipe.app.issue.TrackEvent;
|
|||
import io.xpipe.app.prefs.AppPrefs;
|
||||
import io.xpipe.app.update.XPipeDistributionType;
|
||||
import io.xpipe.app.util.LicenseProvider;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package io.xpipe.app.core;
|
||||
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
|
@ -21,7 +22,7 @@ public class AppDataLock {
|
|||
public static boolean lock() {
|
||||
try {
|
||||
var file = getLockFile().toFile();
|
||||
Files.createDirectories(file.toPath().getParent());
|
||||
FileUtils.forceMkdir(file.getParentFile());
|
||||
if (!Files.exists(file.toPath())) {
|
||||
try {
|
||||
// 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);
|
||||
if (!Files.exists(p)) {
|
||||
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();
|
||||
|
@ -105,8 +105,9 @@ public class AppExtensionManager {
|
|||
var sourceVersion = AppVersion.parse(sv)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Invalid source version: " + sv));
|
||||
if (AppProperties.get().isLocatorVersionCheck() && !installVersion.equals(sourceVersion)) {
|
||||
throw new IllegalStateException("Incompatible development version. Source: " + iv + ", Installation: "
|
||||
+ sv + "\n\nPlease try to check out the matching release version in the repository.");
|
||||
throw new IllegalStateException(
|
||||
"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);
|
||||
|
|
|
@ -55,6 +55,12 @@ public class AppGreetings {
|
|||
if (set || AppProperties.get().isDevelopmentEnvironment()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (AppProperties.get().isAutoAcceptEula()) {
|
||||
AppCache.update("legalAccepted", true);
|
||||
return;
|
||||
}
|
||||
|
||||
var read = new SimpleBooleanProperty();
|
||||
var accepted = new SimpleBooleanProperty();
|
||||
AppWindowHelper.showBlockingAlert(alert -> {
|
||||
|
|
|
@ -78,19 +78,19 @@ public class AppLayoutModel {
|
|||
"mdi2f-file-cabinet",
|
||||
new BrowserSessionComp(BrowserSessionModel.DEFAULT),
|
||||
null,
|
||||
new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.CONTROL_DOWN)),
|
||||
new KeyCodeCombination(KeyCode.DIGIT1, KeyCombination.SHORTCUT_DOWN)),
|
||||
new Entry(
|
||||
AppI18n.observable("connections"),
|
||||
"mdi2c-connection",
|
||||
new StoreLayoutComp(),
|
||||
null,
|
||||
new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.CONTROL_DOWN)),
|
||||
new KeyCodeCombination(KeyCode.DIGIT2, KeyCombination.SHORTCUT_DOWN)),
|
||||
new Entry(
|
||||
AppI18n.observable("settings"),
|
||||
"mdsmz-miscellaneous_services",
|
||||
new AppPrefsComp(),
|
||||
null,
|
||||
new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)),
|
||||
new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.SHORTCUT_DOWN)),
|
||||
new Entry(
|
||||
AppI18n.observable("explorePlans"),
|
||||
"mdi2p-professional-hexagon",
|
||||
|
@ -102,20 +102,20 @@ public class AppLayoutModel {
|
|||
"mdi2g-github",
|
||||
null,
|
||||
() -> Hyperlinks.open(Hyperlinks.GITHUB),
|
||||
new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)),
|
||||
null),
|
||||
new Entry(
|
||||
AppI18n.observable("discord"),
|
||||
"mdi2d-discord",
|
||||
null,
|
||||
() -> Hyperlinks.open(Hyperlinks.DISCORD),
|
||||
new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN)),
|
||||
null),
|
||||
new Entry(
|
||||
AppI18n.observable("api"),
|
||||
"mdi2c-code-json",
|
||||
null,
|
||||
() -> Hyperlinks.open(
|
||||
"http://localhost:" + AppBeaconServer.get().getPort()),
|
||||
new KeyCodeCombination(KeyCode.DIGIT3, KeyCombination.CONTROL_DOWN))));
|
||||
null)));
|
||||
return l;
|
||||
}
|
||||
|
||||
|
@ -128,5 +128,6 @@ public class AppLayoutModel {
|
|||
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();
|
||||
if (shouldLogToFile) {
|
||||
try {
|
||||
Files.createDirectories(usedLogsDir);
|
||||
FileUtils.forceMkdir(usedLogsDir.toFile());
|
||||
var file = usedLogsDir.resolve("xpipe.log");
|
||||
var fos = new FileOutputStream(file.toFile(), true);
|
||||
var buf = new BufferedOutputStream(fos);
|
||||
|
|
|
@ -37,11 +37,13 @@ public class AppProperties {
|
|||
boolean useVirtualThreads;
|
||||
boolean debugThreads;
|
||||
Path dataDir;
|
||||
Path defaultDataDir;
|
||||
boolean showcase;
|
||||
AppVersion canonicalVersion;
|
||||
boolean locatePtb;
|
||||
boolean locatorVersionCheck;
|
||||
boolean isTest;
|
||||
boolean autoAcceptEula;
|
||||
|
||||
public AppProperties() {
|
||||
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"))
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false);
|
||||
defaultDataDir = Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe-ptb" : ".xpipe");
|
||||
dataDir = Optional.ofNullable(System.getProperty("io.xpipe.app.dataDir"))
|
||||
.map(s -> {
|
||||
var p = Path.of(s);
|
||||
|
@ -94,7 +97,7 @@ public class AppProperties {
|
|||
}
|
||||
return p;
|
||||
})
|
||||
.orElse(Path.of(System.getProperty("user.home"), isStaging() ? ".xpipe-ptb" : ".xpipe"));
|
||||
.orElse(defaultDataDir);
|
||||
showcase = Optional.ofNullable(System.getProperty("io.xpipe.app.showcase"))
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false);
|
||||
|
@ -107,6 +110,9 @@ public class AppProperties {
|
|||
.map(s -> !Boolean.parseBoolean(s))
|
||||
.orElse(true);
|
||||
isTest = isJUnitTest();
|
||||
autoAcceptEula = Optional.ofNullable(System.getProperty("io.xpipe.app.acceptEula"))
|
||||
.map(Boolean::parseBoolean)
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
private static boolean isJUnitTest() {
|
||||
|
|
|
@ -43,6 +43,10 @@ public class AppTheme {
|
|||
|
||||
public static void initThemeHandlers(Stage stage) {
|
||||
Runnable r = () -> {
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(
|
||||
PseudoClass.getPseudoClass(OsType.getLocal().getId()), true);
|
||||
if (AppPrefs.get() == null) {
|
||||
var def = Theme.getDefaultLightTheme();
|
||||
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) {
|
||||
ErrorEvent.fromThrowable(t).omit().handle();
|
||||
}
|
||||
|
@ -132,6 +139,9 @@ public class AppTheme {
|
|||
} else {
|
||||
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) {
|
||||
// The color scheme query can fail if the toolkit is not initialized properly
|
||||
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
|
||||
|
@ -206,7 +216,6 @@ public class AppTheme {
|
|||
Application.setUserAgentStylesheet(Styles.toDataURI(builder.toString()));
|
||||
}
|
||||
|
||||
|
||||
public List<String> getAdditionalStylesheets() {
|
||||
return List.of();
|
||||
}
|
||||
|
|
|
@ -90,7 +90,8 @@ public class AppTrayIcon {
|
|||
tray.add(this.trayIcon);
|
||||
fixBackground();
|
||||
} 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.ProcessOutputException;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public class AppShellCheck {
|
||||
|
@ -17,15 +19,19 @@ public class AppShellCheck {
|
|||
.getEffectiveLocalDialect()
|
||||
.equals(ProcessControlProvider.get().getFallbackDialect());
|
||||
if (err.isPresent() && canFallback) {
|
||||
var msg = formatMessage(err.get());
|
||||
var msg = formatMessage(err.get().getMessage());
|
||||
ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle();
|
||||
enableFallback();
|
||||
err = selfTestErrorCheck();
|
||||
}
|
||||
|
||||
if (err.isPresent()) {
|
||||
var msg = formatMessage(err.get());
|
||||
ErrorEvent.fromThrowable(new IllegalStateException(msg)).handle();
|
||||
var msg = formatMessage(err.get().getMessage());
|
||||
var event = ErrorEvent.fromThrowable(new IllegalStateException(msg));
|
||||
if (!err.get().isCanContinue()) {
|
||||
event.term();
|
||||
}
|
||||
event.handle();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,17 +77,24 @@ public class AppShellCheck {
|
|||
LocalShell.init();
|
||||
}
|
||||
|
||||
private static Optional<String> selfTestErrorCheck() {
|
||||
private static Optional<FailureResult> selfTestErrorCheck() {
|
||||
try (var command = LocalShell.getShell().command("echo test").complex().start()) {
|
||||
var out = command.readStdoutOrThrow();
|
||||
if (!out.equals("test")) {
|
||||
return Optional.of("Expected \"test\", got \"" + out + "\"");
|
||||
return Optional.of(new FailureResult("Expected \"test\", got \"" + out + "\"", true));
|
||||
}
|
||||
} 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) {
|
||||
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();
|
||||
}
|
||||
|
||||
@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.issue.ErrorEvent;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
|
@ -12,17 +13,18 @@ public class AppUserDirectoryCheck {
|
|||
var dataDirectory = AppProperties.get().getDataDir();
|
||||
|
||||
try {
|
||||
Files.createDirectories(dataDirectory);
|
||||
FileUtils.forceMkdir(dataDirectory.toFile());
|
||||
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);
|
||||
// if (true) throw new IOException();
|
||||
} catch (IOException e) {
|
||||
ErrorEvent.fromThrowable(
|
||||
new IOException(
|
||||
"Unable to access directory " + dataDirectory
|
||||
+ ". 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."))
|
||||
ErrorEvent.fromThrowable("Unable to access directory " + dataDirectory
|
||||
+ ". 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.", e)
|
||||
.term()
|
||||
.expected()
|
||||
.handle();
|
||||
|
|
|
@ -7,6 +7,7 @@ import io.xpipe.app.comp.store.StoreViewState;
|
|||
import io.xpipe.app.core.*;
|
||||
import io.xpipe.app.core.check.AppAvCheck;
|
||||
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.ext.ActionProvider;
|
||||
import io.xpipe.app.ext.DataStoreProviders;
|
||||
|
@ -51,6 +52,7 @@ public class BaseMode extends OperationMode {
|
|||
AppSid.init();
|
||||
LocalShell.init();
|
||||
AppShellCheck.check();
|
||||
AppRosettaCheck.check();
|
||||
XPipeDistributionType.init();
|
||||
AppPrefs.setLocalDefaultsIfNeeded();
|
||||
// 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) {
|
||||
try {
|
||||
// Register stage theming early to make it apply for any potential early popups
|
||||
ModifiedStage.init();
|
||||
|
||||
// Only for handling SIGTERM
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
TrackEvent.info("Received SIGTERM externally");
|
||||
|
@ -96,7 +99,9 @@ public abstract class OperationMode {
|
|||
Thread.setDefaultUncaughtExceptionHandler((thread, ex) -> {
|
||||
// It seems like a few exceptions are thrown in the quantum renderer
|
||||
// when in shutdown. We can ignore these
|
||||
if (OperationMode.isInShutdown() && Platform.isFxApplicationThread() && ex instanceof NullPointerException) {
|
||||
if (OperationMode.isInShutdown()
|
||||
&& Platform.isFxApplicationThread()
|
||||
&& ex instanceof NullPointerException) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -117,8 +122,6 @@ public abstract class OperationMode {
|
|||
AppExtensionManager.init(true);
|
||||
AppI18n.init();
|
||||
AppPrefs.initLocal();
|
||||
// Register stage theming early to make it apply for any potential early popups
|
||||
ModifiedStage.init();
|
||||
AppBeaconServer.setupPort();
|
||||
TrackEvent.info("Finished initial setup");
|
||||
} catch (Throwable ex) {
|
||||
|
@ -224,7 +227,7 @@ public abstract class OperationMode {
|
|||
CURRENT = null;
|
||||
r.run();
|
||||
} catch (Throwable ex) {
|
||||
ErrorEvent.fromThrowable(ex).build().handle();
|
||||
ErrorEvent.fromThrowable(ex).handle();
|
||||
OperationMode.halt(1);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import io.xpipe.app.prefs.AppPrefs;
|
|||
import io.xpipe.app.update.UpdateAvailableAlert;
|
||||
import io.xpipe.app.util.PlatformState;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
|
||||
import javafx.application.Application;
|
||||
|
||||
public abstract class PlatformMode extends OperationMode {
|
||||
|
|
|
@ -11,7 +11,7 @@ public class TrayMode extends PlatformMode {
|
|||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return !OsType.getLocal().equals(OsType.MACOS)
|
||||
return OsType.getLocal().equals(OsType.WINDOWS)
|
||||
&& super.isSupported()
|
||||
&& Desktop.isDesktopSupported()
|
||||
&& SystemTray.isSupported();
|
||||
|
|
|
@ -16,6 +16,8 @@ import javafx.beans.property.SimpleBooleanProperty;
|
|||
import javafx.geometry.Rectangle2D;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.paint.Color;
|
||||
|
@ -167,8 +169,8 @@ public class AppMainWindow {
|
|||
e.consume();
|
||||
});
|
||||
|
||||
stage.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (event.getCode().equals(KeyCode.Q) && event.isShortcutDown()) {
|
||||
stage.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (new KeyCodeCombination(KeyCode.Q, KeyCombination.SHORTCUT_DOWN).match(event)) {
|
||||
stage.close();
|
||||
AppPrefs.get().closeBehaviour().getValue().run();
|
||||
event.consume();
|
||||
|
@ -184,7 +186,7 @@ public class AppMainWindow {
|
|||
stage.setY(state.windowY);
|
||||
stage.setWidth(state.windowWidth);
|
||||
stage.setHeight(state.windowHeight);
|
||||
// stage.setMaximized(state.maximized);
|
||||
stage.setMaximized(state.maximized);
|
||||
|
||||
TrackEvent.debug("Window loaded saved bounds");
|
||||
} else if (!AppProperties.get().isShowcase()) {
|
||||
|
@ -271,8 +273,8 @@ public class AppMainWindow {
|
|||
contentR.prefHeightProperty().bind(stage.getScene().heightProperty());
|
||||
|
||||
if (OsType.getLocal().equals(OsType.LINUX) || OsType.getLocal().equals(OsType.MACOS)) {
|
||||
stage.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
|
||||
stage.getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
|
||||
AppPrefs.get().closeBehaviour().getValue().run();
|
||||
event.consume();
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import io.xpipe.app.prefs.AppPrefs;
|
|||
import io.xpipe.app.util.InputHelper;
|
||||
import io.xpipe.app.util.ThreadHelper;
|
||||
import io.xpipe.core.process.OsType;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.css.PseudoClass;
|
||||
|
@ -17,6 +18,8 @@ import javafx.scene.Scene;
|
|||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.Region;
|
||||
|
@ -25,7 +28,6 @@ import javafx.scene.paint.Color;
|
|||
import javafx.scene.text.Text;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.StageStyle;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
|
@ -142,8 +144,8 @@ public class AppWindowHelper {
|
|||
event.consume();
|
||||
});
|
||||
AppWindowBounds.fixInvalidStagePosition(s);
|
||||
a.getDialogPane().getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
|
||||
a.getDialogPane().getScene().addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
|
||||
s.close();
|
||||
event.consume();
|
||||
return;
|
||||
|
@ -260,8 +262,8 @@ public class AppWindowHelper {
|
|||
}
|
||||
});
|
||||
|
||||
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (event.getCode().equals(KeyCode.W) && event.isShortcutDown()) {
|
||||
scene.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
|
||||
if (new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN).match(event)) {
|
||||
stage.close();
|
||||
event.consume();
|
||||
}
|
||||
|
|
|
@ -14,21 +14,16 @@ import javafx.stage.StageStyle;
|
|||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
public class ModifiedStage extends Stage {
|
||||
|
||||
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() {
|
||||
var windowsField = Window.class.getDeclaredField("windows");
|
||||
windowsField.setAccessible(true);
|
||||
ObservableList<Window> list = (ObservableList<Window>) windowsField.get(null);
|
||||
ObservableList<Window> list = Window.getWindows();
|
||||
list.addListener((ListChangeListener<Window>) c -> {
|
||||
if (c.next() && c.wasAdded()) {
|
||||
var added = c.getAddedSubList().getFirst();
|
||||
|
@ -62,24 +57,50 @@ public class ModifiedStage extends Stage {
|
|||
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("separate-frame"), true);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
switch (OsType.getLocal()) {
|
||||
case OsType.Linux linux -> {}
|
||||
case OsType.MacOs macOs -> {
|
||||
var ctrl = new NativeMacOsWindowControl(stage);
|
||||
var seamlessFrame = !AppPrefs.get().performanceMode().get() && mergeFrame();
|
||||
var seamlessFrameApplied = ctrl.setAppearance(
|
||||
seamlessFrame, AppPrefs.get().theme.getValue().isDark())
|
||||
&& seamlessFrame;
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied);
|
||||
stage.getScene()
|
||||
.getRoot()
|
||||
.pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied);
|
||||
}
|
||||
case OsType.Windows windows -> {
|
||||
var ctrl = new NativeWinWindowControl(stage);
|
||||
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) {
|
||||
|
|
|
@ -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 {
|
||||
|
||||
List<ActionProvider> ALL = new ArrayList<>();
|
||||
List<ActionProvider> ALL_STANDALONE = new ArrayList<>();
|
||||
|
||||
static void initProviders() {
|
||||
for (ActionProvider actionProvider : ALL) {
|
||||
|
@ -111,7 +112,7 @@ public interface ActionProvider {
|
|||
|
||||
String getIcon(DataStoreEntryRef<T> store);
|
||||
|
||||
Class<T> getApplicableClass();
|
||||
Class<?> getApplicableClass();
|
||||
|
||||
default boolean showBusy() {
|
||||
return true;
|
||||
|
@ -120,9 +121,11 @@ public interface ActionProvider {
|
|||
|
||||
interface BranchDataStoreCallSite<T extends DataStore> extends DataStoreCallSite<T> {
|
||||
|
||||
default List<ActionProvider> getChildren() {
|
||||
return List.of();
|
||||
default boolean isDynamicallyGenerated(){
|
||||
return false;
|
||||
}
|
||||
|
||||
List<? extends ActionProvider> getChildren(DataStoreEntryRef<T> store);
|
||||
}
|
||||
|
||||
interface LeafDataStoreCallSite<T extends DataStore> extends DataStoreCallSite<T> {
|
||||
|
@ -145,6 +148,18 @@ public interface ActionProvider {
|
|||
ALL.addAll(ServiceLoader.load(layer, ActionProvider.class).stream()
|
||||
.map(actionProviderProvider -> actionProviderProvider.get())
|
||||
.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,
|
||||
SCRIPT,
|
||||
CLUSTER,
|
||||
DESKTOP
|
||||
DESKTOP,
|
||||
SERIAL
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import io.xpipe.app.core.AppI18n;
|
|||
import io.xpipe.app.core.AppImages;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.issue.ErrorEvent;
|
||||
import io.xpipe.app.storage.DataStorage;
|
||||
import io.xpipe.app.storage.DataStoreEntry;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.core.util.JacksonizedValue;
|
||||
|
@ -27,10 +26,16 @@ import java.util.List;
|
|||
|
||||
public interface DataStoreProvider {
|
||||
|
||||
default boolean showProviderChoice() {
|
||||
return true;
|
||||
}
|
||||
|
||||
default boolean shouldShow(StoreEntryWrapper w) {
|
||||
return true;
|
||||
}
|
||||
|
||||
default void onParentRefresh(DataStoreEntry entry) {}
|
||||
|
||||
default void onChildrenRefresh(DataStoreEntry entry) {}
|
||||
|
||||
default ObservableBooleanValue busy(StoreEntryWrapper wrapper) {
|
||||
|
@ -71,21 +76,16 @@ public interface DataStoreProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
default String browserDisplayName(DataStore store) {
|
||||
var e = DataStorage.get().getStoreDisplayName(store);
|
||||
return e.orElse("?");
|
||||
default String displayName(DataStoreEntry entry) {
|
||||
return entry.getName();
|
||||
}
|
||||
|
||||
default List<String> getSearchableTerms(DataStore store) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
default boolean shouldEdit() {
|
||||
return false;
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -104,6 +104,10 @@ public interface DataStoreProvider {
|
|||
return Comp.empty();
|
||||
}
|
||||
|
||||
default boolean canConnectDuringCreation() {
|
||||
return false;
|
||||
}
|
||||
|
||||
default Comp<?> createInsightsComp(ObservableValue<DataStore> store) {
|
||||
var content = Bindings.createStringBinding(
|
||||
() -> {
|
||||
|
@ -152,6 +156,10 @@ public interface DataStoreProvider {
|
|||
return DataStoreUsageCategory.DATABASE;
|
||||
}
|
||||
|
||||
if (cc == DataStoreCreationCategory.SERIAL) {
|
||||
return DataStoreUsageCategory.SERIAL;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -191,7 +199,7 @@ public interface DataStoreProvider {
|
|||
return null;
|
||||
}
|
||||
|
||||
default ObservableValue<String> informationString(StoreEntryWrapper wrapper) {
|
||||
default ObservableValue<String> informationString(StoreSection section) {
|
||||
return new SimpleStringProperty(null);
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,10 @@ public class DataStoreProviders {
|
|||
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() {
|
||||
|
|
|
@ -16,5 +16,7 @@ public enum DataStoreUsageCategory {
|
|||
@JsonProperty("desktop")
|
||||
DESKTOP,
|
||||
@JsonProperty("group")
|
||||
GROUP;
|
||||
GROUP,
|
||||
@JsonProperty("serial")
|
||||
SERIAL;
|
||||
}
|
||||
|
|
|
@ -14,8 +14,8 @@ public interface EnabledParentStoreProvider extends DataStoreProvider {
|
|||
|
||||
@Override
|
||||
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
|
||||
if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) {
|
||||
return StoreEntryComp.create(sec.getWrapper(), null, preferLarge);
|
||||
if (sec.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {
|
||||
return StoreEntryComp.create(sec, null, preferLarge);
|
||||
}
|
||||
|
||||
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
|
||||
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
|
||||
if (sec.getWrapper().getValidity().getValue() != DataStoreEntry.Validity.COMPLETE) {
|
||||
return StoreEntryComp.create(sec.getWrapper(), null, preferLarge);
|
||||
if (sec.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {
|
||||
return StoreEntryComp.create(sec, null, preferLarge);
|
||||
}
|
||||
|
||||
var enabled = StoreToggleComp.<StatefulDataStore<EnabledStoreState>>enableToggle(
|
||||
|
@ -20,6 +20,6 @@ public interface EnabledStoreProvider extends DataStoreProvider {
|
|||
var state = s.getState().toBuilder().enabled(aBoolean).build();
|
||||
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.core.process.ShellControl;
|
||||
import io.xpipe.core.store.DataStore;
|
||||
import io.xpipe.core.util.FailableRunnable;
|
||||
import io.xpipe.core.util.ModuleLayerLoader;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
|
@ -22,10 +20,6 @@ public abstract class ScanProvider {
|
|||
return ALL;
|
||||
}
|
||||
|
||||
public ScanOperation create(DataStore store) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public ScanOperation create(DataStoreEntry entry, ShellControl sc) throws Exception {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ public interface SingletonSessionStoreProvider extends DataStoreProvider {
|
|||
@Override
|
||||
default StoreEntryComp customEntryComp(StoreSection sec, boolean preferLarge) {
|
||||
var t = createToggleComp(sec);
|
||||
return StoreEntryComp.create(sec.getWrapper(), t, preferLarge);
|
||||
return StoreEntryComp.create(sec, t, preferLarge);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import io.xpipe.app.browser.session.BrowserChooserComp;
|
||||
import io.xpipe.app.comp.base.ButtonComp;
|
||||
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.core.store.FileNames;
|
||||
import io.xpipe.core.store.FileSystemStore;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
|
||||
import atlantafx.base.theme.Styles;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>> {
|
||||
|
||||
private final Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem;
|
||||
private final Property<String> filePath;
|
||||
private final boolean allowSync;
|
||||
|
||||
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(
|
||||
ObservableValue<DataStoreEntryRef<T>> fileSystem, Property<String> filePath) {
|
||||
this.fileSystem = new SimpleObjectProperty<>();
|
||||
fileSystem.subscribe(val -> {
|
||||
this.fileSystem.setValue(val);
|
||||
});
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(
|
||||
Property<DataStoreEntryRef<T>> fileSystem, Property<String> filePath) {
|
||||
Property<DataStoreEntryRef<T>> fileSystem, Property<String> filePath, boolean allowSync
|
||||
) {
|
||||
this.allowSync = allowSync;
|
||||
this.fileSystem = new SimpleObjectProperty<>();
|
||||
fileSystem.subscribe(val -> {
|
||||
this.fileSystem.setValue(val);
|
||||
|
@ -79,7 +70,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
|||
},
|
||||
false);
|
||||
})
|
||||
.styleClass(Styles.CENTER_PILL)
|
||||
.styleClass(allowSync ? Styles.CENTER_PILL : Styles.RIGHT_PILL)
|
||||
.grow(false, true);
|
||||
|
||||
var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> {
|
||||
|
@ -126,7 +117,13 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
|
|||
gitShareButton.tooltipKey("gitShareFileTooltip");
|
||||
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));
|
||||
|
||||
layout.apply(struc -> {
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
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.store.*;
|
||||
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.LocalStore;
|
||||
import io.xpipe.core.store.ShellStore;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
|
@ -26,6 +25,9 @@ import javafx.scene.control.MenuButton;
|
|||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import atlantafx.base.controls.Popover;
|
||||
import atlantafx.base.theme.Styles;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
|
@ -101,9 +103,9 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
|
|||
comp.disable(new SimpleBooleanProperty(true));
|
||||
}
|
||||
},
|
||||
storeEntryWrapper -> {
|
||||
if (applicable.test(storeEntryWrapper)) {
|
||||
selected.setValue(storeEntryWrapper.getEntry().ref());
|
||||
sec -> {
|
||||
if (applicable.test(sec.getWrapper())) {
|
||||
selected.setValue(sec.getWrapper().getEntry().ref());
|
||||
popover.hide();
|
||||
}
|
||||
});
|
||||
|
@ -112,22 +114,31 @@ public class DataStoreChoiceComp<T extends DataStore> extends SimpleComp {
|
|||
StoreViewState.get().getActiveCategory(),
|
||||
selectedCategory)
|
||||
.styleClass(Styles.LEFT_PILL);
|
||||
var filter =
|
||||
new FilterComp(filterText).styleClass(Styles.CENTER_PILL).hgrow();
|
||||
var filter = new FilterComp(filterText).styleClass(Styles.CENTER_PILL).hgrow();
|
||||
|
||||
var addButton = Comp.of(() -> {
|
||||
MenuButton m = new MenuButton(null, new FontIcon("mdi2p-plus-box-outline"));
|
||||
m.setMaxHeight(100);
|
||||
m.setMinHeight(0);
|
||||
StoreCreationMenu.addButtons(m);
|
||||
return m;
|
||||
})
|
||||
.accessibleTextKey("addConnection")
|
||||
.padding(new Insets(-2))
|
||||
.styleClass(Styles.RIGHT_PILL)
|
||||
.grow(false, true);
|
||||
.padding(new Insets(-5))
|
||||
.styleClass(Styles.RIGHT_PILL);
|
||||
|
||||
var top = new HorizontalComp(List.of(category, filter.hgrow(), addButton))
|
||||
var top = new HorizontalComp(List.of(category, filter, addButton))
|
||||
.styleClass("top")
|
||||
.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 -> {
|
||||
// Ugly solution to focus the text field
|
||||
// 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");
|
||||
},
|
||||
true)
|
||||
false)
|
||||
.padding(new Insets(0))
|
||||
.apply(struc -> struc.get().setMinHeight(0))
|
||||
.apply(struc -> ((VBox) struc.get().getContent()).setSpacing(5));
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
package io.xpipe.app.fxcomps.impl;
|
||||
|
||||
import atlantafx.base.controls.CustomTextField;
|
||||
import io.xpipe.app.core.AppActionLinkDetector;
|
||||
import io.xpipe.app.core.AppI18n;
|
||||
import io.xpipe.app.fxcomps.Comp;
|
||||
import io.xpipe.app.fxcomps.CompStructure;
|
||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.geometry.Pos;
|
||||
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 atlantafx.base.controls.CustomTextField;
|
||||
import org.kordamp.ikonli.javafx.FontIcon;
|
||||
|
||||
import java.util.Objects;
|
||||
|
@ -36,16 +40,29 @@ public class FilterComp extends Comp<CompStructure<CustomTextField>> {
|
|||
}
|
||||
});
|
||||
var filter = new CustomTextField();
|
||||
filter.alignmentProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
return filter.isFocused() || (filter.getText() != null && !filter.getText().isEmpty()) ? Pos.CENTER_LEFT : Pos.CENTER;
|
||||
}, filter.textProperty(), filter.focusedProperty()));
|
||||
filter.setMinHeight(0);
|
||||
filter.setMaxHeight(2000);
|
||||
filter.getStyleClass().add("filter-comp");
|
||||
filter.promptTextProperty().bind(AppI18n.observable("searchFilter"));
|
||||
filter.setLeft(fi);
|
||||
filter.setRight(clear);
|
||||
filter.rightProperty()
|
||||
.bind(Bindings.createObjectBinding(
|
||||
() -> {
|
||||
return filter.isFocused()
|
||||
|| (filter.getText() != null
|
||||
&& !filter.getText().isEmpty())
|
||||
? clear
|
||||
: fi;
|
||||
},
|
||||
filter.focusedProperty()));
|
||||
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 -> {
|
||||
PlatformThread.runLaterIfNeeded(() -> {
|
||||
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