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

3
.gitignore vendored
View file

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

View file

@ -29,7 +29,7 @@ It currently supports:
- Quickly perform various commonly used actions like starting/stopping containers, establishing tunnels, and more
- 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 XPipes 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

View file

@ -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()) {

View file

@ -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));
});

View file

@ -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);

View file

@ -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;
}

View 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

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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

View file

@ -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;

View file

@ -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();
}

View file

@ -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

View file

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

View file

@ -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");

View file

@ -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();

View file

@ -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());

View file

@ -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());
});

View file

@ -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;
});
},

View file

@ -12,10 +12,12 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.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,

View file

@ -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,71 +39,70 @@ 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 sourceItem = syncItems.stream()
var list = new BrowserSelectionListComp(binding, entry -> {
var sourceItem = model.getCurrentItems().stream()
.filter(item -> item.getBrowserEntry() == entry)
.findAny();
if (sourceItem.isEmpty()) {
return "?";
return new SimpleStringProperty("?");
}
var name = entry.getModel() == null
synchronized (sourceItem.get().getProgress()) {
return Bindings.createStringBinding(
() -> {
var p = sourceItem.get().getProgress().getValue();
var progressSuffix = p == null
|| sourceItem
.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", () -> {
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))
var stack = new StackComp(List.of(backgroundStack, listBox))
.apply(DragOverPseudoClassAugment.create())
.apply(struc -> {
struc.get().setOnDragOver(event -> {
@ -110,13 +111,6 @@ public class BrowserTransferComp extends SimpleComp {
event.acceptTransferModes(TransferMode.ANY);
event.consume();
}
// Accept drops from outside the app window
if (event.getGestureSource() == null
&& !event.getDragboard().getFiles().isEmpty()) {
event.acceptTransferModes(TransferMode.ANY);
event.consume();
}
});
struc.get().setOnDragDropped(event -> {
// Accept drops from inside the app window
@ -138,30 +132,13 @@ public class BrowserTransferComp extends SimpleComp {
event.setDropCompleted(true);
event.consume();
}
// Accept drops from outside the app window
if (event.getGestureSource() == null) {
model.dropLocal(event.getDragboard().getFiles());
event.setDropCompleted(true);
event.consume();
}
});
struc.get().setOnDragDetected(event -> {
if (syncDownloaded.getValue()) {
return;
}
var selected = syncItems.stream()
var items = model.getCurrentItems();
var selected = items.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()
var files = items.stream()
.filter(item -> item.downloadFinished().get())
.map(item -> {
try {
@ -170,15 +147,20 @@ public class BrowserTransferComp extends SimpleComp {
return Optional.<File>empty();
}
return Optional.of(
file.toRealPath().toFile());
return Optional.of(file.toRealPath().toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
})
.flatMap(Optional::stream)
.toList();
if (files.isEmpty()) {
return;
}
var cc = new ClipboardContent();
cc.putFiles(files);
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
db.setContent(cc);
Image image = BrowserSelectionListComp.snapshot(FXCollections.observableList(selected));
@ -197,12 +179,11 @@ public class BrowserTransferComp extends SimpleComp {
model.clear(false);
event.consume();
});
}),
syncDownloaded);
});
stack.apply(struc -> {
model.getBrowserSessionModel().getDraggingFiles().addListener((observable, oldValue, newValue) -> {
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("highlighted"),newValue);
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("highlighted"), newValue);
});
});

View file

@ -7,13 +7,12 @@ import io.xpipe.app.browser.file.LocalFileSystem;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.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,56 +21,83 @@ 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) {
synchronized (items) {
entries.forEach(entry -> {
var name = entry.getFileName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
@ -81,40 +107,11 @@ public class BrowserTransferModel {
Path file = TEMP.resolve(name);
var item = new Item(model, name, entry, file);
items.add(item);
allDownloaded.set(false);
});
}
public void dropLocal(List<File> entries) {
if (entries.isEmpty()) {
return;
}
var empty = items.isEmpty();
try {
var paths = entries.stream().map(File::toPath).filter(Files::exists).toList();
for (Path path : paths) {
var entry = LocalFileSystem.getLocalBrowserEntry(path);
var name = entry.getFileName();
if (items.stream().anyMatch(item -> item.getName().equals(name))) {
return;
}
var item = new Item(null, name, entry, path);
item.progress.setValue(BrowserTransferProgress.finished(
entry.getFileName(), entry.getRawFileEntry().getSize()));
items.add(item);
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
if (empty) {
allDownloaded.set(true);
}
}
public void download() {
executor.submit(() -> {
public void downloadSingle(Item item) {
try {
FileUtils.forceMkdir(TEMP.toFile());
} catch (IOException e) {
@ -122,36 +119,58 @@ public class BrowserTransferModel {
return;
}
for (Item item : new ArrayList<>(items)) {
if (item.downloadFinished().get()) {
continue;
return;
}
if (item.getOpenFileSystemModel() != null
&& item.getOpenFileSystemModel().isClosed()) {
continue;
return;
}
try {
try (var ignored = new BooleanScope(downloading).start()) {
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);
}
}
allDownloaded.set(true);
});
}
public void transferToDownloads() throws Exception {
List<Item> toMove;
synchronized (items) {
toMove =
items.stream().filter(item -> item.downloadFinished().get()).toList();
if (toMove.isEmpty()) {
return;
}
items.removeAll(toMove);
}
var files = toMove.stream().map(item -> item.getLocalFile()).toList();
var downloads = DesktopHelper.getDownloadsDirectory();
for (Path file : files) {
var target = downloads.resolve(file.getFileName());
// Prevent DirectoryNotEmptyException
if (Files.exists(target) && Files.isDirectory(target)) {
Files.delete(target);
}
Files.move(file, target, StandardCopyOption.REPLACE_EXISTING);
}
DesktopHelper.browseFileInDirectory(downloads.resolve(files.getFirst().getFileName()));
}
@Value
@ -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);
}
}
}
}

View file

@ -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);
}
}

