This commit is contained in:
crschnick 2025-03-23 18:35:14 +00:00
parent 03d222e5f5
commit 88a3d63885
239 changed files with 3748 additions and 3422 deletions

View file

@ -49,13 +49,9 @@ dependencies {
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
api("com.github.weisj:jsvg:1.7.0")
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
api 'org.bouncycastle:bcprov-jdk18on:1.80'
api 'info.picocli:picocli:4.7.6'
api ('org.kohsuke:github-api:1.326') {
exclude group: 'org.apache.commons', module: 'commons-lang3'
}
api 'org.apache.commons:commons-lang3:3.17.0'
api 'io.sentry:sentry:7.20.0'
api 'commons-io:commons-io:2.18.0'

View file

@ -112,7 +112,8 @@ public class AppBeaconServer {
executor.shutdown();
try {
executor.awaitTermination(30, TimeUnit.SECONDS);
} catch (InterruptedException ignored) {}
} catch (InterruptedException ignored) {
}
}
private void initAuthSecret() throws IOException {

View file

@ -2,15 +2,10 @@ package io.xpipe.app.beacon.impl;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStorageQuery;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.beacon.api.ConnectionQueryExchange;
import com.sun.net.httpserver.HttpExchange;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class ConnectionQueryExchangeImpl extends ConnectionQueryExchange {
@Override

View file

@ -22,14 +22,14 @@ public class FsReadExchangeImpl extends FsReadExchange {
var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
var fs = new ConnectionFileSystem(shell.getControl());
if (!fs.fileExists(msg.getPath().toString())) {
if (!fs.fileExists(msg.getPath())) {
throw new BeaconClientException("File does not exist");
}
var size = fs.getFileSize(msg.getPath().toString());
var size = fs.getFileSize(msg.getPath());
if (size > 100_000_000) {
var file = BlobManager.get().newBlobFile();
try (var in = fs.openInput(msg.getPath().toString())) {
try (var in = fs.openInput(msg.getPath())) {
var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), size);
try (var fileOut =
Files.newOutputStream(file.resolve(msg.getPath().getFileName()))) {
@ -45,7 +45,7 @@ public class FsReadExchangeImpl extends FsReadExchange {
}
} else {
byte[] bytes;
try (var in = fs.openInput(msg.getPath().toString())) {
try (var in = fs.openInput(msg.getPath())) {
var fixedIn = new FixedSizeInputStream(new BufferedInputStream(in), size);
bytes = fixedIn.readAllBytes();
in.transferTo(OutputStream.nullOutputStream());

View file

@ -21,9 +21,7 @@ public class FsScriptExchangeImpl extends FsScriptExchange {
data = new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
data = shell.getControl().getShellDialect().prepareScriptContent(data);
var file = ScriptHelper.getExecScriptFile(shell.getControl());
shell.getControl().view().writeScriptFile(file, data);
file = ScriptHelper.fixScriptPermissions(shell.getControl(), file);
var file = ScriptHelper.createExecScript(shell.getControl(), data);
return Response.builder().path(file).build();
}
}

View file

@ -16,7 +16,7 @@ public class FsWriteExchangeImpl extends FsWriteExchange {
var shell = AppBeaconServer.get().getCache().getShellSession(msg.getConnection());
var fs = new ConnectionFileSystem(shell.getControl());
try (var in = BlobManager.get().getBlob(msg.getBlob());
var os = fs.openOutput(msg.getPath().toString(), in.available())) {
var os = fs.openOutput(msg.getPath(), in.available())) {
in.transferTo(os);
}
return Response.builder().build();

View file

@ -1,8 +1,5 @@
package io.xpipe.app.beacon.impl;
import atlantafx.base.layout.ModalBox;
import com.sun.net.httpserver.HttpExchange;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.ext.ShellStore;
@ -14,6 +11,8 @@ import io.xpipe.beacon.BeaconClientException;
import io.xpipe.beacon.BeaconServerException;
import io.xpipe.beacon.api.TerminalExternalLaunchExchange;
import com.sun.net.httpserver.HttpExchange;
import java.util.List;
public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchExchange {
@ -26,13 +25,15 @@ public class TerminalExternalLaunchExchangeImpl extends TerminalExternalLaunchEx
}
if (found.size() > 1) {
throw new BeaconServerException("Multiple connections found: " + found.stream().map(DataStoreEntry::getName).toList());
throw new BeaconServerException("Multiple connections found: "
+ found.stream().map(DataStoreEntry::getName).toList());
}
var e = found.getFirst();
var isShell = e.getStore() instanceof ShellStore;
if (!isShell) {
throw new BeaconClientException("Connection " + DataStorage.get().getStorePath(e).toString() + " is not a shell connection");
throw new BeaconClientException(
"Connection " + DataStorage.get().getStorePath(e).toString() + " is not a shell connection");
}
if (!checkPermission()) {

View file

@ -177,7 +177,7 @@ public class BrowserFileChooserSessionComp extends DialogComp {
.setAll(c.getList().stream()
.map(s -> {
var field = new TextField(
s.getRawFileEntry().getPath());
s.getRawFileEntry().getPath().toString());
field.setEditable(false);
field.getStyleClass().add("chooser-selection");
HBox.setHgrow(field, Priority.ALWAYS);

View file

@ -7,7 +7,7 @@ import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
@ -78,7 +78,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
@ -96,7 +96,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
sessionEntries.add(model);
}
if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
model.initWithGivenDirectory(path.apply(model).toDirectory());
} else {
model.initWithDefaultDirectory();
}

View file

@ -9,7 +9,7 @@ import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
@ -199,7 +199,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
@ -212,7 +212,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
public BrowserFileSystemTabModel openFileSystemSync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<BrowserFileSystemTabModel, String, Exception> path,
FailableFunction<BrowserFileSystemTabModel, FilePath, Exception> path,
BooleanProperty externalBusy,
boolean select)
throws Exception {
@ -232,7 +232,7 @@ public class BrowserFullSessionModel extends BrowserAbstractSessionModel<Browser
}
}
if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
model.initWithGivenDirectory(path.apply(model).toDirectory());
} else {
model.initWithDefaultDirectory();
}

View file

@ -17,7 +17,7 @@ import java.util.stream.Collectors;
public class BrowserAlerts {
public static FileConflictChoice showFileConflictAlert(String file, boolean multiple) {
public static FileConflictChoice showFileConflictAlert(FilePath file, boolean multiple) {
var map = new LinkedHashMap<ButtonType, FileConflictChoice>();
map.put(new ButtonType(AppI18n.get("cancel"), ButtonBar.ButtonData.CANCEL_CLOSE), FileConflictChoice.CANCEL);
if (multiple) {
@ -96,7 +96,7 @@ public class BrowserAlerts {
var names = namesHeader + "\n"
+ source.stream()
.limit(10)
.map(entry -> "- " + new FilePath(entry.getPath()).getFileName())
.map(entry -> "- " + entry.getPath().getFileName())
.collect(Collectors.joining("\n"));
if (source.size() > 10) {
names += "\n+ " + (source.size() - 10) + " ...";

View file

@ -64,9 +64,9 @@ public class BrowserBreadcrumbBar extends SimpleComp {
});
}
var elements = FileNames.splitHierarchy(val);
var elements = val.splitHierarchy();
var modifiedElements = new ArrayList<>(elements);
if (val.startsWith("/")) {
if (val.toString().startsWith("/")) {
modifiedElements.addFirst("/");
}
Breadcrumbs.BreadCrumbItem<String> items =

View file

@ -4,7 +4,6 @@ import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import lombok.Getter;
@ -74,6 +73,6 @@ public class BrowserEntry {
}
public String getFileName() {
return FileNames.getFileName(getRawFileEntry().getPath());
return getRawFileEntry().getPath().getFileName();
}
}

View file

@ -8,7 +8,6 @@ import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileInfo;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@ -63,8 +62,7 @@ public final class BrowserFileListComp extends SimpleComp {
filenameCol.textProperty().bind(AppI18n.observable("name"));
filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
param.getValue() != null
? FileNames.getFileName(
param.getValue().getRawFileEntry().getPath())
? param.getValue().getRawFileEntry().getPath().getFileName()
: null));
filenameCol.setComparator(Comparator.comparing(String::toLowerCase));
filenameCol.setSortType(ASCENDING);

View file

@ -294,7 +294,8 @@ public class BrowserFileListCompEntry {
return;
}
model.getFileSystemModel().cdAsync(item.getRawFileEntry().getPath());
model.getFileSystemModel()
.cdAsync(item.getRawFileEntry().getPath().toString());
}
};
DROP_TIMER.schedule(activeTask, 1200);

View file

@ -5,7 +5,6 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
@ -66,8 +65,9 @@ public final class BrowserFileListModel {
List<BrowserEntry> filtered = fileSystemModel.getFilter().getValue() != null
? all.getValue().stream()
.filter(entry -> {
var name = FileNames.getFileName(
entry.getRawFileEntry().getPath())
var name = entry.getRawFileEntry()
.getPath()
.getFileName()
.toLowerCase(Locale.ROOT);
var filterString =
fileSystemModel.getFilter().getValue().toLowerCase(Locale.ROOT);
@ -99,8 +99,8 @@ public final class BrowserFileListModel {
return old;
}
var fullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), old.getFileName());
var newFullPath = FileNames.join(fileSystemModel.getCurrentPath().get(), newName);
var fullPath = fileSystemModel.getCurrentPath().get().join(old.getFileName());
var newFullPath = fileSystemModel.getCurrentPath().get().join(newName);
// This check will fail on case-insensitive file systems when changing the case of the file
// So skip it in this case
@ -144,7 +144,7 @@ public final class BrowserFileListModel {
public void onDoubleClick(BrowserEntry entry) {
var r = entry.getRawFileEntry().resolved();
if (r.getKind() == FileKind.DIRECTORY) {
fileSystemModel.cdAsync(r.getPath());
fileSystemModel.cdAsync(r.getPath().toString());
}
if (AppPrefs.get().editFilesWithDoubleClick().get() && r.getKind() == FileKind.FILE) {

View file

@ -10,7 +10,6 @@ import io.xpipe.core.process.ElevationFunction;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileInfo;
import io.xpipe.core.store.FileNames;
import java.io.FilterOutputStream;
import java.io.IOException;
@ -72,7 +71,7 @@ public class BrowserFileOpener {
var key = calculateKey(entry);
FileBridge.get()
.openIO(
FileNames.getFileName(file),
file.getFileName(),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {
@ -93,7 +92,7 @@ public class BrowserFileOpener {
var key = calculateKey(entry);
FileBridge.get()
.openIO(
FileNames.getFileName(file),
file.getFileName(),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {
@ -119,7 +118,7 @@ public class BrowserFileOpener {
var key = calculateKey(entry);
FileBridge.get()
.openIO(
FileNames.getFileName(file),
file.getFileName(),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {

View file

@ -35,10 +35,10 @@ public class BrowserFileOverviewComp extends SimpleComp {
var graphic = new HorizontalComp(List.of(
icon,
new BrowserQuickAccessButtonComp(() -> new BrowserEntry(entry, model.getFileList()), model)));
var l = new Button(entry.getPath(), graphic.createRegion());
var l = new Button(entry.getPath().toString(), graphic.createRegion());
l.setGraphicTextGap(1);
l.setOnAction(event -> {
model.cdAsync(entry.getPath());
model.cdAsync(entry.getPath().toString());
event.consume();
});
l.setAlignment(Pos.CENTER_LEFT);

View file

@ -2,10 +2,7 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.*;
import java.time.Instant;
import java.util.List;
@ -67,7 +64,7 @@ public class BrowserFileSystemHelper {
}
}
public static String resolveDirectoryPath(BrowserFileSystemTabModel model, String path, boolean allowRewrite)
public static FilePath resolveDirectoryPath(BrowserFileSystemTabModel model, FilePath path, boolean allowRewrite)
throws Exception {
if (path == null) {
return null;
@ -82,23 +79,23 @@ public class BrowserFileSystemHelper {
return path;
}
var resolved = shell.get()
var resolved = FilePath.of(shell.get()
.getShellDialect()
.resolveDirectory(shell.get(), path)
.readStdoutOrThrow();
.resolveDirectory(shell.get(), path.toString())
.readStdoutOrThrow());
if (!FileNames.isAbsolute(resolved)) {
if (!resolved.isAbsolute()) {
throw new IllegalArgumentException(String.format("Directory %s is not absolute", resolved));
}
if (allowRewrite && model.getFileSystem().fileExists(path)) {
return FileNames.toDirectory(FileNames.getParent(path));
if (allowRewrite && model.getFileSystem().fileExists(resolved)) {
return resolved.getParent().toDirectory();
}
return FileNames.toDirectory(resolved);
return resolved.toDirectory();
}
public static void validateDirectoryPath(BrowserFileSystemTabModel model, String path, boolean verifyExists)
public static void validateDirectoryPath(BrowserFileSystemTabModel model, FilePath path, boolean verifyExists)
throws Exception {
if (path == null) {
return;
@ -125,7 +122,7 @@ public class BrowserFileSystemHelper {
}
}
public static FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
public static FileEntry getRemoteWrapper(FileSystem fileSystem, FilePath file) throws Exception {
return new FileEntry(
fileSystem,
file,

View file

@ -1,5 +1,7 @@
package io.xpipe.app.browser.file;
import io.xpipe.core.store.FilePath;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
@ -12,33 +14,33 @@ import java.util.Objects;
public final class BrowserFileSystemHistory {
private final IntegerProperty cursor = new SimpleIntegerProperty(-1);
private final List<String> history = new ArrayList<>();
private final List<FilePath> history = new ArrayList<>();
private final BooleanBinding canGoBack =
Bindings.createBooleanBinding(() -> cursor.get() > 0 && history.size() > 1, cursor);
private final BooleanBinding canGoForth =
Bindings.createBooleanBinding(() -> cursor.get() < history.size() - 1, cursor);
public List<String> getForwardHistory(int max) {
var l = new ArrayList<String>();
public List<FilePath> getForwardHistory(int max) {
var l = new ArrayList<FilePath>();
for (var i = cursor.get() + 1; i < Math.min(history.size(), cursor.get() + max); i++) {
l.add(history.get(i));
}
return l;
}
public List<String> getBackwardHistory(int max) {
var l = new ArrayList<String>();
public List<FilePath> getBackwardHistory(int max) {
var l = new ArrayList<FilePath>();
for (var i = cursor.get() - 1; i >= Math.max(0, cursor.get() - max); i--) {
l.add(history.get(i));
}
return l;
}
public String getCurrent() {
public FilePath getCurrent() {
return history.size() > 0 ? history.get(cursor.get()) : null;
}
public void updateCurrent(String s) {
public void updateCurrent(FilePath s) {
var lastString = getCurrent();
if (cursor.get() != -1 && Objects.equals(lastString, s)) {
return;
@ -52,11 +54,11 @@ public final class BrowserFileSystemHistory {
cursor.set(history.size() - 1);
}
public String back() {
public FilePath back() {
return back(1);
}
public String back(int i) {
public FilePath back(int i) {
if (!canGoBack.get()) {
return null;
}
@ -64,7 +66,7 @@ public final class BrowserFileSystemHistory {
return history.get(cursor.get());
}
public String forth(int i) {
public FilePath forth(int i) {
if (!canGoForth.get()) {
return history.getLast();
}

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.core.AppCache;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.JacksonMapper;
import javafx.application.Platform;
@ -41,12 +41,12 @@ public class BrowserFileSystemSavedState {
@Setter
private BrowserFileSystemTabModel model;
private String lastDirectory;
private FilePath lastDirectory;
@NonNull
private ObservableList<RecentEntry> recentDirectories;
public BrowserFileSystemSavedState(String lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
public BrowserFileSystemSavedState(FilePath lastDirectory, @NonNull ObservableList<RecentEntry> recentDirectories) {
this.lastDirectory = lastDirectory;
this.recentDirectories = recentDirectories;
}
@ -73,7 +73,7 @@ public class BrowserFileSystemSavedState {
AppCache.update("fs-state-" + model.getEntry().get().getUuid(), this);
}
public void cd(String dir, boolean delay) {
public void cd(FilePath dir, boolean delay) {
if (dir == null) {
lastDirectory = null;
return;
@ -107,9 +107,9 @@ public class BrowserFileSystemSavedState {
}
}
private synchronized void updateRecent(String dir) {
var without = FileNames.removeTrailingSlash(dir);
var with = FileNames.toDirectory(dir);
private synchronized void updateRecent(FilePath dir) {
var without = dir.removeTrailingSlash();
var with = dir.toDirectory();
recentDirectories.removeIf(recentEntry ->
Objects.equals(recentEntry.directory, without) || Objects.equals(recentEntry.directory, with));
@ -161,7 +161,7 @@ public class BrowserFileSystemSavedState {
recentDirectories = List.of();
}
var cleaned = recentDirectories.stream()
.map(recentEntry -> new RecentEntry(FileNames.toDirectory(recentEntry.directory), recentEntry.time))
.map(recentEntry -> new RecentEntry(recentEntry.directory.toDirectory(), recentEntry.time))
.filter(distinctBy(recentEntry -> recentEntry.getDirectory()))
.collect(Collectors.toCollection(ArrayList::new));
return new BrowserFileSystemSavedState(null, FXCollections.observableList(cleaned));
@ -173,7 +173,7 @@ public class BrowserFileSystemSavedState {
@Builder
public static class RecentEntry {
String directory;
FilePath directory;
Instant time;
}
}

View file

@ -10,6 +10,7 @@ import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.InputHelper;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.store.FilePath;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
@ -49,7 +50,7 @@ public class BrowserFileSystemTabComp extends SimpleComp {
private Region createContent() {
var root = new VBox();
var overview = new Button(null, new FontIcon("mdi2m-monitor"));
overview.setOnAction(e -> model.cdAsync(null));
overview.setOnAction(e -> model.cdAsync((FilePath) null));
new TooltipAugment<>("overview", new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN))
.augment(overview);
overview.disableProperty().bind(model.getInOverview());
@ -158,14 +159,14 @@ public class BrowserFileSystemTabComp extends SimpleComp {
root, new KeyCodeCombination(KeyCode.UP, KeyCombination.ALT_DOWN), true, keyEvent -> {
var p = model.getCurrentParentDirectory();
if (p != null) {
model.cdAsync(p.getPath());
model.cdAsync(p.getPath().toString());
}
keyEvent.consume();
});
InputHelper.onKeyCombination(root, new KeyCodeCombination(KeyCode.BACK_SPACE), false, keyEvent -> {
var p = model.getCurrentParentDirectory();
if (p != null) {
model.cdAsync(p.getPath());
model.cdAsync(p.getPath().toString());
}
keyEvent.consume();
});

View file

@ -41,7 +41,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
private final Property<String> filter = new SimpleStringProperty();
private final BrowserFileListModel fileList;
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
private final ReadOnlyObjectWrapper<FilePath> currentPath = new ReadOnlyObjectWrapper<>();
private final BrowserFileSystemHistory history = new BrowserFileSystemHistory();
private final BooleanProperty inOverview = new SimpleBooleanProperty();
private final Property<BrowserTransferProgress> progress = new SimpleObjectProperty<>();
@ -193,7 +193,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
return null;
}
var parent = FileNames.getParent(currentPath.get());
var parent = currentPath.get().getParent();
if (parent == null) {
return null;
}
@ -213,6 +213,10 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
return new FileEntry(fileSystem, currentPath.get(), null, 0, null, FileKind.DIRECTORY);
}
public void cdAsync(FilePath path) {
cdAsync(path != null ? path.toString() : null);
}
public void cdAsync(String path) {
ThreadHelper.runFailableAsync(() -> {
BooleanScope.executeExclusive(busy, () -> {
@ -260,7 +264,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
public Optional<String> cdSyncOrRetry(String path, boolean customInput) {
if (Objects.equals(path, currentPath.get())) {
var cps = currentPath.get() != null ? currentPath.get().toString() : null;
if (Objects.equals(path, cps)) {
return Optional.empty();
}
@ -273,7 +278,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
startIfNeeded();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
return Optional.ofNullable(cps);
}
// Fix common issues with paths
@ -288,12 +293,15 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
evaluatedPath = BrowserFileSystemHelper.evaluatePath(this, adjustedPath);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
return Optional.ofNullable(cps);
}
if (evaluatedPath == null) {
return Optional.empty();
}
// Handle commands typed into navigation bar
if (customInput
&& evaluatedPath != null
&& !evaluatedPath.isBlank()
&& !FileNames.isAbsolute(evaluatedPath)
&& fileSystem.getShell().isPresent()) {
@ -324,34 +332,34 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
openTerminalAsync(name, directory, cc, true);
}
});
return Optional.ofNullable(currentPath.get());
return Optional.ofNullable(cps);
}
// Evaluate optional links
String resolvedPath;
FilePath resolvedPath;
try {
resolvedPath = BrowserFileSystemHelper.resolveDirectoryPath(this, evaluatedPath, customInput);
resolvedPath = BrowserFileSystemHelper.resolveDirectoryPath(this, FilePath.of(evaluatedPath), customInput);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
return Optional.ofNullable(cps);
}
if (!Objects.equals(path, resolvedPath)) {
return Optional.ofNullable(resolvedPath);
if (!Objects.equals(path, resolvedPath.toString())) {
return Optional.ofNullable(resolvedPath.toString());
}
try {
BrowserFileSystemHelper.validateDirectoryPath(this, resolvedPath, true);
cdSyncWithoutCheck(path);
cdSyncWithoutCheck(resolvedPath);
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
return Optional.ofNullable(cps);
}
return Optional.empty();
}
private void cdSyncWithoutCheck(String path) throws Exception {
private void cdSyncWithoutCheck(FilePath path) throws Exception {
if (fileSystem == null) {
var fs = entry.getStore().createFileSystem();
fs.open();
@ -368,7 +376,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
loadFilesSync(path);
}
public void withFiles(String dir, FailableConsumer<Stream<FileEntry>, Exception> consumer) throws Exception {
public void withFiles(FilePath dir, FailableConsumer<Stream<FileEntry>, Exception> consumer) throws Exception {
BooleanScope.executeExclusive(busy, () -> {
if (dir != null) {
startIfNeeded();
@ -385,7 +393,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
});
}
private boolean loadFilesSync(String dir) {
private boolean loadFilesSync(FilePath dir) {
try {
startIfNeeded();
var fs = getFileSystem();
@ -456,7 +464,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
startIfNeeded();
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
var abs = getCurrentDirectory().getPath().join(name);
if (fileSystem.directoryExists(abs)) {
throw ErrorEvent.expected(
new IllegalStateException(String.format("Directory %s already exists", abs)));
@ -468,8 +476,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
});
}
public void createLinkAsync(String linkName, String targetFile) {
if (linkName == null || linkName.isBlank() || targetFile == null || targetFile.isBlank()) {
public void createLinkAsync(String linkName, FilePath targetFile) {
if (linkName == null || linkName.isBlank() || targetFile == null) {
return;
}
@ -484,7 +492,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
startIfNeeded();
var abs = FileNames.join(getCurrentDirectory().getPath(), linkName);
var abs = getCurrentDirectory().getPath().join(linkName);
fileSystem.symbolicLink(abs, targetFile);
refreshSync();
});
@ -549,7 +557,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
return;
}
var abs = FileNames.join(getCurrentDirectory().getPath(), name);
var abs = getCurrentDirectory().getPath().join(name);
fileSystem.touch(abs);
refreshSync();
});
@ -560,8 +568,8 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
return fileSystem == null;
}
public void initWithGivenDirectory(String dir) {
cdSync(dir);
public void initWithGivenDirectory(FilePath dir) {
cdSync(dir != null ? dir.toString() : null);
}
public void initWithDefaultDirectory() {
@ -570,7 +578,7 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
public void openTerminalAsync(
String name, String directory, ProcessControl processControl, boolean dockIfPossible) {
String name, FilePath directory, ProcessControl processControl, boolean dockIfPossible) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
return;
@ -597,11 +605,17 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
}
public void backSync(int i) {
cdSync(history.back(i));
var b = history.back(i);
if (b != null) {
cdSync(b.toString());
}
}
public void forthSync(int i) {
cdSync(history.forth(i));
var f = history.forth(i);
if (f != null) {
cdSync(f.toString());
}
}
@Getter

View file

@ -12,7 +12,6 @@ import java.nio.file.Path;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Timer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
@ -73,7 +72,7 @@ public class BrowserFileTransferOperation {
this.progress.accept(progress);
}
private BrowserAlerts.FileConflictChoice handleChoice(FileSystem fileSystem, String target, boolean multiple)
private BrowserAlerts.FileConflictChoice handleChoice(FileSystem fileSystem, FilePath target, boolean multiple)
throws Exception {
if (lastConflictChoice == BrowserAlerts.FileConflictChoice.CANCEL) {
return BrowserAlerts.FileConflictChoice.CANCEL;
@ -177,7 +176,7 @@ public class BrowserFileTransferOperation {
}
var sourceFile = source.getPath();
var targetFile = FileNames.join(target.getPath(), FileNames.getFileName(sourceFile));
var targetFile = target.getPath().join(sourceFile.getFileName());
if (sourceFile.equals(targetFile)) {
// Duplicate file by renaming it
@ -209,7 +208,7 @@ public class BrowserFileTransferOperation {
}
}
private String renameFileLoop(FileSystem fileSystem, String target, boolean dir) throws Exception {
private FilePath renameFileLoop(FileSystem fileSystem, FilePath target, boolean dir) throws Exception {
// Who has more than 10 copies?
for (int i = 0; i < 10; i++) {
target = renameFile(target);
@ -220,23 +219,21 @@ public class BrowserFileTransferOperation {
return target;
}
private String renameFile(String target) {
var targetFile = new FilePath(target);
var name = targetFile.getFileName();
private FilePath renameFile(FilePath target) {
var name = target.getFileName();
var pattern = Pattern.compile("(.+) \\((\\d+)\\)\\.(.+?)");
var matcher = pattern.matcher(name);
if (matcher.matches()) {
try {
var number = Integer.parseInt(matcher.group(2));
var newFile =
targetFile.getParent().join(matcher.group(1) + " (" + (number + 1) + ")." + matcher.group(3));
return newFile.toString();
var newFile = target.getParent().join(matcher.group(1) + " (" + (number + 1) + ")." + matcher.group(3));
return newFile;
} catch (NumberFormatException ignored) {
}
}
var noExt = targetFile.getFileName().equals(targetFile.getExtension());
return targetFile.getBaseName() + " (" + 1 + ")" + (noExt ? "" : "." + targetFile.getExtension());
var noExt = target.getFileName().equals(target.getExtension());
return FilePath.of(target.getBaseName() + " (" + 1 + ")" + (noExt ? "" : "." + target.getExtension()));
}
private void handleSingleAcrossFileSystems(FileEntry source) throws Exception {
@ -248,7 +245,7 @@ public class BrowserFileTransferOperation {
// Prevent dropping directory into itself
if (source.getFileSystem().equals(target.getFileSystem())
&& FileNames.startsWith(source.getPath(), target.getPath())) {
&& source.getPath().startsWith(target.getPath())) {
return;
}
@ -260,17 +257,17 @@ public class BrowserFileTransferOperation {
return;
}
var directoryName = FileNames.getFileName(source.getPath());
var directoryName = source.getPath().getFileName();
flatFiles.put(source, directoryName);
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
var baseRelative = source.getPath().getParent().toDirectory();
List<FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
for (FileEntry fileEntry : list) {
if (cancelled()) {
return;
}
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
var rel = baseRelative.relativize(fileEntry.getPath()).toUnix().toString();
flatFiles.put(fileEntry, rel);
if (fileEntry.getKind() == FileKind.FILE) {
// This one is up-to-date and does not need to be recalculated
@ -284,7 +281,7 @@ public class BrowserFileTransferOperation {
return;
}
flatFiles.put(source, FileNames.getFileName(source.getPath()));
flatFiles.put(source, source.getPath().getFileName());
// Recalculate as it could have been changed meanwhile
totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath()));
}
@ -297,10 +294,10 @@ public class BrowserFileTransferOperation {
}
var sourceFile = e.getKey();
var fixedRelPath = new FilePath(e.getValue())
var fixedRelPath = FilePath.of(e.getValue())
.fileSystemCompatible(
target.getFileSystem().getShell().orElseThrow().getOsType());
var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString());
var targetFile = target.getPath().join(fixedRelPath.toString());
if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
throw new IllegalStateException();
}
@ -328,7 +325,7 @@ public class BrowserFileTransferOperation {
}
private void transfer(
FileEntry sourceFile, String targetFile, AtomicLong transferred, AtomicLong totalSize, Instant start)
FileEntry sourceFile, FilePath targetFile, AtomicLong transferred, AtomicLong totalSize, Instant start)
throws Exception {
if (cancelled()) {
return;
@ -437,7 +434,8 @@ public class BrowserFileTransferOperation {
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
updateProgress(
new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
}
} catch (Exception ex) {
exception.set(ex);

View file

@ -1,5 +1,7 @@
package io.xpipe.app.browser.file;
import io.xpipe.core.store.FilePath;
import javafx.collections.ObservableList;
import lombok.AllArgsConstructor;
@ -24,6 +26,6 @@ public interface BrowserHistorySavedState {
class Entry {
UUID uuid;
String path;
FilePath path;
}
}

View file

@ -101,7 +101,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
.grow(true, false)
.accessibleTextKey("restoreAllSessions");
var layout = new VerticalComp(List.of(vbox, Comp.vspacer(5), listBox, Comp.separator(), tile));
var layout = new VerticalComp(List.of(vbox, Comp.vspacer(5), listBox, Comp.hseparator(), tile));
layout.styleClass("welcome");
layout.spacing(14);
layout.maxWidth(1000);
@ -154,7 +154,9 @@ public class BrowserHistoryTabComp extends SimpleComp {
var name = Bindings.createStringBinding(
() -> {
var n = e.getPath();
return AppPrefs.get().censorMode().get() ? "*".repeat(n.length()) : n;
return AppPrefs.get().censorMode().get()
? "*".repeat(n.toString().length())
: n.toString();
},
AppPrefs.get().censorMode());
return new ButtonComp(name, () -> {
@ -162,7 +164,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
model.restoreStateAsync(e, disable);
});
})
.accessibleText(e.getPath())
.accessibleText(e.getPath().toString())
.disable(disable)
.styleClass("directory-button")
.apply(struc -> struc.get().setMaxWidth(20000))

View file

@ -3,6 +3,7 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.ext.LocalStore;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem;
import java.nio.file.Files;
@ -33,7 +34,7 @@ public class BrowserLocalFileSystem {
return new FileEntry(
localFileSystem.open(),
file.toString(),
FilePath.of(file),
Files.getLastModifiedTime(file).toInstant(),
Files.size(file),
null,

View file

@ -133,9 +133,9 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
}
private Comp<CompStructure<TextField>> createPathBar() {
var path = new SimpleStringProperty(model.getCurrentPath().get());
var path = new SimpleStringProperty();
model.getCurrentPath().subscribe((newValue) -> {
path.set(newValue);
path.set(newValue != null ? newValue.toString() : null);
});
path.addListener((observable, oldValue, newValue) -> {
ThreadHelper.runFailableAsync(() -> {
@ -202,7 +202,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
continue;
}
var mi = new MenuItem(f.get(i));
var mi = new MenuItem(f.get(i).toString());
int target = i + 1;
mi.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {
@ -219,7 +219,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
}
if (model.getHistory().getCurrent() != null) {
var current = new MenuItem(model.getHistory().getCurrent());
var current = new MenuItem(model.getHistory().getCurrent().toString());
current.setDisable(true);
cm.getItems().add(current);
}
@ -234,7 +234,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
continue;
}
var mi = new MenuItem(b.get(i));
var mi = new MenuItem(b.get(i).toString());
int target = i + 1;
mi.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> {

View file

@ -41,13 +41,10 @@ public class BrowserOverviewComp extends SimpleComp {
var commonPlatform = FXCollections.<FileEntry>observableArrayList();
ThreadHelper.runFailableAsync(() -> {
var common = sc.getOsType().determineInterestingPaths(sc).stream()
.filter(s -> !s.isBlank())
.map(s -> FileEntry.ofDirectory(model.getFileSystem(), s))
.filter(entry -> {
try {
return sc.getShellDialect()
.directoryExists(sc, entry.getPath())
.executeAndCheck();
return model.getFileSystem().directoryExists(entry.getPath());
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return false;

View file

@ -4,7 +4,6 @@ import io.xpipe.app.browser.BrowserAbstractSessionModel;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserSessionTab;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.AppMainWindowContentComp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
@ -138,7 +137,7 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
dockModel.toggleView(aBoolean);
});
});
AppDialog.getModalOverlay().addListener((ListChangeListener<? super ModalOverlay>) c -> {
AppDialog.getModalOverlays().addListener((ListChangeListener<? super ModalOverlay>) c -> {
if (c.getList().size() > 0) {
dockModel.toggleView(false);
} else {

View file

@ -3,7 +3,6 @@ package io.xpipe.app.browser.icon;
import io.xpipe.app.resources.AppResources;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import lombok.Getter;
@ -38,7 +37,8 @@ public abstract class BrowserIconDirectoryType {
@Override
public boolean matches(FileEntry entry) {
return entry.getPath().equals("/") || entry.getPath().matches("\\w:\\\\");
return entry.getPath().toString().equals("/")
|| entry.getPath().toString().matches("\\w:\\\\");
}
@Override
@ -99,7 +99,7 @@ public abstract class BrowserIconDirectoryType {
return false;
}
var name = FileNames.getFileName(entry.getPath());
var name = entry.getPath().getFileName();
return names.contains(name);
}

View file

@ -3,7 +3,6 @@ package io.xpipe.app.browser.icon;
import io.xpipe.app.resources.AppResources;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import lombok.Getter;
@ -84,8 +83,8 @@ public abstract class BrowserIconFileType {
return false;
}
var name = FileNames.getFileName(entry.getPath());
var ext = FileNames.getExtension(entry.getPath());
var name = entry.getPath().getFileName();
var ext = entry.getPath().getFileName();
return (ext != null && endings.contains("." + ext.toLowerCase(Locale.ROOT))) || endings.contains(name);
}

View file

@ -58,10 +58,14 @@ public abstract class Comp<S extends CompStructure<?>> {
};
}
public static Comp<CompStructure<Separator>> separator() {
public static Comp<CompStructure<Separator>> hseparator() {
return of(() -> new Separator(Orientation.HORIZONTAL));
}
public static Comp<CompStructure<Separator>> vseparator() {
return of(() -> new Separator(Orientation.VERTICAL));
}
@SuppressWarnings("unchecked")
public static <IR extends Region, SIN extends CompStructure<IR>, OR extends Region> Comp<CompStructure<OR>> derive(
Comp<SIN> comp, Function<IR, OR> r) {

View file

@ -88,7 +88,7 @@ public class ContextMenuAugment<S extends CompStructure<?>> implements Augment<S
if (!hide.get()) {
var cm = contextMenu.get();
if (cm != null) {
cm.show(r, Side.BOTTOM, 0, 0);
cm.show(r, Side.TOP, 0, 0);
currentContextMenu.set(cm);
}
}

View file

@ -43,7 +43,7 @@ public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
multi.styleClass("background");
var pane = new BorderPane();
var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries());
var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries(), model.getQueueEntries());
StackPane multiR = (StackPane) multi.createRegion();
pane.setCenter(multiR);
var sidebarR = sidebar.createRegion();

View file

@ -34,7 +34,7 @@ public class AppMainWindowContentComp extends SimpleComp {
@Override
protected Region createSimple() {
var overlay = AppDialog.getModalOverlay();
var overlay = AppDialog.getModalOverlays();
var loaded = AppMainWindow.getLoadedContent();
var bg = Comp.of(() -> {
var loadingIcon = new ImageView();

View file

@ -11,12 +11,15 @@ import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.ContextualFileReference;
import io.xpipe.app.storage.DataStorageSyncHandler;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystemStore;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.ListCell;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@ -40,13 +43,13 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
}
private final Property<DataStoreEntryRef<? extends FileSystemStore>> fileSystem;
private final Property<String> filePath;
private final Property<FilePath> filePath;
private final ContextualFileReferenceSync sync;
private final List<PreviousFileReference> previousFileReferences;
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(
Property<DataStoreEntryRef<T>> fileSystem,
Property<String> filePath,
Property<FilePath> filePath,
ContextualFileReferenceSync sync,
List<PreviousFileReference> previousFileReferences) {
this.sync = sync;
@ -86,7 +89,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
}
var currentPath = filePath.getValue();
if (currentPath == null || currentPath.isBlank()) {
if (currentPath == null) {
return;
}
@ -95,7 +98,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
}
try {
var source = Path.of(currentPath.trim());
var source = Path.of(currentPath.toString());
var target = sync.getTargetLocation().apply(source);
if (Files.exists(source)) {
var shouldCopy = AppWindowHelper.showConfirmationAlert(
@ -108,7 +111,7 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
var syncedTarget = handler.addDataFile(
source, target, sync.getPerUser().test(source));
Platform.runLater(() -> {
filePath.setValue(syncedTarget.toString());
filePath.setValue(FilePath.of(syncedTarget));
});
}
} catch (Exception e) {
@ -147,7 +150,14 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
var items = allFiles.stream()
.map(previousFileReference -> previousFileReference.getPath().toString())
.toList();
var combo = new ComboTextFieldComp(filePath, items, param -> {
var prop = new SimpleStringProperty();
filePath.subscribe(s -> PlatformThread.runLaterIfNeeded(() -> {
prop.set(s != null ? s.toString() : null);
}));
prop.addListener((observable, oldValue, newValue) -> {
filePath.setValue(newValue != null ? FilePath.of(newValue) : null);
});
var combo = new ComboTextFieldComp(prop, items, param -> {
return new ListCell<>() {
@Override
protected void updateItem(String item, boolean empty) {
@ -172,7 +182,14 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
}
private Comp<?> createTextField() {
var fileNameComp = new TextFieldComp(filePath)
var prop = new SimpleStringProperty();
filePath.subscribe(s -> PlatformThread.runLaterIfNeeded(() -> {
prop.set(s != null ? s.toString() : null);
}));
prop.addListener((observable, oldValue, newValue) -> {
filePath.setValue(newValue != null ? FilePath.of(newValue) : null);
});
var fileNameComp = new TextFieldComp(prop)
.apply(struc -> HBox.setHgrow(struc.get(), Priority.ALWAYS))
.styleClass(Styles.LEFT_PILL)
.grow(false, true);

View file

@ -3,7 +3,11 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.layout.HBox;
@ -11,10 +15,14 @@ import java.util.List;
public class HorizontalComp extends Comp<CompStructure<HBox>> {
private final List<Comp<?>> entries;
private final ObservableList<Comp<?>> entries;
public HorizontalComp(List<Comp<?>> comps) {
entries = List.copyOf(comps);
entries = FXCollections.observableArrayList(List.copyOf(comps));
}
public HorizontalComp(ObservableList<Comp<?>> entries) {
this.entries = PlatformThread.sync(entries);
}
public Comp<CompStructure<HBox>> spacing(double spacing) {
@ -23,8 +31,11 @@ public class HorizontalComp extends Comp<CompStructure<HBox>> {
@Override
public CompStructure<HBox> createBase() {
HBox b = new HBox();
var b = new HBox();
b.getStyleClass().add("horizontal-comp");
entries.addListener((ListChangeListener<? super Comp<?>>) c -> {
b.getChildren().setAll(c.getList().stream().map(Comp::createRegion).toList());
});
for (var entry : entries) {
b.getChildren().add(entry.createRegion());
}

View file

@ -4,8 +4,8 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.OsType;
import javafx.application.Platform;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;

View file

@ -1,8 +1,11 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.util.LabelGraphic;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Button;
import lombok.Value;
@ -27,6 +30,12 @@ public class ModalButton {
@NonFinal
Consumer<Button> augment;
public static ModalButton hide(ObservableValue<String> name, LabelGraphic icon, Runnable action) {
return new ModalButton("hide", () -> {
AppLayoutModel.get().getQueueEntries().add(new AppLayoutModel.QueueEntry(name, icon, action));
}, true, false);
}
public static ModalButton finish(Runnable action) {
return new ModalButton("finish", action, true, true);
}

View file

@ -65,7 +65,7 @@ public class ModalOverlay {
}
public boolean isShowing() {
return AppDialog.getModalOverlay().contains(this);
return AppDialog.getModalOverlays().contains(this);
}
public void showAndWait() {

View file

@ -281,6 +281,7 @@ public class ModalOverlayComp extends SimpleComp {
if (mb.getAugment() != null) {
mb.getAugment().accept(button);
}
button.managedProperty().bind(button.visibleProperty());
button.setOnAction(event -> {
if (mb.getAction() != null) {
mb.getAction().run();

View file

@ -12,30 +12,110 @@ import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import lombok.AllArgsConstructor;
import java.util.List;
@AllArgsConstructor
public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
private final Property<AppLayoutModel.Entry> value;
private final List<AppLayoutModel.Entry> entries;
public SideMenuBarComp(Property<AppLayoutModel.Entry> value, List<AppLayoutModel.Entry> entries) {
this.value = value;
this.entries = entries;
}
private final ObservableList<AppLayoutModel.QueueEntry> queueEntries;
@Override
public CompStructure<VBox> createBase() {
var vbox = new VBox();
vbox.setFillWidth(true);
for (AppLayoutModel.Entry e : entries) {
var b = new IconButtonComp(e.icon(), () -> {
if (e.action() != null) {
e.action().run();
return;
}
value.setValue(e);
});
b.tooltip(e.name());
b.accessibleText(e.name());
var stack = createStyle(e, b);
var shortcut = e.combination();
if (shortcut != null) {
stack.apply(struc -> struc.get().getProperties().put("shortcut", shortcut));
}
vbox.getChildren().add(stack.createRegion());
}
{
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableDialog.showIfNeeded());
b
.tooltipKey("updateAvailableTooltip")
.accessibleTextKey("updateAvailableTooltip");
var stack = createStyle(null, b);
stack.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
return AppDistributionType.get()
.getUpdateHandler()
.getPreparedUpdate()
.getValue()
== null;
},
AppDistributionType.get().getUpdateHandler().getPreparedUpdate())));
vbox.getChildren().add(stack.createRegion());
}
var filler = new Button();
filler.setDisable(true);
filler.setMaxHeight(3000);
vbox.getChildren().add(filler);
VBox.setVgrow(filler, Priority.ALWAYS);
vbox.getStyleClass().add("sidebar-comp");
var queueButtons = new VBox();
queueEntries.addListener((ListChangeListener<? super AppLayoutModel.QueueEntry>) c -> {
queueButtons.getChildren().clear();
for (int i = c.getList().size() - 1; i >= 0; i--) {
var item = c.getList().get(i);
var b = new IconButtonComp(item.getIcon(), () -> {
item.getAction().run();
queueEntries.remove(item);
});
b.tooltip(item.getName());
b.accessibleText(item.getName());
var stack = createStyle(null, b);
queueButtons.getChildren().add(stack.createRegion());
}
});
vbox.getChildren().add(queueButtons);
return new SimpleCompStructure<>(vbox);
}
private Comp<?> createStyle(AppLayoutModel.Entry e, IconButtonComp b) {
var selected = PseudoClass.getPseudoClass("selected");
b.apply(struc -> {
AppFontSizes.lg(struc.get());
struc.get().setAlignment(Pos.CENTER);
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
struc.get().pseudoClassStateChanged(selected, n.equals(e));
});
});
});
var selectedBorder = Bindings.createObjectBinding(
() -> {
var c = Platform.getPreferences()
@ -45,7 +125,6 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(14, 1, 14, 2)));
},
Platform.getPreferences().accentColorProperty());
var hoverBorder = Bindings.createObjectBinding(
() -> {
var c = Platform.getPreferences()
@ -56,95 +135,38 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
return new Background(new BackgroundFill(c, new CornerRadii(8), new Insets(14, 1, 14, 2)));
},
Platform.getPreferences().accentColorProperty());
var noneBorder = Bindings.createObjectBinding(
() -> {
return Background.fill(Color.TRANSPARENT);
},
Platform.getPreferences().accentColorProperty());
var selected = PseudoClass.getPseudoClass("selected");
for (AppLayoutModel.Entry e : entries) {
var b = new IconButtonComp(e.icon(), () -> {
if (e.action() != null) {
e.action().run();
return;
}
var indicator = Comp.empty().styleClass("indicator");
var stack = new StackComp(List.of(indicator, b))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
stack.apply(struc -> {
var indicatorRegion = (Region) struc.get().getChildren().getFirst();
indicatorRegion.setMaxWidth(7);
indicatorRegion
.backgroundProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (value.getValue().equals(e)) {
return selectedBorder.get();
}
value.setValue(e);
});
var shortcut = e.combination();
b.apply(new TooltipAugment<>(e.name(), shortcut));
b.apply(struc -> {
AppFontSizes.lg(struc.get());
struc.get().setAlignment(Pos.CENTER);
if (struc.get().isHover()) {
return hoverBorder.get();
}
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
value.addListener((c, o, n) -> {
PlatformThread.runLaterIfNeeded(() -> {
struc.get().pseudoClassStateChanged(selected, n.equals(e));
});
});
});
b.accessibleText(e.name());
var indicator = Comp.empty().styleClass("indicator");
var stack = new StackComp(List.of(indicator, b))
.apply(struc -> struc.get().setAlignment(Pos.CENTER_RIGHT));
stack.apply(struc -> {
var indicatorRegion = (Region) struc.get().getChildren().getFirst();
indicatorRegion.setMaxWidth(7);
indicatorRegion
.backgroundProperty()
.bind(Bindings.createObjectBinding(
() -> {
if (value.getValue().equals(e)) {
return selectedBorder.get();
}
if (struc.get().isHover()) {
return hoverBorder.get();
}
return noneBorder.get();
},
struc.get().hoverProperty(),
value,
hoverBorder,
selectedBorder,
noneBorder));
});
if (shortcut != null) {
stack.apply(struc -> struc.get().getProperties().put("shortcut", shortcut));
}
vbox.getChildren().add(stack.createRegion());
}
{
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableDialog.showIfNeeded())
.tooltipKey("updateAvailableTooltip")
.accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> {
AppFontSizes.lg(struc.get());
});
b.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
return AppDistributionType.get()
.getUpdateHandler()
.getPreparedUpdate()
.getValue()
== null;
},
AppDistributionType.get().getUpdateHandler().getPreparedUpdate())));
vbox.getChildren().add(b.createRegion());
}
var filler = new Button();
filler.setDisable(true);
filler.setMaxHeight(3000);
vbox.getChildren().add(filler);
VBox.setVgrow(filler, Priority.ALWAYS);
vbox.getStyleClass().add("sidebar-comp");
return new SimpleCompStructure<>(vbox);
return noneBorder.get();
},
struc.get().hoverProperty(),
value,
hoverBorder,
selectedBorder,
noneBorder));
});
return stack;
}
}

View file

@ -0,0 +1,41 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.control.ToolBar;
import java.util.List;
public class ToolbarComp extends Comp<CompStructure<ToolBar>> {
private final ObservableList<Comp<?>> entries;
public ToolbarComp(List<Comp<?>> comps) {
entries = FXCollections.observableArrayList(List.copyOf(comps));
}
public ToolbarComp(ObservableList<Comp<?>> entries) {
this.entries = PlatformThread.sync(entries);
}
@Override
public CompStructure<ToolBar> createBase() {
var b = new ToolBar();
b.getStyleClass().add("horizontal-comp");
entries.addListener((ListChangeListener<? super Comp<?>>) c -> {
b.getItems().setAll(c.getList().stream().map(Comp::createRegion).toList());
});
for (var entry : entries) {
b.getItems().add(entry.createRegion());
}
b.visibleProperty().bind(Bindings.isNotEmpty(entries));
return new SimpleCompStructure<>(b);
}
}

View file

@ -77,9 +77,20 @@ public class DenseStoreEntryComp extends StoreEntryComp {
var notes = new StoreNotesComp(getWrapper()).createRegion();
var userIcon = createUserIcon().createRegion();
var selection = createBatchSelection().createRegion();
grid.add(selection, 0, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(25));
StoreViewState.get().getBatchMode().subscribe(batch -> {
if (batch) {
grid.getColumnConstraints().set(0, new ColumnConstraints(25));
} else {
grid.getColumnConstraints().set(0, new ColumnConstraints(-8));
}
});
var storeIcon = createIcon(28, 24);
GridPane.setHalignment(storeIcon, HPos.CENTER);
grid.add(storeIcon, 0, 0);
grid.add(storeIcon, 1, 0);
grid.getColumnConstraints().add(new ColumnConstraints(34));
var customSize = content != null ? 100 : 0;

View file

@ -43,15 +43,26 @@ public class StandardStoreEntryComp extends StoreEntryComp {
grid.setHgap(6);
grid.setVgap(OsType.getLocal() == OsType.MACOS ? 2 : 0);
var selection = createBatchSelection();
grid.add(selection.createRegion(), 0, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(25));
StoreViewState.get().getBatchMode().subscribe(batch -> {
if (batch) {
grid.getColumnConstraints().set(0, new ColumnConstraints(25));
} else {
grid.getColumnConstraints().set(0, new ColumnConstraints(-6));
}
});
var storeIcon = createIcon(46, 40);
grid.add(storeIcon, 0, 0, 1, 2);
grid.add(storeIcon, 1, 0, 1, 2);
grid.getColumnConstraints().add(new ColumnConstraints(52));
var active = new StoreActiveComp(getWrapper()).createRegion();
var nameBox = new HBox(name, userIcon, notes);
nameBox.setSpacing(6);
nameBox.setAlignment(Pos.CENTER_LEFT);
grid.add(nameBox, 1, 0);
grid.add(nameBox, 2, 0);
GridPane.setVgrow(nameBox, Priority.ALWAYS);
getWrapper().getSessionActive().subscribe(aBoolean -> {
if (!aBoolean) {
@ -64,7 +75,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
var summaryBox = new HBox(createSummary());
summaryBox.setAlignment(Pos.TOP_LEFT);
GridPane.setVgrow(summaryBox, Priority.ALWAYS);
grid.add(summaryBox, 1, 1);
grid.add(summaryBox, 2, 1);
var nameCC = new ColumnConstraints();
nameCC.setMinWidth(100);
@ -72,7 +83,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
nameCC.setPrefWidth(100);
grid.getColumnConstraints().addAll(nameCC);
grid.add(createInformation(), 2, 0, 1, 2);
grid.add(createInformation(), 3, 0, 1, 2);
var info = new ColumnConstraints();
info.prefWidthProperty().bind(content != null ? INFO_WITH_CONTENT_WIDTH : INFO_NO_CONTENT_WIDTH);
info.setHalignment(HPos.LEFT);
@ -89,7 +100,7 @@ public class StandardStoreEntryComp extends StoreEntryComp {
controls.setAlignment(Pos.CENTER_RIGHT);
controls.setSpacing(10);
controls.setPadding(new Insets(0, 0, 0, 10));
grid.add(controls, 3, 0, 1, 2);
grid.add(controls, 4, 0, 1, 2);
grid.getColumnConstraints().add(custom);
grid.getStyleClass().add("store-entry-grid");

View file

@ -13,8 +13,8 @@ import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.util.ClipboardHelper;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;

View file

@ -1,6 +1,7 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
@ -41,467 +42,39 @@ import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Predicate;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
public class StoreCreationComp extends DialogComp {
public class StoreCreationComp extends SimpleComp {
Stage window;
CreationConsumer consumer;
Property<DataStoreProvider> provider;
ObjectProperty<DataStore> store;
Predicate<DataStoreProvider> filter;
BooleanProperty busy = new SimpleBooleanProperty();
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
Property<ModalOverlay> messageProp = new SimpleObjectProperty<>();
BooleanProperty finished = new SimpleBooleanProperty();
ObservableValue<DataStoreEntry> entry;
BooleanProperty changedSinceError = new SimpleBooleanProperty();
BooleanProperty skippable = new SimpleBooleanProperty();
BooleanProperty connectable = new SimpleBooleanProperty();
StringProperty name;
DataStoreEntry existingEntry;
boolean staticDisplay;
private final StoreCreationModel model;
public StoreCreationComp(
Stage window,
CreationConsumer consumer,
Property<DataStoreProvider> provider,
ObjectProperty<DataStore> store,
Predicate<DataStoreProvider> filter,
String initialName,
DataStoreEntry existingEntry,
boolean staticDisplay) {
this.window = window;
this.consumer = consumer;
this.provider = provider;
this.store = store;
this.filter = filter;
this.name = new SimpleStringProperty(initialName != null && !initialName.isEmpty() ? initialName : null);
this.existingEntry = existingEntry;
this.staticDisplay = staticDisplay;
this.store.addListener((c, o, n) -> {
changedSinceError.setValue(true);
});
this.name.addListener((c, o, n) -> {
changedSinceError.setValue(true);
});
this.provider.addListener((c, o, n) -> {
store.unbind();
store.setValue(null);
if (n != null) {
store.setValue(n.defaultStore());
}
});
this.provider.subscribe((n) -> {
if (n != null) {
connectable.setValue(n.canConnectDuringCreation());
}
});
this.apply(r -> {
r.get().setPrefWidth(650);
r.get().setPrefHeight(750);
});
this.validator.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
newValue.validate();
});
});
this.entry = Bindings.createObjectBinding(
() -> {
if (name.getValue() == null || store.getValue() == null) {
return null;
}
var testE = DataStoreEntry.createNew(
UUID.randomUUID(),
DataStorage.get().getSelectedCategory().getUuid(),
name.getValue(),
store.getValue());
var p = DataStorage.get().getDefaultDisplayParent(testE).orElse(null);
var targetCategory = p != null
? p.getCategoryUuid()
: DataStorage.get().getSelectedCategory().getUuid();
var rootCategory = DataStorage.get()
.getRootCategory(DataStorage.get()
.getStoreCategoryIfPresent(targetCategory)
.orElseThrow());
// Don't put it in the wrong root category
if ((provider.getValue().getCreationCategory() == null
|| !provider.getValue()
.getCreationCategory()
.getCategory()
.equals(rootCategory.getUuid()))) {
targetCategory = provider.getValue().getCreationCategory() != null
? provider.getValue().getCreationCategory().getCategory()
: DataStorage.ALL_CONNECTIONS_CATEGORY_UUID;
}
// Don't use the all connections category
if (targetCategory.equals(
DataStorage.get().getAllConnectionsCategory().getUuid())) {
targetCategory = DataStorage.get()
.getDefaultConnectionsCategory()
.getUuid();
}
// Don't use the all scripts category
if (targetCategory.equals(
DataStorage.get().getAllScriptsCategory().getUuid())) {
targetCategory = DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID;
}
// Don't use the all identities category
if (targetCategory.equals(
DataStorage.get().getAllIdentitiesCategory().getUuid())) {
targetCategory = DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID;
}
// Custom category stuff
targetCategory = provider.getValue().getTargetCategory(store.getValue(), targetCategory);
return DataStoreEntry.createNew(
UUID.randomUUID(), targetCategory, name.getValue(), store.getValue());
},
name,
store);
skippable.bind(Bindings.createBooleanBinding(
() -> {
if (name.get() != null && store.get().isComplete() && store.get() instanceof ValidatableStore) {
return true;
} else {
return false;
}
},
store,
name));
}
public static void showEdit(DataStoreEntry e) {
showEdit(e, dataStoreEntry -> {});
}
public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> consumer) {
show(
e.getName(),
e.getProvider(),
e.getStore(),
v -> true,
(newE, validated) -> {
ThreadHelper.runAsync(() -> {
if (!DataStorage.get().getStoreEntries().contains(e)) {
DataStorage.get().addStoreEntryIfNotPresent(newE);
} else {
// We didn't change anything
if (e.getStore().equals(newE.getStore())) {
e.setName(newE.getName());
} else {
var madeValid = !e.getValidity().isUsable() && newE.getValidity().isUsable();
DataStorage.get().updateEntry(e, newE);
if (madeValid) {
StoreViewState.get().toggleStoreListUpdate();
}
}
}
consumer.accept(e);
});
},
true,
e);
}
public static void showCreation(DataStoreProvider selected, DataStoreCreationCategory category) {
showCreation(selected != null ? selected.defaultStore() : null, category, dataStoreEntry -> {}, true);
}
public static void showCreation(
DataStore base,
DataStoreCreationCategory category,
Consumer<DataStoreEntry> listener,
boolean selectCategory) {
var prov = base != null ? DataStoreProviders.byStore(base) : null;
show(
null,
prov,
base,
dataStoreProvider -> (category != null && category.equals(dataStoreProvider.getCreationCategory()))
|| dataStoreProvider.equals(prov),
(e, validated) -> {
try {
var returned = DataStorage.get().addStoreEntryIfNotPresent(e);
listener.accept(returned);
if (validated
&& e.getProvider().shouldShowScan()
&& AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation()
.get()) {
ScanDialog.showAsync(e);
}
if (selectCategory) {
// Select new category if needed
var cat = DataStorage.get()
.getStoreCategoryIfPresent(e.getCategoryUuid())
.orElseThrow();
PlatformThread.runLaterIfNeeded(() -> {
StoreViewState.get()
.getActiveCategory()
.setValue(StoreViewState.get().getCategoryWrapper(cat));
});
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
},
false,
null);
}
public interface CreationConsumer {
void consume(DataStoreEntry entry, boolean validated);
}
private static void show(
String initialName,
DataStoreProvider provider,
DataStore s,
Predicate<DataStoreProvider> filter,
CreationConsumer con,
boolean staticDisplay,
DataStoreEntry existingEntry) {
var prop = new SimpleObjectProperty<>(provider);
var store = new SimpleObjectProperty<>(s);
DialogComp.showWindow(
"addConnection",
stage -> new StoreCreationComp(
stage, con, prop, store, filter, initialName, existingEntry, staticDisplay));
}
private static boolean showInvalidConfirmAlert() {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmInvalidStoreTitle"));
alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader"));
alert.getDialogPane()
.setContent(AppWindowHelper.alertContentText(AppI18n.get("confirmInvalidStoreContent")));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.getButtonTypes().clear();
alert.getButtonTypes().add(new ButtonType(AppI18n.get("retry"), ButtonBar.ButtonData.CANCEL_CLOSE));
alert.getButtonTypes().add(new ButtonType(AppI18n.get("skip"), ButtonBar.ButtonData.OK_DONE));
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
@Override
protected List<Comp<?>> customButtons() {
return List.of(
new ButtonComp(AppI18n.observable("skipValidation"), () -> {
if (showInvalidConfirmAlert()) {
commit(false);
} else {
finish();
}
})
.visible(skippable),
new ButtonComp(AppI18n.observable("connect"), () -> {
var temp = DataStoreEntry.createTempWrapper(store.getValue());
var action = provider.getValue().launchAction(temp);
ThreadHelper.runFailableAsync(() -> {
action.execute();
});
})
.hide(connectable
.not()
.or(Bindings.createBooleanBinding(
() -> {
return store.getValue() == null
|| !store.getValue().isComplete();
},
store))));
}
@Override
protected ObservableValue<Boolean> busy() {
return busy;
}
@Override
protected void discard() {}
@Override
protected void finish() {
if (finished.get()) {
return;
}
if (store.getValue() == null) {
return;
}
// We didn't change anything
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
commit(false);
return;
}
if (!validator.getValue().validate()) {
var msg = validator
.getValue()
.getValidationResult()
.getMessages()
.getFirst()
.getText();
TrackEvent.info(msg);
messageProp.setValue(createErrorOverlay(msg));
changedSinceError.setValue(false);
return;
}
ThreadHelper.runAsync(() -> {
// Might have changed since last time
if (entry.getValue() == null) {
return;
}
try (var ignored = new BooleanScope(busy).start()) {
DataStorage.get().addStoreEntryInProgress(entry.getValue());
entry.getValue().validateOrThrow();
commit(true);
} catch (Throwable ex) {
String message;
if (ex instanceof ValidationException) {
ErrorEvent.expected(ex);
message = ex.getMessage();
} else if (ex instanceof StackOverflowError) {
// Cycles in connection graphs can fail hard but are expected
ErrorEvent.expected(ex);
message = "StackOverflowError";
} else {
message = ex.getMessage();
}
messageProp.setValue(createErrorOverlay(message));
changedSinceError.setValue(false);
ErrorEvent.fromThrowable(ex).omit().handle();
} finally {
DataStorage.get().removeStoreEntryInProgress(entry.getValue());
}
});
}
@Override
public Comp<?> content() {
return Comp.of(this::createLayout);
}
@Override
protected Comp<?> pane(Comp<?> content) {
var back = super.pane(content);
return new ModalOverlayComp(back, messageProp);
}
private ModalOverlay createErrorOverlay(String message) {
var comp = Comp.of(() -> {
var l = new TextArea();
l.setText(message);
l.setWrapText(true);
l.getStyleClass().add("error-overlay-comp");
l.setEditable(false);
return l;
});
var overlay = ModalOverlay.of("error", comp, new LabelGraphic.NodeGraphic(() -> {
var graphic = new FontIcon("mdomz-warning");
graphic.setIconColor(Color.RED);
return new StackPane(graphic);
}));
return overlay;
}
@Override
public Comp<?> bottom() {
var disable = Bindings.createBooleanBinding(
() -> {
return provider.getValue() == null
|| store.getValue() == null
|| !store.getValue().isComplete()
// When switching providers, both observables change one after another.
// So temporarily there might be a store class mismatch
|| provider.getValue().getStoreClasses().stream()
.noneMatch(aClass -> aClass.isAssignableFrom(
store.getValue().getClass()))
|| provider.getValue().createInsightsMarkdown(store.getValue()) == null;
},
provider,
store);
return new PopupMenuButtonComp(
new SimpleStringProperty("Insights >"),
Comp.of(() -> {
return provider.getValue() != null
? provider.getValue()
.createInsightsComp(store)
.createRegion()
: null;
}),
true)
.hide(disable)
.styleClass("button-comp");
}
public StoreCreationComp(StoreCreationModel model) {this.model = model;}
private Region createStoreProperties(Comp<?> comp, Validator propVal) {
var p = provider.getValue();
var nameKey = p == null
|| p.getCreationCategory() == null
|| p.getCreationCategory().getCategory().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)
? "connection"
: p.getCreationCategory().getCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)
? "script"
: "identity";
var nameKey = model.storeTypeNameKey();
return new OptionsBuilder()
.addComp(comp, store)
.addComp(comp, model.getStore())
.name(nameKey + "Name")
.description(nameKey + "NameDescription")
.addString(name, false)
.addString(model.getName(), false)
.nonNull(propVal)
.buildComp()
.onSceneAssign(struc -> {
if (staticDisplay) {
if (model.isStaticDisplay()) {
struc.get().requestFocus();
}
})
.styleClass("store-creator-options")
.createRegion();
}
private void commit(boolean validated) {
if (finished.get()) {
return;
}
finished.setValue(true);
if (entry.getValue() != null) {
consumer.consume(entry.getValue(), validated);
}
PlatformThread.runLaterIfNeeded(() -> {
window.close();
});
}
private Region createLayout() {
var layout = new BorderPane();
layout.getStyleClass().add("store-creator");
var providerChoice = new StoreProviderChoiceComp(filter, provider);
var showProviders = (!staticDisplay
var providerChoice = new StoreProviderChoiceComp(model.getFilter(), model.getProvider());
var showProviders = (!model.isStaticDisplay()
&& (providerChoice.getProviders().size() > 1
|| providerChoice.getProviders().getFirst().showProviderChoice()))
|| (staticDisplay && provider.getValue().showProviderChoice());
if (staticDisplay) {
|| (model.isStaticDisplay() && model.getProvider().getValue().showProviderChoice());
if (model.isStaticDisplay()) {
providerChoice.apply(struc -> struc.get().setDisable(true));
}
if (showProviders) {
@ -509,21 +82,25 @@ public class StoreCreationComp extends DialogComp {
}
providerChoice.apply(GrowAugment.create(true, false));
provider.subscribe(n -> {
model.getProvider().subscribe(n -> {
if (n != null) {
var d = n.guiDialog(existingEntry, store);
var d = n.guiDialog(model.getExistingEntry(), model.getStore());
var propVal = new SimpleValidator();
var propR = createStoreProperties(d == null || d.getComp() == null ? null : d.getComp(), propVal);
var sp = new ScrollPane(propR);
var valSp = new GraphicDecorationStackPane();
valSp.getChildren().add(propR);
var sp = new ScrollPane(valSp);
sp.setFitToWidth(true);
layout.setCenter(sp);
validator.setValue(new ChainedValidator(List.of(
model.getValidator().setValue(new ChainedValidator(List.of(
d != null && d.getValidator() != null ? d.getValidator() : new SimpleValidator(), propVal)));
} else {
layout.setCenter(null);
validator.setValue(new SimpleValidator());
model.getValidator().setValue(new SimpleValidator());
}
});
@ -533,13 +110,12 @@ public class StoreCreationComp extends DialogComp {
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));
}
return layout;
}
var valSp = new GraphicDecorationStackPane();
valSp.getChildren().add(layout);
return valSp;
@Override
protected Region createSimple() {
return createLayout();
}
}

View file

@ -0,0 +1,180 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.ext.DataStoreCreationCategory;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.ButtonType;
import org.bouncycastle.math.raw.Mod;
import java.util.function.Consumer;
import java.util.function.Predicate;
public class StoreCreationDialog {
public static void showEdit(DataStoreEntry e) {
showEdit(e, dataStoreEntry -> {});
}
public static void showEdit(DataStoreEntry e, Consumer<DataStoreEntry> consumer) {
show(
e.getName(),
e.getProvider(),
e.getStore(),
v -> true,
(newE, validated) -> {
ThreadHelper.runAsync(() -> {
if (!DataStorage.get().getStoreEntries().contains(e)) {
DataStorage.get().addStoreEntryIfNotPresent(newE);
} else {
// We didn't change anything
if (e.getStore().equals(newE.getStore())) {
e.setName(newE.getName());
} else {
var madeValid = !e.getValidity().isUsable()
&& newE.getValidity().isUsable();
DataStorage.get().updateEntry(e, newE);
if (madeValid) {
StoreViewState.get().toggleStoreListUpdate();
}
}
}
consumer.accept(e);
});
},
true,
e);
}
public static void showCreation(DataStoreProvider selected, DataStoreCreationCategory category) {
showCreation(selected != null ? selected.defaultStore() : null, category, dataStoreEntry -> {}, true);
}
public static void showCreation(
DataStore base,
DataStoreCreationCategory category,
Consumer<DataStoreEntry> listener,
boolean selectCategory) {
var prov = base != null ? DataStoreProviders.byStore(base) : null;
show(
null,
prov,
base,
dataStoreProvider -> (category != null && category.equals(dataStoreProvider.getCreationCategory()))
|| dataStoreProvider.equals(prov),
(e, validated) -> {
try {
var returned = DataStorage.get().addStoreEntryIfNotPresent(e);
listener.accept(returned);
if (validated
&& e.getProvider().shouldShowScan()
&& AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation()
.get()) {
ScanDialog.showAsync(e);
}
if (selectCategory) {
// Select new category if needed
var cat = DataStorage.get()
.getStoreCategoryIfPresent(e.getCategoryUuid())
.orElseThrow();
PlatformThread.runLaterIfNeeded(() -> {
StoreViewState.get()
.getActiveCategory()
.setValue(StoreViewState.get().getCategoryWrapper(cat));
});
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
}
},
false,
null);
}
public interface CreationConsumer {
void consume(DataStoreEntry entry, boolean validated);
}
private static void show(
String initialName,
DataStoreProvider provider,
DataStore s,
Predicate<DataStoreProvider> filter,
CreationConsumer con,
boolean staticDisplay,
DataStoreEntry existingEntry) {
var prop = new SimpleObjectProperty<>(provider);
var store = new SimpleObjectProperty<>(s);
var model = new StoreCreationModel(prop, store, filter, initialName, existingEntry, staticDisplay);
var modal = createModalOverlay(model);
modal.show();
}
private static boolean showInvalidConfirmAlert() {
return AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("confirmInvalidStoreTitle"));
alert.setHeaderText(AppI18n.get("confirmInvalidStoreHeader"));
alert.getDialogPane()
.setContent(AppWindowHelper.alertContentText(AppI18n.get("confirmInvalidStoreContent")));
alert.setAlertType(Alert.AlertType.CONFIRMATION);
alert.getButtonTypes().clear();
alert.getButtonTypes().add(new ButtonType(AppI18n.get("retry"), ButtonBar.ButtonData.CANCEL_CLOSE));
alert.getButtonTypes().add(new ButtonType(AppI18n.get("skip"), ButtonBar.ButtonData.OK_DONE));
})
.map(b -> b.getButtonData().isDefaultButton())
.orElse(false);
}
private static ModalOverlay createModalOverlay(StoreCreationModel model) {
var comp = new StoreCreationComp(model);
comp.prefWidth(650);
var nameKey = model.storeTypeNameKey() + "Add";
var modal = ModalOverlay.of(nameKey, comp);
modal.persist();
modal.addButton(new ModalButton("docs", () -> {
model.showDocs();
}, false, false).augment(button -> {
button.visibleProperty().bind(Bindings.not(model.canShowDocs()));
}));
modal.addButton(ModalButton.cancel());
var graphic = model.getProvider().getValue() != null ?
new LabelGraphic.ImageGraphic(model.getProvider().getValue().getDisplayIconFileName(null), 20) :
new LabelGraphic.IconGraphic("mdi2b-beaker-plus-outline");
modal.addButton(ModalButton.hide(AppI18n.observable(model.storeTypeNameKey() + "Add"), graphic, () -> {
modal.show();
}));
modal.addButton(new ModalButton("connect", () -> {
model.connect();
}, false, false).augment(button -> {
button.visibleProperty().bind(Bindings.not(model.canConnect()));
}));
modal.addButton(new ModalButton("skipValidation", () -> {
if (showInvalidConfirmAlert()) {
model.commit();
} else {
model.finish();
}
}, true, false));
modal.addButton(new ModalButton("finish", () -> {
model.finish();
}, true, true));
return modal;
}
}

View file

@ -67,7 +67,7 @@ public class StoreCreationMenu {
item.setGraphic(new FontIcon(graphic));
item.textProperty().bind(AppI18n.observable(name));
item.setOnAction(event -> {
StoreCreationComp.showCreation(
StoreCreationDialog.showCreation(
defaultProvider != null
? DataStoreProviders.byId(defaultProvider).orElseThrow()
: null,
@ -85,7 +85,7 @@ public class StoreCreationMenu {
return;
}
StoreCreationComp.showCreation(
StoreCreationDialog.showCreation(
defaultProvider != null
? DataStoreProviders.byId(defaultProvider).orElseThrow()
: null,
@ -108,7 +108,7 @@ public class StoreCreationMenu {
item.setGraphic(PrettyImageHelper.ofFixedSizeSquare(dataStoreProvider.getDisplayIconFileName(null), 16)
.createRegion());
item.setOnAction(event -> {
StoreCreationComp.showCreation(dataStoreProvider, category);
StoreCreationDialog.showCreation(dataStoreProvider, category);
event.consume();
});
menu.getItems().add(item);

View file

@ -0,0 +1,284 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import io.xpipe.app.ext.DataStoreCreationCategory;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ValidatableStore;
import io.xpipe.core.util.ValidationException;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import net.synedra.validatorfx.GraphicDecorationStackPane;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import java.util.function.Predicate;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@Getter
public class StoreCreationModel {
Property<DataStoreProvider> provider;
ObjectProperty<DataStore> store;
Predicate<DataStoreProvider> filter;
BooleanProperty busy = new SimpleBooleanProperty();
Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
BooleanProperty finished = new SimpleBooleanProperty();
ObservableValue<DataStoreEntry> entry;
BooleanProperty changedSinceError = new SimpleBooleanProperty();
BooleanProperty skippable = new SimpleBooleanProperty();
BooleanProperty connectable = new SimpleBooleanProperty();
StringProperty name;
DataStoreEntry existingEntry;
boolean staticDisplay;
public StoreCreationModel(
Property<DataStoreProvider> provider,
ObjectProperty<DataStore> store, Predicate<DataStoreProvider> filter,
String initialName,
DataStoreEntry existingEntry,
boolean staticDisplay) {
this.provider = provider;
this.store = store;
this.filter = filter;
this.name = new SimpleStringProperty(initialName != null && !initialName.isEmpty() ? initialName : null);
this.existingEntry = existingEntry;
this.staticDisplay = staticDisplay;
this.store.addListener((c, o, n) -> {
changedSinceError.setValue(true);
});
this.name.addListener((c, o, n) -> {
changedSinceError.setValue(true);
});
this.provider.addListener((c, o, n) -> {
store.unbind();
store.setValue(null);
if (n != null) {
store.setValue(n.defaultStore());
}
});
this.provider.subscribe((n) -> {
if (n != null) {
connectable.setValue(n.canConnectDuringCreation());
}
});
this.validator.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
newValue.validate();
});
});
this.entry = Bindings.createObjectBinding(
() -> {
if (name.getValue() == null || store.getValue() == null) {
return null;
}
var testE = DataStoreEntry.createNew(
UUID.randomUUID(),
DataStorage.get().getSelectedCategory().getUuid(),
name.getValue(),
store.getValue());
var p = DataStorage.get().getDefaultDisplayParent(testE).orElse(null);
var targetCategory = p != null
? p.getCategoryUuid()
: DataStorage.get().getSelectedCategory().getUuid();
var rootCategory = DataStorage.get()
.getRootCategory(DataStorage.get()
.getStoreCategoryIfPresent(targetCategory)
.orElseThrow());
// Don't put it in the wrong root category
if ((provider.getValue().getCreationCategory() == null
|| !provider.getValue()
.getCreationCategory()
.getCategory()
.equals(rootCategory.getUuid()))) {
targetCategory = provider.getValue().getCreationCategory() != null
? provider.getValue().getCreationCategory().getCategory()
: DataStorage.ALL_CONNECTIONS_CATEGORY_UUID;
}
// Don't use the all connections category
if (targetCategory.equals(
DataStorage.get().getAllConnectionsCategory().getUuid())) {
targetCategory = DataStorage.get()
.getDefaultConnectionsCategory()
.getUuid();
}
// Don't use the all scripts category
if (targetCategory.equals(
DataStorage.get().getAllScriptsCategory().getUuid())) {
targetCategory = DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID;
}
// Don't use the all identities category
if (targetCategory.equals(
DataStorage.get().getAllIdentitiesCategory().getUuid())) {
targetCategory = DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID;
}
// Custom category stuff
targetCategory = provider.getValue().getTargetCategory(store.getValue(), targetCategory);
return DataStoreEntry.createNew(
UUID.randomUUID(), targetCategory, name.getValue(), store.getValue());
},
name,
store);
skippable.bind(Bindings.createBooleanBinding(
() -> {
if (name.get() != null && store.get().isComplete() && store.get() instanceof ValidatableStore) {
return true;
} else {
return false;
}
},
store,
name));
}
ObservableBooleanValue canConnect() {
return connectable
.not()
.or(Bindings.createBooleanBinding(
() -> {
return store.getValue() == null
|| !store.getValue().isComplete();
},
store));
}
void connect() {
var temp = DataStoreEntry.createTempWrapper(store.getValue());
var action = provider.getValue().launchAction(temp);
ThreadHelper.runFailableAsync(() -> {
action.execute();
});
}
ObservableValue<Boolean> busy() {
return busy;
}
void finish() {
if (finished.get()) {
return;
}
if (store.getValue() == null) {
return;
}
// We didn't change anything
if (existingEntry != null && existingEntry.getStore().equals(store.getValue())) {
commit();
return;
}
if (!validator.getValue().validate()) {
var msg = validator
.getValue()
.getValidationResult()
.getMessages()
.getFirst()
.getText();
ErrorEvent.fromMessage(msg).handle();
changedSinceError.setValue(false);
return;
}
ThreadHelper.runAsync(() -> {
// Might have changed since last time
if (entry.getValue() == null) {
return;
}
try (var ignored = new BooleanScope(busy).start()) {
DataStorage.get().addStoreEntryInProgress(entry.getValue());
entry.getValue().validateOrThrow();
commit();
} catch (Throwable ex) {
if (ex instanceof ValidationException) {
ErrorEvent.expected(ex);
} else if (ex instanceof StackOverflowError) {
// Cycles in connection graphs can fail hard but are expected
ErrorEvent.expected(ex);
}
changedSinceError.setValue(false);
ErrorEvent.fromThrowable(ex).handle();
} finally {
DataStorage.get().removeStoreEntryInProgress(entry.getValue());
}
});
}
void showDocs() {
Hyperlinks.open(provider.getValue().getHelpLink());
}
ObservableBooleanValue canShowDocs() {
var disable = Bindings.createBooleanBinding(
() -> {
return provider.getValue() == null || provider.getValue().getHelpLink() == null;
},
provider);
return disable;
}
void commit() {
if (finished.get()) {
return;
}
finished.setValue(true);
}
public String storeTypeNameKey() {
var p = provider.getValue();
var nameKey = p == null
|| p.getCreationCategory() == null
|| p.getCreationCategory().getCategory().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)
? "connection"
: p.getCreationCategory().getCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)
? "script"
: "identity";
return nameKey;
}
}

View file

@ -0,0 +1,73 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.scene.control.CheckBox;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
public class StoreEntryBatchSelectComp extends SimpleComp {
private final StoreSection section;
public StoreEntryBatchSelectComp(StoreSection section) {
this.section = section;
}
@Override
protected Region createSimple() {
var cb = new CheckBox();
cb.setAllowIndeterminate(true);
cb.selectedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
StoreViewState.get().selectBatchMode(section);
} else {
StoreViewState.get().unselectBatchMode(section);
}
});
StoreViewState.get().getBatchModeSelection().getList().addListener((ListChangeListener<
? super StoreEntryWrapper>)
c -> {
Platform.runLater(() -> {
update(cb);
});
});
section.getShownChildren().getList().addListener((ListChangeListener<? super StoreSection>) c -> {
if (cb.isSelected()) {
StoreViewState.get().selectBatchMode(section);
} else {
StoreViewState.get().unselectBatchMode(section);
}
});
cb.getStyleClass().add("batch-mode-selector");
cb.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (event.getButton() == MouseButton.PRIMARY) {
cb.setSelected(!cb.isSelected());
event.consume();
}
});
return cb;
}
private void update(CheckBox checkBox) {
var isSelected = StoreViewState.get().isSectionSelected(section);
checkBox.setSelected(isSelected);
if (section.getShownChildren().getList().size() == 0) {
checkBox.setIndeterminate(false);
return;
}
var count = section.getShownChildren().getList().stream()
.filter(c ->
StoreViewState.get().getBatchModeSelection().getList().contains(c.getWrapper()))
.count();
checkBox.setIndeterminate(
count > 0 && count != section.getShownChildren().getList().size());
return;
}
}

View file

@ -88,6 +88,7 @@ public abstract class StoreEntryComp extends SimpleComp {
var r = createContent();
var buttonBar = r.lookup(".button-bar");
var iconChooser = r.lookup(".icon");
var batchMode = r.lookup(".batch-mode-selector");
var button = new Button();
button.setGraphic(r);
@ -105,6 +106,7 @@ public abstract class StoreEntryComp extends SimpleComp {
});
button.addEventFilter(MouseEvent.MOUSE_CLICKED, event -> {
var notOnButton = NodeHelper.isParent(iconChooser, event.getTarget())
|| NodeHelper.isParent(batchMode, event.getTarget())
|| NodeHelper.isParent(buttonBar, event.getTarget());
if (AppPrefs.get().requireDoubleClickForConnections().get() && !notOnButton) {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
@ -118,6 +120,7 @@ public abstract class StoreEntryComp extends SimpleComp {
});
button.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
var notOnButton = NodeHelper.isParent(iconChooser, event.getTarget())
|| NodeHelper.isParent(batchMode, event.getTarget())
|| NodeHelper.isParent(buttonBar, event.getTarget());
if (AppPrefs.get().requireDoubleClickForConnections().get() && !notOnButton) {
if (event.getButton() == MouseButton.PRIMARY && event.getClickCount() != 2) {
@ -276,6 +279,12 @@ public abstract class StoreEntryComp extends SimpleComp {
return settingsButton;
}
protected Comp<?> createBatchSelection() {
var c = new StoreEntryBatchSelectComp(section);
c.hide(StoreViewState.get().getBatchMode().not());
return c;
}
protected ContextMenu createContextMenu() {
var contextMenu = ContextMenuHelper.create();

View file

@ -4,15 +4,19 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.core.AppLayoutModel;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.LinkedHashMap;
import java.util.List;
public class StoreEntryListComp extends SimpleComp {
@ -48,7 +52,15 @@ public class StoreEntryListComp extends SimpleComp {
struc.get().setVvalue(0);
});
});
return content.styleClass("store-list-comp");
content.styleClass("store-list-comp");
content.vgrow();
var statusBar = new StoreEntryListStatusBarComp();
statusBar.apply(struc -> {
VBox.setMargin(struc.get(), new Insets(3, 6, 4, 2));
});
statusBar.hide(StoreViewState.get().getBatchMode().not());
return new VerticalComp(List.of(content, statusBar));
}
@Override

View file

@ -13,8 +13,6 @@ import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -26,6 +24,7 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import atlantafx.base.theme.Styles;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.function.Function;
@ -91,24 +90,30 @@ public class StoreEntryListOverviewComp extends SimpleComp {
StoreViewState.get().getFilterString().setValue(newValue);
});
});
var filter = new FilterComp(StoreViewState.get().getFilterString());
var f = filter.createRegion();
var button = createAddButton();
var hbox = new HBox(button, f);
f.minHeightProperty().bind(button.heightProperty());
f.prefHeightProperty().bind(button.heightProperty());
f.maxHeightProperty().bind(button.heightProperty());
var filter = new FilterComp(StoreViewState.get().getFilterString()).createRegion();
var add = createAddButton();
var batchMode = createBatchModeButton().createRegion();
var hbox = new HBox(add, filter, batchMode);
filter.minHeightProperty().bind(add.heightProperty());
filter.prefHeightProperty().bind(add.heightProperty());
filter.maxHeightProperty().bind(add.heightProperty());
batchMode.minHeightProperty().bind(add.heightProperty());
batchMode.prefHeightProperty().bind(add.heightProperty());
batchMode.maxHeightProperty().bind(add.heightProperty());
batchMode.minWidthProperty().bind(add.heightProperty());
batchMode.prefWidthProperty().bind(add.heightProperty());
batchMode.maxWidthProperty().bind(add.heightProperty());
hbox.setSpacing(8);
hbox.setAlignment(Pos.CENTER);
HBox.setHgrow(f, Priority.ALWAYS);
HBox.setHgrow(filter, Priority.ALWAYS);
f.getStyleClass().add("filter-bar");
filter.getStyleClass().add("filter-bar");
return hbox;
}
private Region createAddButton() {
var menu = new MenuButton(null, new FontIcon("mdi2p-plus-thick"));
menu.textProperty().bind(AppI18n.observable("addConnections"));
menu.textProperty().bind(AppI18n.observable("new"));
menu.setAlignment(Pos.CENTER);
menu.setTextAlignment(TextAlignment.CENTER);
StoreCreationMenu.addButtons(menu);
@ -124,6 +129,27 @@ public class StoreEntryListOverviewComp extends SimpleComp {
return menu;
}
private Comp<?> createBatchModeButton() {
var batchMode = StoreViewState.get().getBatchMode();
var b = new IconButtonComp("mdi2l-layers", () -> {
batchMode.setValue(!batchMode.getValue());
});
b.apply(struc -> {
struc.get()
.opacityProperty()
.bind(Bindings.createDoubleBinding(
() -> {
if (batchMode.getValue()) {
return 1.0;
}
return 0.4;
},
batchMode));
struc.get().getStyleClass().remove(Styles.FLAT);
});
return b;
}
private Comp<?> createAlphabeticalSortButton() {
var sortMode = StoreViewState.get().getSortMode();
var icon = Bindings.createObjectBinding(

View file

@ -0,0 +1,196 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.*;
import io.xpipe.core.store.DataStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import atlantafx.base.theme.Styles;
import java.util.ArrayList;
import java.util.List;
public class StoreEntryListStatusBarComp extends SimpleComp {
@Override
protected Region createSimple() {
var checkbox = new StoreEntryBatchSelectComp(StoreViewState.get().getCurrentTopLevelSection());
var l = new LabelComp(Bindings.createStringBinding(
() -> {
return AppI18n.get(
"connectionsSelected",
StoreViewState.get()
.getEffectiveBatchModeSelection()
.getList()
.size());
},
StoreViewState.get().getEffectiveBatchModeSelection().getList(),
AppI18n.activeLanguage()));
l.minWidth(Region.USE_PREF_SIZE);
l.apply(struc -> {
struc.get().setAlignment(Pos.CENTER);
});
var actions = new ToolbarComp(createActions());
var close = new IconButtonComp("mdi2c-close", () -> {
StoreViewState.get().getBatchMode().setValue(false);
});
close.apply(struc -> {
struc.get().getStyleClass().remove(Styles.FLAT);
struc.get().minWidthProperty().bind(struc.get().heightProperty());
struc.get().prefWidthProperty().bind(struc.get().heightProperty());
struc.get().maxWidthProperty().bind(struc.get().heightProperty());
});
var bar = new HorizontalComp(List.of(
checkbox, Comp.hspacer(12), l, Comp.hspacer(20), actions, Comp.hspacer(), Comp.hspacer(20), close));
bar.apply(struc -> {
struc.get().setFillHeight(true);
struc.get().setAlignment(Pos.CENTER_LEFT);
});
bar.minHeight(40);
bar.prefHeight(40);
bar.styleClass("bar");
bar.styleClass("store-entry-list-status-bar");
return bar.createRegion();
}
private ObservableList<Comp<?>> createActions() {
var l = new DerivedObservableList<ActionProvider>(FXCollections.observableArrayList(), true);
StoreViewState.get().getEffectiveBatchModeSelection().getList().addListener((ListChangeListener<
? super StoreEntryWrapper>)
c -> {
l.setContent(getCompatibleActionProviders());
});
return l.<Comp<?>>mapped(actionProvider -> {
return buildButton(actionProvider);
})
.getList();
}
private List<ActionProvider> getCompatibleActionProviders() {
var l = StoreViewState.get().getEffectiveBatchModeSelection().getList();
if (l.isEmpty()) {
return List.of();
}
var all = new ArrayList<>(ActionProvider.ALL);
for (StoreEntryWrapper w : l) {
var actions = ActionProvider.ALL.stream()
.filter(actionProvider -> {
var s = actionProvider.getBatchDataStoreCallSite();
if (s == null) {
return false;
}
if (!s.getApplicableClass()
.isAssignableFrom(w.getStore().getValue().getClass())) {
return false;
}
if (!s.isApplicable(w.getEntry().ref())) {
return false;
}
return true;
})
.toList();
all.removeIf(actionProvider -> !actions.contains(actionProvider));
}
return all;
}
@SuppressWarnings("unchecked")
private <T extends DataStore> Comp<?> buildButton(ActionProvider p) {
ActionProvider.BatchDataStoreCallSite<T> s =
(ActionProvider.BatchDataStoreCallSite<T>) p.getBatchDataStoreCallSite();
if (s == null) {
return Comp.empty();
}
List<DataStoreEntryRef<T>> childrenRefs =
StoreViewState.get().getEffectiveBatchModeSelection().getList().stream()
.map(storeEntryWrapper -> storeEntryWrapper.getEntry().<T>ref())
.toList();
var batchActions = s.getChildren(childrenRefs);
var button = new ButtonComp(
s.getName(), new SimpleObjectProperty<>(new LabelGraphic.IconGraphic(s.getIcon())), () -> {
if (batchActions.size() > 0) {
return;
}
runActions(s);
});
if (batchActions.size() > 0) {
button.apply(new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.PRIMARY, keyEvent -> false, () -> {
var cm = ContextMenuHelper.create();
s.getChildren(childrenRefs).forEach(childProvider -> {
var menu = buildMenuItemForAction(childrenRefs, childProvider);
cm.getItems().add(menu);
});
return cm;
}));
}
return button;
}
@SuppressWarnings("unchecked")
private <T extends DataStore> MenuItem buildMenuItemForAction(List<DataStoreEntryRef<T>> batch, ActionProvider a) {
ActionProvider.BatchDataStoreCallSite<T> s =
(ActionProvider.BatchDataStoreCallSite<T>) a.getBatchDataStoreCallSite();
var name = s.getName();
var icon = s.getIcon();
var children = s.getChildren(batch);
if (children.size() > 0) {
var menu = new Menu();
menu.textProperty().bind(name);
menu.setGraphic(new LabelGraphic.IconGraphic(icon).createGraphicNode());
var items = children.stream()
.filter(actionProvider -> actionProvider.getBatchDataStoreCallSite() != null)
.map(c -> buildMenuItemForAction(batch, c))
.toList();
menu.getItems().addAll(items);
return menu;
} else {
var item = new MenuItem();
item.textProperty().bind(name);
item.setGraphic(new LabelGraphic.IconGraphic(icon).createGraphicNode());
item.setOnAction(event -> {
runActions(s);
event.consume();
if (event.getTarget() instanceof Menu m) {
m.getParentPopup().hide();
}
});
return item;
}
}
@SuppressWarnings("unchecked")
private <T extends DataStore> void runActions(ActionProvider.BatchDataStoreCallSite<?> s) {
ThreadHelper.runFailableAsync(() -> {
var l = new ArrayList<>(
StoreViewState.get().getEffectiveBatchModeSelection().getList());
var mapped = l.stream().map(w -> w.getEntry().<T>ref()).toList();
var action = ((ActionProvider.BatchDataStoreCallSite<T>) s).createAction(mapped);
if (action != null) {
action.execute();
}
});
}
}

View file

@ -120,7 +120,7 @@ public class StoreEntryWrapper {
}
public void editDialog() {
StoreCreationComp.showEdit(entry);
StoreCreationDialog.showEdit(entry);
}
public void delete() {

View file

@ -4,6 +4,7 @@ import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.input.MouseButton;
@ -54,6 +55,10 @@ public class StoreIconComp extends SimpleComp {
stack.addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
if (event.getButton() == MouseButton.PRIMARY) {
if (wrapper.getValidity().getValue() == DataStoreEntry.Validity.LOAD_FAILED) {
return;
}
StoreIconChoiceDialog.show(wrapper.getEntry());
event.consume();
}

View file

@ -52,7 +52,7 @@ public class StoreIdentitiesIntroComp extends SimpleComp {
var prov = canSync
? DataStoreProviders.byId("syncedIdentity").orElseThrow()
: DataStoreProviders.byId("localIdentity").orElseThrow();
StoreCreationComp.showCreation(prov, DataStoreCreationCategory.IDENTITY);
StoreCreationDialog.showCreation(prov, DataStoreCreationCategory.IDENTITY);
event.consume();
});

View file

@ -21,7 +21,7 @@ public class StoreLayoutComp extends SimpleComp {
AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble);
})
.createStructure();
struc.getLeft().setMinWidth(260);
struc.getLeft().setMinWidth(270);
struc.getLeft().setMaxWidth(500);
struc.get().getStyleClass().add("store-layout");
InputHelper.onKeyCombination(

View file

@ -84,7 +84,7 @@ public class StoreSectionComp extends StoreSectionBaseComp {
var full = new VerticalComp(List.of(
topEntryList,
Comp.separator().hide(Bindings.not(effectiveExpanded)),
Comp.hseparator().hide(Bindings.not(effectiveExpanded)),
content));
full.styleClass("store-entry-section-comp");
full.apply(struc -> {

View file

@ -1,6 +1,7 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppCache;
import io.xpipe.app.ext.DataStoreUsageCategory;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
@ -13,6 +14,7 @@ import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import lombok.Getter;
@ -42,6 +44,27 @@ public class StoreViewState {
@Getter
private final Property<StoreSortMode> sortMode = new SimpleObjectProperty<>();
@Getter
private final BooleanProperty batchMode = new SimpleBooleanProperty(true);
@Getter
private final DerivedObservableList<StoreEntryWrapper> batchModeSelection =
new DerivedObservableList<>(FXCollections.observableArrayList(), true);
@Getter
private final DerivedObservableList<StoreEntryWrapper> effectiveBatchModeSelection =
batchModeSelection.filtered(storeEntryWrapper -> {
if (!storeEntryWrapper.getValidity().getValue().isUsable()) {
return false;
}
if (storeEntryWrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP) {
return false;
}
return true;
});
@Getter
private StoreSection currentTopLevelSection;
@ -60,6 +83,7 @@ public class StoreViewState {
INSTANCE.initSections();
INSTANCE.updateContent();
INSTANCE.initFilterListener();
INSTANCE.initBatchListener();
}
public static void reset() {
@ -80,6 +104,42 @@ public class StoreViewState {
return INSTANCE;
}
public void selectBatchMode(StoreSection section) {
var wrapper = section.getWrapper();
if (wrapper != null && !batchModeSelection.getList().contains(wrapper)) {
batchModeSelection.getList().add(wrapper);
}
if (wrapper == null
|| (wrapper.getValidity().getValue().isUsable()
&& wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP)) {
section.getShownChildren().getList().forEach(c -> selectBatchMode(c));
}
}
public void unselectBatchMode(StoreSection section) {
var wrapper = section.getWrapper();
if (wrapper != null) {
batchModeSelection.getList().remove(wrapper);
}
if (wrapper == null
|| (wrapper.getValidity().getValue().isUsable()
&& wrapper.getEntry().getProvider().getUsageCategory() == DataStoreUsageCategory.GROUP)) {
section.getShownChildren().getList().forEach(c -> unselectBatchMode(c));
}
}
public boolean isSectionSelected(StoreSection section) {
if (section.getWrapper() == null) {
var batchSet = new HashSet<>(batchModeSelection.getList());
var childSet = section.getShownChildren().getList().stream()
.map(s -> s.getWrapper())
.toList();
return batchSet.containsAll(childSet);
}
return getBatchModeSelection().getList().contains(section.getWrapper());
}
private void updateContent() {
categories.getList().forEach(c -> c.update());
allEntries.getList().forEach(e -> e.update());
@ -115,6 +175,14 @@ public class StoreViewState {
});
}
private void initBatchListener() {
allEntries.getList().addListener((ListChangeListener<? super StoreEntryWrapper>) c -> {
batchModeSelection.getList().removeIf(storeEntryWrapper -> {
return allEntries.getList().contains(storeEntryWrapper);
});
});
}
private void initContent() {
allEntries
.getList()

View file

@ -12,6 +12,8 @@ import io.xpipe.app.util.LicenseProvider;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
@ -19,6 +21,7 @@ import javafx.scene.input.KeyCombination;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
import java.util.ArrayList;
@ -35,10 +38,13 @@ public class AppLayoutModel {
private final Property<Entry> selected;
private final ObservableList<QueueEntry> queueEntries;
public AppLayoutModel(SavedState savedState) {
this.savedState = savedState;
this.entries = createEntryList();
this.selected = new SimpleObjectProperty<>(entries.getFirst());
this.queueEntries = FXCollections.observableArrayList();
}
public static AppLayoutModel get() {
@ -46,7 +52,7 @@ public class AppLayoutModel {
}
public static void init() {
var state = AppCache.getNonNull("layoutState", SavedState.class, () -> new SavedState(260, 300));
var state = AppCache.getNonNull("layoutState", SavedState.class, () -> new SavedState(270, 300));
INSTANCE = new AppLayoutModel(state);
}
@ -121,18 +127,18 @@ public class AppLayoutModel {
// "http://localhost:" + AppBeaconServer.get().getPort()),
// null),
new Entry(
AppI18n.observable("documentation"),
AppI18n.observable("docs"),
new LabelGraphic.IconGraphic("mdi2b-book-open-variant"),
null,
() -> Hyperlinks.open(Hyperlinks.DOCS),
null)));
if (AppDistributionType.get() != AppDistributionType.WEBTOP) {
l.add(new Entry(
AppI18n.observable("webtop"),
new LabelGraphic.IconGraphic("mdi2d-desktop-mac"),
null,
() -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP),
null));
AppI18n.observable("webtop"),
new LabelGraphic.IconGraphic("mdi2d-desktop-mac"),
null,
() -> Hyperlinks.open(Hyperlinks.GITHUB_WEBTOP),
null));
}
return l;
}
@ -152,4 +158,12 @@ public class AppLayoutModel {
Comp<?> comp,
Runnable action,
KeyCombination combination) {}
@Value
public static class QueueEntry {
ObservableValue<String> name;
LabelGraphic icon;
Runnable action;
}
}

View file

@ -6,6 +6,7 @@ import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.store.FilePath;
import lombok.Value;
@ -133,7 +134,7 @@ public class AppOpenArguments {
var dir = Files.isDirectory(file) ? file : file.getParent();
AppLayoutModel.get().selectBrowser();
BrowserFullSessionModel.DEFAULT.openFileSystemAsync(
DataStorage.get().local().ref(), model -> dir.toString(), null);
DataStorage.get().local().ref(), model -> FilePath.of(dir.toString()), null);
}
}
}

View file

@ -104,7 +104,9 @@ public class AppTheme {
}
Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
TrackEvent.withTrace("Platform preference changed").tag("change", change.toString()).handle();
TrackEvent.withTrace("Platform preference changed")
.tag("change", change.toString())
.handle();
});
Platform.getPreferences().addListener((MapChangeListener<? super String, ? super Object>) change -> {
@ -140,10 +142,10 @@ public class AppTheme {
private static void updateThemeToThemeName(Object oldName, Object newName) {
if (OsType.getLocal() == OsType.LINUX && newName != null) {
var toDark = (oldName == null || !oldName.toString().contains("-dark")) &&
newName.toString().contains("-dark");
var toLight = (oldName == null || oldName.toString().contains("-dark")) &&
!newName.toString().contains("-dark");
var toDark = (oldName == null || !oldName.toString().contains("-dark"))
&& newName.toString().contains("-dark");
var toLight = (oldName == null || oldName.toString().contains("-dark"))
&& !newName.toString().contains("-dark");
if (toDark) {
updateThemeToColorScheme(ColorScheme.DARK);
} else if (toLight) {
@ -172,8 +174,7 @@ public class AppTheme {
AppPrefs.get().theme.setValue(Theme.getDefaultDarkTheme());
}
if (colorScheme != ColorScheme.DARK
&& AppPrefs.get().theme().getValue().isDark()) {
if (colorScheme != ColorScheme.DARK && AppPrefs.get().theme().getValue().isDark()) {
AppPrefs.get().theme.setValue(Theme.getDefaultLightTheme());
}
}

View file

@ -0,0 +1,88 @@
package io.xpipe.app.core;
import com.sun.jna.*;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinUser;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import java.util.List;
public class AppWindowsShutdown {
// Prevent GC
private static final WinShutdownHookProc PROC = new WinShutdownHookProc();
public static void registerHook(WinDef.HWND hwnd) {
int windowThreadID = User32.INSTANCE.GetWindowThreadProcessId(hwnd, null);
if (windowThreadID == 0) {
return;
}
PROC.hwnd = hwnd;
PROC.hhook = User32.INSTANCE.SetWindowsHookEx(4, PROC, null, windowThreadID);
}
public static class CWPSSTRUCT extends Structure {
public WinDef.LPARAM lParam;
public WinDef.WPARAM wParam;
public WinDef.DWORD message;
public WinDef.HWND hwnd;
@Override
protected List<String> getFieldOrder() {
return List.of("lParam", "wParam", "message", "hwnd");
}
}
public interface WinHookProc extends WinUser.HOOKPROC {
WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, CWPSSTRUCT hookProcStruct);
}
public static final int WM_ENDSESSION = 0x16;
public static final int WM_QUERYENDSESSION = 0x11;
public static final long ENDSESSION_CRITICAL = 0x40000000L;
@RequiredArgsConstructor
public static final class WinShutdownHookProc implements WinHookProc {
@Setter
private WinUser.HHOOK hhook;
@Setter
private WinDef.HWND hwnd;
@Override
public WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, CWPSSTRUCT hookProcStruct) {
if (nCode >= 0 && hookProcStruct.hwnd.equals(hwnd)) {
if (hookProcStruct.message.longValue() == WM_QUERYENDSESSION) {
// Indicates that we need to run the endsession case blocking
return new WinDef.LRESULT(0);
}
if (hookProcStruct.message.longValue() == WM_ENDSESSION) {
// Instant exit for critical shutdowns
if (hookProcStruct.lParam.longValue() == ENDSESSION_CRITICAL) {
OperationMode.halt(0);
}
// A shutdown hook will be started in parallel while we exit
// The only thing we have to do is wait for it to exit the platform
while (PlatformState.getCurrent() != PlatformState.EXITED) {
ThreadHelper.sleep(100);
PlatformThread.runNestedLoopIteration();
}
return new WinDef.LRESULT(0);
}
}
return User32.INSTANCE.CallNextHookEx(hhook, nCode, wParam, new WinDef.LPARAM(Pointer.nativeValue(hookProcStruct.getPointer())));
}
}
}

View file

@ -20,7 +20,6 @@ public class AppJavaOptionsCheck {
.formatted(env)
+ " This will forcefully apply all custom JVM options to XPipe and can cause a variety of different issues."
+ " Please remove this global environment variable and use local configuration instead for your other JVM programs.")
.noDefaultActions()
.expected()
.handle();
AppCache.update("javaOptionsWarningShown", true);

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 io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.OsType;
@ -27,7 +28,7 @@ public class AppRosettaCheck {
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.")
.noDefaultActions()
.documentationLink(DocumentationLink.MACOS_SETUP)
.expected()
.handle();
}

View file

@ -85,9 +85,8 @@ public abstract class AppShellChecker {
private Optional<FailureResult> selfTestErrorCheck() {
try (var sc = LocalShell.getShell().start()) {
var scriptFile = ScriptHelper.getExecScriptFile(sc);
var scriptContent = sc.getShellDialect().prepareScriptContent("echo test");
sc.view().writeScriptFile(scriptFile, scriptContent);
var scriptContent = "echo test";
var scriptFile = ScriptHelper.createExecScript(sc, scriptContent);
var out = sc.command(sc.getShellDialect().runScriptCommand(sc, scriptFile.toString()))
.readStdoutOrThrow();
if (!out.equals("test")) {

View file

@ -4,6 +4,7 @@ import io.xpipe.app.core.window.AppMainWindow;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.stage.Stage;
public class GuiMode extends PlatformMode {

View file

@ -19,6 +19,7 @@ import javafx.application.Platform;
import lombok.Getter;
import lombok.SneakyThrows;
import java.time.Duration;
import java.util.List;
public abstract class OperationMode {
@ -34,9 +35,6 @@ public abstract class OperationMode {
@Getter
private static boolean inShutdown;
@Getter
private static boolean inShutdownHook;
private static OperationMode CURRENT = null;
public static OperationMode map(XPipeDaemonMode mode) {
@ -73,7 +71,7 @@ public abstract class OperationMode {
}
TrackEvent.info("Received SIGTERM externally");
OperationMode.shutdown(true, false);
OperationMode.shutdown(false);
}));
// Handle uncaught exceptions
@ -174,7 +172,7 @@ public abstract class OperationMode {
if (OsType.getLocal() != OsType.LINUX) {
OperationMode.switchToSyncOrThrow(OperationMode.GUI);
}
OperationMode.shutdown(false, false);
OperationMode.shutdown(false);
return;
}
@ -256,7 +254,8 @@ public abstract class OperationMode {
var exec = XPipeInstallation.createExternalAsyncLaunchCommand(
loc,
XPipeDaemonMode.GUI,
"\"-Dio.xpipe.app.acceptEula=true\" \"-Dio.xpipe.app.dataDir=" + dataDir + "\" \"-Dio.xpipe.app.restarted=true\"",
"\"-Dio.xpipe.app.acceptEula=true\" \"-Dio.xpipe.app.dataDir=" + dataDir
+ "\" \"-Dio.xpipe.app.restarted=true\"",
true);
LocalShell.getShell().executeSimpleCommand(exec);
}
@ -274,7 +273,6 @@ public abstract class OperationMode {
}
inShutdown = true;
inShutdownHook = false;
try {
if (CURRENT != null) {
CURRENT.finalTeardown();
@ -319,35 +317,20 @@ public abstract class OperationMode {
});
}
public static void shutdown(boolean inShutdownHook, boolean hasError) {
@SneakyThrows
public static void shutdown(boolean hasError) {
if (isInStartup()) {
TrackEvent.info("Received shutdown request while in startup. Halting ...");
OperationMode.halt(1);
}
// In case we are stuck while in shutdown, instantly exit this application
if (inShutdown && inShutdownHook) {
TrackEvent.info("Received another shutdown request while in shutdown hook. Halting ...");
OperationMode.halt(1);
}
if (inShutdown) {
return;
}
// Run a timer to always exit after some time in case we get stuck
if (!hasError && !AppProperties.get().isDevelopmentEnvironment()) {
ThreadHelper.runAsync(() -> {
ThreadHelper.sleep(25000);
TrackEvent.info("Shutdown took too long. Halting ...");
OperationMode.halt(1);
});
}
TrackEvent.info("Starting shutdown ...");
inShutdown = true;
OperationMode.inShutdownHook = inShutdownHook;
// Keep a non-daemon thread running
var thread = ThreadHelper.createPlatformThread("shutdown", false, () -> {
try {
@ -363,6 +346,14 @@ public abstract class OperationMode {
OperationMode.halt(hasError ? 1 : 0);
});
thread.start();
// Use a timer to always exit after some time in case we get stuck
var limit = !hasError && !AppProperties.get().isDevelopmentEnvironment() ? 25000 : Integer.MAX_VALUE;
var exited = thread.join(Duration.ofMillis(limit));
if (!exited) {
TrackEvent.info("Shutdown took too long. Halting ...");
OperationMode.halt(1);
}
}
private static synchronized void set(OperationMode newMode) {
@ -380,7 +371,7 @@ public abstract class OperationMode {
try {
if (newMode == null) {
shutdown(false, false);
shutdown(false);
return;
}

View file

@ -25,7 +25,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
public class AppDialog {
@Getter
private static final ObservableList<ModalOverlay> modalOverlay = FXCollections.observableArrayList();
private static final ObservableList<ModalOverlay> modalOverlays = FXCollections.observableArrayList();
private static void showMainWindow() {
PlatformInit.init(true);
@ -34,20 +34,20 @@ public class AppDialog {
public static void closeDialog(ModalOverlay overlay) {
PlatformThread.runLaterIfNeeded(() -> {
synchronized (modalOverlay) {
modalOverlay.remove(overlay);
synchronized (modalOverlays) {
modalOverlays.remove(overlay);
}
});
}
public static void waitForAllDialogsClose() {
while (!modalOverlay.isEmpty()) {
while (!modalOverlays.isEmpty()) {
ThreadHelper.sleep(10);
}
}
private static void waitForDialogClose(ModalOverlay overlay) {
while (modalOverlay.contains(overlay)) {
while (modalOverlays.contains(overlay)) {
ThreadHelper.sleep(10);
}
}
@ -64,8 +64,8 @@ public class AppDialog {
showMainWindow();
if (!Platform.isFxApplicationThread()) {
PlatformThread.runLaterIfNeededBlocking(() -> {
synchronized (modalOverlay) {
modalOverlay.add(o);
synchronized (modalOverlays) {
modalOverlays.add(o);
}
});
if (wait) {
@ -75,9 +75,9 @@ public class AppDialog {
} else {
var key = new Object();
PlatformThread.runLaterIfNeededBlocking(() -> {
synchronized (modalOverlay) {
modalOverlay.add(o);
modalOverlay.addListener(new ListChangeListener<>() {
synchronized (modalOverlays) {
modalOverlays.add(o);
modalOverlays.addListener(new ListChangeListener<>() {
@Override
public void onChanged(Change<? extends ModalOverlay> c) {
if (!c.getList().contains(o)) {
@ -88,7 +88,7 @@ public class AppDialog {
}
});
transition.play();
modalOverlay.removeListener(this);
modalOverlays.removeListener(this);
}
}
});

View file

@ -56,6 +56,8 @@ public class AppMainWindow {
@Getter
private static final Property<String> loadingText = new SimpleObjectProperty<>();
private boolean shown = false;
private AppMainWindow(Stage stage) {
this.stage = stage;
}
@ -145,9 +147,12 @@ public class AppMainWindow {
public void show() {
stage.show();
if (OsType.getLocal() == OsType.WINDOWS) {
NativeWinWindowControl.MAIN_WINDOW = new NativeWinWindowControl(stage);
if (OsType.getLocal() == OsType.WINDOWS && !shown) {
var ctrl = new NativeWinWindowControl(stage);
NativeWinWindowControl.MAIN_WINDOW = ctrl;
AppWindowsShutdown.registerHook(ctrl.getWindowHandle());
}
shown = true;
}
public void focus() {
@ -306,7 +311,7 @@ public class AppMainWindow {
}
// Close dialogs
AppDialog.getModalOverlay().clear();
AppDialog.getModalOverlays().clear();
// Close other windows
Stage.getWindows().stream().filter(w -> !w.equals(stage)).toList().forEach(w -> w.fireEvent(e));

View file

@ -1,7 +1,12 @@
package io.xpipe.app.core.window;
import io.xpipe.app.core.AppLogs;
import io.xpipe.app.core.AppWindowsShutdown;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType;
import javafx.animation.PauseTransition;
@ -14,8 +19,11 @@ import javafx.stage.StageStyle;
import javafx.stage.Window;
import javafx.util.Duration;
import lombok.SneakyThrows;
import org.apache.commons.lang3.SystemUtils;
import java.awt.*;
public class ModifiedStage extends Stage {
public static boolean mergeFrame() {
@ -55,6 +63,7 @@ public class ModifiedStage extends Stage {
});
}
@SneakyThrows
private static void applyModes(Stage stage) {
if (stage.getScene() == null) {
return;
@ -71,47 +80,40 @@ public class ModifiedStage extends Stage {
return;
}
switch (OsType.getLocal()) {
case OsType.Linux linux -> {}
case OsType.MacOs macOs -> {
var ctrl = new NativeMacOsWindowControl(stage);
var seamlessFrame = AppMainWindow.getInstance() != null
&& AppMainWindow.getInstance().getStage() == stage
&& !AppPrefs.get().performanceMode().get()
&& mergeFrame();
var seamlessFrameApplied = ctrl.setAppearance(
seamlessFrame, AppPrefs.get().theme().getValue().isDark())
&& seamlessFrame;
stage.getScene()
.getRoot()
.pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied);
stage.getScene()
.getRoot()
.pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied);
}
case OsType.Windows windows -> {
var ctrl = new NativeWinWindowControl(stage);
ctrl.setWindowAttribute(
NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
AppPrefs.get().theme().getValue().isDark());
boolean seamlessFrame;
if (AppPrefs.get().performanceMode().get()
|| !mergeFrame()
|| AppMainWindow.getInstance() == null
|| stage != AppMainWindow.getInstance().getStage()) {
seamlessFrame = false;
} else {
// This is not available on Windows 10
seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT)
|| SystemUtils.IS_OS_WINDOWS_10;
try {
switch (OsType.getLocal()) {
case OsType.Linux linux -> {
}
case OsType.MacOs macOs -> {
var ctrl = new NativeMacOsWindowControl(stage);
var seamlessFrame = AppMainWindow.getInstance() != null &&
AppMainWindow.getInstance().getStage() == stage &&
!AppPrefs.get().performanceMode().get() &&
mergeFrame();
var seamlessFrameApplied = ctrl.setAppearance(seamlessFrame, AppPrefs.get().theme().getValue().isDark()) && seamlessFrame;
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("seamless-frame"), seamlessFrameApplied);
stage.getScene().getRoot().pseudoClassStateChanged(PseudoClass.getPseudoClass("separate-frame"), !seamlessFrameApplied);
}
case OsType.Windows windows -> {
var ctrl = new NativeWinWindowControl(stage);
ctrl.setWindowAttribute(NativeWinWindowControl.DmwaWindowAttribute.DWMWA_USE_IMMERSIVE_DARK_MODE.get(),
AppPrefs.get().theme().getValue().isDark());
boolean seamlessFrame;
if (AppPrefs.get().performanceMode().get() ||
!mergeFrame() ||
AppMainWindow.getInstance() == null ||
stage != AppMainWindow.getInstance().getStage()) {
seamlessFrame = false;
} else {
// This is not available on Windows 10
seamlessFrame = ctrl.setWindowBackdrop(NativeWinWindowControl.DwmSystemBackDropType.MICA_ALT) || SystemUtils.IS_OS_WINDOWS_10;
}
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);
}
} catch (Throwable t) {
ErrorEvent.fromThrowable(t).omit().handle();
}
}

View file

@ -24,6 +24,21 @@ import java.util.List;
@EqualsAndHashCode
public class NativeWinWindowControl {
@SneakyThrows
public static WinDef.HWND byWindow(Window window) {
Method tkStageGetter = Window.class.getDeclaredMethod("getPeer");
tkStageGetter.setAccessible(true);
Object tkStage = tkStageGetter.invoke(window);
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);
var hwnd = new WinDef.HWND(new Pointer((long) nativeHandle));
return hwnd;
}
public static List<NativeWinWindowControl> byPid(long pid) {
var refs = new ArrayList<NativeWinWindowControl>();
User32.INSTANCE.EnumWindows(
@ -50,17 +65,7 @@ public class NativeWinWindowControl {
@SneakyThrows
public NativeWinWindowControl(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);
var hwnd = new WinDef.HWND(new Pointer((long) nativeHandle));
this.windowHandle = hwnd;
this.windowHandle = byWindow(stage);
}
public NativeWinWindowControl(WinDef.HWND windowHandle) {

View file

@ -52,6 +52,10 @@ public interface ActionProvider {
return null;
}
default BatchDataStoreCallSite<?> getBatchDataStoreCallSite() {
return null;
}
default DefaultDataStoreCallSite<?> getDefaultDataStoreCallSite() {
return null;
}
@ -191,6 +195,44 @@ public interface ActionProvider {
}
}
interface BatchDataStoreCallSite<T extends DataStore> {
ObservableValue<String> getName();
String getIcon();
Class<?> getApplicableClass();
default boolean isApplicable(DataStoreEntryRef<T> o) {
return true;
}
default Action createAction(List<DataStoreEntryRef<T>> stores) {
var individual = stores.stream()
.map(ref -> {
return createAction(ref);
})
.filter(action -> action != null)
.toList();
return new Action() {
@Override
public void execute() throws Exception {
for (Action action : individual) {
action.execute();
}
}
};
}
default Action createAction(DataStoreEntryRef<T> store) {
return null;
}
default List<? extends ActionProvider> getChildren(List<DataStoreEntryRef<T>> batch) {
return List.of();
}
}
class Loader implements ModuleLayerLoader {
@Override

View file

@ -1,10 +1,12 @@
package io.xpipe.app.ext;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl;
import io.xpipe.core.store.FileEntry;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.store.FileSystem;
import com.fasterxml.jackson.annotation.JsonIgnore;
@ -28,9 +30,11 @@ public class ConnectionFileSystem implements FileSystem {
}
@Override
public long getFileSize(String file) throws Exception {
return Long.parseLong(
shellControl.getShellDialect().queryFileSize(shellControl, file).readStdoutOrThrow());
public long getFileSize(FilePath file) throws Exception {
return Long.parseLong(shellControl
.getShellDialect()
.queryFileSize(shellControl, file.toString())
.readStdoutOrThrow());
}
@Override
@ -54,8 +58,11 @@ public class ConnectionFileSystem implements FileSystem {
if (!shellControl.getTtyState().isPreservesOutput()
|| !shellControl.getTtyState().isSupportsInput()) {
throw ErrorEvent.expected(new UnsupportedOperationException(
"Shell has a PTY allocated and as a result does not support file system operations. For more information see " + Hyperlinks.DOCS_TTY));
var ex = new UnsupportedOperationException(
"Shell has a PTY allocated and as a result does not support file system operations.");
ErrorEvent.preconfigure(ErrorEvent.fromThrowable(ex)
.documentationLink(DocumentationLink.TTY));
throw ex;
}
shellControl.checkLicenseOrThrow();
@ -64,114 +71,119 @@ public class ConnectionFileSystem implements FileSystem {
}
@Override
public InputStream openInput(String file) throws Exception {
public InputStream openInput(FilePath file) throws Exception {
return shellControl
.getShellDialect()
.getFileReadCommand(shellControl, file)
.getFileReadCommand(shellControl, file.toString())
.startExternalStdout();
}
@Override
public OutputStream openOutput(String file, long totalBytes) throws Exception {
var cmd = shellControl.getShellDialect().createStreamFileWriteCommand(shellControl, file, totalBytes);
public OutputStream openOutput(FilePath file, long totalBytes) throws Exception {
var cmd =
shellControl.getShellDialect().createStreamFileWriteCommand(shellControl, file.toString(), totalBytes);
cmd.setExitTimeout(Duration.ofMillis(Long.MAX_VALUE));
return cmd.startExternalStdin();
}
@Override
public boolean fileExists(String file) throws Exception {
public boolean fileExists(FilePath file) throws Exception {
try (var pc = shellControl
.getShellDialect()
.createFileExistsCommand(shellControl, file)
.createFileExistsCommand(shellControl, file.toString())
.start()) {
return pc.discardAndCheckExit();
}
}
@Override
public void delete(String file) throws Exception {
public void delete(FilePath file) throws Exception {
try (var pc = shellControl
.getShellDialect()
.deleteFileOrDirectory(shellControl, file)
.deleteFileOrDirectory(shellControl, file.toString())
.start()) {
pc.discardOrThrow();
}
}
@Override
public void copy(String file, String newFile) throws Exception {
public void copy(FilePath file, FilePath newFile) throws Exception {
try (var pc = shellControl
.getShellDialect()
.getFileCopyCommand(shellControl, file, newFile)
.getFileCopyCommand(shellControl, file.toString(), newFile.toString())
.start()) {
pc.discardOrThrow();
}
}
@Override
public void move(String file, String newFile) throws Exception {
public void move(FilePath file, FilePath newFile) throws Exception {
try (var pc = shellControl
.getShellDialect()
.getFileMoveCommand(shellControl, file, newFile)
.getFileMoveCommand(shellControl, file.toString(), newFile.toString())
.start()) {
pc.discardOrThrow();
}
}
@Override
public void mkdirs(String file) throws Exception {
public void mkdirs(FilePath file) throws Exception {
try (var pc = shellControl
.command(
CommandBuilder.ofFunction(proc -> proc.getShellDialect().getMkdirsCommand(file)))
CommandBuilder.ofFunction(proc -> proc.getShellDialect().getMkdirsCommand(file.toString())))
.start()) {
pc.discardOrThrow();
}
}
@Override
public void touch(String file) throws Exception {
public void touch(FilePath file) throws Exception {
try (var pc = shellControl
.getShellDialect()
.getFileTouchCommand(shellControl, file)
.getFileTouchCommand(shellControl, file.toString())
.start()) {
pc.discardOrThrow();
}
}
@Override
public void symbolicLink(String linkFile, String targetFile) throws Exception {
public void symbolicLink(FilePath linkFile, FilePath targetFile) throws Exception {
try (var pc = shellControl
.getShellDialect()
.symbolicLink(shellControl, linkFile, targetFile)
.symbolicLink(shellControl, linkFile.toString(), targetFile.toString())
.start()) {
pc.discardOrThrow();
}
}
@Override
public boolean directoryExists(String file) throws Exception {
public boolean directoryExists(FilePath file) throws Exception {
return shellControl
.getShellDialect()
.directoryExists(shellControl, file)
.directoryExists(shellControl, file.toString())
.executeAndCheck();
}
@Override
public void directoryAccessible(String file) throws Exception {
public void directoryAccessible(FilePath file) throws Exception {
var current = shellControl.executeSimpleStringCommand(
shellControl.getShellDialect().getPrintWorkingDirectoryCommand());
shellControl.command(shellControl.getShellDialect().getCdCommand(file));
shellControl.command(shellControl.getShellDialect().getCdCommand(file.toString()));
shellControl.command(shellControl.getShellDialect().getCdCommand(current));
}
@Override
public Stream<FileEntry> listFiles(String file) throws Exception {
return shellControl.getShellDialect().listFiles(this, shellControl, file);
public Stream<FileEntry> listFiles(FilePath file) throws Exception {
return shellControl.getShellDialect().listFiles(this, shellControl, file.toString());
}
@Override
public List<String> listRoots() throws Exception {
return shellControl.getShellDialect().listRoots(shellControl).toList();
public List<FilePath> listRoots() throws Exception {
return shellControl
.getShellDialect()
.listRoots(shellControl)
.map(s -> FilePath.of(s))
.toList();
}
@Override

View file

@ -19,6 +19,7 @@ public class ContainerStoreState extends ShellStoreState {
String imageName;
String containerState;
Boolean shellMissing;
@Override
public DataStoreState mergeCopy(DataStoreState newer) {
@ -32,5 +33,6 @@ public class ContainerStoreState extends ShellStoreState {
super.mergeBuilder(css, b);
b.containerState(useNewer(containerState, css.getContainerState()));
b.imageName(useNewer(imageName, css.getImageName()));
b.shellMissing(useNewer(shellMissing, css.getShellMissing()));
}
}

View file

@ -2,18 +2,15 @@ package io.xpipe.app.ext;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.comp.store.StoreEntryComp;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.comp.store.StoreSection;
import io.xpipe.app.comp.store.StoreSectionComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.resources.AppImages;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.core.store.DataStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
@ -26,6 +23,10 @@ import java.util.UUID;
public interface DataStoreProvider {
default String getHelpLink() {
return null;
}
default boolean canMoveCategories() {
return true;
}
@ -109,32 +110,6 @@ public interface DataStoreProvider {
return false;
}
default Comp<?> createInsightsComp(ObservableValue<DataStore> store) {
var content = Bindings.createStringBinding(
() -> {
if (store.getValue() == null
|| !store.getValue().isComplete()
|| !getStoreClasses().contains(store.getValue().getClass())) {
return null;
}
try {
return "## Insights\n\n" + createInsightsMarkdown(store.getValue());
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return "?";
}
},
store);
return new MarkdownComp(content, s -> s, true)
.apply(struc -> struc.get().setPrefWidth(450))
.apply(struc -> struc.get().setPrefHeight(250));
}
default String createInsightsMarkdown(DataStore store) {
return null;
}
default DataStoreCreationCategory getCreationCategory() {
return null;
}

View file

@ -0,0 +1,6 @@
package io.xpipe.app.ext;
public interface NameableStore {
String getName();
}

View file

@ -7,6 +7,10 @@ import io.xpipe.core.store.*;
public interface ShellStore extends DataStore, FileSystemStore, ValidatableStore, SingletonSessionStore<ShellSession> {
default boolean isConnectionAttemptCostly() {
return false;
}
default ShellControl getOrStartSession() throws Exception {
var session = getSession();
if (session != null) {

View file

@ -23,7 +23,6 @@ import javax.imageio.ImageIO;
public class SystemIconCache {
private static enum ImageColorScheme {
TRANSPARENT,
MIXED,
LIGHT,
@ -71,14 +70,17 @@ public class SystemIconCache {
continue;
}
if (scheme != ImageColorScheme.DARK || icon.getColorSchemeData() != SystemIconSourceFile.ColorSchemeData.DEFAULT) {
if (scheme != ImageColorScheme.DARK
|| icon.getColorSchemeData() != SystemIconSourceFile.ColorSchemeData.DEFAULT) {
continue;
}
var hasExplicitDark = e.getValue().getIcons().stream().anyMatch(
systemIconSourceFile -> systemIconSourceFile.getSource().equals(icon.getSource()) &&
systemIconSourceFile.getName().equals(icon.getName()) &&
systemIconSourceFile.getColorSchemeData() == SystemIconSourceFile.ColorSchemeData.DARK);
var hasExplicitDark = e.getValue().getIcons().stream()
.anyMatch(systemIconSourceFile ->
systemIconSourceFile.getSource().equals(icon.getSource())
&& systemIconSourceFile.getName().equals(icon.getName())
&& systemIconSourceFile.getColorSchemeData()
== SystemIconSourceFile.ColorSchemeData.DARK);
if (hasExplicitDark) {
continue;
}
@ -131,7 +133,8 @@ public class SystemIconCache {
}
}
private static ImageColorScheme rasterizeSizesInverted(Path path, Path dir, String name, boolean dark) throws IOException {
private static ImageColorScheme rasterizeSizesInverted(Path path, Path dir, String name, boolean dark)
throws IOException {
try {
ImageColorScheme c = null;
for (var size : sizes) {
@ -173,8 +176,8 @@ public class SystemIconCache {
return image;
}
private static BufferedImage write(Path dir, String name, boolean dark, int px, BufferedImage image) throws IOException {
private static BufferedImage write(Path dir, String name, boolean dark, int px, BufferedImage image)
throws IOException {
var out = dir.resolve(name + "-" + px + (dark ? "-dark" : "") + ".png");
ImageIO.write(image, "png", out.toFile());
return image;
@ -184,12 +187,12 @@ public class SystemIconCache {
var buffer = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int clr = image.getRGB(x, y);
int alpha = (clr >> 24) & 0xff;
int red = (clr & 0x00ff0000) >> 16;
int green = (clr & 0x0000ff00) >> 8;
int blue = clr & 0x000000ff;
buffer.setRGB(x, y, new Color(255- red, 255- green, 255- blue, alpha).getRGB());
int clr = image.getRGB(x, y);
int alpha = (clr >> 24) & 0xff;
int red = (clr & 0x00ff0000) >> 16;
int green = (clr & 0x0000ff00) >> 8;
int blue = clr & 0x000000ff;
buffer.setRGB(x, y, new Color(255 - red, 255 - green, 255 - blue, alpha).getRGB());
}
}
return buffer;
@ -201,11 +204,11 @@ public class SystemIconCache {
var mean = 0.0;
for (int y = 0; y < image.getHeight(); y++) {
for (int x = 0; x < image.getWidth(); x++) {
int clr = image.getRGB(x, y);
int alpha = (clr >> 24) & 0xff;
int red = (clr & 0x00ff0000) >> 16;
int green = (clr & 0x0000ff00) >> 8;
int blue = clr & 0x000000ff;
int clr = image.getRGB(x, y);
int alpha = (clr >> 24) & 0xff;
int red = (clr & 0x00ff0000) >> 16;
int green = (clr & 0x0000ff00) >> 8;
int blue = clr & 0x000000ff;
if (alpha > 0) {
transparent = false;

View file

@ -1,11 +1,14 @@
package io.xpipe.app.icon;
import io.xpipe.app.ext.ProcessControlProvider;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.DesktopHelper;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.Validators;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ProcessOutputException;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.ValidationException;
import com.fasterxml.jackson.annotation.JsonSubTypes;
@ -91,6 +94,13 @@ public interface SystemIconSource {
public void refresh() throws Exception {
try (var sc =
ProcessControlProvider.get().createLocalProcessControl(true).start()) {
var present = sc.view().findProgram("git").isPresent();
if (!present) {
var msg = "Git command-line tools are not available in the PATH but are required to use icons from a git repository. For more details, see https://git-scm.com/downloads.";
ErrorEvent.fromMessage(msg).expected().handle();
return;
}
var dir = SystemIconManager.getPoolPath().resolve(id);
if (!Files.exists(dir)) {
sc.command(CommandBuilder.of()
@ -100,7 +110,7 @@ public interface SystemIconSource {
.execute();
} else {
sc.command(CommandBuilder.of().add("git", "pull"))
.withWorkingDirectory(dir.toString())
.withWorkingDirectory(FilePath.of(dir))
.execute();
}
}

View file

@ -35,11 +35,12 @@ public class SystemIconSourceData {
.filter(path -> Files.isRegularFile(path))
.filter(path -> path.toString().endsWith(".svg"))
.map(path -> {
var name = FilenameUtils.getBaseName(path.getFileName().toString());
var cleanedName = name.replaceFirst("-light$", "").replaceFirst("-dark$", "");
var cleanedPath = path.getParent().resolve(cleanedName + ".svg");
return cleanedPath;
}).toList();
var name = FilenameUtils.getBaseName(path.getFileName().toString());
var cleanedName = name.replaceFirst("-light$", "").replaceFirst("-dark$", "");
var cleanedPath = path.getParent().resolve(cleanedName + ".svg");
return cleanedPath;
})
.toList();
for (var file : flatFiles) {
var name = FilenameUtils.getBaseName(file.getFileName().toString());
var displayName = name.toLowerCase(Locale.ROOT);
@ -51,33 +52,40 @@ public class SystemIconSourceData {
var hasLightModeVariant = Files.exists(lightModeFile);
if (hasBaseVariant && hasDarkModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
sourceFiles.add(new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
sourceFiles.add(new SystemIconSourceFile(
source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
sourceFiles.add(new SystemIconSourceFile(
source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
continue;
}
if (hasBaseVariant && hasLightModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DARK));
sourceFiles.add(new SystemIconSourceFile(source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
sourceFiles.add(new SystemIconSourceFile(
source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DARK));
sourceFiles.add(new SystemIconSourceFile(
source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
continue;
}
if (!hasBaseVariant) {
if (hasLightModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
sourceFiles.add(new SystemIconSourceFile(
source, displayName, lightModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
if (hasDarkModeVariant) {
sourceFiles.add(new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
sourceFiles.add(new SystemIconSourceFile(
source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DARK));
}
} else {
if (hasDarkModeVariant) {
sourceFiles.add(
new SystemIconSourceFile(source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
sourceFiles.add(new SystemIconSourceFile(
source, displayName, darkModeFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
}
}
continue;
}
sourceFiles.add(new SystemIconSourceFile(source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
sourceFiles.add(new SystemIconSourceFile(
source, displayName, baseFile, SystemIconSourceFile.ColorSchemeData.DEFAULT));
}
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();

View file

@ -1,11 +1,12 @@
package io.xpipe.app.issue;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.Hyperlinks;
public interface ErrorAction {
static ErrorAction openDocumentation(String link) {
static ErrorAction openDocumentation(DocumentationLink link) {
return new ErrorAction() {
@Override
public String getName() {
@ -19,53 +20,12 @@ public interface ErrorAction {
@Override
public boolean handle(ErrorEvent event) {
Hyperlinks.open(link);
link.open();
return false;
}
};
}
static ErrorAction reportOnGithub() {
return new ErrorAction() {
@Override
public String getName() {
return AppI18n.get("reportOnGithub");
}
@Override
public String getDescription() {
return AppI18n.get("reportOnGithubDescription");
}
@Override
public boolean handle(ErrorEvent event) {
var url = "https://github.com/xpipe-io/xpipe/issues/new";
Hyperlinks.open(url);
return false;
}
};
}
static ErrorAction automaticallyReport() {
return new ErrorAction() {
@Override
public String getName() {
return AppI18n.get("reportError");
}
@Override
public String getDescription() {
return AppI18n.get("reportErrorDescription");
}
@Override
public boolean handle(ErrorEvent event) {
UserReportComp.show(event);
return true;
}
};
}
static ErrorAction ignore() {
return new ErrorAction() {
@Override

View file

@ -1,5 +1,6 @@
package io.xpipe.app.issue;
import io.xpipe.app.util.DocumentationLink;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@ -23,9 +24,6 @@ public class ErrorEvent {
@Builder.Default
private final boolean reportable = true;
@Setter
private boolean disableDefaultActions;
private final Throwable throwable;
@Singular
@ -43,6 +41,8 @@ public class ErrorEvent {
@Singular
private List<Path> attachments;
private DocumentationLink documentationLink;
private String email;
private String userReport;
private boolean unhandled;
@ -162,10 +162,6 @@ public class ErrorEvent {
return omit().expected();
}
public ErrorEventBuilder noDefaultActions() {
return disableDefaultActions(true);
}
public void handle() {
build().handle();
}

View file

@ -66,22 +66,6 @@ public class ErrorHandlerComp extends SimpleComp {
return b.createRegion();
}
private Region createDetails() {
var content = new ErrorDetailsComp(event).prefWidth(600).prefHeight(750);
var modal = ModalOverlay.of("errorDetails", content);
var button = new ButtonComp(
null,
new SimpleObjectProperty<>(new LabelGraphic.NodeGraphic(() -> {
return createActionButtonGraphic(AppI18n.get("showDetails"), AppI18n.get("showDetailsDescription"));
})),
() -> {
modal.show();
});
var r = button.grow(true, false).createRegion();
r.getStyleClass().add("details");
return r;
}
private Region createTop() {
var desc = event.getDescription();
if (desc == null && event.getThrowable() != null) {
@ -112,10 +96,10 @@ public class ErrorHandlerComp extends SimpleComp {
@Override
protected Region createSimple() {
var top = createTop();
var content = new VBox(top, new Separator(Orientation.HORIZONTAL));
var content = new VBox(top);
var header = new Label(AppI18n.get("possibleActions"));
AppFontSizes.xl(header);
var actionBox = new VBox(header);
var actionBox = new VBox();
actionBox.getStyleClass().add("actions");
actionBox.setFillWidth(true);
@ -137,7 +121,6 @@ public class ErrorHandlerComp extends SimpleComp {
return true;
}
});
event.setDisableDefaultActions(true);
}
var custom = event.getCustomActions();
@ -147,21 +130,17 @@ public class ErrorHandlerComp extends SimpleComp {
actionBox.getChildren().add(ac);
}
if (!event.isDisableDefaultActions()) {
for (var action :
List.of(ErrorAction.automaticallyReport(), ErrorAction.reportOnGithub(), ErrorAction.ignore())) {
var ac = createActionComp(action);
actionBox.getChildren().add(ac);
}
} else if (event.getCustomActions().isEmpty()) {
for (var action : List.of(ErrorAction.ignore())) {
var ac = createActionComp(action);
actionBox.getChildren().add(ac);
}
if (event.getDocumentationLink() != null) {
actionBox.getChildren().add(createActionComp(ErrorAction.openDocumentation(event.getDocumentationLink())));
}
if (actionBox.getChildren().size() > 0) {
actionBox.getChildren().addFirst(header);
content.getChildren().add(new Separator(Orientation.HORIZONTAL));
actionBox.getChildren().get(1).getStyleClass().addAll(BUTTON_OUTLINED);
content.getChildren().addAll(actionBox);
}
actionBox.getChildren().get(1).getStyleClass().addAll(BUTTON_OUTLINED, ACCENT);
content.getChildren().addAll(actionBox);
content.getStyleClass().add("top");
content.setFillWidth(true);
content.setMinHeight(Region.USE_PREF_SIZE);
@ -170,13 +149,6 @@ public class ErrorHandlerComp extends SimpleComp {
layout.getChildren().add(content);
layout.getStyleClass().add("error-handler-comp");
if (event.getThrowable() != null) {
content.getChildren().add(new Separator(Orientation.HORIZONTAL));
var details = createDetails();
layout.getChildren().add(details);
layout.prefHeightProperty().bind(content.heightProperty().add(65).add(details.prefHeightProperty()));
}
return layout;
}
}

View file

@ -1,5 +1,6 @@
package io.xpipe.app.issue;
import io.xpipe.app.comp.base.ModalButton;
import io.xpipe.app.comp.base.ModalOverlay;
import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.core.window.AppDialog;
@ -44,13 +45,25 @@ public class ErrorHandlerDialog {
var comp = new ErrorHandlerComp(event, () -> {
AppDialog.closeDialog(modal.get());
});
comp.prefWidth(550);
comp.prefWidth(500);
var headerId = event.isTerminal() ? "terminalErrorOccured" : "errorOccured";
modal.set(ModalOverlay.of(headerId, comp, new LabelGraphic.NodeGraphic(() -> {
var errorModal = ModalOverlay.of(headerId, comp, new LabelGraphic.NodeGraphic(() -> {
var graphic = new FontIcon("mdomz-warning");
graphic.setIconColor(Color.RED);
return graphic;
})));
}));
if (event.getThrowable() != null && event.isReportable()) {
errorModal.addButton(new ModalButton("stackTrace", () -> {
var content = new ErrorDetailsComp(event).prefWidth(600).prefHeight(750);
var detailsModal = ModalOverlay.of("errorDetails", content);
detailsModal.show();
}, false, false));
}
errorModal.addButton(new ModalButton("report", () -> {
UserReportComp.show(event);
}, false, false));
errorModal.addButton(ModalButton.ok());
modal.set(errorModal);
AppDialog.showAndWait(modal.get());
if (comp.getTakenAction().getValue() == null) {
ErrorAction.ignore().handle(event);

View file

@ -8,6 +8,7 @@ import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.core.AppDistributionType;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.JfxHelper;
import io.xpipe.app.util.OptionsBuilder;
@ -32,10 +33,11 @@ public class AboutCategory extends AppPrefsCategory {
.grow(true, false),
null)
.addComp(
new TileButtonComp("documentation", "documentationDescription", "mdi2b-book-open-variant", e -> {
Hyperlinks.open(Hyperlinks.DOCS);
e.consume();
})
new TileButtonComp(
"documentation", "documentationDescription", "mdi2b-book-open-variant", e -> {
Hyperlinks.open(Hyperlinks.DOCS);
e.consume();
})
.grow(true, false),
null)
.addComp(
@ -47,23 +49,23 @@ public class AboutCategory extends AppPrefsCategory {
null)
.addComp(
new TileButtonComp("privacy", "privacyDescription", "mdomz-privacy_tip", e -> {
Hyperlinks.open(Hyperlinks.DOCS_PRIVACY);
DocumentationLink.PRIVACY.open();
e.consume();
})
.grow(true, false),
null)
.addComp(
new TileButtonComp("thirdParty", "thirdPartyDescription", "mdi2o-open-source-initiative", e -> {
var comp = new ThirdPartyDependencyListComp()
.prefWidth(650)
.styleClass("open-source-notices");
var modal = ModalOverlay.of("openSourceNotices", comp);
modal.show();
})
.grow(true, false))
var comp = new ThirdPartyDependencyListComp()
.prefWidth(650)
.styleClass("open-source-notices");
var modal = ModalOverlay.of("openSourceNotices", comp);
modal.show();
})
.grow(true, false))
.addComp(
new TileButtonComp("eula", "eulaDescription", "mdi2c-card-text-outline", e -> {
Hyperlinks.open(Hyperlinks.DOCS_EULA);
DocumentationLink.EULA.open();
e.consume();
})
.grow(true, false),
@ -80,7 +82,7 @@ public class AboutCategory extends AppPrefsCategory {
protected Comp<?> create() {
var props = createProperties().padding(new Insets(0, 0, 0, 5));
var update = new UpdateCheckComp().grow(true, false);
return new VerticalComp(List.of(props, Comp.separator(), update, Comp.separator(), createLinks()))
return new VerticalComp(List.of(props, Comp.hseparator(), update, Comp.hseparator(), createLinks()))
.apply(s -> s.get().setFillWidth(true))
.apply(struc -> struc.get().setSpacing(15))
.styleClass("information")

View file

@ -131,6 +131,8 @@ public class AppPrefs {
mapLocal(new SimpleBooleanProperty(false), "enforceWindowModality", Boolean.class, false);
final BooleanProperty checkForSecurityUpdates =
mapLocal(new SimpleBooleanProperty(true), "checkForSecurityUpdates", Boolean.class, false);
final BooleanProperty disableApiHttpsTlsCheck =
mapLocal(new SimpleBooleanProperty(false), "disableApiHttpsTlsCheck", Boolean.class, false);
final BooleanProperty condenseConnectionDisplay =
mapLocal(new SimpleBooleanProperty(false), "condenseConnectionDisplay", Boolean.class, false);
final BooleanProperty showChildCategoriesInParentCategory =

View file

@ -10,7 +10,7 @@ public enum CloseBehaviour implements PrefsChoiceValue {
QUIT("app.quit") {
@Override
public void run() {
OperationMode.shutdown(false, false);
OperationMode.shutdown(false);
}
},

View file

@ -29,8 +29,7 @@ public class ConnectionsCategory extends AppPrefsCategory {
.sub(new OptionsBuilder().pref(prefs.useLocalFallbackShell).addToggle(prefs.useLocalFallbackShell));
if (OsType.getLocal() == OsType.WINDOWS) {
options.addTitle("sshConfiguration")
.sub(new OptionsBuilder()
.addComp(prefs.getCustomComp("x11WslInstance")));
.sub(new OptionsBuilder().addComp(prefs.getCustomComp("x11WslInstance")));
}
return options.buildComp();
}

View file

@ -3,10 +3,10 @@ package io.xpipe.app.prefs;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.util.LocalShell;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellDialects;
import java.util.Arrays;
import java.util.Locale;
import java.util.stream.Collectors;
public class ExternalApplicationHelper {
@ -21,17 +21,32 @@ public class ExternalApplicationHelper {
}
public static void startAsync(String raw) throws Exception {
try (var sc = LocalShell.getShell().start()) {
if (ShellDialects.isPowershell(sc)) {
// Do the best effort here
// This does not respect quoting rules, but otherwise powershell wouldn't work at all
var split = raw.split("\\s+");
var splitBuilder = CommandBuilder.of().addAll(Arrays.asList(split));
startAsync(splitBuilder);
} else {
startAsync(CommandBuilder.ofString(raw));
}
if (raw == null) {
return;
}
raw = raw.trim();
var split = Arrays.asList(raw.split("\\s+"));
if (split.size() == 0) {
return;
}
String exec;
String args;
if (raw.startsWith("\"")) {
var end = raw.substring(1).indexOf("\"");
if (end == -1) {
return;
}
end++;
exec = raw.substring(1, end);
args = raw.substring(end + 1).trim();
} else {
exec = split.getFirst();
args = split.stream().skip(1).collect(Collectors.joining(" "));
}
startAsync(CommandBuilder.of().addFile(exec).add(args));
}
public static void startAsync(CommandBuilder b) throws Exception {

View file

@ -133,7 +133,7 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue {
try (var sc = LocalShell.getShell().start()) {
var out = CommandSupport.findProgram(sc, executable);
if (out.isPresent()) {
return out.map(Path::of);
return out.map(filePath -> Path.of(filePath.toString()));
}
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).omit().handle();

View file

@ -187,12 +187,36 @@ public interface ExternalEditorType extends PrefsChoiceValue {
ExternalEditorType PYCHARM = new GenericPathType("app.pycharm", "pycharm", false);
ExternalEditorType WEBSTORM = new GenericPathType("app.webstorm", "webstorm", false);
ExternalEditorType CLION = new GenericPathType("app.clion", "clion", false);
List<ExternalEditorType> WINDOWS_EDITORS =
List.of(CURSOR_WINDOWS, WINDSURF_WINDOWS, TRAE_WINDOWS, VSCODIUM_WINDOWS, VSCODE_INSIDERS_WINDOWS, VSCODE_WINDOWS, NOTEPADPLUSPLUS, NOTEPAD);
List<LinuxPathType> LINUX_EDITORS =
List.of(ExternalEditorType.WINDSURF_LINUX, VSCODIUM_LINUX, VSCODE_LINUX, ZED_LINUX, KATE, GEDIT, PLUMA, LEAFPAD, MOUSEPAD, GNOME);
List<ExternalEditorType> MACOS_EDITORS =
List.of(CURSOR_MACOS, WINDSURF_MACOS, TRAE_MACOS, BBEDIT, VSCODIUM_MACOS, VSCODE_MACOS, SUBLIME_MACOS, ZED_MACOS, TEXT_EDIT);
List<ExternalEditorType> WINDOWS_EDITORS = List.of(
CURSOR_WINDOWS,
WINDSURF_WINDOWS,
TRAE_WINDOWS,
VSCODIUM_WINDOWS,
VSCODE_INSIDERS_WINDOWS,
VSCODE_WINDOWS,
NOTEPADPLUSPLUS,
NOTEPAD);
List<LinuxPathType> LINUX_EDITORS = List.of(
ExternalEditorType.WINDSURF_LINUX,
VSCODIUM_LINUX,
VSCODE_LINUX,
ZED_LINUX,
KATE,
GEDIT,
PLUMA,
LEAFPAD,
MOUSEPAD,
GNOME);
List<ExternalEditorType> MACOS_EDITORS = List.of(
CURSOR_MACOS,
WINDSURF_MACOS,
TRAE_MACOS,
BBEDIT,
VSCODIUM_MACOS,
VSCODE_MACOS,
SUBLIME_MACOS,
ZED_MACOS,
TEXT_EDIT);
List<ExternalEditorType> CROSS_PLATFORM_EDITORS = List.of(FLEET, INTELLIJ, PYCHARM, WEBSTORM, CLION);
@SuppressWarnings("TrivialFunctionalExpressionUsage")

View file

@ -10,16 +10,15 @@ import io.xpipe.core.util.SecretValue;
import lombok.Value;
import org.apache.commons.io.FileUtils;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Supplier;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public interface ExternalRdpClientType extends PrefsChoiceValue {
@ -126,7 +125,12 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
public void launch(LaunchConfiguration configuration) throws Exception {
var file = writeRdpConfigFile(configuration.getTitle(), configuration.getConfig());
var escapedPw = configuration.getPassword().getSecretValue().replaceAll("'", "\\\\'");
launch(configuration.getTitle(), CommandBuilder.of().addFile(file.toString()).add("/cert-ignore").add("/p:'" + escapedPw + "'"));
launch(
configuration.getTitle(),
CommandBuilder.of()
.addFile(file.toString())
.add("/cert-ignore")
.add("/p:'" + escapedPw + "'"));
}
@Override
@ -293,7 +297,8 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
.add(ExternalApplicationHelper.replaceFileArgument(
format,
"FILE",
writeRdpConfigFile(configuration.getTitle(), configuration.getConfig()).toString())));
writeRdpConfigFile(configuration.getTitle(), configuration.getConfig())
.toString())));
}
@Override
@ -307,9 +312,11 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
}
}
class RemminaRdpType extends ExternalApplicationType.PathApplication implements ExternalRdpClientType {
class RemminaRdpType extends ExternalApplicationType.PathApplication implements ExternalRdpClientType {
public RemminaRdpType() {super("app.remmina", "remmina", true);}
public RemminaRdpType() {
super("app.remmina", "remmina", true);
}
private List<String> toStrip() {
return List.of("auto connect", "password 51", "prompt for credentials", "smart sizing");
@ -324,7 +331,9 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
var encrypted = encryptPassword(configuration.getPassword());
if (encrypted.isPresent()) {
var file = writeRemminaConfigFile(configuration, encrypted.get());
launch(configuration.getTitle(), CommandBuilder.of().add("-c").addFile(file.toString()));
launch(
configuration.getTitle(),
CommandBuilder.of().add("-c").addFile(file.toString()));
ThreadHelper.runFailableAsync(() -> {
ThreadHelper.sleep(5000);
FileUtils.deleteQuietly(file.toFile());
@ -343,7 +352,8 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
}
try (var sc = LocalShell.getShell().start()) {
var prefSecretBase64 = sc.command("sed -n 's/^secret=//p' ~/.config/remmina/remmina.pref").readStdoutIfPossible();
var prefSecretBase64 = sc.command("sed -n 's/^secret=//p' ~/.config/remmina/remmina.pref")
.readStdoutIfPossible();
if (prefSecretBase64.isEmpty()) {
return Optional.empty();
}
@ -367,7 +377,8 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
private Path writeRemminaConfigFile(LaunchConfiguration configuration, String password) throws Exception {
var name = OsType.getLocal().makeFileSystemCompatible(configuration.getTitle());
var file = LocalShell.getShell().getSystemTemporaryDirectory().join(name + ".remmina");
var string = """
var string =
"""
[remmina]
protocol=RDP
name=%s
@ -375,11 +386,20 @@ public interface ExternalRdpClientType extends PrefsChoiceValue {
server=%s
password=%s
cert_ignore=1
""".formatted(configuration.getTitle(),
configuration.getConfig().get("username").orElseThrow().getValue(),
configuration.getConfig().get("full address").orElseThrow().getValue(),
password
);
"""
.formatted(
configuration.getTitle(),
configuration
.getConfig()
.get("username")
.orElseThrow()
.getValue(),
configuration
.getConfig()
.get("full address")
.orElseThrow()
.getValue(),
password);
Files.writeString(file.toLocalPath(), string);
return file.toLocalPath();
}

View file

@ -7,6 +7,7 @@ import io.xpipe.app.icon.SystemIconManager;
import io.xpipe.app.icon.SystemIconSource;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.*;
import io.xpipe.core.store.FilePath;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
@ -15,7 +16,6 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.scene.control.TextField;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@ -96,7 +96,7 @@ public class IconsCategory extends AppPrefsCategory {
var addDirectoryButton = new TileButtonComp(
"addDirectoryIconSource", "addDirectoryIconSourceDescription", "mdi2f-folder-plus", e -> {
var dir = new SimpleStringProperty();
var dir = new SimpleObjectProperty<FilePath>();
var modal = ModalOverlay.of(
"iconDirectory",
new ContextualFileReferenceChoiceComp(
@ -107,12 +107,12 @@ public class IconsCategory extends AppPrefsCategory {
List.of())
.prefWidth(350));
modal.withDefaultButtons(() -> {
if (dir.get() == null || dir.get().isBlank()) {
if (dir.get() == null) {
return;
}
var source = SystemIconSource.Directory.builder()
.path(Path.of(dir.get()))
.path(dir.get().asLocalPath())
.id(UUID.randomUUID().toString())
.build();
if (!sources.contains(source)) {
@ -131,9 +131,9 @@ public class IconsCategory extends AppPrefsCategory {
var vbox = new VerticalComp(List.of(
Comp.vspacer(10),
box,
Comp.separator(),
Comp.hseparator(),
refreshButton,
Comp.separator(),
Comp.hseparator(),
addDirectoryButton,
addGitButton));
vbox.spacing(10);

View file

@ -92,7 +92,7 @@ public class PasswordManagerCategory extends AppPrefsCategory {
var docsLinkProperty = new SimpleStringProperty();
var docsLinkButton =
new ButtonComp(AppI18n.observable("documentation"), new FontIcon("mdi2h-help-circle-outline"), () -> {
new ButtonComp(AppI18n.observable("docs"), new FontIcon("mdi2h-help-circle-outline"), () -> {
var l = docsLinkProperty.get();
if (l != null) {
Hyperlinks.open(l);

View file

@ -30,7 +30,9 @@ public class SecurityCategory extends AppPrefsCategory {
.pref(prefs.dontAutomaticallyStartVmSshServer)
.addToggle(prefs.dontAutomaticallyStartVmSshServer)
.pref(prefs.disableTerminalRemotePasswordPreparation)
.addToggle(prefs.disableTerminalRemotePasswordPreparation));
.addToggle(prefs.disableTerminalRemotePasswordPreparation)
.pref(prefs.disableApiHttpsTlsCheck)
.addToggle(prefs.disableApiHttpsTlsCheck));
return builder.buildComp();
}
}

View file

@ -3,8 +3,9 @@ package io.xpipe.app.prefs;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.storage.DataStorageSyncHandler;
import io.xpipe.app.util.DocumentationLink;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.ThreadHelper;
@ -27,14 +28,6 @@ public class SyncCategory extends AppPrefsCategory {
return "vaultSync";
}
private static void showHelpAlert() {
var md = AppI18n.get().getMarkdownDocumentation("vault");
var markdown = new MarkdownComp(md, s -> s, true).prefWidth(600);
var modal = ModalOverlay.of(markdown);
modal.addButton(ModalButton.ok());
AppDialog.show(modal);
}
public Comp<?> create() {
var prefs = AppPrefs.get();
AtomicReference<Region> button = new AtomicReference<>();
@ -61,7 +54,7 @@ public class SyncCategory extends AppPrefsCategory {
var remoteRepo = new TextFieldComp(prefs.storageGitRemote).hgrow();
var helpButton = new ButtonComp(AppI18n.observable("help"), new FontIcon("mdi2h-help-circle-outline"), () -> {
showHelpAlert();
DocumentationLink.SYNC.open();
});
var remoteRow = new HorizontalComp(List.of(remoteRepo, helpButton)).spacing(10);
remoteRow.apply(struc -> struc.get().setAlignment(Pos.CENTER_LEFT));

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