This commit is contained in:
crschnick 2025-04-07 16:50:42 +00:00
parent ab71d178f3
commit 6f60d4e2d6
1620 changed files with 10776 additions and 6041 deletions

View file

@ -49,18 +49,14 @@ dependencies {
api 'com.vladsch.flexmark:flexmark-ext-toc:0.64.8'
api("com.github.weisj:jsvg:1.7.1")
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'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.2"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.2"
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.18.3"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.18.3"
api group: 'org.kordamp.ikonli', name: 'ikonli-material2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign2-pack', version: "12.2.0"
api group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: "12.2.0"

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

@ -111,8 +111,9 @@ public class BeaconRequestHandler<T> implements HttpHandler {
return;
} catch (BeaconServerException serverException) {
var cause = serverException.getCause() != null ? serverException.getCause() : serverException;
ErrorEvent.fromThrowable(cause).omit().handle();
writeError(exchange, new BeaconServerErrorResponse(cause), 500);
var event = ErrorEvent.fromThrowable(cause).omit().handle();
var link = event.getLink();
writeError(exchange, new BeaconServerErrorResponse(cause, link), 500);
return;
} catch (IOException ex) {
// Handle serialization errors as normal exceptions and other IO exceptions as assuming that the connection
@ -132,8 +133,9 @@ public class BeaconRequestHandler<T> implements HttpHandler {
}
return;
} catch (Throwable other) {
ErrorEvent.fromThrowable(other).omit().expected().handle();
writeError(exchange, new BeaconServerErrorResponse(other), 500);
var event = ErrorEvent.fromThrowable(other).omit().expected().handle();
var link = event.getLink();
writeError(exchange, new BeaconServerErrorResponse(other, link), 500);
return;
}
@ -160,8 +162,9 @@ public class BeaconRequestHandler<T> implements HttpHandler {
ErrorEvent.fromThrowable(ioException).omit().expected().handle();
}
} catch (Throwable other) {
ErrorEvent.fromThrowable(other).handle();
writeError(exchange, new BeaconServerErrorResponse(other), 500);
var event = ErrorEvent.fromThrowable(other).handle();
var link = event.getLink();
writeError(exchange, new BeaconServerErrorResponse(other, link), 500);
}
}

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