View file

@ -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)

View file

@ -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);
}

View file

@ -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() {

View file

@ -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) + " ...";
}

View file

@ -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));
}
}
}

View file

@ -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()) {

View file

@ -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;

View file

@ -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;
}

View file

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

View file

@ -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);

View file

@ -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;
}

View file

@ -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);
}

View file

@ -72,13 +72,15 @@ 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;
if (delay) {
// After 10 seconds
TIMEOUT_TIMER.schedule(
new TimerTask() {
@ -98,6 +100,10 @@ public class OpenFileSystemSavedState {
}
},
10000);
} else {
updateRecent(dir);
save();
}
}
private void updateRecent(String dir) {

View file

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

View file

@ -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);
});

View file

@ -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,

View file

@ -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);

View file

@ -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,13 +80,15 @@ 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()) {
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
@ -103,6 +98,7 @@ public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSess
selectedEntry.setValue(model);
}
}
}
if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
} else {

View file

@ -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();
}

View file

@ -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);

View file

@ -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");

View file

@ -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()));
}

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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(
() -> {

View file

@ -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");

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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())) {

View file

@ -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);
@ -162,9 +167,14 @@ public class StoreCreationComp extends DialogComp {
ThreadHelper.runAsync(() -> {
if (!DataStorage.get().getStoreEntries().contains(e)) {
DataStorage.get().addStoreEntryIfNotPresent(newE);
} else {
// We didn't change anything
if (e.getStore().equals(newE.getStore())) {
e.setName(newE.getName());
} else {
DataStorage.get().updateEntry(e, newE);
}
}
});
},
true,
@ -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");
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);

View file

@ -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));
}

View file

@ -14,6 +14,7 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.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());
});
});

View file

@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.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(

View file

@ -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,7 +171,7 @@ 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()
@ -174,7 +182,7 @@ public class StoreEntryWrapper {
.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);
});
}

View file

@ -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();

View file

@ -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();

View file

@ -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();
}

View file

@ -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())

View file

@ -7,6 +7,7 @@ import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.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()),
() -> {

View file

@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.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")

View file

@ -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();
}

View file

@ -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();
});
}

View file

@ -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;

View file

@ -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

View file

@ -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);

View file

@ -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 -> {

View file

@ -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) {}
}

View file

@ -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);

View file

@ -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() {

View file

@ -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();
}

View file

@ -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();
}
});
}

View file

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

View file

@ -6,6 +6,8 @@ import io.xpipe.core.process.OsType;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.process.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;
}
}

View file

@ -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
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."))
+ "In case you use cloud storage, verify that your cloud storage is working and you are logged in.", e)
.term()
.expected()
.handle();

View file

@ -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

View file

@ -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);
}

View file

@ -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 {

View file

@ -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();

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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,12 +57,32 @@ 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;
}
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(),
@ -78,8 +93,14 @@ public class ModifiedStage extends Stage {
} 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) {

View file

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

View file

@ -16,6 +16,7 @@ import java.util.ServiceLoader;
public interface ActionProvider {
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());
}
}
}

View file

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

View file

@ -10,7 +10,6 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppImages;
import io.xpipe.app.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);
}

View file

@ -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() {

View file

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

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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) {

View file

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

View file

@ -1,5 +1,6 @@
package io.xpipe.app.fxcomps.impl;
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 -> {

View file

@ -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

View file

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

View file

@ -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