@ -6,11 +6,10 @@ import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabComp;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.base.DialogComp;
import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.storage.DataStoreEntryRef;
@ -21,16 +20,12 @@ import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.ListChangeListener;
import javafx.geometry.Pos;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import java.util.List;
import java.util.function.BiConsumer;
@ -38,65 +33,53 @@ import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class BrowserFileChooserSessionComp extends DialogComp {
public class BrowserFileChooserSessionComp extends ModalOverlayContentComp {
private final Stage stage;
private final BrowserFileChooserSessionModel model;
public BrowserFileChooserSessionComp(Stage stage, BrowserFileChooserSessionModel model) {
this.stage = stage;
public BrowserFileChooserSessionComp(BrowserFileChooserSessionModel model) {
this.model = model;
}
public static void openSingleFile(
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file, boolean save) {
PlatformThread.runLaterIfNeeded(() -> {
var lastWindow = Window.getWindows().stream()
.filter(window -> window.isFocused())
.findFirst();
var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE);
DialogComp.showWindow(save ? "saveFileTitle" : "openFileTitle", stage -> {
stage.addEventFilter(WindowEvent.WINDOW_HIDDEN, event -> {
lastWindow.ifPresent(window -> window.requestFocus());
});
var comp = new BrowserFileChooserSessionComp(stage, model);
comp.apply(struc -> struc.get().setPrefSize(1200, 700))
.styleClass("browser")
.styleClass("chooser");
return comp;
});
model.setOnFinish(fileStores -> {
file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);
});
ThreadHelper.runAsync(() -> {
model.openFileSystemAsync(store.get(), null, null);
});
var model = new BrowserFileChooserSessionModel(BrowserFileSystemTabModel.SelectionMode.SINGLE_FILE);
model.setOnFinish(fileStores -> {
file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);
});
var comp =
new BrowserFileChooserSessionComp(model).styleClass("browser").styleClass("chooser");
var selection = new SimpleStringProperty();
model.getFileSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
selection.set(
c.getList().size() > 0
? c.getList().getFirst().getRawFileEntry().getPath().toString()
: null);
});
var selectionField = new TextFieldComp(selection);
selectionField.apply(struc -> {
struc.get().setEditable(false);
AppFontSizes.base(struc.get());
});
selectionField.styleClass("chooser-selection");
selectionField.hgrow();
var modal = ModalOverlay.of(save ? "saveFileTitle" : "openFileTitle", comp);
modal.setRequireCloseButtonForClose(true);
modal.addButtonBarComp(selectionField);
modal.addButton(new ModalButton("select", () -> model.finishChooser(), true, true));
modal.show();
ThreadHelper.runAsync(() -> {
model.openFileSystemAsync(store.get(), null, null);
});
}
@Override
protected String finishKey() {
return "select";
protected void onClose() {
model.closeFileSystem();
}
@Override
protected Comp<?> pane(Comp<?> content) {
return content;
}
@Override
protected void finish() {
stage.close();
model.finishChooser();
}
@Override
protected void discard() {
model.finishWithoutChoice();
}
@Override
public Comp<?> content() {
protected Region createSimple() {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore)
&& storeEntryWrapper.getEntry().getValidity().isUsable();
@ -120,14 +103,16 @@ public class BrowserFileChooserSessionComp extends DialogComp {
});
};
var bookmarkTopBar = new BrowserConnectionListFilterComp();
var category = new SimpleObjectProperty<>(StoreViewState.get().getActiveCategory().getValue());
var filter = new SimpleStringProperty();
var bookmarkTopBar = new BrowserConnectionListFilterComp(category, filter);
var bookmarksList = new BrowserConnectionListComp(
BindingsHelper.map(
model.getSelectedEntry(), v -> v != null ? v.getEntry().get() : null),
applicable,
action,
bookmarkTopBar.getCategory(),
bookmarkTopBar.getFilter());
category,
filter);
var bookmarksContainer = new StackComp(List.of(bookmarksList)).styleClass("bookmarks-container");
bookmarksContainer
.apply(struc -> {
@ -163,33 +148,6 @@ public class BrowserFileChooserSessionComp extends DialogComp {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
});
return splitPane;
}
@Override
public Comp<?> bottom() {
return Comp.of(() -> {
var selected = new HBox();
selected.setAlignment(Pos.CENTER_LEFT);
model.getFileSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
PlatformThread.runLaterIfNeeded(() -> {
selected.getChildren()
.setAll(c.getList().stream()
.map(s -> {
var field = new TextField(
s.getRawFileEntry().getPath());
field.setEditable(false);
field.getStyleClass().add("chooser-selection");
HBox.setHgrow(field, Priority.ALWAYS);
return field;
})
.toList());
});
});
var bottomBar = new HBox(selected);
HBox.setHgrow(selected, Priority.ALWAYS);
bottomBar.setAlignment(Pos.CENTER);
return bottomBar;
});
return splitPane.createRegion();
}
}

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;
@ -65,7 +65,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
onFinish.accept(stores);
}
public void finishWithoutChoice() {
public void closeFileSystem() {
synchronized (BrowserFileChooserSessionModel.this) {
var open = selectedEntry.getValue();
if (open != null) {
@ -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

@ -6,15 +6,13 @@ import io.xpipe.app.browser.file.BrowserTransferComp;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.AnchorComp;
import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.base.StackComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.GlobalTimer;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
@ -22,12 +20,16 @@ import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.function.BiConsumer;
@ -43,7 +45,7 @@ public class BrowserFullSessionComp extends SimpleComp {
@Override
protected Region createSimple() {
var vertical = createLeftSide();
var left = Comp.of(() -> createLeftSide());
var leftSplit = new SimpleDoubleProperty();
var rightSplit = new SimpleDoubleProperty();
@ -57,7 +59,7 @@ public class BrowserFullSessionComp extends SimpleComp {
AnchorPane.setRightAnchor(struc.get(), 0.0);
});
vertical.apply(struc -> {
left.apply(struc -> {
struc.get()
.paddingProperty()
.bind(Bindings.createObjectBinding(
@ -74,7 +76,8 @@ public class BrowserFullSessionComp extends SimpleComp {
var loadingStack = new AnchorComp(List.of(tabs, pinnedStack, loadingIndicator));
loadingStack.apply(struc -> struc.get().setPickOnBounds(false));
var splitPane = new LeftSplitPaneComp(vertical, loadingStack)
var delayedStack = new DelayedInitComp(left, () -> StoreViewState.get() != null && StoreViewState.get().isInitialized());
var splitPane = new LeftSplitPaneComp(delayedStack, loadingStack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(d -> {
AppLayoutModel.get().getSavedState().setBrowserConnectionsWidth(d);
@ -104,7 +107,7 @@ public class BrowserFullSessionComp extends SimpleComp {
return r;
}
private Comp<CompStructure<VBox>> createLeftSide() {
private Region createLeftSide() {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
if (!storeEntryWrapper.getEntry().getValidity().isUsable()) {
return false;
@ -131,7 +134,10 @@ public class BrowserFullSessionComp extends SimpleComp {
});
};
var bookmarkTopBar = new BrowserConnectionListFilterComp();
var category = new SimpleObjectProperty<>(StoreViewState.get().getActiveCategory().getValue());
var filter = new SimpleStringProperty();
var bookmarkTopBar = new BrowserConnectionListFilterComp(category, filter);
var bookmarksList = new BrowserConnectionListComp(
BindingsHelper.map(
model.getSelectedEntry(),
@ -140,8 +146,8 @@ public class BrowserFullSessionComp extends SimpleComp {
: null),
applicable,
action,
bookmarkTopBar.getCategory(),
bookmarkTopBar.getFilter());
category,
filter);
var bookmarksContainer = new StackComp(List.of(bookmarksList)).styleClass("bookmarks-container");
bookmarksContainer
.apply(struc -> {
@ -166,9 +172,8 @@ public class BrowserFullSessionComp extends SimpleComp {
model.getSelectedEntry())));
localDownloadStage.prefHeight(200);
localDownloadStage.maxHeight(200);
var vertical =
new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
return vertical;
var vertical = new VerticalComp(List.of(bookmarkTopBar, bookmarksContainer, localDownloadStage)).styleClass("left");
return vertical.createRegion();
}
private StackComp createSplitStack(SimpleDoubleProperty rightSplit, BrowserSessionTabsComp tabs) {

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

@ -1,7 +1,7 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStoreColor;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
@ -34,7 +34,7 @@ public abstract class BrowserSessionTab {
public abstract String getIcon();
public abstract DataColor getColor();
public abstract DataStoreColor getColor();
public boolean isCloseable() {
return true;

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
@ -42,7 +42,7 @@ public abstract class BrowserStoreSessionTab<T extends DataStore> extends Browse
}
@Override
public DataColor getColor() {
public DataStoreColor getColor() {
return DataStorage.get().getEffectiveColor(entry.get());
}
}

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser.action;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.file.BrowserFileSystemTabModel;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.LicenseProvider;
@ -10,6 +10,7 @@ import io.xpipe.app.util.ThreadHelper;
import javafx.scene.control.Button;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Region;
@ -38,7 +39,7 @@ public interface BrowserLeafAction extends BrowserAction {
event.consume();
});
var name = getName(model, selected);
new TooltipAugment<>(name, getShortcut()).augment(b);
Tooltip.install(b, TooltipHelper.create(name, getShortcut()));
var graphic = getIcon(model, selected);
if (graphic != null) {
b.setGraphic(graphic);

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

@ -70,6 +70,7 @@ public final class BrowserConnectionListComp extends SimpleComp {
this::filter,
filter,
category,
StoreViewState.get().getEntriesListVisibilityObservable(),
StoreViewState.get().getEntriesListUpdateObservable()),
augment,
selectedAction -> {

View file

@ -14,16 +14,17 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.scene.layout.Region;
import atlantafx.base.theme.Styles;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;
@Getter
@AllArgsConstructor
public final class BrowserConnectionListFilterComp extends SimpleComp {
private final Property<StoreCategoryWrapper> category =
new SimpleObjectProperty<>(StoreViewState.get().getActiveCategory().getValue());
private final Property<String> filter = new SimpleStringProperty();
private final Property<StoreCategoryWrapper> category;
private final Property<String> filter;
@Override
protected Region createSimple() {

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;
@ -46,7 +45,6 @@ public final class BrowserFileListComp extends SimpleComp {
private final BrowserFileListModel fileList;
private final StringProperty typedSelection = new SimpleStringProperty("");
private final DoubleProperty ownerWidth = new SimpleDoubleProperty();
public BrowserFileListComp(BrowserFileListModel fileList) {
this.fileList = fileList;
@ -63,8 +61,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);
@ -73,9 +70,9 @@ public final class BrowserFileListComp extends SimpleComp {
filenameCol.setReorderable(false);
filenameCol.setResizable(false);
var sizeCol = new TableColumn<BrowserEntry, Number>();
var sizeCol = new TableColumn<BrowserEntry, String>();
sizeCol.textProperty().bind(AppI18n.observable("size"));
sizeCol.setCellValueFactory(param -> new SimpleLongProperty(
sizeCol.setCellValueFactory(param -> new ReadOnlyStringWrapper(
param.getValue().getRawFileEntry().resolved().getSize()));
sizeCol.setCellFactory(col -> new FileSizeCell());
sizeCol.setResizable(false);
@ -183,6 +180,10 @@ public final class BrowserFileListComp extends SimpleComp {
return null;
}
if (unix.getUid() == null && unix.getGid() == null && unix.getUser() == null && unix.getGroup() == null) {
return null;
}
var m = fileList.getFileSystemModel();
var user = unix.getUser() != null
? unix.getUser()
@ -487,12 +488,16 @@ public final class BrowserFileListComp extends SimpleComp {
mtimeCol.setVisible(true);
}
ownerWidth.set(fileList.getAll().getValue().stream()
var hasOwner = fileList.getAll().getValue().stream()
.map(browserEntry -> formatOwner(browserEntry))
.map(s -> s != null ? s.length() * 9 : 0)
.max(Comparator.naturalOrder())
.orElse(150));
ownerCol.setPrefWidth(ownerWidth.get());
.filter(s -> s != null)
.count() > 0;
if (hasOwner) {
ownerCol.setPrefWidth(fileList.getAll().getValue().stream().map(browserEntry -> formatOwner(browserEntry)).map(
s -> s != null ? s.length() * 9 : 0).max(Comparator.naturalOrder()).orElse(150));
} else {
ownerCol.setPrefWidth(0);
}
if (fileList.getFileSystemModel().getFileSystem() != null) {
var shell = fileList.getFileSystemModel()
@ -505,7 +510,9 @@ public final class BrowserFileListComp extends SimpleComp {
} else {
modeCol.setVisible(true);
if (table.getWidth() > 1000) {
ownerCol.setVisible(true);
ownerCol.setVisible(hasOwner);
} else if (!hasOwner) {
ownerCol.setVisible(false);
}
}
}
@ -572,19 +579,24 @@ public final class BrowserFileListComp extends SimpleComp {
}
}
private static class FileSizeCell extends TableCell<BrowserEntry, Number> {
private static class FileSizeCell extends TableCell<BrowserEntry, String> {
@Override
protected void updateItem(Number fileSize, boolean empty) {
protected void updateItem(String fileSize, boolean empty) {
super.updateItem(fileSize, empty);
if (empty || getTableRow() == null || getTableRow().getItem() == null) {
setText(null);
} else {
var path = getTableRow().getItem();
if (path.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
setText("");
} else {
setText(byteCount(fileSize.longValue()));
setText(null);
} else if (fileSize != null) {
try {
var l = Long.parseLong(fileSize);
setText(byteCount(l));
} catch (NumberFormatException e) {
setText(fileSize);
}
}
}
}

View file

@ -3,6 +3,7 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.GlobalTimer;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileKind;
@ -16,23 +17,20 @@ import javafx.scene.input.*;
import lombok.Getter;
import java.io.File;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Timer;
import java.util.TimerTask;
@Getter
public class BrowserFileListCompEntry {
public static final Timer DROP_TIMER = new Timer("dnd", true);
private final TableView<BrowserEntry> tv;
private final Node row;
private final BrowserEntry item;
private final BrowserFileListModel model;
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
private Runnable activeTask;
private ContextMenu lastContextMenu;
public BrowserFileListCompEntry(
@ -283,7 +281,7 @@ public class BrowserFileListCompEntry {
}
lastOver = (new Point2D(event.getX(), event.getY()));
activeTask = new TimerTask() {
activeTask = new Runnable() {
@Override
public void run() {
if (activeTask != this) {
@ -294,10 +292,11 @@ public class BrowserFileListCompEntry {
return;
}
model.getFileSystemModel().cdAsync(item.getRawFileEntry().getPath());
model.getFileSystemModel()
.cdAsync(item.getRawFileEntry().getPath().toString());
}
};
DROP_TIMER.schedule(activeTask, 1200);
GlobalTimer.delayAsync(activeTask, Duration.ofMillis(1200));
}
public void onDragEntered(DragEvent event) {

View file

@ -3,7 +3,8 @@ package io.xpipe.app.browser.file;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.InputHelper;
import javafx.beans.property.Property;
@ -11,6 +12,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
@ -45,8 +47,7 @@ public class BrowserFileListFilterComp extends Comp<BrowserFileListFilterComp.St
button.fire();
keyEvent.consume();
});
new TooltipAugment<>("app.search", new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN))
.augment(button);
Tooltip.install(button, TooltipHelper.create(AppI18n.observable("app.search"), new KeyCodeCombination(KeyCode.F, KeyCombination.SHORTCUT_DOWN)));
text.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue && filterString.getValue() == null) {
if (button.isFocused()) {

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

@ -82,6 +82,10 @@ class BrowserFileListNameCell extends TableCell<BrowserEntry, String> {
}
var item = getTableRow().getItem();
if (item == null) {
return false;
}
var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY;
var isParentLink = item.getRawFileEntry()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());

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;
@ -32,6 +31,10 @@ public class BrowserFileOpener {
}
var info = (FileInfo.Unix) file.getInfo();
if (info.getPermissions() == null) {
return fileSystem.openOutput(file.getPath(), totalBytes);
}
var zero = Integer.valueOf(0);
var otherWrite = info.getPermissions().charAt(7) == 'w';
var requiresRoot = zero.equals(info.getUid()) && zero.equals(info.getGid()) && !otherWrite;
@ -72,7 +75,7 @@ public class BrowserFileOpener {
var key = calculateKey(entry);
FileBridge.get()
.openIO(
FileNames.getFileName(file),
file.getFileName(),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {
@ -93,7 +96,7 @@ public class BrowserFileOpener {
var key = calculateKey(entry);
FileBridge.get()
.openIO(
FileNames.getFileName(file),
file.getFileName(),
key,
new BooleanScope(model.getBusy()).exclusive(),
() -> {
@ -119,7 +122,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;
@ -114,7 +111,7 @@ public class BrowserFileSystemHelper {
}
if (verifyExists && !model.getFileSystem().directoryExists(path)) {
throw ErrorEvent.expected(new IllegalArgumentException(String.format("Directory %s does not exist", path)));
throw ErrorEvent.expected(new IllegalArgumentException(String.format("Directory %s does not exist or is not accessible", path)));
}
try {
@ -125,12 +122,12 @@ 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,
Instant.now(),
fileSystem.getFileSize(file),
"" + fileSystem.getFileSize(file),
null,
fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.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,37 @@ 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) {
if (s == null) {
return;
}
var lastString = getCurrent();
if (cursor.get() != -1 && Objects.equals(lastString, s)) {
return;
@ -52,11 +58,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 +70,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,8 @@
package io.xpipe.app.browser.file;
import io.xpipe.app.core.AppCache;
import io.xpipe.core.store.FileNames;
import io.xpipe.app.util.GlobalTimer;
import io.xpipe.core.store.FilePath;
import io.xpipe.core.util.JacksonMapper;
import javafx.application.Platform;
@ -23,8 +24,10 @@ import lombok.*;
import lombok.extern.jackson.Jacksonized;
import java.io.IOException;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@ -35,25 +38,24 @@ import java.util.stream.Collectors;
@JsonDeserialize(using = BrowserFileSystemSavedState.Deserializer.class)
public class BrowserFileSystemSavedState {
private static final Timer TIMEOUT_TIMER = new Timer(true);
private static final int STORED = 15;
@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;
}
public BrowserFileSystemSavedState() {
lastDirectory = null;
recentDirectories = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<>(STORED)));
recentDirectories = FXCollections.observableList(new CopyOnWriteArrayList<>());
}
static BrowserFileSystemSavedState loadForStore(BrowserFileSystemTabModel model) {
@ -73,7 +75,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;
@ -83,8 +85,8 @@ public class BrowserFileSystemSavedState {
if (delay) {
// After 10 seconds
TIMEOUT_TIMER.schedule(
new TimerTask() {
GlobalTimer.delayAsync(
new Runnable() {
@Override
public void run() {
// Synchronize with platform thread
@ -100,16 +102,16 @@ public class BrowserFileSystemSavedState {
});
}
},
10000);
Duration.ofMillis(10000));
} else {
updateRecent(dir);
save();
}
}
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,10 +163,11 @@ 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.synchronizedObservableList(FXCollections.observableList(cleaned)));
.collect(Collectors.toCollection(CopyOnWriteArrayList::new));
return new BrowserFileSystemSavedState(
null, FXCollections.observableList(cleaned));
}
}
@ -173,7 +176,7 @@ public class BrowserFileSystemSavedState {
@Builder
public static class RecentEntry {
String directory;
FilePath directory;
Instant time;
}
}

View file

@ -8,13 +8,16 @@ import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
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;
import javafx.scene.control.Button;
import javafx.scene.control.MenuButton;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
@ -49,9 +52,8 @@ 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));
new TooltipAugment<>("overview", new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN))
.augment(overview);
overview.setOnAction(e -> model.cdAsync((FilePath) null));
Tooltip.install(overview, TooltipHelper.create(AppI18n.observable("overview"), new KeyCodeCombination(KeyCode.HOME, KeyCombination.ALT_DOWN)));
overview.disableProperty().bind(model.getInOverview());
overview.setAccessibleText("System overview");
InputHelper.onKeyCombination(
@ -158,14 +160,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();
});
@ -203,11 +205,13 @@ public class BrowserFileSystemTabComp extends SimpleComp {
});
var home = new BrowserOverviewComp(model).styleClass("browser-overview");
var stack = new MultiContentComp(Map.of(
home,
model.getCurrentPath().isNull(),
fileList,
model.getCurrentPath().isNull().not()), false);
var stack = new MultiContentComp(
Map.of(
home,
model.getCurrentPath().isNull(),
fileList,
model.getCurrentPath().isNull().not()),
false);
var r = stack.styleClass("browser-content-container").createRegion();
r.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {

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,12 +193,12 @@ 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;
}
return new FileEntry(fileSystem, parent, null, 0, null, FileKind.DIRECTORY);
return new FileEntry(fileSystem, parent, null, null, null, FileKind.DIRECTORY);
}
public FileEntry getCurrentDirectory() {
@ -210,7 +210,11 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
return null;
}
return new FileEntry(fileSystem, currentPath.get(), null, 0, null, FileKind.DIRECTORY);
return new FileEntry(fileSystem, currentPath.get(), null, null, null, FileKind.DIRECTORY);
}
public void cdAsync(FilePath path) {
cdAsync(path != null ? path.toString() : null);
}
public void cdAsync(String path) {
@ -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,12 @@ public final class BrowserFileSystemTabModel extends BrowserStoreSessionTab<File
startIfNeeded();
} catch (Exception ex) {
ErrorEvent.fromThrowable(ex).handle();
return Optional.ofNullable(currentPath.get());
return Optional.ofNullable(cps);
}
if (path == null) {
currentPath.set(null);
return Optional.empty();
}
// Fix common issues with paths
@ -288,12 +298,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 +337,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 +381,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 +398,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 +469,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 +481,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 +497,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 +562,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 +573,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 +583,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 +610,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,21 +257,22 @@ 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 = fileEntry.getPath().relativize(baseRelative).toUnix().toString();
flatFiles.put(fileEntry, rel);
if (fileEntry.getKind() == FileKind.FILE) {
// This one is up-to-date and does not need to be recalculated
totalSize.addAndGet(fileEntry.getSize());
// If we don't have a size, it doesn't matter that much as the total size is only for display
totalSize.addAndGet(fileEntry.getFileSizeLong().orElse(0));
}
}
} else {
@ -284,9 +282,9 @@ public class BrowserFileTransferOperation {
return;
}
flatFiles.put(source, FileNames.getFileName(source.getPath()));
// Recalculate as it could have been changed meanwhile
totalSize.addAndGet(source.getFileSystem().getFileSize(source.getPath()));
flatFiles.put(source, source.getPath().getFileName());
// If we don't have a size, it doesn't matter that much as the total size is only for display
totalSize.addAndGet(source.getFileSizeLong().orElse(0));
}
var start = Instant.now();
@ -297,10 +295,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 +326,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;
@ -353,7 +351,8 @@ public class BrowserFileTransferOperation {
}
outputStream = target.getFileSystem().openOutput(targetFile, fileSize);
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start);
transferFile(sourceFile, inputStream, outputStream, transferred, totalSize, start, fileSize);
outputStream.flush();
inputStream.transferTo(OutputStream.nullOutputStream());
} catch (Exception ex) {
// Mark progress as finished to reset any progress display
@ -412,7 +411,8 @@ public class BrowserFileTransferOperation {
OutputStream outputStream,
AtomicLong transferred,
AtomicLong total,
Instant start)
Instant start,
long expectedFileSize)
throws Exception {
// Initialize progress immediately prior to reading anything
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
@ -421,7 +421,8 @@ public class BrowserFileTransferOperation {
var exception = new AtomicReference<Exception>();
var thread = ThreadHelper.createPlatformThread("transfer", true, () -> {
try {
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, sourceFile.getSize());
long readCount = 0;
var bs = (int) Math.min(DEFAULT_BUFFER_SIZE, expectedFileSize);
byte[] buffer = new byte[bs];
int read;
while ((read = inputStream.read(buffer, 0, bs)) > 0) {
@ -437,10 +438,17 @@ public class BrowserFileTransferOperation {
outputStream.write(buffer, 0, read);
transferred.addAndGet(read);
readCount += read;
updateProgress(new BrowserTransferProgress(sourceFile.getName(), transferred.get(), total.get(), start));
}
var incomplete = readCount < expectedFileSize;
if (incomplete) {
throw new IOException("Source file " + sourceFile.getPath() + " input did end prematurely");
}
} catch (Exception ex) {
exception.set(ex);
killStreams.set(true);
}
});

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

@ -17,6 +17,7 @@ import lombok.SneakyThrows;
import lombok.Value;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@Value
@JsonDeserialize(using = BrowserHistorySavedStateImpl.Deserializer.class)
@ -26,7 +27,7 @@ public class BrowserHistorySavedStateImpl implements BrowserHistorySavedState {
ObservableList<Entry> lastSystems;
public BrowserHistorySavedStateImpl(List<Entry> lastSystems) {
this.lastSystems = FXCollections.synchronizedObservableList(FXCollections.observableArrayList(lastSystems));
this.lastSystems = FXCollections.observableList(new CopyOnWriteArrayList<>(lastSystems));
}
private static BrowserHistorySavedStateImpl INSTANCE;

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

@ -5,7 +5,7 @@ import io.xpipe.app.browser.BrowserFullSessionModel;
import io.xpipe.app.browser.BrowserSessionTab;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStoreColor;
import javafx.beans.value.ObservableValue;
@ -42,7 +42,7 @@ public final class BrowserHistoryTabModel extends BrowserSessionTab {
}
@Override
public DataColor getColor() {
public DataStoreColor getColor() {
return null;
}

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,9 +34,9 @@ public class BrowserLocalFileSystem {
return new FileEntry(
localFileSystem.open(),
file.toString(),
FilePath.of(file),
Files.getLastModifiedTime(file).toInstant(),
Files.size(file),
"" + Files.size(file),
null,
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
}

View file

@ -7,7 +7,8 @@ import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.augment.ContextMenuAugment;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.TextFieldComp;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ContextMenuHelper;
import io.xpipe.app.util.PlatformThread;
@ -46,7 +47,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
? BrowserIconManager.getFileIcon(model.getCurrentDirectory())
: null;
},
model.getCurrentPath());
PlatformThread.sync(model.getCurrentPath()));
var breadcrumbsGraphic = PrettyImageHelper.ofFixedSize(graphic, 24, 24)
.styleClass("path-graphic")
.createRegion();
@ -65,8 +66,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
historyButton.getStyleClass().add(Styles.RIGHT_PILL);
new ContextMenuAugment<>(event -> event.getButton() == MouseButton.PRIMARY, null, this::createContextMenu)
.augment(new SimpleCompStructure<>(historyButton));
new TooltipAugment<>("history", new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN))
.augment(historyButton);
Tooltip.install(historyButton, TooltipHelper.create(AppI18n.observable("history"), new KeyCodeCombination(KeyCode.H, KeyCombination.ALT_DOWN)));
var breadcrumbs = new BrowserBreadcrumbBar(model);
@ -85,7 +85,7 @@ public class BrowserNavBarComp extends Comp<BrowserNavBarComp.Structure> {
&& !model.getInOverview().get();
},
pathRegion.focusedProperty(),
model.getInOverview()));
PlatformThread.sync(model.getInOverview())));
var stack = new StackPane(pathRegion, breadcrumbsRegion);
stack.setAlignment(Pos.CENTER_LEFT);
pathRegion.prefHeightProperty().bind(stack.heightProperty());
@ -133,9 +133,11 @@ 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);
PlatformThread.runLaterIfNeeded(() -> {
path.set(newValue != null ? newValue.toString() : null);
});
});
path.addListener((observable, oldValue, newValue) -> {
ThreadHelper.runFailableAsync(() -> {
@ -202,7 +204,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 +221,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 +236,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

@ -18,7 +18,10 @@ import javafx.scene.layout.VBox;
import lombok.SneakyThrows;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class BrowserOverviewComp extends SimpleComp {
@ -38,16 +41,13 @@ public class BrowserOverviewComp extends SimpleComp {
ShellControl sc = model.getFileSystem().getShell().orElseThrow();
var commonPlatform = FXCollections.<FileEntry>synchronizedObservableList(FXCollections.observableArrayList());
var commonPlatform = FXCollections.<FileEntry>observableList(new CopyOnWriteArrayList<>());
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

@ -88,7 +88,7 @@ public class BrowserStatusBarComp extends SimpleComp {
} else {
var expected = p.expectedTimeRemaining();
var show = p.elapsedTime().compareTo(Duration.of(200, ChronoUnit.MILLIS)) > 0
&& (p.getTotal() > 50_000_000 || expected.toMillis() > 5000);
&& (!p.hasKnownTotalSize() || p.getTotal() > 50_000_000 || expected.toMillis() > 5000);
var time = show ? HumanReadableFormat.duration(p.expectedTimeRemaining()) : "";
return time;
}
@ -106,6 +106,10 @@ public class BrowserStatusBarComp extends SimpleComp {
return null;
} else {
var transferred = HumanReadableFormat.progressByteCount(p.getTransferred());
if (!p.hasKnownTotalSize()) {
return transferred;
}
var all = HumanReadableFormat.byteCount(p.getTotal());
return transferred + " / " + all;
}

View file

@ -4,13 +4,12 @@ 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;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.terminal.TerminalDockComp;
import io.xpipe.app.terminal.TerminalDockModel;
import io.xpipe.app.terminal.TerminalView;
@ -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 {
@ -177,7 +176,7 @@ public final class BrowserTerminalDockTabModel extends BrowserSessionTab {
}
@Override
public DataColor getColor() {
public DataStoreColor getColor() {
return null;
}
}

View file

@ -65,14 +65,14 @@ public class BrowserTransferComp extends SimpleComp {
return Bindings.createStringBinding(
() -> {
var p = sourceItem.get().getProgress().getValue();
var hideProgress = sourceItem
.get()
.downloadFinished()
.get();
if (!p.hasKnownTotalSize()) {
return entry.getFileName();
}
var hideProgress =
sourceItem.get().downloadFinished().get();
var share = p != null ? (p.getTransferred() * 100 / p.getTotal()) : 0;
var progressSuffix = hideProgress
? ""
: " " + share + "%";
var progressSuffix = hideProgress ? "" : " " + share + "%";
return entry.getFileName() + progressSuffix;
},
sourceItem.get().getProgress());

View file

@ -22,6 +22,10 @@ public class BrowserTransferProgress {
return transferred >= total;
}
public boolean hasKnownTotalSize() {
return total > 0;
}
public Duration elapsedTime() {
var now = Instant.now();
var elapsed = Duration.between(start, now);

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().getExtension();
return (ext != null && endings.contains("." + ext.toLowerCase(Locale.ROOT))) || endings.contains(name);
}

View file

@ -2,7 +2,7 @@ package io.xpipe.app.comp;
import io.xpipe.app.comp.augment.Augment;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.PlatformThread;
@ -12,6 +12,7 @@ import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.Separator;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@ -58,10 +59,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) {
@ -208,15 +213,24 @@ public abstract class Comp<S extends CompStructure<?>> {
}
public Comp<S> tooltip(ObservableValue<String> text) {
return apply(new TooltipAugment<>(text, null));
return apply(struc -> {
var tt = TooltipHelper.create(text, null);
Tooltip.install(struc.get(), tt);
});
}
public Comp<S> tooltipKey(String key) {
return apply(new TooltipAugment<>(key, null));
return apply(struc -> {
var tt = TooltipHelper.create(AppI18n.observable(key), null);
Tooltip.install(struc.get(), tt);
});
}
public Comp<S> tooltipKey(String key, KeyCombination shortcut) {
return apply(new TooltipAugment<>(key, shortcut));
return apply(struc -> {
var tt = TooltipHelper.create(AppI18n.observable(key), shortcut);
Tooltip.install(struc.get(), tt);
});
}
public Region createRegion() {

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

@ -26,10 +26,9 @@ import java.util.stream.Collectors;
public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
private final AppLayoutModel model = AppLayoutModel.get();
@Override
public Structure createBase() {
var model = AppLayoutModel.get();
Map<Comp<?>, ObservableValue<Boolean>> map = model.getEntries().stream()
.filter(entry -> entry.comp() != null)
.collect(Collectors.toMap(
@ -43,7 +42,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();
@ -55,7 +54,7 @@ public class AppLayoutComp extends Comp<AppLayoutComp.Structure> {
}
if (o != null && o.equals(model.getEntries().get(0))) {
StoreViewState.get().updateDisplay();
StoreViewState.get().triggerStoreListUpdate();
}
});
pane.addEventHandler(KeyEvent.KEY_PRESSED, event -> {

View file

@ -35,7 +35,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();
@ -110,6 +110,14 @@ public class AppMainWindowContentComp extends SimpleComp {
}
});
loaded.addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
Platform.runLater(() -> {
stage.requestFocus();
});
}
});
return pane;
});
var modal = new ModalOverlayStackComp(bg, overlay);

View file

@ -14,20 +14,23 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.util.StringConverter;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.Setter;
import java.util.List;
import java.util.function.Function;
@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@AllArgsConstructor
public class ChoicePaneComp extends Comp<CompStructure<VBox>> {
List<Entry> entries;
Property<Entry> selected;
Function<ComboBox<Entry>, Region> transformer = c -> c;
private final List<Entry> entries;
private final Property<Entry> selected;
@Setter
private Function<ComboBox<Entry>, Region> transformer = c -> c;
public ChoicePaneComp(List<Entry> entries, Property<Entry> selected) {
this.entries = entries;
this.selected = selected;
}
@Override
public CompStructure<VBox> createBase() {

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(
@ -105,10 +108,16 @@ public class ContextualFileReferenceChoiceComp extends Comp<CompStructure<HBox>>
}
var handler = DataStorageSyncHandler.getInstance();
var syncedTarget = handler.addDataFile(
source, target, sync.getPerUser().test(source));
var syncedTarget = handler.addDataFile(source, target, sync.getPerUser().test(source));
var pubSource = Path.of(source + ".pub");
if (Files.exists(pubSource)) {
var pubTarget = sync.getTargetLocation().apply(pubSource);
DataStorageSyncHandler.getInstance().addDataFile(pubSource, pubTarget, sync.getPerUser().test(pubSource));
}
Platform.runLater(() -> {
filePath.setValue(syncedTarget.toString());
filePath.setValue(FilePath.of(syncedTarget));
});
}
} catch (Exception e) {
@ -147,7 +156,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 +188,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

@ -0,0 +1,38 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.util.GlobalTimer;
import javafx.application.Platform;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import lombok.AllArgsConstructor;
import java.time.Duration;
import java.util.function.Supplier;
@AllArgsConstructor
public class DelayedInitComp extends SimpleComp {
private final Comp<?> comp;
private final Supplier<Boolean> condition;
@Override
protected Region createSimple() {
var stack = new StackPane();
GlobalTimer.scheduleUntil(Duration.ofMillis(10), () -> {
if (!condition.get()) {
return false;
}
Platform.runLater(() -> {
var r = comp.createRegion();
stack.getChildren().add(r);
});
return true;
});
return stack;
}
}

View file

@ -4,9 +4,7 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.window.AppWindowHelper;
import javafx.application.Platform;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
@ -15,39 +13,13 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import atlantafx.base.theme.Styles;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
public abstract class DialogComp extends Comp<CompStructure<Region>> {
public static void showWindow(String titleKey, Function<Stage, DialogComp> f) {
var loading = new SimpleBooleanProperty();
var dialog = new AtomicReference<DialogComp>();
Platform.runLater(() -> {
var stage = AppWindowHelper.sideWindow(
AppI18n.get(titleKey),
window -> {
var c = f.apply(window);
dialog.set(c);
loading.bind(c.busy());
return c;
},
false,
loading);
stage.setOnCloseRequest(event -> {
if (dialog.get() != null) {
dialog.get().discard();
}
});
stage.show();
});
}
protected Region createNavigation() {
HBox buttons = new HBox();
buttons.setFillHeight(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

@ -29,8 +29,10 @@ public class IntegratedTextAreaComp extends Comp<IntegratedTextAreaComp.Structur
public static IntegratedTextAreaComp script(
ObservableValue<DataStoreEntryRef<ShellStore>> host, Property<ShellScript> value) {
var string = new SimpleStringProperty(
value.getValue() != null ? value.getValue().getValue() : null);
var string = new SimpleStringProperty();
value.subscribe(shellScript -> {
string.set(shellScript != null ? shellScript.getValue() : null);
});
string.addListener((observable, oldValue, newValue) -> {
value.setValue(newValue != null ? new ShellScript(newValue) : null);
});

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

@ -7,14 +7,11 @@ import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.PlatformState;
import io.xpipe.app.util.PlatformThread;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
@ -172,10 +169,10 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
Node c = vbox;
do {
c.boundsInParentProperty().addListener((change, oldBounds,newBounds) -> {
c.boundsInParentProperty().addListener((change, oldBounds, newBounds) -> {
dirty.set(true);
});
// Don't listen to root node changes, that seemingly can cause exceptions
// Don't listen to root node changes, we don't need that
} while ((c = c.getParent()) != null && c.getParent() != null);
if (newValue != null) {
@ -226,6 +223,10 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
}
private void updateVisibilities(ScrollPane scroll, VBox vbox) {
if (!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Not in FxApplication thread");
}
if (!visibilityControl) {
return;
}
@ -251,6 +252,10 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
Map<T, Region> cache,
boolean refreshVisibilities) {
Runnable update = () -> {
if (!Platform.isFxApplicationThread()) {
throw new IllegalStateException("Not in FxApplication thread");
}
synchronized (cache) {
var set = new HashSet<T>();
// These lists might diverge on updates, so add both
@ -269,22 +274,25 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
synchronized (shown) {
shownCopy = new ArrayList<>(shown);
}
List<Region> newShown = shownCopy.stream().map(v -> {
if (!cache.containsKey(v)) {
var comp = compFunction.apply(v);
if (comp != null) {
var r = comp.createRegion();
if (visibilityControl) {
r.setVisible(false);
List<Region> newShown = shownCopy.stream()
.map(v -> {
if (!cache.containsKey(v)) {
var comp = compFunction.apply(v);
if (comp != null) {
var r = comp.createRegion();
if (visibilityControl) {
r.setVisible(false);
}
cache.put(v, r);
} else {
cache.put(v, null);
}
}
cache.put(v, r);
} else {
cache.put(v, null);
}
}
return cache.get(v);
}).filter(region -> region != null).toList();
return cache.get(v);
})
.filter(region -> region != null)
.toList();
if (listView.getChildren().equals(newShown)) {
return;

View file

@ -53,7 +53,7 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
private Path getHtmlFile(String markdown) {
if (TEMP == null) {
TEMP = ShellTemp.getLocalTempDataDirectory("wv");
TEMP = ShellTemp.getLocalTempDataDirectory("webview");
}
if (markdown == null) {

View file

@ -1,9 +1,12 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.window.AppDialog;
import io.xpipe.app.util.LabelGraphic;
import javafx.beans.value.ObservableValue;
import lombok.*;
import lombok.experimental.NonFinal;
@ -24,7 +27,7 @@ public class ModalOverlay {
}
public static ModalOverlay of(String titleKey, Comp<?> content, LabelGraphic graphic) {
return new ModalOverlay(titleKey, content, graphic, new ArrayList<>(), false);
return new ModalOverlay(titleKey, content, graphic, new ArrayList<>(), true, false, null);
}
public ModalOverlay withDefaultButtons(Runnable action) {
@ -45,27 +48,47 @@ public class ModalOverlay {
List<Object> buttons;
@NonFinal
boolean persistent;
@Setter
boolean hasCloseButton;
@NonFinal
@Setter
boolean requireCloseButtonForClose;
@NonFinal
@Setter
Runnable hideAction;
public ModalButton addButton(ModalButton button) {
buttons.add(button);
return button;
}
public void hideable(ObservableValue<String> name, LabelGraphic icon, Runnable action) {
setHideAction(() -> {
AppLayoutModel.get().getQueueEntries().add(new AppLayoutModel.QueueEntry(name, icon, action));
});
}
public void addButtonBarComp(Comp<?> comp) {
buttons.add(comp);
}
public void persist() {
persistent = true;
this.hasCloseButton = false;
this.requireCloseButtonForClose = true;
}
public void show() {
AppDialog.show(this, false);
}
public void hide() {
AppDialog.hide(this);
}
public boolean isShowing() {
return AppDialog.getModalOverlay().contains(this);
return AppDialog.getModalOverlays().contains(this);
}
public void showAndWait() {

View file

@ -1,5 +1,6 @@
package io.xpipe.app.comp.base;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.core.AppFontSizes;
@ -14,13 +15,13 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.Property;
import javafx.beans.value.ObservableDoubleValue;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBar;
import javafx.scene.control.Label;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
@ -45,7 +46,8 @@ public class ModalOverlayComp extends SimpleComp {
protected Region createSimple() {
var bgRegion = background.createRegion();
var modal = new ModalPane();
modal.setInTransitionFactory(null);
modal.setInTransitionFactory(
OsType.getLocal() == OsType.LINUX ? null : node -> Animations.fadeIn(node, Duration.millis(150)));
modal.setOutTransitionFactory(
OsType.getLocal() == OsType.LINUX ? null : node -> Animations.fadeOut(node, Duration.millis(50)));
modal.focusedProperty().addListener((observable, oldValue, newValue) -> {
@ -86,7 +88,7 @@ public class ModalOverlayComp extends SimpleComp {
}
});
modal.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
modal.addEventHandler(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ENTER) {
var ov = overlayContent.getValue();
if (ov != null) {
@ -150,9 +152,9 @@ public class ModalOverlayComp extends SimpleComp {
private void showModalBox(ModalPane modal, ModalOverlay overlay) {
var modalBox = toBox(modal, overlay);
modal.setPersistent(overlay.isPersistent());
modal.setPersistent(overlay.isRequireCloseButtonForClose());
modal.show(modalBox);
if (overlay.isPersistent() || overlay.getTitleKey() == null) {
if (!overlay.isHasCloseButton() || overlay.getTitleKey() == null) {
var closeButton = modalBox.lookup(".close-button");
if (closeButton != null) {
closeButton.setVisible(false);
@ -164,13 +166,13 @@ public class ModalOverlayComp extends SimpleComp {
Region r = newValue.getContent().createRegion();
var content = new VBox(r);
content.getStyleClass().add("content");
content.focusedProperty().addListener((o, old, n) -> {
if (n) {
r.requestFocus();
}
});
content.setSpacing(25);
content.setPadding(new Insets(13, 27, 20, 27));
content.setSpacing(20);
if (newValue.getTitleKey() != null) {
var l = new Label(
@ -184,20 +186,22 @@ public class ModalOverlayComp extends SimpleComp {
}
if (newValue.getButtons().size() > 0) {
var buttonBar = new ButtonBar();
var buttonBar = new HBox();
buttonBar.getStyleClass().add("button-bar");
buttonBar.setSpacing(10);
buttonBar.setAlignment(Pos.CENTER_RIGHT);
for (var o : newValue.getButtons()) {
var node = o instanceof ModalButton mb ? toButton(mb) : ((Comp<?>) o).createRegion();
buttonBar.getButtons().add(node);
ButtonBar.setButtonUniformSize(node, o instanceof ModalButton);
buttonBar.getChildren().add(node);
if (o instanceof ModalButton) {
node.prefHeightProperty().bind(buttonBar.heightProperty());
}
}
content.getChildren().add(buttonBar);
AppFontSizes.xs(buttonBar);
AppFontSizes.sm(buttonBar);
}
var modalBox = new ModalBox(content) {
var modalBox = new ModalBox(pane, content) {
@Override
protected void setCloseButtonPosition() {
@ -205,18 +209,23 @@ public class ModalOverlayComp extends SimpleComp {
setRightAnchor(closeButton, 19d);
}
};
if (newValue.getHideAction() != null) {
modalBox.setOnMinimize(event -> {
newValue.getHideAction().run();
event.consume();
});
}
modalBox.setOnClose(event -> {
overlayContent.setValue(null);
event.consume();
});
r.maxHeightProperty().bind(pane.heightProperty().subtract(200));
content.maxHeightProperty().bind(pane.heightProperty().subtract(40));
modalBox.minHeightProperty().bind(content.heightProperty());
content.prefWidthProperty().bind(modalBox.widthProperty());
modalBox.setMinWidth(100);
modalBox.setMinHeight(100);
modalBox.prefWidthProperty().bind(modalBoxWidth(pane, r));
modalBox.maxWidthProperty().bind(modalBox.prefWidthProperty());
modalBox.prefHeightProperty().bind(modalBoxHeight(pane, content));
modalBox.setMaxHeight(Region.USE_PREF_SIZE);
modalBox.focusedProperty().addListener((o, old, n) -> {
if (n) {
@ -228,14 +237,6 @@ public class ModalOverlayComp extends SimpleComp {
var busy = mocc.busy();
if (busy != null) {
var loading = LoadingOverlayComp.noProgress(Comp.of(() -> modalBox), busy);
// loading.apply(struc -> {
// var bg = struc.get().getChildren().getFirst();
// struc.get().getChildren().get(1).addEventFilter(MouseEvent.MOUSE_PRESSED, event ->
// {
// bg.fireEvent(event);
// event.consume();
// });
// });
return loading.createRegion();
}
}
@ -256,23 +257,6 @@ public class ModalOverlayComp extends SimpleComp {
r.prefWidthProperty());
}
private ObservableDoubleValue modalBoxHeight(ModalPane pane, Region content) {
return Bindings.createDoubleBinding(
() -> {
var max = pane.getHeight() - 20;
if (content.getPrefHeight() != Region.USE_COMPUTED_SIZE) {
return Math.min(max, content.getPrefHeight());
}
return Math.min(max, content.getHeight());
},
pane.heightProperty(),
pane.prefHeightProperty(),
content.prefHeightProperty(),
content.heightProperty(),
content.maxHeightProperty());
}
private Button toButton(ModalButton mb) {
var button = new Button(mb.getKey() != null ? AppI18n.get(mb.getKey()) : null);
if (mb.isDefaultButton()) {
@ -281,6 +265,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();
@ -292,19 +277,4 @@ public class ModalOverlayComp extends SimpleComp {
});
return button;
}
private Timeline fadeInDelyed(Node node) {
var t = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(node.opacityProperty(), 0.01)),
new KeyFrame(Duration.millis(50), new KeyValue(node.opacityProperty(), 0.01, Animations.EASE)),
new KeyFrame(Duration.millis(1250), new KeyValue(node.opacityProperty(), 1, Animations.EASE)));
t.statusProperty().addListener((obs, old, val) -> {
if (val == Animation.Status.STOPPED) {
node.setOpacity(1);
}
});
return t;
}
}

View file

@ -4,20 +4,24 @@ import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.*;
import atlantafx.base.controls.Popover;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles;
import javafx.util.Duration;
import lombok.Getter;
import java.util.ArrayList;
@ -26,9 +30,9 @@ import java.util.List;
@Getter
public class OptionsComp extends Comp<CompStructure<Pane>> {
private final List<OptionsComp.Entry> entries;
private final List<Entry> entries;
public OptionsComp(List<OptionsComp.Entry> entries) {
public OptionsComp(List<Entry> entries) {
this.entries = entries;
}
@ -84,15 +88,19 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
description.managedProperty().bind(PlatformThread.sync(compRegion.managedProperty()));
}
if (entry.longDescriptionSource() != null) {
var markDown = new MarkdownComp(entry.longDescriptionSource(), s -> s, true)
.apply(struc -> struc.get().setMaxWidth(500))
.apply(struc -> struc.get().setMaxHeight(400));
var popover = new Popover(markDown.createRegion());
popover.setCloseButtonEnabled(false);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
AppFontSizes.xs(popover.getContentNode());
if (entry.longDescription() != null) {
Popover popover;
if (!entry.longDescription().startsWith("http")) {
var markDown = new MarkdownComp(entry.longDescription(), s -> s, true).apply(struc -> struc.get().setMaxWidth(500)).apply(
struc -> struc.get().setMaxHeight(400));
popover = new Popover(markDown.createRegion());
popover.setCloseButtonEnabled(false);
popover.setHeaderAlwaysVisible(false);
popover.setDetachable(true);
AppFontSizes.xs(popover.getContentNode());
} else {
popover = null;
}
var extendedDescription = new Button("... ?");
extendedDescription.setMinWidth(Region.USE_PREF_SIZE);
@ -102,10 +110,20 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
extendedDescription.setAccessibleText("Help");
AppFontSizes.xl(extendedDescription);
extendedDescription.setOnAction(e -> {
popover.show(extendedDescription);
if (entry.longDescription().startsWith("http")) {
Hyperlinks.open(entry.longDescription());
} else if (popover != null) {
popover.show(extendedDescription);
}
e.consume();
});
if (entry.longDescription().startsWith("http")) {
var tt = TooltipHelper.create(new SimpleStringProperty(entry.longDescription()), null);
tt.setShowDelay(Duration.millis(1));
Tooltip.install(extendedDescription, tt);
}
var descriptionBox =
new HBox(description, new Spacer(Orientation.HORIZONTAL), extendedDescription);
descriptionBox.setSpacing(5);
@ -196,7 +214,7 @@ public class OptionsComp extends Comp<CompStructure<Pane>> {
public record Entry(
String key,
ObservableValue<String> description,
String longDescriptionSource,
String longDescription,
ObservableValue<String> name,
Comp<?> comp) {}
}

View file

@ -12,6 +12,8 @@ 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;
@ -19,23 +21,100 @@ 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(false));
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 +124,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 +134,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

@ -1,34 +1,23 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.augment.Augment;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.util.PlatformThread;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCombination;
import javafx.stage.Window;
public class TooltipAugment<S extends CompStructure<?>> implements Augment<S> {
public class TooltipHelper {
private final ObservableValue<String> text;
private final KeyCombination shortcut;
public TooltipAugment(ObservableValue<String> text, KeyCombination shortcut) {
this.text = text;
this.shortcut = shortcut;
public static Tooltip create(String text) {
return create(new SimpleStringProperty(text), null);
}
public TooltipAugment(String key, KeyCombination shortcut) {
this.text = AppI18n.observable(key);
this.shortcut = shortcut;
}
@Override
public void augment(S struc) {
public static Tooltip create(ObservableValue<String> text, KeyCombination shortcut) {
var tt = new FixedTooltip();
if (shortcut != null) {
var s = AppI18n.observable("shortcut");
@ -46,7 +35,7 @@ public class TooltipAugment<S extends CompStructure<?>> implements Augment<S> {
tt.setWrapText(true);
tt.setMaxWidth(400);
tt.getStyleClass().add("fancy-tooltip");
Tooltip.install(struc.get(), tt);
return tt;
}
private static class FixedTooltip extends Tooltip {

View file

@ -3,8 +3,8 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.util.PlatformThread;
import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
@ -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

@ -82,8 +82,8 @@ public class OsLogoComp extends SimpleComp {
}
return ICONS.entrySet().stream()
.filter(e -> name.toLowerCase().contains(e.getKey()) ||
name.toLowerCase().replaceAll("\\s+", "").contains(e.getKey()))
.filter(e -> name.toLowerCase().contains(e.getKey())
|| name.toLowerCase().replaceAll("\\s+", "").contains(e.getKey()))
.findAny()
.map(e -> e.getValue())
.orElse("os/linux.svg");

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

@ -1,11 +1,12 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.core.AppI18n;
import javafx.geometry.Pos;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.control.Tooltip;
import javafx.scene.input.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
@ -32,7 +33,7 @@ public class StoreActiveComp extends SimpleComp {
pane.setAlignment(Pos.CENTER);
pane.visibleProperty().bind(wrapper.getSessionActive());
pane.getStyleClass().add("store-active-comp");
new TooltipAugment<>("sessionActive", null).augment(pane);
Tooltip.install(pane, TooltipHelper.create(AppI18n.observable("sessionActive"), null));
return pane;
}
}

View file

@ -7,14 +7,14 @@ import io.xpipe.app.comp.base.*;
import io.xpipe.app.core.AppFontSizes;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.storage.DataStorage;
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;
@ -70,7 +70,7 @@ public class StoreCategoryComp extends SimpleComp {
() -> {
var exp = category.getExpanded().get()
&& category.getChildren().getList().size() > 0;
return new LabelGraphic.IconGraphic(exp ? "mdi2m-menu-down-outline" : "mdi2m-menu-right-outline");
return new LabelGraphic.IconGraphic(exp ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right");
},
category.getExpanded(),
category.getChildren().getList());
@ -187,7 +187,7 @@ public class StoreCategoryComp extends SimpleComp {
});
category.getColor().subscribe((c) -> {
DataColor.applyStyleClasses(c, struc.get());
DataStoreColor.applyStyleClasses(c, struc.get());
});
});
@ -222,7 +222,7 @@ public class StoreCategoryComp extends SimpleComp {
event.consume();
});
color.getItems().add(none);
Arrays.stream(DataColor.values()).forEach(dataStoreColor -> {
Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> {
MenuItem m = new MenuItem();
m.textProperty().bind(AppI18n.observable(dataStoreColor.getId()));
m.setOnAction(event -> {

View file

@ -0,0 +1,3 @@
package io.xpipe.app.comp.store;
public class StoreCategoryConfigComp {}

View file

@ -2,7 +2,7 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.util.DerivedObservableList;
@ -35,7 +35,7 @@ public class StoreCategoryWrapper {
private final IntegerProperty shownContainedEntriesCount = new SimpleIntegerProperty();
private final IntegerProperty allContainedEntriesCount = new SimpleIntegerProperty();
private final BooleanProperty expanded = new SimpleBooleanProperty();
private final Property<DataColor> color = new SimpleObjectProperty<>();
private final Property<DataStoreColor> color = new SimpleObjectProperty<>();
private final BooleanProperty largeCategoryOptimizations = new SimpleBooleanProperty();
private StoreCategoryWrapper cachedParent;

View file

@ -100,6 +100,7 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
applicable,
filterText,
selectedCategory,
StoreViewState.get().getEntriesListVisibilityObservable(),
StoreViewState.get().getEntriesListUpdateObservable()),
(s, comp) -> {
if (!applicable.test(s.getWrapper())) {
@ -230,7 +231,8 @@ public class StoreChoiceComp<T extends DataStore> extends SimpleComp {
return;
}
selected.setValue(mode == Mode.PROXY ? DataStorage.get().local().ref() : null);
selected.setValue(
mode == Mode.PROXY ? DataStorage.get().local().ref() : null);
event.consume();
});
})

View file

@ -3,505 +3,58 @@ 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.*;
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.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.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)
public class StoreCreationComp extends DialogComp {
public class StoreCreationComp extends ModalOverlayContentComp {
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))));
public StoreCreationComp(StoreCreationModel model) {
this.model = model;
}
@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");
return model.getBusy();
}
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
&& (providerChoice.getProviders().size() > 1
|| providerChoice.getProviders().getFirst().showProviderChoice()))
|| (staticDisplay && provider.getValue().showProviderChoice());
if (staticDisplay) {
var providerChoice = new StoreProviderChoiceComp(model.getFilter(), model.getProvider());
var provider = model.getProvider().getValue() != null ? model.getProvider().getValue() : providerChoice.getProviders().getFirst();
var showProviders = (!model.isStaticDisplay() && provider.showProviderChoice())
|| (model.isStaticDisplay() && provider.showProviderChoice());
if (model.isStaticDisplay()) {
providerChoice.apply(struc -> struc.get().setDisable(true));
}
if (showProviders) {
@ -509,21 +62,27 @@ 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(
d != null && d.getValidator() != null ? d.getValidator() : new SimpleValidator(), propVal)));
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 +92,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,8 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.storage.DataStoreEntry;
public interface StoreCreationConsumer {
void consume(DataStoreEntry entry, boolean validated);
}

View file

@ -0,0 +1,205 @@
package io.xpipe.app.comp.store;
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.AppLayoutModel;
import io.xpipe.app.core.window.AppDialog;
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.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
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> c) {
StoreCreationConsumer consumer = (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().triggerStoreListUpdate();
if (e.getProvider().shouldShowScan()
&& AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation()
.get()) {
ScanDialog.showAsync(e);
}
}
}
}
c.accept(e);
});
};
show(e.getName(), e.getProvider(), e.getStore(), v -> true, consumer, 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;
StoreCreationConsumer consumer = (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();
}
};
show(
null,
prov,
base,
dataStoreProvider -> (category != null && category.equals(dataStoreProvider.getCreationCategory()))
|| dataStoreProvider.equals(prov),
consumer,
false,
null);
}
private static void show(
String initialName,
DataStoreProvider provider,
DataStore s,
Predicate<DataStoreProvider> filter,
StoreCreationConsumer 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, con);
var modal = createModalOverlay(model);
modal.show();
}
private static boolean showInvalidConfirmAlert() {
var skipped = new SimpleBooleanProperty();
var modal = ModalOverlay.of("confirmInvalidStoreTitle", AppDialog.dialogTextKey("confirmInvalidStoreContent"));
modal.addButton(new ModalButton("retry", null, true, false));
modal.addButton(new ModalButton("skip", () -> skipped.set(true), true, true));
modal.showAndWait();
return skipped.get();
}
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);
var provider = model.getProvider().getValue();
var graphic = provider != null
&& provider.getDisplayIconFileName(model.getStore().get()) != null
? new LabelGraphic.ImageGraphic(
provider.getDisplayIconFileName(model.getStore().get()), 20)
: new LabelGraphic.IconGraphic("mdi2b-beaker-plus-outline");
modal.hideable(AppI18n.observable(model.storeTypeNameKey() + "Add"), graphic, () -> {
modal.show();
});
AppLayoutModel.get().getSelected().addListener((observable, oldValue, newValue) -> {
if (model.getFinished().get() || !modal.isShowing()) {
return;
}
modal.hide();
AppLayoutModel.get().getQueueEntries().add(new AppLayoutModel.QueueEntry(AppI18n.observable(model.storeTypeNameKey() + "Add"), graphic, () -> {
modal.show();
}));
});
modal.setRequireCloseButtonForClose(true);
modal.addButton(new ModalButton(
"docs",
() -> {
model.showDocs();
},
false,
false)
.augment(button -> {
button.visibleProperty().bind(Bindings.not(model.canShowDocs()));
}));
modal.addButton(new ModalButton(
"connect",
() -> {
model.connect();
},
false,
false)
.augment(button -> {
button.visibleProperty().bind(Bindings.not(model.canConnect()));
}));
modal.addButton(new ModalButton(
"skip",
() -> {
if (showInvalidConfirmAlert()) {
model.commit(false);
modal.close();
} else {
model.finish();
}
},
false,
false))
.augment(button -> {
button.visibleProperty().bind(model.getSkippable());
});
modal.addButton(new ModalButton(
"finish",
() -> {
model.finish();
},
false,
true));
model.getFinished().addListener((obs, oldValue, newValue) -> {
modal.close();
});
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,264 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.issue.ErrorEvent;
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.scene.control.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;
import java.util.UUID;
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;
StoreCreationConsumer consumer;
public StoreCreationModel(
Property<DataStoreProvider> provider,
ObjectProperty<DataStore> store,
Predicate<DataStoreProvider> filter,
String initialName,
DataStoreEntry existingEntry,
boolean staticDisplay,
StoreCreationConsumer 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.consumer = consumer;
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();
});
}
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();
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(true);
} 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(boolean validated) {
if (finished.get()) {
return;
}
finished.setValue(true);
consumer.consume(entry.getValue(), validated);
}
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

@ -8,12 +8,11 @@ import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.LabelComp;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.core.*;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.resources.AppResources;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.*;
@ -88,6 +87,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 +105,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 +119,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) {
@ -260,7 +262,7 @@ public abstract class StoreEntryComp extends SimpleComp {
}));
}
button.accessibleText(cs.getName(getWrapper().getEntry().ref()).getValue());
button.apply(new TooltipAugment<>(cs.getName(getWrapper().getEntry().ref()), null));
button.tooltip(cs.getName(getWrapper().getEntry().ref()));
return button;
}
@ -276,6 +278,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();
@ -332,7 +340,7 @@ public abstract class StoreEntryComp extends SimpleComp {
event.consume();
});
color.getItems().add(none);
Arrays.stream(DataColor.values()).forEach(dataStoreColor -> {
Arrays.stream(DataStoreColor.values()).forEach(dataStoreColor -> {
MenuItem m = new MenuItem();
m.textProperty().bind(AppI18n.observable(dataStoreColor.getId()));
m.setOnAction(event -> {
@ -429,8 +437,8 @@ public abstract class StoreEntryComp extends SimpleComp {
var name = cs.getName(getWrapper().getEntry().ref());
var icon = cs.getIcon(getWrapper().getEntry().ref());
var item = (leaf != null && leaf.canLinkTo()) || branch != null
? new Menu(null, new FontIcon(icon))
: new MenuItem(null, new FontIcon(icon));
? new Menu(null, icon.createGraphicNode())
: new MenuItem(null, icon.createGraphicNode());
var proRequired = p.getProFeatureId() != null
&& !LicenseProvider.get().getFeature(p.getProFeatureId()).isSupported();

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,9 +13,8 @@ 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.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
@ -26,6 +25,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 +91,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 +130,21 @@ 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.styleClass("batch-mode-button");
b.apply(struc -> {
batchMode.subscribe(a -> {
struc.get().pseudoClassStateChanged(PseudoClass.getPseudoClass("active"), a);
});
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

@ -1,10 +1,11 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.ext.ActionProvider;
import io.xpipe.app.ext.LocalStore;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
@ -42,7 +43,7 @@ public class StoreEntryWrapper {
private final BooleanProperty expanded = new SimpleBooleanProperty();
private final Property<Object> persistentState = new SimpleObjectProperty<>();
private final Property<Map<String, Object>> cache = new SimpleObjectProperty<>(Map.of());
private final Property<DataColor> color = new SimpleObjectProperty<>();
private final Property<DataStoreColor> color = new SimpleObjectProperty<>();
private final Property<StoreCategoryWrapper> category = new SimpleObjectProperty<>();
private final Property<String> summary = new SimpleObjectProperty<>();
private final Property<StoreNotes> notes;
@ -120,7 +121,7 @@ public class StoreEntryWrapper {
}
public void editDialog() {
StoreCreationComp.showEdit(entry);
StoreCreationDialog.showEdit(entry);
}
public void delete() {
@ -201,7 +202,7 @@ public class StoreEntryWrapper {
customIcon.setValue(entry.getIcon());
iconFile.setValue(entry.getEffectiveIconFile());
busy.setValue(entry.getBusyCounter().get() != 0);
deletable.setValue(entry.getConfiguration().isDeletable());
deletable.setValue(!(entry.getStore() instanceof LocalStore));
sessionActive.setValue(entry.getStore() instanceof SingletonSessionStore<?> ss
&& entry.getStore() instanceof ShellStore
&& ss.isSessionRunning());

View file

@ -11,7 +11,6 @@ import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.control.*;
@ -109,20 +108,20 @@ public class StoreIconChoiceComp extends SimpleComp {
private void updateData(TableView<List<SystemIcon>> table, String filterString) {
var available = icons.stream()
.filter(systemIcon -> AppImages.hasNormalImage("icons/" + systemIcon.getSource().getId() + "/" + systemIcon.getId() + "-40.png"))
.filter(systemIcon -> AppImages.hasNormalImage(
"icons/" + systemIcon.getSource().getId() + "/" + systemIcon.getId() + "-40.png"))
.sorted(Comparator.comparing(systemIcon -> systemIcon.getId()))
.toList();
table.getPlaceholder().setVisible(available.size() == 0);
var filtered = available;
if (filterString != null && !filterString.isBlank() && filterString.length() >= 2) {
filtered = available.stream().filter(icon -> containsString(icon.getId(), filterString)).toList();
filtered = available.stream()
.filter(icon -> containsString(icon.getId(), filterString))
.toList();
}
var data = partitionList(filtered, columns);
table.getItems().setAll(data);
var selectMatch = filtered.size() == 1 || filtered.stream().anyMatch(systemIcon -> systemIcon.getId().equals(filterString));
// Table updates seem to not always be instant, sometimes the column is not there yet
if (selectMatch && table.getColumns().size() > 0) {
if (filtered.size() == 1) {
table.getSelectionModel().select(0, table.getColumns().getFirst());
selected.setValue(filtered.getFirst());
} else {

View file

@ -34,7 +34,7 @@ public class StoreIconChoiceDialog {
private ModalOverlay createOverlay() {
var filterText = new SimpleStringProperty();
var filter = new FilterComp(filterText).grow(true, false);
var filter = new FilterComp(filterText).hgrow();
filter.focusOnShow();
var github = new ButtonComp(null, new FontIcon("mdomz-settings"), () -> {
overlay.close();

View file

@ -2,12 +2,13 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.PrettyImageHelper;
import io.xpipe.app.comp.base.TooltipAugment;
import io.xpipe.app.comp.base.TooltipHelper;
import io.xpipe.app.storage.DataStoreEntry;
import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.control.Tooltip;
import javafx.scene.input.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
@ -26,7 +27,7 @@ public class StoreIconComp extends SimpleComp {
var imageComp = PrettyImageHelper.ofFixedSize(wrapper.getIconFile(), w, h);
var storeIcon = imageComp.createRegion();
if (wrapper.getValidity().getValue().isUsable()) {
new TooltipAugment<>(wrapper.getEntry().getProvider().displayName(), null).augment(storeIcon);
Tooltip.install(storeIcon, TooltipHelper.create(wrapper.getEntry().getProvider().displayName(), null));
}
var background = new Region();
@ -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

@ -1,34 +1,48 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.SimpleComp;
import io.xpipe.app.comp.base.DelayedInitComp;
import io.xpipe.app.comp.base.LeftSplitPaneComp;
import io.xpipe.app.core.AppActionLinkDetector;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.util.GlobalTimer;
import io.xpipe.app.util.InputHelper;
import javafx.application.Platform;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.time.Duration;
public class StoreLayoutComp extends SimpleComp {
@Override
protected Region createSimple() {
var struc = new LeftSplitPaneComp(new StoreSidebarComp(), new StoreEntryListComp())
var delayed = new DelayedInitComp(Comp.of(() -> createContent()), () -> StoreViewState.get() != null && StoreViewState.get().isInitialized());
return delayed.createRegion();
}
private Region createContent() {
var left = new StoreSidebarComp();
left.minWidth(270);
left.maxWidth(500);
var comp = new LeftSplitPaneComp(left, new StoreEntryListComp())
.withInitialWidth(AppLayoutModel.get().getSavedState().getSidebarWidth())
.withOnDividerChange(aDouble -> {
AppLayoutModel.get().getSavedState().setSidebarWidth(aDouble);
})
.createStructure();
struc.getLeft().setMinWidth(260);
struc.getLeft().setMaxWidth(500);
struc.get().getStyleClass().add("store-layout");
InputHelper.onKeyCombination(
struc.get(), new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> {
AppActionLinkDetector.detectOnPaste();
keyEvent.consume();
});
return struc.get();
comp.styleClass("store-layout");
comp.apply(struc -> {
InputHelper.onKeyCombination(
struc.get(), new KeyCodeCombination(KeyCode.V, KeyCombination.SHORTCUT_DOWN), true, keyEvent -> {
AppActionLinkDetector.detectOnPaste();
keyEvent.consume();
});
});
return comp.createRegion();
}
}

View file

@ -6,6 +6,7 @@ import io.xpipe.app.comp.base.*;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Insets;
@ -67,7 +68,7 @@ public class StoreListChoiceComp<T extends DataStore> extends SimpleComp {
selected.setValue(null);
}
});
var vbox = new VerticalComp(List.of(list, Comp.vspacer(5), add))
var vbox = new VerticalComp(List.of(list, Comp.vspacer(5).hide(Bindings.isEmpty(selectedList)), add))
.apply(struc -> struc.get().setFillWidth(true));
return vbox.styleClass("data-store-list-choice-comp").createRegion();
}

View file

@ -110,6 +110,7 @@ public class StoreSection {
Predicate<StoreEntryWrapper> entryFilter,
ObservableValue<String> filterString,
ObservableValue<StoreCategoryWrapper> category,
ObservableIntegerValue visibilityObservable,
ObservableIntegerValue updateObservable) {
var topLevel = all.filtered(
section -> {
@ -118,8 +119,16 @@ public class StoreSection {
},
category,
updateObservable);
var cached = topLevel.mapped(storeEntryWrapper ->
create(List.of(), storeEntryWrapper, 1, all, entryFilter, filterString, category, updateObservable));
var cached = topLevel.mapped(storeEntryWrapper -> create(
List.of(),
storeEntryWrapper,
1,
all,
entryFilter,
filterString,
category,
visibilityObservable,
updateObservable));
var ordered = sorted(cached, category, updateObservable);
var shown = ordered.filtered(
section -> {
@ -146,6 +155,7 @@ public class StoreSection {
Predicate<StoreEntryWrapper> entryFilter,
ObservableValue<String> filterString,
ObservableValue<StoreCategoryWrapper> category,
ObservableIntegerValue visibilityObservable,
ObservableIntegerValue updateObservable) {
if (e.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
return new StoreSection(
@ -169,43 +179,66 @@ public class StoreSection {
return false;
}
var showProvider = true;
try {
showProvider = other.getEntry().getProvider().shouldShow(other);
} catch (Exception ignored) {
}
return showProvider;
return true;
},
e.getPersistentState(),
e.getCache(),
visibilityObservable,
updateObservable);
var l = new ArrayList<>(parents);
l.add(e);
var cached = allChildren.mapped(
c -> create(l, c, depth + 1, all, entryFilter, filterString, category, updateObservable));
var cached = allChildren.mapped(c -> create(
l, c, depth + 1, all, entryFilter, filterString, category, visibilityObservable, updateObservable));
var ordered = sorted(cached, category, updateObservable);
var filtered = ordered.filtered(
section -> {
// matches filter
return (filterString == null
|| section.matchesFilter(filterString.getValue())
|| l.stream().anyMatch(p -> p.matchesFilter(filterString.getValue())))
&&
// matches selector
section.anyMatches(entryFilter)
&&
// matches category
// Prevent updates for children on category switching by checking depth
(showInCategory(category.getValue(), section.getWrapper()) || depth > 0)
&&
// not root
// If this entry is already shown as root due to a different category than parent, don't
// show it
// again here
!DataStorage.get()
.isRootEntry(
section.getWrapper().getEntry(),
category.getValue().getCategory());
var matchesFilter = filterString == null
|| section.matchesFilter(filterString.getValue())
|| l.stream().anyMatch(p -> p.matchesFilter(filterString.getValue()));
if (!matchesFilter) {
return false;
}
var hasFilter = filterString != null
&& filterString.getValue() != null
&& filterString.getValue().length() > 0;
if (!hasFilter) {
var showProvider = true;
try {
showProvider = section.getWrapper()
.getEntry()
.getProvider()
.shouldShow(section.getWrapper());
} catch (Exception ignored) {
}
if (!showProvider) {
return false;
}
}
var matchesSelector = section.anyMatches(entryFilter);
if (!matchesSelector) {
return false;
}
// Prevent updates for children on category switching by checking depth
var showCategory = showInCategory(category.getValue(), section.getWrapper()) || depth > 0;
if (!showCategory) {
return false;
}
// If this entry is already shown as root due to a different category than parent, don't
// show it
// again here
var notRoot = !DataStorage.get()
.isRootEntry(
section.getWrapper().getEntry(),
category.getValue().getCategory());
if (!notRoot) {
return false;
}
return true;
},
category,
filterString,

View file

@ -2,35 +2,22 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.util.BindingsHelper;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
@ -50,13 +37,15 @@ public abstract class StoreSectionBaseComp extends Comp<CompStructure<VBox>> {
}
protected ObservableBooleanValue effectiveExpanded(ObservableBooleanValue expanded) {
return section.getWrapper() != null ? Bindings.createBooleanBinding(
return section.getWrapper() != null
? Bindings.createBooleanBinding(
() -> {
return expanded.get()
&& section.getShownChildren().getList().size() > 0;
},
expanded,
section.getShownChildren().getList()) : new SimpleBooleanProperty(true);
section.getShownChildren().getList())
: new SimpleBooleanProperty(true);
}
protected void addPseudoClassListeners(VBox vbox, ObservableBooleanValue expanded) {
@ -76,7 +65,8 @@ public abstract class StoreSectionBaseComp extends Comp<CompStructure<VBox>> {
if (section.getDepth() == 1) {
section.getWrapper().getColor().subscribe(val -> {
var newList = new ArrayList<>(vbox.getStyleClass());
newList.removeIf(s -> Arrays.stream(DataColor.values()).anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s)));
newList.removeIf(s -> Arrays.stream(DataStoreColor.values())
.anyMatch(dataStoreColor -> dataStoreColor.getId().equals(s)));
newList.remove("gray");
newList.add("color-box");
if (val != null) {
@ -106,7 +96,8 @@ public abstract class StoreSectionBaseComp extends Comp<CompStructure<VBox>> {
});
}
protected ListBoxViewComp<StoreSection> createChildrenList(Function<StoreSection, Comp<?>> function, ObservableBooleanValue hide) {
protected ListBoxViewComp<StoreSection> createChildrenList(
Function<StoreSection, Comp<?>> function, ObservableBooleanValue hide) {
var content = new ListBoxViewComp<>(
section.getShownChildren().getList(),
section.getAllChildren().getList(),
@ -122,13 +113,16 @@ public abstract class StoreSectionBaseComp extends Comp<CompStructure<VBox>> {
return content;
}
protected Comp<CompStructure<Button>> createExpandButton(Runnable action, int width, ObservableBooleanValue expanded) {
var icon = Bindings.createObjectBinding(() -> new LabelGraphic.IconGraphic(
expanded.get() && section.getShownChildren().getList().size() > 0 ?
"mdal-keyboard_arrow_down" :
"mdal-keyboard_arrow_right"), expanded, section.getShownChildren().getList());
var expandButton = new IconButtonComp(icon,
action);
protected Comp<CompStructure<Button>> createExpandButton(
Runnable action, int width, ObservableBooleanValue expanded) {
var icon = Bindings.createObjectBinding(
() -> new LabelGraphic.IconGraphic(
expanded.get() && section.getShownChildren().getList().size() > 0
? "mdal-keyboard_arrow_down"
: "mdal-keyboard_arrow_right"),
expanded,
section.getShownChildren().getList());
var expandButton = new IconButtonComp(icon, action);
expandButton
.minWidth(width)
.prefWidth(width)

View file

@ -2,18 +2,11 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.augment.GrowAugment;
import io.xpipe.app.comp.base.HorizontalComp;
import io.xpipe.app.comp.base.IconButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.VerticalComp;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.util.LabelGraphic;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
@ -23,9 +16,7 @@ import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
public class StoreSectionComp extends StoreSectionBaseComp {
@ -44,7 +35,8 @@ public class StoreSectionComp extends StoreSectionBaseComp {
event.consume();
}
if (event.getCode() == KeyCode.RIGHT) {
var ref = (VBox) ((HBox) struc.get().getParent()).getChildren().getFirst();
var ref = (VBox)
((HBox) struc.get().getParent()).getChildren().getFirst();
if (entryButton.isFullSize()) {
var btn = (Button) ref.getChildren().getFirst();
btn.fire();
@ -63,7 +55,10 @@ public class StoreSectionComp extends StoreSectionBaseComp {
quickAccessButton.focusTraversableForAccessibility();
quickAccessButton.tooltipKey("accessSubConnections", new KeyCodeCombination(KeyCode.RIGHT));
var expandButton = createExpandButton(() -> section.getWrapper().toggleExpanded(), 30, section.getWrapper().getExpanded());
var expandButton = createExpandButton(
() -> section.getWrapper().toggleExpanded(),
30,
section.getWrapper().getExpanded());
expandButton.vgrow();
expandButton.focusTraversableForAccessibility();
expandButton.tooltipKey("expand", new KeyCodeCombination(KeyCode.SPACE));
@ -82,17 +77,15 @@ public class StoreSectionComp extends StoreSectionBaseComp {
var effectiveExpanded = effectiveExpanded(section.getWrapper().getExpanded());
var content = createChildrenList(c -> StoreSection.customSection(c), Bindings.not(effectiveExpanded));
var full = new VerticalComp(List.of(
topEntryList,
Comp.separator().hide(Bindings.not(effectiveExpanded)),
content));
var full = new VerticalComp(
List.of(topEntryList, Comp.hseparator().hide(Bindings.not(effectiveExpanded)), content));
full.styleClass("store-entry-section-comp");
full.apply(struc -> {
struc.get().setFillWidth(true);
var hbox = ((HBox) struc.get().getChildren().getFirst());
addPseudoClassListeners(struc.get(), section.getWrapper().getExpanded());
addVisibilityListeners(struc.get(), hbox);
});
struc.get().setFillWidth(true);
var hbox = ((HBox) struc.get().getChildren().getFirst());
addPseudoClassListeners(struc.get(), section.getWrapper().getExpanded());
addVisibilityListeners(struc.get(), hbox);
});
return full.createStructure();
}
}

View file

@ -3,21 +3,16 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.base.*;
import io.xpipe.app.storage.DataColor;
import io.xpipe.app.util.LabelGraphic;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@ -34,7 +29,8 @@ public class StoreSectionMiniComp extends StoreSectionBaseComp {
super(section);
this.augment = augment;
this.action = action;
this.expanded = new SimpleBooleanProperty(section.getWrapper() == null || section.getWrapper().getExpanded().getValue());
this.expanded = new SimpleBooleanProperty(section.getWrapper() == null
|| section.getWrapper().getExpanded().getValue());
}
@Override
@ -48,12 +44,13 @@ public class StoreSectionMiniComp extends StoreSectionBaseComp {
root.maxWidth(2000);
root.styleClass("item");
root.apply(struc -> {
struc.get().setAlignment(Pos.CENTER_LEFT);
struc.get().setGraphic(PrettyImageHelper.ofFixedSize(
section.getWrapper().getIconFile(), 16, 16)
.createRegion());
struc.get().setMnemonicParsing(false);
});
struc.get().setAlignment(Pos.CENTER_LEFT);
struc.get()
.setGraphic(PrettyImageHelper.ofFixedSize(
section.getWrapper().getIconFile(), 16, 16)
.createRegion());
struc.get().setMnemonicParsing(false);
});
augment.accept(section, root);
var expandButton = createExpandButton(() -> expanded.set(!expanded.get()), 20, expanded);
@ -73,19 +70,20 @@ public class StoreSectionMiniComp extends StoreSectionBaseComp {
list.add(h);
}
var content = createChildrenList(c -> new StoreSectionMiniComp(c, this.augment, this.action), Bindings.not(expanded));
var content =
createChildrenList(c -> new StoreSectionMiniComp(c, this.augment, this.action), Bindings.not(expanded));
list.add(content);
var full = new VerticalComp(list);
full.styleClass("store-section-mini-comp");
full.apply(struc -> {
struc.get().setFillWidth(true);
addPseudoClassListeners(struc.get(), expanded);
if (section.getWrapper() != null) {
var hbox = ((HBox) struc.get().getChildren().getFirst());
addVisibilityListeners(struc.get(), hbox);
}
});
struc.get().setFillWidth(true);
addPseudoClassListeners(struc.get(), expanded);
if (section.getWrapper() != null) {
var hbox = ((HBox) struc.get().getChildren().getFirst());
addVisibilityListeners(struc.get(), hbox);
}
});
return full.createStructure();
}
}

View file

@ -112,7 +112,8 @@ public interface StoreSortMode {
private StoreSection getRepresentative(StoreSection s) {
if (StoreViewState.get().getEntriesListUpdateObservable().get() != entriesListOberservableIndex) {
cachedRepresentatives.clear();
entriesListOberservableIndex = StoreViewState.get().getEntriesListUpdateObservable().get();
entriesListOberservableIndex =
StoreViewState.get().getEntriesListUpdateObservable().get();
}
if (cachedRepresentatives.containsKey(s)) {

View file

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

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;
@ -26,12 +28,13 @@ public class StoreViewState {
private final StringProperty filter = new SimpleStringProperty();
@Getter
private final DerivedObservableList<StoreEntryWrapper> allEntries =
new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true);
private final DerivedObservableList<StoreEntryWrapper> allEntries = new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true);
@Getter
private final DerivedObservableList<StoreCategoryWrapper> categories =
new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true);
private final DerivedObservableList<StoreCategoryWrapper> categories = new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true);
@Getter
private final IntegerProperty entriesListVisibilityObservable = new SimpleIntegerProperty();
@Getter
private final IntegerProperty entriesListUpdateObservable = new SimpleIntegerProperty();
@ -42,6 +45,30 @@ public class StoreViewState {
@Getter
private final Property<StoreSortMode> sortMode = new SimpleObjectProperty<>();
@Getter
private final BooleanProperty batchMode = new SimpleBooleanProperty(false);
@Getter
private final DerivedObservableList<StoreEntryWrapper> batchModeSelection =
new DerivedObservableList<>(FXCollections.observableArrayList(), true);
@Getter
private boolean initialized = false;
@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 +87,8 @@ public class StoreViewState {
INSTANCE.initSections();
INSTANCE.updateContent();
INSTANCE.initFilterListener();
INSTANCE.initBatchListener();
INSTANCE.initialized = true;
}
public static void reset() {
@ -80,6 +109,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());
@ -88,7 +153,12 @@ public class StoreViewState {
private void initSections() {
try {
currentTopLevelSection = StoreSection.createTopLevel(
allEntries, storeEntryWrapper -> true, filter, activeCategory, entriesListUpdateObservable);
allEntries,
storeEntryWrapper -> true,
filter,
activeCategory,
entriesListVisibilityObservable,
entriesListUpdateObservable);
} catch (Exception exception) {
currentTopLevelSection = new StoreSection(
null,
@ -115,6 +185,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()
@ -151,11 +229,13 @@ public class StoreViewState {
.orElseThrow()));
}
public void updateDisplay() {
toggleStoreListUpdate();
public void triggerStoreListVisibilityUpdate() {
PlatformThread.runLaterIfNeeded(() -> {
entriesListVisibilityObservable.set(entriesListVisibilityObservable.get() + 1);
});
}
public void toggleStoreListUpdate() {
public void triggerStoreListUpdate() {
PlatformThread.runLaterIfNeeded(() -> {
entriesListUpdateObservable.set(entriesListUpdateObservable.get() + 1);
});
@ -180,7 +260,7 @@ public class StoreViewState {
@Override
public void onStoreListUpdate() {
Platform.runLater(() -> {
toggleStoreListUpdate();
triggerStoreListUpdate();
});
}

View file

@ -11,6 +11,8 @@ import org.apache.commons.io.FileUtils;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Optional;
import java.util.function.Supplier;
public class AppCache {
@ -107,4 +109,22 @@ public class AppCache {
.handle();
}
}
public static Optional<Instant> getModifiedTime(String key) {
var path = getPath(key);
if (Files.exists(path)) {
try {
var t = Files.getLastModifiedTime(path);
return Optional.of(t.toInstant());
} catch (Exception e) {
ErrorEvent.fromThrowable("Could not get modified date for " + key, e)
.omitted(true)
.build()
.handle();
return Optional.empty();
}
} else {
return Optional.empty();
}
}
}

View file

@ -209,10 +209,6 @@ public class AppExtensionManager {
return Optional.empty();
}
ext.get().getModule().getPackages().forEach(pkg -> {
ModuleHelper.exportAndOpen(pkg, ext.get().getModule());
});
TrackEvent.withInfo("Loaded extension module")
.tag("name", ext.get().getName())
.tag("dir", dir.toString())

View file

@ -50,7 +50,7 @@ public class AppGreetingsDialog {
return tp;
}
public static void showIfNeeded() {
public static void showAndWaitIfNeeded() {
boolean set = AppCache.getBoolean("legalAccepted", false);
if (set
|| AppProperties.get().isDevelopmentEnvironment()

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