Merge branch vnc into master

This commit is contained in:
crschnick 2024-04-13 16:23:09 +00:00
parent 6bd105b1de
commit cbc5ad473a
636 changed files with 16725 additions and 3220 deletions

2
.gitattributes vendored
View file

@ -1,4 +1,4 @@
* text=auto * text=auto eol=lf
*.sh text eol=lf *.sh text eol=lf
*.bat text eol=crlf *.bat text eol=crlf
*.png binary *.png binary

View file

@ -14,19 +14,6 @@ There are no real formal contribution guidelines right now, they will maybe come
- [dist](dist) - Tools to create a distributable package of XPipe - [dist](dist) - Tools to create a distributable package of XPipe
- [ext](ext) - Available XPipe extensions. Essentially every concrete feature implementation is implemented as an extension - [ext](ext) - Available XPipe extensions. Essentially every concrete feature implementation is implemented as an extension
## Modularity
All XPipe components target [Java 21](https://openjdk.java.net/projects/jdk/20/) and make full use of the Java Module System (JPMS).
All components are modularized, including all their dependencies.
In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
many IDEs still have problems building this project properly.
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
The tested and recommended IDE is IntelliJ.
When setting up the project in IntelliJ, make sure that the correct JDK (Java 21)
is selected both for the project and for gradle itself.
## Development Setup ## Development Setup
You need to have an up-to-date version of XPipe installed on your local system in order to properly You need to have an up-to-date version of XPipe installed on your local system in order to properly
@ -39,9 +26,9 @@ Note that in case the current master branch is ahead of the latest release, it m
It is therefore recommended to always check out the matching version tag for your local repository and local XPipe installation. It is therefore recommended to always check out the matching version tag for your local repository and local XPipe installation.
You can find the available version tags at https://github.com/xpipe-io/xpipe/tags You can find the available version tags at https://github.com/xpipe-io/xpipe/tags
You need to have GraalVM Community Edition for Java 21 installed as a JDK to compile the project. You need to have JDK for Java 22 installed to compile the project.
If you are on Linux or macOS, you can easily accomplish that by running the `setup.sh` script. If you are on Linux or macOS, you can easily accomplish that by running the `setup.sh` script.
On Windows, you have to manually install the JDK. On Windows, you have to manually install a JDK, e.g. from [Adoptium](https://adoptium.net/temurin/releases/?version=22).
## Building and Running ## Building and Running
@ -58,6 +45,19 @@ You are also able to properly debug the built production application through two
Note that when any unit test is run using a debugger, the XPipe daemon process that is started will also attempt Note that when any unit test is run using a debugger, the XPipe daemon process that is started will also attempt
to connect to that debugger through [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme) as well. to connect to that debugger through [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme) as well.
## Modularity and IDEs
All XPipe components target [Java 22](https://openjdk.java.net/projects/jdk/22/) and make full use of the Java Module System (JPMS).
All components are modularized, including all their dependencies.
In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
many IDEs still have problems building this project properly.
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
The tested and recommended IDE is IntelliJ.
When setting up the project in IntelliJ, make sure that the correct JDK (Java 22)
is selected both for the project and for gradle itself.
## Contributing guide ## Contributing guide
Especially when starting out, it might be a good idea to start with easy tasks first. Here's a selection of suitable common tasks that are very easy to implement: Especially when starting out, it might be a good idea to start with easy tasks first. Here's a selection of suitable common tasks that are very easy to implement:
@ -96,3 +96,7 @@ The [sample action](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/m
### Implementing something else ### Implementing something else
if you want to work on something that was not listed here, you can still do so of course. You can reach out on the [Discord server](https://discord.gg/8y89vS8cRb) to discuss any development plans and get you started. if you want to work on something that was not listed here, you can still do so of course. You can reach out on the [Discord server](https://discord.gg/8y89vS8cRb) to discuss any development plans and get you started.
### Translations
See the [translation guide](/lang/README.md) for details.

View file

@ -39,11 +39,12 @@ dependencies {
api 'com.vladsch.flexmark:flexmark-util-visitor:0.64.8' api 'com.vladsch.flexmark:flexmark-util-visitor:0.64.8'
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar") api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
api 'info.picocli:picocli:4.7.5' api 'info.picocli:picocli:4.7.5'
api 'org.kohsuke:github-api:1.321' api 'org.kohsuke:github-api:1.321'
api 'io.sentry:sentry:7.6.0' api 'io.sentry:sentry:7.8.0'
api 'org.ocpsoft.prettytime:prettytime:5.0.7.Final' api 'org.ocpsoft.prettytime:prettytime:5.0.7.Final'
api 'commons-io:commons-io:2.15.1' api 'commons-io:commons-io:2.16.1'
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.0" api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.0"
api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.17.0" api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.17.0"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.0" api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.0"

View file

@ -12,9 +12,11 @@ public class Main {
return; return;
} }
// Since this is not marked as a console application, it will not print anything when you run it in a console on Windows // Since this is not marked as a console application, it will not print anything when you run it in a console on
// Windows
if (args.length == 1 && args[0].equals("--help")) { if (args.length == 1 && args[0].equals("--help")) {
System.out.println(""" System.out.println(
"""
The daemon executable xpiped does not accept any command-line arguments. The daemon executable xpiped does not accept any command-line arguments.
For a reference on what you can do from the CLI, take a look at the xpipe CLI executable instead. For a reference on what you can do from the CLI, take a look at the xpipe CLI executable instead.

View file

@ -12,72 +12,46 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.DataStoreCategoryChoiceComp; import io.xpipe.app.util.DataStoreCategoryChoiceComp;
import io.xpipe.app.util.FixedHierarchyStore;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.DataStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.Point2D;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.input.DragEvent;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import java.util.List; import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate; import java.util.function.Predicate;
final class BrowserBookmarkComp extends SimpleComp { public final class BrowserBookmarkComp extends SimpleComp {
public static final Timer DROP_TIMER = new Timer("dnd", true);
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected"); private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
private final BrowserModel model; private final ObservableValue<DataStoreEntry> selected;
private Point2D lastOver = new Point2D(-1, -1); private final Predicate<StoreEntryWrapper> applicable;
private TimerTask activeTask; private final BiConsumer<StoreEntryWrapper, BooleanProperty> action;
BrowserBookmarkComp(BrowserModel model) { public BrowserBookmarkComp(
this.model = model; ObservableValue<DataStoreEntry> selected,
Predicate<StoreEntryWrapper> applicable,
BiConsumer<StoreEntryWrapper, BooleanProperty> action) {
this.selected = selected;
this.applicable = applicable;
this.action = action;
} }
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var filterText = new SimpleStringProperty(); var filterText = new SimpleStringProperty();
var open = PlatformThread.sync(model.getSelected());
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore
|| storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore)
&& storeEntryWrapper.getEntry().getValidity().isUsable();
};
var selectedCategory = new SimpleObjectProperty<>( var selectedCategory = new SimpleObjectProperty<>(
StoreViewState.get().getActiveCategory().getValue()); StoreViewState.get().getActiveCategory().getValue());
BooleanProperty busy = new SimpleBooleanProperty(false); BooleanProperty busy = new SimpleBooleanProperty(false);
Consumer<StoreEntryWrapper> action = w -> {
ThreadHelper.runFailableAsync(() -> {
var entry = w.getEntry();
if (!entry.getValidity().isUsable()) {
return;
}
if (entry.getStore() instanceof ShellStore fileSystem) {
model.openFileSystemAsync(entry.ref(), null, busy);
} else if (entry.getStore() instanceof FixedHierarchyStore) {
BooleanScope.execute(busy, () -> {
w.refreshChildren();
});
}
});
};
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (s, comp) -> { BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (s, comp) -> {
comp.disable(Bindings.createBooleanBinding( comp.disable(Bindings.createBooleanBinding(
() -> { () -> {
@ -85,14 +59,15 @@ final class BrowserBookmarkComp extends SimpleComp {
}, },
busy)); busy));
comp.apply(struc -> { comp.apply(struc -> {
open.addListener((observable, oldValue, newValue) -> { selected.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
struc.get() struc.get()
.pseudoClassStateChanged( .pseudoClassStateChanged(
SELECTED, SELECTED,
newValue != null newValue != null
&& newValue.getEntry() && newValue.equals(
.get() s.getWrapper().getEntry()));
.equals(s.getWrapper().getEntry())); });
}); });
}); });
}; };
@ -101,7 +76,7 @@ final class BrowserBookmarkComp extends SimpleComp {
StoreSection.createTopLevel( StoreSection.createTopLevel(
StoreViewState.get().getAllEntries(), storeEntryWrapper -> true, filterText, selectedCategory), StoreViewState.get().getAllEntries(), storeEntryWrapper -> true, filterText, selectedCategory),
augment, augment,
action, entryWrapper -> action.accept(entryWrapper, busy),
true); true);
var category = new DataStoreCategoryChoiceComp( var category = new DataStoreCategoryChoiceComp(
StoreViewState.get().getAllConnectionsCategory(), StoreViewState.get().getAllConnectionsCategory(),
@ -125,21 +100,4 @@ final class BrowserBookmarkComp extends SimpleComp {
content.getStyleClass().add("bookmark-list"); content.getStyleClass().add("bookmark-list");
return content; return content;
} }
private void handleHoverTimer(DataStore store, DragEvent event) {
if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) {
return;
}
lastOver = (new Point2D(event.getX(), event.getY()));
activeTask = new TimerTask() {
@Override
public void run() {
if (activeTask != this) {}
// Platform.runLater(() -> model.openExistingFileSystemIfPresent(store.asNeeded()));
}
};
DROP_TIMER.schedule(activeTask, 500);
}
} }

View file

@ -1,9 +1,9 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.controls.Breadcrumbs; import atlantafx.base.controls.Breadcrumbs;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -40,7 +40,7 @@ public class BrowserBreadcrumbBar extends SimpleComp {
var breadcrumbs = new Breadcrumbs<String>(); var breadcrumbs = new Breadcrumbs<String>();
breadcrumbs.setMinWidth(0); breadcrumbs.setMinWidth(0);
SimpleChangeListener.apply(PlatformThread.sync(model.getCurrentPath()), val -> { PlatformThread.sync(model.getCurrentPath()).subscribe(val -> {
if (val == null) { if (val == null) {
breadcrumbs.setSelectedCrumb(null); breadcrumbs.setSelectedCrumb(null);
return; return;

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import io.xpipe.app.browser.file.FileSystemHelper;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ProcessControlProvider; import io.xpipe.core.process.ProcessControlProvider;

View file

@ -1,11 +1,11 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.TextFieldComp; import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -29,7 +29,7 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
var expanded = new SimpleBooleanProperty(); var expanded = new SimpleBooleanProperty();
var text = new TextFieldComp(filterString, false).createRegion(); var text = new TextFieldComp(filterString, false).createRegion();
var button = new Button(); var button = new Button();
new FancyTooltipAugment<>("app.search").augment(button); new TooltipAugment<>("app.search").augment(button);
text.focusedProperty().addListener((observable, oldValue, newValue) -> { text.focusedProperty().addListener((observable, oldValue, newValue) -> {
if (!newValue && filterString.getValue() == null) { if (!newValue && filterString.getValue() == null) {
if (button.isFocused()) { if (button.isFocused()) {
@ -47,7 +47,7 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
text.setMinWidth(0); text.setMinWidth(0);
Styles.toggleStyleClass(text, Styles.LEFT_PILL); Styles.toggleStyleClass(text, Styles.LEFT_PILL);
SimpleChangeListener.apply(filterString, val -> { filterString.subscribe(val -> {
if (val == null) { if (val == null) {
text.getStyleClass().remove(Styles.SUCCESS); text.getStyleClass().remove(Styles.SUCCESS);
} else { } else {

View file

@ -2,8 +2,10 @@ package io.xpipe.app.browser;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel; import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
@ -15,8 +17,10 @@ public class BrowserGreetingComp extends SimpleComp {
protected Region createSimple() { protected Region createSimple() {
var r = new Label(getText()); var r = new Label(getText());
AppLayoutModel.get().getSelected().addListener((observableValue, entry, t1) -> { AppLayoutModel.get().getSelected().addListener((observableValue, entry, t1) -> {
PlatformThread.runLaterIfNeeded(() -> {
r.setText(getText()); r.setText(getText());
}); });
});
AppFont.setSize(r, 7); AppFont.setSize(r, 7);
r.getStyleClass().add(Styles.TEXT_BOLD); r.getStyleClass().add(Styles.TEXT_BOLD);
return r; return r;
@ -27,11 +31,11 @@ public class BrowserGreetingComp extends SimpleComp {
var hour = ldt.getHour(); var hour = ldt.getHour();
String text; String text;
if (hour > 18 || hour < 5) { if (hour > 18 || hour < 5) {
text = "Good evening"; text = AppI18n.get("goodEvening");
} else if (hour < 12) { } else if (hour < 12) {
text = "Good morning"; text = AppI18n.get("goodMorning");
} else { } else {
text = "Good afternoon"; text = AppI18n.get("goodAfternoon");
} }
return text; return text;
} }

View file

@ -1,185 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@Getter
public class BrowserModel {
public static final BrowserModel DEFAULT = new BrowserModel(Mode.BROWSER, BrowserSavedStateImpl.load());
private final Mode mode;
private final ObservableList<OpenFileSystemModel> openFileSystems = FXCollections.observableArrayList();
private final Property<OpenFileSystemModel> selected = new SimpleObjectProperty<>();
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
private final BrowserSavedState savedState;
@Setter
private Consumer<List<FileReference>> onFinish;
public BrowserModel(Mode mode, BrowserSavedState savedState) {
this.mode = mode;
this.savedState = savedState;
selected.addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
selection.clear();
return;
}
BindingsHelper.bindContent(selection, newValue.getFileList().getSelection());
});
}
public void restoreState(BrowserSavedState state) {
ThreadHelper.runAsync(() -> {
state.getEntries().forEach(e -> {
restoreStateAsync(e, null);
// Don't try to run everything in parallel as that can be taxing
ThreadHelper.sleep(1000);
});
});
}
public void restoreStateAsync(BrowserSavedState.Entry e, BooleanProperty busy) {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
storageEntry.ifPresent(entry -> {
openFileSystemAsync(entry.ref(), model -> e.getPath(), busy);
});
}
public void reset() {
synchronized (BrowserModel.this) {
for (OpenFileSystemModel o : new ArrayList<>(openFileSystems)) {
// Don't close busy connections gracefully
// as we otherwise might lock up
if (o.isBusy()) {
continue;
}
closeFileSystemSync(o);
}
if (savedState != null) {
savedState.save();
}
}
// Delete all files
localTransfersStage.clear();
}
public void finishChooser() {
if (!getMode().isChooser()) {
throw new IllegalStateException();
}
var chosen = new ArrayList<>(selection);
synchronized (BrowserModel.this) {
for (OpenFileSystemModel openFileSystem : openFileSystems) {
closeFileSystemAsync(openFileSystem);
}
}
if (chosen.size() == 0) {
return;
}
var stores = chosen.stream()
.map(entry -> new FileReference(
selected.getValue().getEntry(), entry.getRawFileEntry().getPath()))
.toList();
onFinish.accept(stores);
}
public void closeFileSystemAsync(OpenFileSystemModel open) {
ThreadHelper.runAsync(() -> {
closeFileSystemSync(open);
});
}
private void closeFileSystemSync(OpenFileSystemModel open) {
if (DataStorage.get().getStoreEntries().contains(open.getEntry().get())
&& savedState != null
&& open.getCurrentPath().get() != null) {
savedState.add(new BrowserSavedState.Entry(
open.getEntry().get().getUuid(), open.getCurrentPath().get()));
}
open.closeSync();
synchronized (BrowserModel.this) {
openFileSystems.remove(open);
}
}
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
}
ThreadHelper.runFailableAsync(() -> {
OpenFileSystemModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
model = new OpenFileSystemModel(this, store);
model.initFileSystem();
model.initSavedState();
// Prevent multiple calls from interfering with each other
synchronized (BrowserModel.this) {
openFileSystems.add(model);
// The tab pane doesn't automatically select new tabs
selected.setValue(model);
}
}
if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
} else {
model.initWithDefaultDirectory();
}
});
}
@Getter
public enum Mode {
BROWSER(false, true, true, true),
SINGLE_FILE_CHOOSER(true, false, true, false),
SINGLE_FILE_SAVE(true, false, true, false),
MULTIPLE_FILE_CHOOSER(true, true, true, false),
SINGLE_DIRECTORY_CHOOSER(true, false, false, true),
MULTIPLE_DIRECTORY_CHOOSER(true, true, false, true);
private final boolean chooser;
private final boolean multiple;
private final boolean acceptsFiles;
private final boolean acceptsDirectories;
Mode(boolean chooser, boolean multiple, boolean acceptsFiles, boolean acceptsDirectories) {
this.chooser = chooser;
this.multiple = multiple;
this.acceptsFiles = acceptsFiles;
this.acceptsDirectories = acceptsDirectories;
}
}
}

View file

@ -1,6 +1,8 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.icon.FileIconManager; import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
@ -10,7 +12,6 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.StackComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp; import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform; import javafx.application.Platform;
@ -42,7 +43,7 @@ public class BrowserNavBar extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var path = new SimpleStringProperty(model.getCurrentPath().get()); var path = new SimpleStringProperty(model.getCurrentPath().get());
SimpleChangeListener.apply(model.getCurrentPath(), (newValue) -> { model.getCurrentPath().subscribe((newValue) -> {
path.set(newValue); path.set(newValue);
}); });
path.addListener((observable, oldValue, newValue) -> { path.addListener((observable, oldValue, newValue) -> {
@ -58,7 +59,7 @@ public class BrowserNavBar extends SimpleComp {
.styleClass(Styles.CENTER_PILL) .styleClass(Styles.CENTER_PILL)
.styleClass("path-text") .styleClass("path-text")
.apply(struc -> { .apply(struc -> {
SimpleChangeListener.apply(struc.get().focusedProperty(), val -> { struc.get().focusedProperty().subscribe(val -> {
struc.get() struc.get()
.pseudoClassStateChanged( .pseudoClassStateChanged(
INVISIBLE, INVISIBLE,
@ -71,7 +72,7 @@ public class BrowserNavBar extends SimpleComp {
} }
}); });
SimpleChangeListener.apply(model.getInOverview(), val -> { model.getInOverview().subscribe(val -> {
// Pseudo classes do not apply if set instantly before shown // Pseudo classes do not apply if set instantly before shown
// If we start a new tab with a directory set, we have to set the pseudo class one pulse later // If we start a new tab with a directory set, we have to set the pseudo class one pulse later
Platform.runLater(() -> { Platform.runLater(() -> {

View file

@ -1,10 +1,12 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import io.xpipe.app.browser.file.BrowserFileOverviewComp;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.comp.base.SimpleTitledPaneComp; import io.xpipe.app.comp.base.SimpleTitledPaneComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
@ -66,7 +68,7 @@ public class BrowserOverviewComp extends SimpleComp {
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false); var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview); var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
var recent = BindingsHelper.mappedContentBinding( var recent = ListBindingsHelper.mappedContentBinding(
model.getSavedState().getRecentDirectories(), model.getSavedState().getRecentDirectories(),
s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())); s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()));
var recentOverview = new BrowserFileOverviewComp(model, recent, true); var recentOverview = new BrowserFileOverviewComp(model, recent, true);

View file

@ -1,6 +1,7 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import javafx.collections.ObservableList; import javafx.collections.ObservableList;
import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Value; import lombok.Value;
import lombok.extern.jackson.Jacksonized; import lombok.extern.jackson.Jacksonized;
@ -18,6 +19,7 @@ public interface BrowserSavedState {
@Value @Value
@Jacksonized @Jacksonized
@Builder @Builder
@AllArgsConstructor
class Entry { class Entry {
UUID uuid; UUID uuid;

View file

@ -27,7 +27,7 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
this.lastSystems = FXCollections.observableArrayList(lastSystems); this.lastSystems = FXCollections.observableArrayList(lastSystems);
} }
static BrowserSavedStateImpl load() { public static BrowserSavedStateImpl load() {
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> { return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
return new BrowserSavedStateImpl(FXCollections.observableArrayList()); return new BrowserSavedStateImpl(FXCollections.observableArrayList());
}); });

View file

@ -52,9 +52,9 @@ public class BrowserSelectionListComp extends SimpleComp {
protected Region createSimple() { protected Region createSimple() {
var c = new ListBoxViewComp<>(list, list, entry -> { var c = new ListBoxViewComp<>(list, list, entry -> {
return Comp.of(() -> { return Comp.of(() -> {
var wv = PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 20) var image = PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 24)
.createRegion(); .createRegion();
var l = new Label(null, wv); var l = new Label(null, image);
l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS); l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
l.textProperty().bind(PlatformThread.sync(nameTransformation.apply(entry))); l.textProperty().bind(PlatformThread.sync(nameTransformation.apply(entry)));
return l; return l;

View file

@ -1,6 +1,9 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.file.BrowserFileListCompEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
@ -11,6 +14,7 @@ import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.app.util.HumanReadableFormat;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.scene.control.ToolBar; import javafx.scene.control.ToolBar;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Value; import lombok.Value;
@ -59,7 +63,7 @@ public class BrowserStatusBarComp extends SimpleComp {
private Comp<?> createClipboardStatus() { private Comp<?> createClipboardStatus() {
var cc = BrowserClipboard.currentCopyClipboard; var cc = BrowserClipboard.currentCopyClipboard;
var ccCount = (BindingsHelper.persist(Bindings.createStringBinding( var ccCount = Bindings.createStringBinding(
() -> { () -> {
if (cc.getValue() != null && cc.getValue().getEntries().size() > 0) { if (cc.getValue() != null && cc.getValue().getEntries().size() > 0) {
return cc.getValue().getEntries().size() + " file" return cc.getValue().getEntries().size() + " file"
@ -68,7 +72,7 @@ public class BrowserStatusBarComp extends SimpleComp {
return null; return null;
} }
}, },
cc))); cc);
return new LabelComp(ccCount); return new LabelComp(ccCount);
} }
@ -86,7 +90,7 @@ public class BrowserStatusBarComp extends SimpleComp {
.count(); .count();
}, },
model.getFileList().getAll()); model.getFileList().getAll());
var selectedComp = new LabelComp(BindingsHelper.persist(Bindings.createStringBinding( var selectedComp = new LabelComp(Bindings.createStringBinding(
() -> { () -> {
if (selectedCount.getValue().intValue() == 0) { if (selectedCount.getValue().intValue() == 0) {
return null; return null;
@ -95,7 +99,7 @@ public class BrowserStatusBarComp extends SimpleComp {
} }
}, },
selectedCount, selectedCount,
allCount))); allCount));
return selectedComp; return selectedComp;
} }
@ -124,6 +128,10 @@ public class BrowserStatusBarComp extends SimpleComp {
}); });
// Use status bar as an extension of file list // Use status bar as an extension of file list
new ContextMenuAugment<>(mouseEvent -> mouseEvent.isSecondaryButtonDown(), null, () -> new BrowserContextMenu(model, null)).augment(new SimpleCompStructure<>(r)); new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
null,
() -> new BrowserContextMenu(model, null))
.augment(new SimpleCompStructure<>(r));
} }
} }

View file

@ -1,12 +1,13 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.comp.base.LoadingOverlayComp; import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment; import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
@ -37,18 +38,22 @@ public class BrowserTransferComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var syncItems = PlatformThread.sync(model.getItems());
var syncDownloaded = PlatformThread.sync(model.getDownloading());
var syncAllDownloaded = PlatformThread.sync(model.getAllDownloaded());
var background = new LabelComp(AppI18n.observable("transferDescription")) var background = new LabelComp(AppI18n.observable("transferDescription"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.visible(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))); .visible(Bindings.isEmpty(syncItems));
var backgroundStack = var backgroundStack =
new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
var binding = BindingsHelper.mappedContentBinding(model.getItems(), item -> item.getFileEntry()); var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getFileEntry());
var list = new BrowserSelectionListComp( var list = new BrowserSelectionListComp(
binding, binding,
entry -> Bindings.createStringBinding( entry -> Bindings.createStringBinding(
() -> { () -> {
var sourceItem = model.getItems().stream() var sourceItem = syncItems.stream()
.filter(item -> item.getFileEntry() == entry) .filter(item -> item.getFileEntry() == entry)
.findAny(); .findAny();
if (sourceItem.isEmpty()) { if (sourceItem.isEmpty()) {
@ -63,27 +68,27 @@ public class BrowserTransferComp extends SimpleComp {
.orElse("?"); .orElse("?");
return FileNames.getFileName(entry.getPath()) + " (" + name + ")"; return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
}, },
model.getAllDownloaded())) syncAllDownloaded))
.apply(struc -> struc.get().setMinHeight(150)) .apply(struc -> struc.get().setMinHeight(150))
.grow(false, true); .grow(false, true);
var dragNotice = new LabelComp(model.getAllDownloaded() var dragNotice = new LabelComp(syncAllDownloaded
.flatMap(aBoolean -> .flatMap(aBoolean ->
aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles"))) aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left"))) .apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
.hide(PlatformThread.sync(BindingsHelper.persist(Bindings.isEmpty(model.getItems())))) .hide(Bindings.isEmpty(syncItems))
.grow(true, false) .grow(true, false)
.apply(struc -> struc.get().setPadding(new Insets(8))); .apply(struc -> struc.get().setPadding(new Insets(8)));
var downloadButton = new IconButtonComp("mdi2d-download", () -> { var downloadButton = new IconButtonComp("mdi2d-download", () -> {
model.download(); model.download();
}) })
.hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))) .hide(Bindings.isEmpty(syncItems))
.disable(PlatformThread.sync(model.getAllDownloaded())) .disable(syncAllDownloaded)
.apply(new FancyTooltipAugment<>("downloadStageDescription")); .apply(new TooltipAugment<>("downloadStageDescription"));
var clearButton = new IconButtonComp("mdi2c-close", () -> { var clearButton = new IconButtonComp("mdi2c-close", () -> {
model.clear(); model.clear();
}) })
.hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))); .hide(Bindings.isEmpty(syncItems));
var clearPane = Comp.derive( var clearPane = Comp.derive(
new HorizontalComp(List.of(downloadButton, clearButton)) new HorizontalComp(List.of(downloadButton, clearButton))
.apply(struc -> struc.get().setSpacing(10)), .apply(struc -> struc.get().setSpacing(10)),
@ -122,12 +127,15 @@ public class BrowserTransferComp extends SimpleComp {
return; return;
} }
if (!(model.getBrowserSessionModel()
.getSelectedEntry()
.getValue()
instanceof OpenFileSystemModel fileSystemModel)) {
return;
}
var files = drag.getEntries(); var files = drag.getEntries();
model.drop( model.drop(fileSystemModel, files);
model.getBrowserModel()
.getSelected()
.getValue(),
files);
event.setDropCompleted(true); event.setDropCompleted(true);
event.consume(); event.consume();
} }
@ -140,11 +148,11 @@ public class BrowserTransferComp extends SimpleComp {
} }
}); });
struc.get().setOnDragDetected(event -> { struc.get().setOnDragDetected(event -> {
if (model.getDownloading().get()) { if (syncDownloaded.getValue()) {
return; return;
} }
var selected = model.getItems().stream() var selected = syncItems.stream()
.map(BrowserTransferModel.Item::getFileEntry) .map(BrowserTransferModel.Item::getFileEntry)
.toList(); .toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY); Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
@ -154,7 +162,7 @@ public class BrowserTransferComp extends SimpleComp {
return; return;
} }
var files = model.getItems().stream() var files = syncItems.stream()
.filter(item -> item.downloadFinished().get()) .filter(item -> item.downloadFinished().get())
.map(item -> { .map(item -> {
try { try {
@ -191,7 +199,7 @@ public class BrowserTransferComp extends SimpleComp {
event.consume(); event.consume();
}); });
}), }),
PlatformThread.sync(model.getDownloading())); syncDownloaded);
return stack.styleClass("transfer").createRegion(); return stack.styleClass("transfer").createRegion();
} }
} }

View file

@ -1,5 +1,8 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import io.xpipe.app.browser.file.FileSystemHelper;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ShellTemp; import io.xpipe.app.util.ShellTemp;
@ -36,7 +39,7 @@ public class BrowserTransferModel {
t.setName("file downloader"); t.setName("file downloader");
return t; return t;
}); });
BrowserModel browserModel; BrowserSessionModel browserSessionModel;
ObservableList<Item> items = FXCollections.observableArrayList(); ObservableList<Item> items = FXCollections.observableArrayList();
BooleanProperty downloading = new SimpleBooleanProperty(); BooleanProperty downloading = new SimpleBooleanProperty();
BooleanProperty allDownloaded = new SimpleBooleanProperty(); BooleanProperty allDownloaded = new SimpleBooleanProperty();

View file

@ -9,7 +9,7 @@ public class BrowserTransferProgress {
long transferred; long transferred;
long total; long total;
static BrowserTransferProgress empty() { public static BrowserTransferProgress empty() {
return new BrowserTransferProgress(null, 0, 0); return new BrowserTransferProgress(null, 0, 0);
} }
@ -17,7 +17,7 @@ public class BrowserTransferProgress {
return new BrowserTransferProgress(name, 0, size); return new BrowserTransferProgress(name, 0, size);
} }
static BrowserTransferProgress finished(String name, long size) { public static BrowserTransferProgress finished(String name, long size) {
return new BrowserTransferProgress(name, size, size); return new BrowserTransferProgress(name, size, size);
} }

View file

@ -1,10 +1,12 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.comp.base.ListBoxViewComp; import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.TileButtonComp; import io.xpipe.app.comp.base.TileButtonComp;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
@ -12,6 +14,7 @@ import io.xpipe.app.fxcomps.impl.LabelComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.PrettySvgComp; import io.xpipe.app.fxcomps.impl.PrettySvgComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -31,9 +34,9 @@ import java.util.List;
public class BrowserWelcomeComp extends SimpleComp { public class BrowserWelcomeComp extends SimpleComp {
private final BrowserModel model; private final BrowserSessionModel model;
public BrowserWelcomeComp(BrowserModel model) { public BrowserWelcomeComp(BrowserSessionModel model) {
this.model = model; this.model = model;
} }
@ -54,13 +57,14 @@ public class BrowserWelcomeComp extends SimpleComp {
hbox.setSpacing(15); hbox.setSpacing(15);
if (state == null) { if (state == null) {
var header = new Label("Here you will be able to see where you left off last time."); var header = new Label();
header.textProperty().bind(AppI18n.observable("browserWelcomeEmpty"));
vbox.getChildren().add(header); vbox.getChildren().add(header);
hbox.setPadding(new Insets(40, 40, 40, 50)); hbox.setPadding(new Insets(40, 40, 40, 50));
return new VBox(hbox); return new VBox(hbox);
} }
var list = BindingsHelper.filteredContentBinding(state.getEntries(), e -> { var list = ListBindingsHelper.filteredContentBinding(state.getEntries(), e -> {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
if (entry.isEmpty()) { if (entry.isEmpty()) {
return false; return false;
@ -74,14 +78,14 @@ public class BrowserWelcomeComp extends SimpleComp {
}); });
var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list); var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list);
var header = new LabelComp(Bindings.createStringBinding( var headerBinding = BindingsHelper.flatMap(empty, b -> {
() -> { if (b) {
return !empty.get() return AppI18n.observable("browserWelcomeEmpty");
? "You were recently connected to the following systems:" } else {
: "Here you will be able to see where you left off last time."; return AppI18n.observable("browserWelcomeSystems");
}, }
empty)) });
.createRegion(); var header = new LabelComp(headerBinding).createRegion();
AppFont.setSize(header, 1); AppFont.setSize(header, 1);
vbox.getChildren().add(header); vbox.getChildren().add(header);
@ -92,7 +96,10 @@ public class BrowserWelcomeComp extends SimpleComp {
var disable = new SimpleBooleanProperty(); var disable = new SimpleBooleanProperty();
var entryButton = entryButton(e, disable); var entryButton = entryButton(e, disable);
var dirButton = dirButton(e, disable); var dirButton = dirButton(e, disable);
return new HorizontalComp(List.of(entryButton, dirButton)); return new HorizontalComp(List.of(entryButton, dirButton)).apply(struc -> {
((Region) struc.get().getChildren().get(0)).prefHeightProperty().bind(struc.get().heightProperty());
((Region) struc.get().getChildren().get(1)).prefHeightProperty().bind(struc.get().heightProperty());
});
}) })
.apply(struc -> { .apply(struc -> {
VBox vBox = (VBox) struc.get().getContent(); VBox vBox = (VBox) struc.get().getContent();
@ -125,11 +132,13 @@ public class BrowserWelcomeComp extends SimpleComp {
private Comp<?> entryButton(BrowserSavedState.Entry e, BooleanProperty disable) { private Comp<?> entryButton(BrowserSavedState.Entry e, BooleanProperty disable) {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
var graphic = entry.get() var graphic =
.getProvider() entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
.getDisplayIconFileName(entry.get().getStore());
var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24); var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24);
return new ButtonComp(new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())), view.createRegion(), () -> { return new ButtonComp(
new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())),
view.createRegion(),
() -> {
ThreadHelper.runAsync(() -> { ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable); model.restoreStateAsync(e, disable);
}); });

View file

@ -1,73 +0,0 @@
package io.xpipe.app.browser;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.FileReference;
import io.xpipe.core.store.FileSystemStore;
import javafx.beans.property.Property;
import javafx.stage.FileChooser;
import javafx.stage.Window;
import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Supplier;
public class StandaloneFileBrowser {
public static void localOpenFileChooser(
Property<FileReference> fileStoreProperty, Window owner, Map<String, List<String>> extensions) {
PlatformThread.runLaterIfNeeded(() -> {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle(AppI18n.get("browseFileTitle"));
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(AppI18n.get("anyFile"), "*"));
extensions.forEach((key, value) -> {
fileChooser
.getExtensionFilters()
.add(new FileChooser.ExtensionFilter(
key, value.stream().map(v -> "*." + v).toArray(String[]::new)));
});
File file = fileChooser.showOpenDialog(owner);
if (file != null && file.exists()) {
fileStoreProperty.setValue(FileReference.local(file.toPath()));
}
});
}
public static void openSingleFile(
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file) {
PlatformThread.runLaterIfNeeded(() -> {
var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_CHOOSER, null);
var comp = new BrowserComp(model)
.apply(struc -> struc.get().setPrefSize(1200, 700))
.apply(struc -> AppFont.normal(struc.get()));
var window = AppWindowHelper.sideWindow(AppI18n.get("openFileTitle"), stage -> comp, false, null);
model.setOnFinish(fileStores -> {
file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);
window.close();
});
window.show();
model.openFileSystemAsync(store.get(), null, null);
});
}
public static void saveSingleFile(Property<FileReference> file) {
PlatformThread.runLaterIfNeeded(() -> {
var model = new BrowserModel(BrowserModel.Mode.SINGLE_FILE_SAVE, null);
var comp = new BrowserComp(model)
.apply(struc -> struc.get().setPrefSize(1200, 700))
.apply(struc -> AppFont.normal(struc.get()));
var window = AppWindowHelper.sideWindow(AppI18n.get("saveFileTitle"), stage -> comp, true, null);
model.setOnFinish(fileStores -> {
file.setValue(fileStores.size() > 0 ? fileStores.getFirst() : null);
window.close();
});
window.show();
});
}
}

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.action; package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import java.util.List; import java.util.List;

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.action; package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import java.util.List; import java.util.List;

View file

@ -1,9 +1,10 @@
package io.xpipe.app.browser.action; package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.util.ModuleLayerLoader; import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyCombination;
@ -53,7 +54,7 @@ public interface BrowserAction {
return false; return false;
} }
String getName(OpenFileSystemModel model, List<BrowserEntry> entries); ObservableValue<String> getName(OpenFileSystemModel model, List<BrowserEntry> entries);
default boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) { default boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
return true; return true;
@ -91,15 +92,5 @@ public interface BrowserAction {
}) })
.toList()); .toList());
} }
@Override
public boolean requiresFullDaemon() {
return true;
}
@Override
public boolean prioritizeLoading() {
return false;
}
} }
} }

View file

@ -1,6 +1,6 @@
package io.xpipe.app.browser.action; package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.file.BrowserEntry;
import java.util.List; import java.util.List;

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.action; package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import java.util.List; import java.util.List;

View file

@ -1,19 +1,17 @@
package io.xpipe.app.browser.action; package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.Shortcuts; import io.xpipe.app.fxcomps.util.Shortcuts;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.LicenseProvider;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.MenuItem; import javafx.scene.control.MenuItem;
import org.kordamp.ikonli.javafx.FontIcon; import org.kordamp.ikonli.javafx.FontIcon;
import java.util.List; import java.util.List;
import java.util.function.UnaryOperator;
public interface LeafAction extends BrowserAction { public interface LeafAction extends BrowserAction {
@ -23,7 +21,7 @@ public interface LeafAction extends BrowserAction {
var b = new Button(); var b = new Button();
b.setOnAction(event -> { b.setOnAction(event -> {
// Only accept shortcut actions in the current tab // Only accept shortcut actions in the current tab
if (!model.equals(model.getBrowserModel().getSelected().getValue())) { if (!model.equals(model.getBrowserModel().getSelectedEntry().getValue())) {
return; return;
} }
@ -39,13 +37,14 @@ public interface LeafAction extends BrowserAction {
if (getShortcut() != null) { if (getShortcut() != null) {
Shortcuts.addShortcut(b, getShortcut()); Shortcuts.addShortcut(b, getShortcut());
} }
new FancyTooltipAugment<>(new SimpleStringProperty(getName(model, selected))).augment(b); var name = getName(model, selected);
new TooltipAugment<>(name).augment(b);
var graphic = getIcon(model, selected); var graphic = getIcon(model, selected);
if (graphic != null) { if (graphic != null) {
b.setGraphic(graphic); b.setGraphic(graphic);
} }
b.setMnemonicParsing(false); b.setMnemonicParsing(false);
b.setAccessibleText(getName(model, selected)); b.accessibleTextProperty().bind(name);
b.setDisable(!isActive(model, selected)); b.setDisable(!isActive(model, selected));
model.getCurrentPath().addListener((observable, oldValue, newValue) -> { model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
@ -61,10 +60,10 @@ public interface LeafAction extends BrowserAction {
return b; return b;
} }
default MenuItem toMenuItem( default MenuItem toMenuItem(OpenFileSystemModel model, List<BrowserEntry> selected) {
OpenFileSystemModel model, List<BrowserEntry> selected, UnaryOperator<String> nameFunc) { var name = getName(model, selected);
var name = nameFunc.apply(getName(model, selected)); var mi = new MenuItem();
var mi = new MenuItem(name); mi.textProperty().bind(name);
mi.setOnAction(event -> { mi.setOnAction(event -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
BooleanScope.execute(model.getBusy(), () -> { BooleanScope.execute(model.getBusy(), () -> {

View file

@ -1,11 +1,13 @@
package io.xpipe.app.browser.action; package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.TerminalLauncher; import io.xpipe.app.util.TerminalLauncher;
import io.xpipe.core.process.CommandBuilder; import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;
import javafx.beans.value.ObservableValue;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import java.util.List; import java.util.List;
@ -39,9 +41,11 @@ public abstract class MultiExecuteAction implements BranchAction {
} }
@Override @Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) { public ObservableValue<String> getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
var t = AppPrefs.get().terminalType().getValue(); var t = AppPrefs.get().terminalType().getValue();
return "in " + (t != null ? t.toTranslatedString().getValue() : "?"); return AppI18n.observable(
"executeInTerminal",
t != null ? t.toTranslatedString().getValue() : "?");
} }
@Override @Override
@ -66,8 +70,8 @@ public abstract class MultiExecuteAction implements BranchAction {
} }
@Override @Override
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) { public ObservableValue<String> getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
return "in background"; return AppI18n.observable("executeInBackground");
} }
}); });
} }

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.action; package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.util.FileOpener; import io.xpipe.app.util.FileOpener;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.file;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.core.AppWindowHelper;

View file

@ -1,8 +1,9 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.file;
import io.xpipe.app.browser.action.BranchAction; import io.xpipe.app.browser.action.BranchAction;
import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.action.LeafAction; import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.LicenseProvider;
import javafx.scene.control.ContextMenu; import javafx.scene.control.ContextMenu;
@ -13,7 +14,7 @@ import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
final class BrowserContextMenu extends ContextMenu { public final class BrowserContextMenu extends ContextMenu {
private final OpenFileSystemModel model; private final OpenFileSystemModel model;
private final BrowserEntry source; private final BrowserEntry source;
@ -74,17 +75,17 @@ final class BrowserContextMenu extends ContextMenu {
for (BrowserAction a : all) { for (BrowserAction a : all) {
var used = resolveIfNeeded(a, selected); var used = resolveIfNeeded(a, selected);
if (a instanceof LeafAction la) { if (a instanceof LeafAction la) {
getItems().add(la.toMenuItem(model, used, s -> s)); getItems().add(la.toMenuItem(model, used));
} }
if (a instanceof BranchAction la) { if (a instanceof BranchAction la) {
var m = new Menu(a.getName(model, used) + " ..."); var m = new Menu(a.getName(model, used).getValue() + " ...");
for (LeafAction sub : la.getBranchingActions(model, used)) { for (LeafAction sub : la.getBranchingActions(model, used)) {
var subUsed = resolveIfNeeded(sub, selected); var subUsed = resolveIfNeeded(sub, selected);
if (!sub.isApplicable(model, subUsed)) { if (!sub.isApplicable(model, subUsed)) {
continue; continue;
} }
m.getItems().add(sub.toMenuItem(model, subUsed, s -> s)); m.getItems().add(sub.toMenuItem(model, subUsed));
} }
var graphic = a.getIcon(model, used); var graphic = a.getIcon(model, used);
if (graphic != null) { if (graphic != null) {

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.file;
import io.xpipe.app.browser.icon.BrowserIconDirectoryType; import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
import io.xpipe.app.browser.icon.BrowserIconFileType; import io.xpipe.app.browser.icon.BrowserIconFileType;
@ -29,7 +29,7 @@ public class BrowserEntry {
return null; return null;
} }
for (var f : BrowserIconFileType.ALL) { for (var f : BrowserIconFileType.getAll()) {
if (f.matches(rawFileEntry)) { if (f.matches(rawFileEntry)) {
return f; return f;
} }
@ -43,7 +43,7 @@ public class BrowserEntry {
return null; return null;
} }
for (var f : BrowserIconDirectoryType.ALL) { for (var f : BrowserIconDirectoryType.getAll()) {
if (f.matches(rawFileEntry)) { if (f.matches(rawFileEntry)) {
return f; return f;
} }

View file

@ -1,15 +1,15 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.file;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.icon.FileIconManager; import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.comp.base.LazyTextFieldComp; import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.HumanReadableFormat; import io.xpipe.app.util.HumanReadableFormat;
@ -48,7 +48,7 @@ import java.util.Objects;
import static io.xpipe.app.util.HumanReadableFormat.byteCount; import static io.xpipe.app.util.HumanReadableFormat.byteCount;
import static javafx.scene.control.TableColumn.SortType.ASCENDING; import static javafx.scene.control.TableColumn.SortType.ASCENDING;
final class BrowserFileListComp extends SimpleComp { public final class BrowserFileListComp extends SimpleComp {
private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden"); private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden");
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty"); private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
@ -71,7 +71,8 @@ final class BrowserFileListComp extends SimpleComp {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private TableView<BrowserEntry> createTable() { private TableView<BrowserEntry> createTable() {
var filenameCol = new TableColumn<BrowserEntry, String>("Name"); var filenameCol = new TableColumn<BrowserEntry, String>();
filenameCol.textProperty().bind(AppI18n.observable("name"));
filenameCol.setCellValueFactory(param -> new SimpleStringProperty( filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
param.getValue() != null param.getValue() != null
? FileNames.getFileName( ? FileNames.getFileName(
@ -81,17 +82,20 @@ final class BrowserFileListComp extends SimpleComp {
filenameCol.setSortType(ASCENDING); filenameCol.setSortType(ASCENDING);
filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing())); filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing()));
var sizeCol = new TableColumn<BrowserEntry, Number>("Size"); var sizeCol = new TableColumn<BrowserEntry, Number>();
sizeCol.textProperty().bind(AppI18n.observable("size"));
sizeCol.setCellValueFactory(param -> new SimpleLongProperty( sizeCol.setCellValueFactory(param -> new SimpleLongProperty(
param.getValue().getRawFileEntry().resolved().getSize())); param.getValue().getRawFileEntry().resolved().getSize()));
sizeCol.setCellFactory(col -> new FileSizeCell()); sizeCol.setCellFactory(col -> new FileSizeCell());
var mtimeCol = new TableColumn<BrowserEntry, Instant>("Modified"); var mtimeCol = new TableColumn<BrowserEntry, Instant>();
mtimeCol.textProperty().bind(AppI18n.observable("modified"));
mtimeCol.setCellValueFactory(param -> new SimpleObjectProperty<>( mtimeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
param.getValue().getRawFileEntry().resolved().getDate())); param.getValue().getRawFileEntry().resolved().getDate()));
mtimeCol.setCellFactory(col -> new FileTimeCell()); mtimeCol.setCellFactory(col -> new FileTimeCell());
var modeCol = new TableColumn<BrowserEntry, String>("Attributes"); var modeCol = new TableColumn<BrowserEntry, String>();
modeCol.textProperty().bind(AppI18n.observable("attributes"));
modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>( modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
param.getValue().getRawFileEntry().resolved().getMode())); param.getValue().getRawFileEntry().resolved().getMode()));
modeCol.setCellFactory(col -> new FileModeCell()); modeCol.setCellFactory(col -> new FileModeCell());
@ -122,7 +126,7 @@ final class BrowserFileListComp extends SimpleComp {
} }
private void prepareTableSelectionModel(TableView<BrowserEntry> table) { private void prepareTableSelectionModel(TableView<BrowserEntry> table) {
if (!fileList.getMode().isMultiple()) { if (!fileList.getSelectionMode().isMultiple()) {
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
} else { } else {
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE); table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
@ -142,9 +146,9 @@ final class BrowserFileListComp extends SimpleComp {
.getPath())); .getPath()));
// Remove unsuitable selection // Remove unsuitable selection
toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY
&& !fileList.getMode().isAcceptsDirectories()) && !fileList.getSelectionMode().isAcceptsDirectories())
|| (browserEntry.getRawFileEntry().getKind() != FileKind.DIRECTORY || (browserEntry.getRawFileEntry().getKind() != FileKind.DIRECTORY
&& !fileList.getMode().isAcceptsFiles())); && !fileList.getSelectionMode().isAcceptsFiles()));
fileList.getSelection().setAll(toSelect); fileList.getSelection().setAll(toSelect);
Platform.runLater(() -> { Platform.runLater(() -> {
@ -505,16 +509,21 @@ final class BrowserFileListComp extends SimpleComp {
.get(); .get();
var quickAccess = new BrowserQuickAccessButtonComp( var quickAccess = new BrowserQuickAccessButtonComp(
() -> getTableRow().getItem(), fileList.getFileSystemModel()) () -> getTableRow().getItem(), fileList.getFileSystemModel())
.hide(BindingsHelper.persist(Bindings.createBooleanBinding( .hide(Bindings.createBooleanBinding(
() -> { () -> {
var item = getTableRow().getItem(); var item = getTableRow().getItem();
var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY; var notDir = item.getRawFileEntry()
var isParentLink = item .resolved()
.getRawFileEntry() .getKind()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory()); != FileKind.DIRECTORY;
var isParentLink = item.getRawFileEntry()
.equals(fileList.getFileSystemModel()
.getCurrentParentDirectory());
return notDir || isParentLink; return notDir || isParentLink;
}, },
itemProperty()))) itemProperty())
.not()
.not())
.createRegion(); .createRegion();
editing.addListener((observable, oldValue, newValue) -> { editing.addListener((observable, oldValue, newValue) -> {

View file

@ -1,5 +1,7 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserClipboard;
import io.xpipe.app.browser.BrowserSelectionListComp;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import javafx.geometry.Point2D; import javafx.geometry.Point2D;
import javafx.scene.Node; import javafx.scene.Node;

View file

@ -1,6 +1,7 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.file;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
@ -25,6 +26,8 @@ public final class BrowserFileListModel {
static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR = static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR =
Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY); Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
private final OpenFileSystemModel.SelectionMode selectionMode;
private final OpenFileSystemModel fileSystemModel; private final OpenFileSystemModel fileSystemModel;
private final Property<Comparator<BrowserEntry>> comparatorProperty = private final Property<Comparator<BrowserEntry>> comparatorProperty =
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR); new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
@ -33,13 +36,14 @@ public final class BrowserFileListModel {
private final ObservableList<BrowserEntry> previousSelection = FXCollections.observableArrayList(); private final ObservableList<BrowserEntry> previousSelection = FXCollections.observableArrayList();
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList(); private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
private final ObservableList<FileSystem.FileEntry> selectedRaw = private final ObservableList<FileSystem.FileEntry> selectedRaw =
BindingsHelper.mappedContentBinding(selection, entry -> entry.getRawFileEntry()); ListBindingsHelper.mappedContentBinding(selection, entry -> entry.getRawFileEntry());
private final Property<BrowserEntry> draggedOverDirectory = new SimpleObjectProperty<>(); private final Property<BrowserEntry> draggedOverDirectory = new SimpleObjectProperty<>();
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty(); private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
private final Property<BrowserEntry> editing = new SimpleObjectProperty<>(); private final Property<BrowserEntry> editing = new SimpleObjectProperty<>();
public BrowserFileListModel(OpenFileSystemModel fileSystemModel) { public BrowserFileListModel(OpenFileSystemModel.SelectionMode selectionMode, OpenFileSystemModel fileSystemModel) {
this.selectionMode = selectionMode;
this.fileSystemModel = fileSystemModel; this.fileSystemModel = fileSystemModel;
fileSystemModel.getFilter().addListener((observable, oldValue, newValue) -> { fileSystemModel.getFilter().addListener((observable, oldValue, newValue) -> {
@ -51,10 +55,6 @@ public final class BrowserFileListModel {
}); });
} }
public BrowserModel.Mode getMode() {
return fileSystemModel.getBrowserModel().getMode();
}
public void setAll(Stream<FileSystem.FileEntry> newFiles) { public void setAll(Stream<FileSystem.FileEntry> newFiles) {
try (var s = newFiles) { try (var s = newFiles) {
var parent = fileSystemModel.getCurrentParentDirectory(); var parent = fileSystemModel.getCurrentParentDirectory();
@ -135,12 +135,6 @@ public final class BrowserFileListModel {
} }
public void onDoubleClick(BrowserEntry entry) { public void onDoubleClick(BrowserEntry entry) {
if (entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY
&& getMode().equals(BrowserModel.Mode.SINGLE_FILE_CHOOSER)) {
getFileSystemModel().getBrowserModel().finishChooser();
return;
}
if (entry.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) { if (entry.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
fileSystemModel.cdAsync(entry.getRawFileEntry().resolved().getPath()); fileSystemModel.cdAsync(entry.getRawFileEntry().resolved().getPath());
} }

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.icon.BrowserIcons; import io.xpipe.app.browser.icon.BrowserIcons;
import io.xpipe.app.comp.base.ListBoxViewComp; import io.xpipe.app.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.VBoxViewComp; import io.xpipe.app.comp.base.VBoxViewComp;
@ -31,8 +32,10 @@ public class BrowserFileOverviewComp extends SimpleComp {
Function<FileSystem.FileEntry, Comp<?>> factory = entry -> { Function<FileSystem.FileEntry, Comp<?>> factory = entry -> {
return Comp.of(() -> { return Comp.of(() -> {
var icon = BrowserIcons.createIcon(entry); var icon = BrowserIcons.createIcon(entry);
var graphic = new HorizontalComp(List.of(icon, var graphic = new HorizontalComp(List.of(
new BrowserQuickAccessButtonComp(() -> new BrowserEntry(entry, model.getFileList(),false),model))); icon,
new BrowserQuickAccessButtonComp(
() -> new BrowserEntry(entry, model.getFileList(), false), model)));
var l = new Button(entry.getPath(), graphic.createRegion()); var l = new Button(entry.getPath(), graphic.createRegion());
l.setGraphicTextGap(1); l.setGraphicTextGap(1);
l.setOnAction(event -> { l.setOnAction(event -> {

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.util.InputHelper; import io.xpipe.app.util.InputHelper;

View file

@ -1,5 +1,6 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.file;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.icon.FileIconManager; import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.util.BooleanAnimationTimer; import io.xpipe.app.util.BooleanAnimationTimer;
@ -28,18 +29,105 @@ import java.util.stream.Collectors;
public class BrowserQuickAccessContextMenu extends ContextMenu { public class BrowserQuickAccessContextMenu extends ContextMenu {
private final Supplier<BrowserEntry> base;
private final OpenFileSystemModel model;
private ContextMenu shownBrowserActionsMenu;
private boolean expandBrowserActionMenuKey;
private boolean keyBasedNavigation;
private boolean closeBrowserActionMenuKey;
public BrowserQuickAccessContextMenu(Supplier<BrowserEntry> base, OpenFileSystemModel model) {
this.base = base;
this.model = model;
addEventFilter(Menu.ON_SHOWING, e -> {
Node content = getSkin().getNode();
if (content instanceof Region r) {
r.setMaxWidth(500);
}
});
addEventFilter(Menu.ON_SHOWN, e -> {
Platform.runLater(() -> {
getItems().getFirst().getStyleableNode().requestFocus();
});
});
InputHelper.onLeft(this, false, e -> {
hide();
e.consume();
});
setAutoHide(true);
getStyleClass().add("condensed");
}
public void showMenu(Node anchor) {
getItems().clear();
ThreadHelper.runFailableAsync(() -> {
var entry = base.get();
if (entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
return;
}
var actionsMenu = new AtomicReference<ContextMenu>();
var r = new Menu();
var newItems = updateMenuItems(r, entry, true);
Platform.runLater(() -> {
getItems().addAll(r.getItems());
show(anchor, Side.RIGHT, 0, 0);
});
});
}
private MenuItem createItem(BrowserEntry browserEntry) {
return new QuickAccessMenu(browserEntry).getMenu();
}
private List<MenuItem> updateMenuItems(Menu m, BrowserEntry entry, boolean updateInstantly) throws Exception {
var newFiles = model.getFileSystem()
.listFiles(entry.getRawFileEntry().resolved().getPath());
try (var s = newFiles) {
var list = s.map(fileEntry -> fileEntry.resolved()).toList();
// Wait until all files are listed, i.e. do not skip the stream elements
list = list.subList(0, Math.min(list.size(), 150));
var newItems = new ArrayList<MenuItem>();
if (list.isEmpty()) {
var empty = new Menu("<empty>");
empty.getStyleClass().add("leaf");
newItems.add(empty);
} else {
var browserEntries = list.stream()
.map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList(), false))
.toList();
var menus = browserEntries.stream()
.sorted(model.getFileList().order())
.collect(Collectors.toMap(e -> e, e -> createItem(e), (v1, v2) -> v2, LinkedHashMap::new));
var dirs = browserEntries.stream()
.filter(e -> e.getRawFileEntry().getKind() == FileKind.DIRECTORY)
.toList();
if (dirs.size() == 1) {
updateMenuItems((Menu) menus.get(dirs.getFirst()), dirs.getFirst(), true);
}
newItems.addAll(menus.values());
}
if (updateInstantly) {
m.getItems().setAll(newItems);
}
return newItems;
}
}
@Getter @Getter
class QuickAccessMenu { class QuickAccessMenu {
private final BrowserEntry browserEntry; private final BrowserEntry browserEntry;
private ContextMenu browserActionMenu;
private final Menu menu; private final Menu menu;
private ContextMenu browserActionMenu;
public QuickAccessMenu(BrowserEntry browserEntry) { public QuickAccessMenu(BrowserEntry browserEntry) {
this.browserEntry = browserEntry; this.browserEntry = browserEntry;
this.menu = new Menu( this.menu = new Menu(
// Use original name, not the link target // Use original name, not the link target
browserEntry.getRawFileEntry().getName(), browserEntry.getRawFileEntry().getName(),
PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24) PrettyImageHelper.ofFixedSizeSquare(
FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24)
.createRegion()); .createRegion());
createMenu(); createMenu();
addInputListeners(); addInputListeners();
@ -141,7 +229,8 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
}); });
new BooleanAnimationTimer(hover, 100, () -> { new BooleanAnimationTimer(hover, 100, () -> {
expandDirectoryMenu(empty); expandDirectoryMenu(empty);
}).start(); })
.start();
} }
private void addInputListeners() { private void addInputListeners() {
@ -154,7 +243,9 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
} else { } else {
expandBrowserActionMenuKey = false; expandBrowserActionMenuKey = false;
} }
if (event.getCode().equals(KeyCode.LEFT) && browserActionMenu != null && browserActionMenu.isShowing()) { if (event.getCode().equals(KeyCode.LEFT)
&& browserActionMenu != null
&& browserActionMenu.isShowing()) {
closeBrowserActionMenuKey = true; closeBrowserActionMenuKey = true;
} else { } else {
closeBrowserActionMenuKey = false; closeBrowserActionMenuKey = false;
@ -216,102 +307,4 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
}); });
} }
} }
private final Supplier<BrowserEntry> base;
private final OpenFileSystemModel model;
private ContextMenu shownBrowserActionsMenu;
private boolean expandBrowserActionMenuKey;
private boolean keyBasedNavigation;
private boolean closeBrowserActionMenuKey;
public BrowserQuickAccessContextMenu(Supplier<BrowserEntry> base, OpenFileSystemModel model) {
this.base = base;
this.model = model;
addEventFilter(Menu.ON_SHOWING, e -> {
Node content = getSkin().getNode();
if (content instanceof Region r) {
r.setMaxWidth(500);
}
});
addEventFilter(Menu.ON_SHOWN, e -> {
Platform.runLater(() -> {
getItems().getFirst().getStyleableNode().requestFocus();
});
});
InputHelper.onLeft(this, false, e -> {
hide();
e.consume();
});
setAutoHide(true);
getStyleClass().add("condensed");
}
public void showMenu(Node anchor) {
getItems().clear();
ThreadHelper.runFailableAsync(() -> {
var entry = base.get();
if (entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
return;
}
var actionsMenu = new AtomicReference<ContextMenu>();
var r = new Menu();
var newItems = updateMenuItems(r, entry, true);
Platform.runLater(() -> {
getItems().addAll(r.getItems());
show(anchor, Side.RIGHT, 0, 0);
});
});
}
private MenuItem createItem(BrowserEntry browserEntry) {
return new QuickAccessMenu(browserEntry).getMenu();
}
private List<MenuItem> updateMenuItems(
Menu m,
BrowserEntry entry,
boolean updateInstantly)
throws Exception {
var newFiles = model.getFileSystem().listFiles(entry.getRawFileEntry().resolved().getPath());
try (var s = newFiles) {
var list = s.map(fileEntry -> fileEntry.resolved()).toList();
// Wait until all files are listed, i.e. do not skip the stream elements
list = list.subList(0, Math.min(list.size(), 150));
var newItems = new ArrayList<MenuItem>();
if (list.isEmpty()) {
var empty = new Menu("<empty>");
empty.getStyleClass().add("leaf");
newItems.add(empty);
} else {
var browserEntries = list.stream()
.map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList(), false))
.toList();
var menus = browserEntries.stream()
.sorted(model.getFileList().order())
.collect(Collectors.toMap(
e -> e,
e -> createItem(e),
(v1, v2) -> v2,
LinkedHashMap::new));
var dirs = browserEntries.stream()
.filter(e -> e.getRawFileEntry().getKind() == FileKind.DIRECTORY)
.toList();
if (dirs.size() == 1) {
updateMenuItems(
(Menu) menus.get(dirs.getFirst()),
dirs.getFirst(),
true);
}
newItems.addAll(menus.values());
}
if (updateInstantly) {
m.getItems().setAll(newItems);
}
return newItems;
}
}
} }

View file

@ -1,17 +1,17 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.file;
import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import io.xpipe.core.store.FileKind; import io.xpipe.core.store.*;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.LocalStore;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Instant;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
@ -151,6 +151,18 @@ public class FileSystemHelper {
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE); Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
} }
public static FileSystem.FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
return new FileSystem.FileEntry(
fileSystem,
file,
Instant.now(),
false,
false,
fileSystem.getFileSize(file),
null,
fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE);
}
public static void dropLocalFilesInto( public static void dropLocalFilesInto(
FileSystem.FileEntry entry, FileSystem.FileEntry entry,
List<Path> files, List<Path> files,
@ -278,7 +290,8 @@ public class FileSystemHelper {
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath())); var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath()); List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
for (FileSystem.FileEntry fileEntry : list) { for (FileSystem.FileEntry fileEntry : list) {
flatFiles.put(fileEntry, FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()))); var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
flatFiles.put(fileEntry, rel);
if (fileEntry.getKind() == FileKind.FILE) { if (fileEntry.getKind() == FileKind.FILE) {
// This one is up-to-date and does not need to be recalculated // This one is up-to-date and does not need to be recalculated
totalSize.addAndGet(fileEntry.getSize()); totalSize.addAndGet(fileEntry.getSize());
@ -293,7 +306,8 @@ public class FileSystemHelper {
AtomicLong transferred = new AtomicLong(); AtomicLong transferred = new AtomicLong();
for (var e : flatFiles.entrySet()) { for (var e : flatFiles.entrySet()) {
var sourceFile = e.getKey(); var sourceFile = e.getKey();
var targetFile = FileNames.join(target.getPath(), e.getValue()); var fixedRelPath = new FilePath(e.getValue()).fileSystemCompatible(target.getFileSystem().getShell().orElseThrow().getOsType());
var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString());
if (sourceFile.getFileSystem().equals(target.getFileSystem())) { if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
throw new IllegalStateException(); throw new IllegalStateException();
} }

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.fs;
import io.xpipe.app.util.ShellControlCache; import io.xpipe.app.util.ShellControlCache;
import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellControl;

View file

@ -1,7 +1,13 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.fs;
import atlantafx.base.controls.Spacer; import atlantafx.base.controls.Spacer;
import io.xpipe.app.browser.BrowserFilterComp;
import io.xpipe.app.browser.BrowserNavBar;
import io.xpipe.app.browser.BrowserOverviewComp;
import io.xpipe.app.browser.BrowserStatusBarComp;
import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.file.BrowserContextMenu;
import io.xpipe.app.browser.file.BrowserFileListComp;
import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
@ -9,7 +15,6 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.Shortcuts; import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -55,7 +60,9 @@ public class OpenFileSystemComp extends SimpleComp {
var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open")); var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open"));
new ContextMenuAugment<>( new ContextMenuAugment<>(
event -> event.getButton() == MouseButton.PRIMARY, null, () -> new BrowserContextMenu(model, null)) event -> event.getButton() == MouseButton.PRIMARY,
null,
() -> new BrowserContextMenu(model, null))
.augment(new SimpleCompStructure<>(menuButton)); .augment(new SimpleCompStructure<>(menuButton));
menuButton.disableProperty().bind(model.getInOverview()); menuButton.disableProperty().bind(model.getInOverview());
menuButton.setAccessibleText("Directory options"); menuButton.setAccessibleText("Directory options");
@ -97,7 +104,7 @@ public class OpenFileSystemComp extends SimpleComp {
home, home,
model.getCurrentPath().isNull(), model.getCurrentPath().isNull(),
fileList, fileList,
BindingsHelper.persist(model.getCurrentPath().isNull().not()))); model.getCurrentPath().isNull().not()));
return stack.createRegion(); return stack.createRegion();
} }
} }

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.fs;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.BooleanBinding;

View file

@ -1,7 +1,15 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.fs;
import io.xpipe.app.browser.BrowserSavedState;
import io.xpipe.app.browser.BrowserTransferProgress;
import io.xpipe.app.browser.action.BrowserAction; import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.file.BrowserFileListModel;
import io.xpipe.app.browser.file.FileSystemHelper;
import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.browser.session.BrowserSessionTab;
import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
@ -27,45 +35,90 @@ import java.util.Optional;
import java.util.stream.Stream; import java.util.stream.Stream;
@Getter @Getter
public final class OpenFileSystemModel { public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore> {
private final DataStoreEntryRef<? extends FileSystemStore> entry;
private final Property<String> filter = new SimpleStringProperty(); private final Property<String> filter = new SimpleStringProperty();
private final BrowserFileListModel fileList; private final BrowserFileListModel fileList;
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>(); private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
private final OpenFileSystemHistory history = new OpenFileSystemHistory(); private final OpenFileSystemHistory history = new OpenFileSystemHistory();
private final BooleanProperty busy = new SimpleBooleanProperty();
private final BrowserModel browserModel;
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>(); private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
private final BooleanProperty inOverview = new SimpleBooleanProperty(); private final BooleanProperty inOverview = new SimpleBooleanProperty();
private final String name;
private final String tooltip;
private final Property<BrowserTransferProgress> progress = private final Property<BrowserTransferProgress> progress =
new SimpleObjectProperty<>(BrowserTransferProgress.empty()); new SimpleObjectProperty<>(BrowserTransferProgress.empty());
private FileSystem fileSystem; private FileSystem fileSystem;
private OpenFileSystemSavedState savedState; private OpenFileSystemSavedState savedState;
private OpenFileSystemCache cache; private OpenFileSystemCache cache;
public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef<? extends FileSystemStore> entry) { public OpenFileSystemModel(
this.browserModel = browserModel; BrowserAbstractSessionModel<?> model,
this.entry = entry; DataStoreEntryRef<? extends FileSystemStore> entry,
this.name = DataStorage.get().getStoreDisplayName(entry.get()); SelectionMode selectionMode) {
this.tooltip = DataStorage.get().getId(entry.getEntry()).toString(); super(model, entry);
this.inOverview.bind(Bindings.createBooleanBinding( this.inOverview.bind(Bindings.createBooleanBinding(
() -> { () -> {
return currentPath.get() == null; return currentPath.get() == null;
}, },
currentPath)); currentPath));
fileList = new BrowserFileListModel(this); fileList = new BrowserFileListModel(selectionMode, this);
} }
public boolean isBusy() { @Override
public Comp<?> comp() {
return new OpenFileSystemComp(this);
}
@Override
public boolean canImmediatelyClose() {
return !progress.getValue().done() return !progress.getValue().done()
|| (fileSystem != null || (fileSystem != null
&& fileSystem.getShell().isPresent() && fileSystem.getShell().isPresent()
&& fileSystem.getShell().get().getLock().isLocked()); && fileSystem.getShell().get().getLock().isLocked());
} }
@Override
public void init() throws Exception {
BooleanScope.execute(busy, () -> {
var fs = entry.getStore().createFileSystem();
if (fs.getShell().isPresent()) {
ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());
fs.getShell().get().onKill(() -> {
browserModel.closeAsync(this);
});
}
fs.open();
this.fileSystem = fs;
this.cache = new OpenFileSystemCache(this);
for (BrowserAction b : BrowserAction.ALL) {
b.init(this);
}
});
this.savedState = OpenFileSystemSavedState.loadForStore(this);
}
@Override
public void close() {
if (fileSystem == null) {
return;
}
if (DataStorage.get().getStoreEntries().contains(getEntry().get())
&& savedState != null
&& getCurrentPath().get() != null) {
if (getBrowserModel() instanceof BrowserSessionModel bm) {
bm.getSavedState()
.add(new BrowserSavedState.Entry(
getEntry().get().getUuid(), getCurrentPath().get()));
}
}
try {
fileSystem.close();
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
}
fileSystem = null;
}
private void startIfNeeded() throws Exception { private void startIfNeeded() throws Exception {
if (fileSystem == null) { if (fileSystem == null) {
return; return;
@ -185,7 +238,7 @@ public final class OpenFileSystemModel {
var name = adjustedPath + " - " + entry.get().getName(); var name = adjustedPath + " - " + entry.get().getName();
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.getStartableDialects().stream() if (ShellDialects.getStartableDialects().stream()
.anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand(null)))) { .anyMatch(dialect -> adjustedPath.toLowerCase().startsWith(dialect.getExecutableName().toLowerCase()))) {
TerminalLauncher.open( TerminalLauncher.open(
entry.getEntry(), entry.getEntry(),
name, name,
@ -369,42 +422,10 @@ public final class OpenFileSystemModel {
}); });
} }
void closeSync() {
if (fileSystem == null) {
return;
}
try {
fileSystem.close();
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
}
fileSystem = null;
}
public boolean isClosed() { public boolean isClosed() {
return fileSystem == null; return fileSystem == null;
} }
public void initFileSystem() throws Exception {
BooleanScope.execute(busy, () -> {
var fs = entry.getStore().createFileSystem();
if (fs.getShell().isPresent()) {
ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());
fs.getShell().get().onKill(() -> {
browserModel.closeFileSystemAsync(this);
});
}
fs.open();
this.fileSystem = fs;
this.cache = new OpenFileSystemCache(this);
for (BrowserAction b : BrowserAction.ALL) {
b.init(this);
}
});
}
public void initWithGivenDirectory(String dir) throws Exception { public void initWithGivenDirectory(String dir) throws Exception {
cdSyncWithoutCheck(dir); cdSyncWithoutCheck(dir);
} }
@ -414,10 +435,6 @@ public final class OpenFileSystemModel {
history.updateCurrent(null); history.updateCurrent(null);
} }
void initSavedState() {
this.savedState = OpenFileSystemSavedState.loadForStore(this);
}
public void openTerminalAsync(String directory) { public void openTerminalAsync(String directory) {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) { if (fileSystem == null) {
@ -445,4 +462,23 @@ public final class OpenFileSystemModel {
public void forthSync(int i) throws Exception { public void forthSync(int i) throws Exception {
cdSyncWithoutCheck(history.forth(i)); cdSyncWithoutCheck(history.forth(i));
} }
@Getter
public enum SelectionMode {
SINGLE_FILE(false, true, false),
MULTIPLE_FILE(true, true, false),
SINGLE_DIRECTORY(false, false, true),
MULTIPLE_DIRECTORY(true, false, true),
ALL(true, true, true);
private final boolean multiple;
private final boolean acceptsFiles;
private final boolean acceptsDirectories;
SelectionMode(boolean multiple, boolean acceptsFiles, boolean acceptsDirectories) {
this.multiple = multiple;
this.acceptsFiles = acceptsFiles;
this.acceptsDirectories = acceptsDirectories;
}
}
} }

View file

@ -1,4 +1,4 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.fs;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonParser;

View file

@ -15,18 +15,18 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public interface BrowserIconDirectoryType { public abstract class BrowserIconDirectoryType {
List<BrowserIconDirectoryType> ALL = new ArrayList<>(); private static final List<BrowserIconDirectoryType> ALL = new ArrayList<>();
static BrowserIconDirectoryType byId(String id) { public static synchronized BrowserIconDirectoryType byId(String id) {
return ALL.stream() return ALL.stream()
.filter(fileType -> fileType.getId().equals(id)) .filter(fileType -> fileType.getId().equals(id))
.findAny() .findAny()
.orElseThrow(); .orElseThrow();
} }
static void loadDefinitions() { public static synchronized void loadDefinitions() {
ALL.add(new BrowserIconDirectoryType() { ALL.add(new BrowserIconDirectoryType() {
@Override @Override
@ -74,13 +74,17 @@ public interface BrowserIconDirectoryType {
}); });
} }
String getId(); public static synchronized List<BrowserIconDirectoryType> getAll() {
return ALL;
}
boolean matches(FileSystem.FileEntry entry); public abstract String getId();
String getIcon(FileSystem.FileEntry entry, boolean open); public abstract boolean matches(FileSystem.FileEntry entry);
class Simple implements BrowserIconDirectoryType { public abstract String getIcon(FileSystem.FileEntry entry, boolean open);
public static class Simple extends BrowserIconDirectoryType {
@Getter @Getter
private final String id; private final String id;

View file

@ -12,18 +12,18 @@ import java.nio.file.Files;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public interface BrowserIconFileType { public abstract class BrowserIconFileType {
List<BrowserIconFileType> ALL = new ArrayList<>(); private static final List<BrowserIconFileType> ALL = new ArrayList<>();
static BrowserIconFileType byId(String id) { public static synchronized BrowserIconFileType byId(String id) {
return ALL.stream() return ALL.stream()
.filter(fileType -> fileType.getId().equals(id)) .filter(fileType -> fileType.getId().equals(id))
.findAny() .findAny()
.orElseThrow(); .orElseThrow();
} }
static void loadDefinitions() { public static synchronized void loadDefinitions() {
AppResources.with(AppResources.XPIPE_MODULE, "file_list.txt", path -> { AppResources.with(AppResources.XPIPE_MODULE, "file_list.txt", path -> {
try (var reader = try (var reader =
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) { new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
@ -53,14 +53,18 @@ public interface BrowserIconFileType {
}); });
} }
String getId(); public static synchronized List<BrowserIconFileType> getAll() {
return ALL;
}
boolean matches(FileSystem.FileEntry entry); public abstract String getId();
String getIcon(); public abstract boolean matches(FileSystem.FileEntry entry);
public abstract String getIcon();
@Getter @Getter
class Simple implements BrowserIconFileType { public static class Simple extends BrowserIconFileType {
private final String id; private final String id;
private final IconVariant icon; private final IconVariant icon;

View file

@ -11,29 +11,27 @@ public class FileIconManager {
public static synchronized void loadIfNecessary() { public static synchronized void loadIfNecessary() {
if (!loaded) { if (!loaded) {
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons"); AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons", true, false);
loaded = true; loaded = true;
} }
} }
public static String getFileIcon(FileSystem.FileEntry entry, boolean open) { public static synchronized String getFileIcon(FileSystem.FileEntry entry, boolean open) {
if (entry == null) { if (entry == null) {
return null; return null;
} }
loadIfNecessary();
var r = entry.resolved(); var r = entry.resolved();
if (r.getKind() != FileKind.DIRECTORY) { if (r.getKind() != FileKind.DIRECTORY) {
for (var f : BrowserIconFileType.ALL) { for (var f : BrowserIconFileType.getAll()) {
if (f.matches(r)) { if (f.matches(r)) {
return getIconPath(f.getIcon()); return f.getIcon();
} }
} }
} else { } else {
for (var f : BrowserIconDirectoryType.ALL) { for (var f : BrowserIconDirectoryType.getAll()) {
if (f.matches(r)) { if (f.matches(r)) {
return getIconPath(f.getIcon(r, open)); return f.getIcon(r, open);
} }
} }
} }
@ -42,8 +40,4 @@ public class FileIconManager {
? (open ? "default_folder_opened.svg" : "default_folder.svg") ? (open ? "default_folder_opened.svg" : "default_folder.svg")
: "default_file.svg"; : "default_file.svg";
} }
private static String getIconPath(String name) {
return name;
}
} }

View file

@ -0,0 +1,43 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
@Getter
public class BrowserAbstractSessionModel<T extends BrowserSessionTab<?>> {
protected final ObservableList<T> sessionEntries = FXCollections.observableArrayList();
protected final Property<T> selectedEntry = new SimpleObjectProperty<>();
public void closeAsync(BrowserSessionTab<?> e) {
ThreadHelper.runAsync(() -> {
closeSync(e);
});
}
public void openSync(T e, BooleanProperty externalBusy) throws Exception {
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
e.init();
// Prevent multiple calls from interfering with each other
synchronized (this) {
sessionEntries.add(e);
// The tab pane doesn't automatically select new tabs
selectedEntry.setValue(e);
}
}
}
void closeSync(BrowserSessionTab<?> e) {
e.close();
synchronized (BrowserAbstractSessionModel.this) {
this.sessionEntries.remove(e);
}
}
}

View file

@ -0,0 +1,150 @@
package io.xpipe.app.browser.session;
import atlantafx.base.controls.Spacer;
import io.xpipe.app.browser.BrowserBookmarkComp;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemComp;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.comp.base.SideSplitPaneComp;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppWindowHelper;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.store.ShellStore;
import javafx.beans.property.BooleanProperty;
import javafx.collections.ListChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
public class BrowserChooserComp extends SimpleComp {
private final BrowserChooserModel model;
public BrowserChooserComp(BrowserChooserModel model) {
this.model = model;
}
public static void openSingleFile(
Supplier<DataStoreEntryRef<? extends FileSystemStore>> store, Consumer<FileReference> file, boolean save) {
PlatformThread.runLaterIfNeeded(() -> {
var model = new BrowserChooserModel(OpenFileSystemModel.SelectionMode.SINGLE_FILE);
var comp = new BrowserChooserComp(model)
.apply(struc -> struc.get().setPrefSize(1200, 700))
.apply(struc -> AppFont.normal(struc.get()));
var window = AppWindowHelper.sideWindow(
AppI18n.get(save ? "saveFileTitle" : "openFileTitle"), stage -> comp, false, null);
model.setOnFinish(fileStores -> {
file.accept(fileStores.size() > 0 ? fileStores.getFirst() : null);
window.close();
});
window.show();
ThreadHelper.runAsync(() -> {
model.openFileSystemAsync(store.get(), null, null);
});
});
}
@Override
protected Region createSimple() {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore)
&& storeEntryWrapper.getEntry().getValidity().isUsable();
};
BiConsumer<StoreEntryWrapper, BooleanProperty> action = (w, busy) -> {
ThreadHelper.runFailableAsync(() -> {
var entry = w.getEntry();
if (!entry.getValidity().isUsable()) {
return;
}
if (entry.getStore() instanceof ShellStore fileSystem) {
model.openFileSystemAsync(entry.ref(), null, busy);
}
});
};
var bookmarksList = new BrowserBookmarkComp(
BindingsHelper.map(
model.getSelectedEntry(), v -> v.getEntry().get()),
applicable,
action)
.vgrow();
var stack = Comp.of(() -> {
var s = new StackPane();
model.getSelectedEntry().subscribe(selected -> {
PlatformThread.runLaterIfNeeded(() -> {
if (selected != null) {
s.getChildren().setAll(new OpenFileSystemComp(selected).createRegion());
} else {
s.getChildren().clear();
}
});
});
return s;
});
var splitPane = new SideSplitPaneComp(bookmarksList, stack)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
});
var r = addBottomBar(splitPane.createRegion());
r.getStyleClass().add("browser");
return r;
}
private Region addBottomBar(Region r) {
var selectedLabel = new Label("Selected: ");
selectedLabel.setAlignment(Pos.CENTER);
var selected = new HBox();
selected.setAlignment(Pos.CENTER_LEFT);
selected.setSpacing(10);
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.setPrefWidth(500);
return field;
})
.toList());
});
});
var spacer = new Spacer(Orientation.HORIZONTAL);
var button = new Button("Select");
button.setPadding(new Insets(5, 10, 5, 10));
button.setOnAction(event -> model.finishChooser());
button.setDefaultButton(true);
var bottomBar = new HBox(selectedLabel, selected, spacer, button);
HBox.setHgrow(selected, Priority.ALWAYS);
bottomBar.setAlignment(Pos.CENTER);
bottomBar.getStyleClass().add("chooser-bar");
var layout = new VBox(r, bottomBar);
VBox.setVgrow(r, Priority.ALWAYS);
return layout;
}
}

View file

@ -0,0 +1,106 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.FileReference;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
@Getter
public class BrowserChooserModel extends BrowserAbstractSessionModel<OpenFileSystemModel> {
private final OpenFileSystemModel.SelectionMode selectionMode;
private final ObservableList<BrowserEntry> fileSelection = FXCollections.observableArrayList();
@Setter
private Consumer<List<FileReference>> onFinish;
public BrowserChooserModel(OpenFileSystemModel.SelectionMode selectionMode) {
this.selectionMode = selectionMode;
selectedEntry.addListener((observable, oldValue, newValue) -> {
if (newValue == null) {
fileSelection.clear();
return;
}
ListBindingsHelper.bindContent(fileSelection, newValue.getFileList().getSelection());
});
}
public void finishChooser() {
var chosen = new ArrayList<>(fileSelection);
synchronized (BrowserChooserModel.this) {
var open = selectedEntry.getValue();
if (open != null) {
ThreadHelper.runAsync(() -> {
open.close();
});
}
}
if (chosen.size() == 0) {
return;
}
var stores = chosen.stream()
.map(entry -> new FileReference(
selectedEntry.getValue().getEntry(),
entry.getRawFileEntry().getPath()))
.toList();
onFinish.accept(stores);
}
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
}
// Only load icons when a file system is opened
ThreadHelper.runAsync(() -> {
BrowserIconFileType.loadDefinitions();
BrowserIconDirectoryType.loadDefinitions();
FileIconManager.loadIfNecessary();
});
ThreadHelper.runFailableAsync(() -> {
OpenFileSystemModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
model = new OpenFileSystemModel(this, store, selectionMode);
model.init();
// Prevent multiple calls from interfering with each other
synchronized (BrowserChooserModel.this) {
selectedEntry.setValue(model);
sessionEntries.add(model);
}
if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
} else {
model.initWithDefaultDirectory();
}
}
});
}
}

View file

@ -0,0 +1,95 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.browser.BrowserBookmarkComp;
import io.xpipe.app.browser.BrowserTransferComp;
import io.xpipe.app.comp.base.SideSplitPaneComp;
import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.store.ShellStore;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.scene.layout.Region;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
public class BrowserSessionComp extends SimpleComp {
private final BrowserSessionModel model;
public BrowserSessionComp(BrowserSessionModel model) {
this.model = model;
}
@Override
protected Region createSimple() {
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
if (!storeEntryWrapper.getEntry().getValidity().isUsable()) {
return false;
}
if (storeEntryWrapper.getEntry().getStore() instanceof ShellStore) {
return true;
}
return storeEntryWrapper.getEntry().getProvider().browserAction(model,storeEntryWrapper.getEntry(), null) != null;
};
BiConsumer<StoreEntryWrapper, BooleanProperty> action = (w, busy) -> {
ThreadHelper.runFailableAsync(() -> {
var entry = w.getEntry();
if (!entry.getValidity().isUsable()) {
return;
}
if (entry.getStore() instanceof ShellStore fileSystem) {
model.openFileSystemAsync(entry.ref(), null, busy);
}
var a = entry.getProvider().browserAction(model, entry, busy);
if (a != null) {
a.execute();
}
});
};
var bookmarksList = new BrowserBookmarkComp(
BindingsHelper.map(
model.getSelectedEntry(), v -> v.getEntry().get()),
applicable,
action)
.vgrow();
var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage())
.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
if (model.getSessionEntries().size() == 0) {
return true;
}
return false;
},
model.getSessionEntries(),
model.getSelectedEntry())));
localDownloadStage.prefHeight(200);
localDownloadStage.maxHeight(200);
var vertical = new VerticalComp(List.of(bookmarksList, localDownloadStage));
var tabs = new BrowserSessionTabsComp(model);
var splitPane = new SideSplitPaneComp(vertical, tabs)
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
});
var r = splitPane.createRegion();
r.getStyleClass().add("browser");
return r;
}
}

View file

@ -0,0 +1,107 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.browser.BrowserSavedState;
import io.xpipe.app.browser.BrowserSavedStateImpl;
import io.xpipe.app.browser.BrowserTransferModel;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.app.browser.icon.FileIconManager;
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.FileSystemStore;
import io.xpipe.core.util.FailableFunction;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import lombok.Getter;
import java.util.ArrayList;
@Getter
public class BrowserSessionModel extends BrowserAbstractSessionModel<BrowserSessionTab<?>> {
public static final BrowserSessionModel DEFAULT = new BrowserSessionModel(BrowserSavedStateImpl.load());
private final BrowserTransferModel localTransfersStage = new BrowserTransferModel(this);
private final BrowserSavedState savedState;
public BrowserSessionModel(BrowserSavedState savedState) {
this.savedState = savedState;
}
public void restoreState(BrowserSavedState state) {
ThreadHelper.runAsync(() -> {
state.getEntries().forEach(e -> {
restoreStateAsync(e, null);
// Don't try to run everything in parallel as that can be taxing
ThreadHelper.sleep(1000);
});
});
}
public void restoreStateAsync(BrowserSavedState.Entry e, BooleanProperty busy) {
var storageEntry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
storageEntry.ifPresent(entry -> {
openFileSystemAsync(entry.ref(), model -> e.getPath(), busy);
});
}
public void reset() {
synchronized (BrowserSessionModel.this) {
for (var o : new ArrayList<>(sessionEntries)) {
// Don't close busy connections gracefully
// as we otherwise might lock up
if (o.canImmediatelyClose()) {
continue;
}
closeSync(o);
}
if (savedState != null) {
savedState.save();
}
}
// Delete all files
localTransfersStage.clear();
}
public void openFileSystemAsync(
DataStoreEntryRef<? extends FileSystemStore> store,
FailableFunction<OpenFileSystemModel, String, Exception> path,
BooleanProperty externalBusy) {
if (store == null) {
return;
}
// Only load icons when a file system is opened
ThreadHelper.runAsync(() -> {
BrowserIconFileType.loadDefinitions();
BrowserIconDirectoryType.loadDefinitions();
FileIconManager.loadIfNecessary();
});
ThreadHelper.runFailableAsync(() -> {
OpenFileSystemModel model;
try (var b = new BooleanScope(externalBusy != null ? externalBusy : new SimpleBooleanProperty()).start()) {
model = new OpenFileSystemModel(this, store, OpenFileSystemModel.SelectionMode.ALL);
model.init();
// Prevent multiple calls from interfering with each other
synchronized (BrowserSessionModel.this) {
sessionEntries.add(model);
// The tab pane doesn't automatically select new tabs
selectedEntry.setValue(model);
}
}
if (path != null) {
model.initWithGivenDirectory(FileNames.toDirectory(path.apply(model)));
} else {
model.initWithDefaultDirectory();
}
});
}
}

View file

@ -0,0 +1,44 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import lombok.Getter;
@Getter
public class BrowserSessionMultiTab extends BrowserSessionTab<DataStore> {
protected final Property<BrowserSessionTab<?>> currentTab = new SimpleObjectProperty<>();
private final ObservableList<BrowserSessionTab<?>> allTabs = FXCollections.observableArrayList();
public BrowserSessionMultiTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<?> entry) {
super(browserModel, entry);
}
public Comp<?> comp() {
var map = FXCollections.<Comp<?>, ObservableValue<Boolean>>observableHashMap();
allTabs.addListener((ListChangeListener<? super BrowserSessionTab<?>>) c -> {
for (BrowserSessionTab<?> a : c.getAddedSubList()) {
map.put(a.comp(), BindingsHelper.map(currentTab, browserSessionTab -> a.equals(browserSessionTab)));
}
});
var mt = new MultiContentComp(map);
return mt;
}
public boolean canImmediatelyClose() {
return true;
}
public void init() throws Exception {}
public void close() {}
}

View file

@ -0,0 +1,34 @@
package io.xpipe.app.browser.session;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.core.store.DataStore;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import lombok.Getter;
@Getter
public abstract class BrowserSessionTab<T extends DataStore> {
protected final DataStoreEntryRef<? extends T> entry;
protected final BooleanProperty busy = new SimpleBooleanProperty();
protected final BrowserAbstractSessionModel<?> browserModel;
protected final String name;
protected final String tooltip;
public BrowserSessionTab(BrowserAbstractSessionModel<?> browserModel, DataStoreEntryRef<? extends T> entry) {
this.browserModel = browserModel;
this.entry = entry;
this.name = DataStorage.get().getStoreDisplayName(entry.get());
this.tooltip = DataStorage.get().getId(entry.getEntry()).toString();
}
public abstract Comp<?> comp();
public abstract boolean canImmediatelyClose();
public abstract void init() throws Exception;
public abstract void close();
}

View file

@ -1,40 +1,31 @@
package io.xpipe.app.browser; package io.xpipe.app.browser.session;
import atlantafx.base.controls.RingProgressIndicator; import atlantafx.base.controls.RingProgressIndicator;
import atlantafx.base.controls.Spacer;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.icon.BrowserIconDirectoryType; import io.xpipe.app.browser.BrowserWelcomeComp;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.comp.base.MultiContentComp; import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.comp.base.SideSplitPaneComp;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.*; import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.input.DragEvent; import javafx.scene.input.DragEvent;
import javafx.scene.layout.*; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -42,106 +33,25 @@ import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass; import static atlantafx.base.theme.Styles.toggleStyleClass;
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS; import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
public class BrowserComp extends SimpleComp { public class BrowserSessionTabsComp extends SimpleComp {
private final BrowserModel model; private final BrowserSessionModel model;
public BrowserComp(BrowserModel model) { public BrowserSessionTabsComp(BrowserSessionModel model) {
this.model = model; this.model = model;
} }
@Override public Region createSimple() {
protected Region createSimple() {
BrowserIconFileType.loadDefinitions();
BrowserIconDirectoryType.loadDefinitions();
ThreadHelper.runAsync(() -> {
FileIconManager.loadIfNecessary();
});
var bookmarksList = new BrowserBookmarkComp(model).vgrow();
var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage())
.hide(PlatformThread.sync(Bindings.createBooleanBinding(
() -> {
if (model.getOpenFileSystems().size() == 0) {
return true;
}
if (model.getMode().isChooser()) {
return true;
}
return false;
},
model.getOpenFileSystems(),
model.getSelected())));
localDownloadStage.prefHeight(200);
localDownloadStage.maxHeight(200);
var vertical = new VerticalComp(List.of(bookmarksList, localDownloadStage));
var splitPane = new SideSplitPaneComp(vertical, createTabs())
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
.withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
.apply(struc -> {
struc.getLeft().setMinWidth(200);
struc.getLeft().setMaxWidth(500);
});
var r = addBottomBar(splitPane.createRegion());
r.getStyleClass().add("browser");
// AppFont.small(r);
return r;
}
private Region addBottomBar(Region r) {
if (!model.getMode().isChooser()) {
return r;
}
var selectedLabel = new Label("Selected: ");
selectedLabel.setAlignment(Pos.CENTER);
var selected = new HBox();
selected.setAlignment(Pos.CENTER_LEFT);
selected.setSpacing(10);
model.getSelection().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.setPrefWidth(500);
return field;
})
.toList());
});
});
var spacer = new Spacer(Orientation.HORIZONTAL);
var button = new Button("Select");
button.setPadding(new Insets(5, 10, 5, 10));
button.setOnAction(event -> model.finishChooser());
button.setDefaultButton(true);
var bottomBar = new HBox(selectedLabel, selected, spacer, button);
HBox.setHgrow(selected, Priority.ALWAYS);
bottomBar.setAlignment(Pos.CENTER);
bottomBar.getStyleClass().add("chooser-bar");
var layout = new VBox(r, bottomBar);
VBox.setVgrow(r, Priority.ALWAYS);
return layout;
}
private Comp<?> createTabs() {
var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of( var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of(
Comp.of(() -> createTabPane()), Comp.of(() -> createTabPane()),
BindingsHelper.persist(Bindings.isNotEmpty(model.getOpenFileSystems())), Bindings.isNotEmpty(model.getSessionEntries()),
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)), new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
Bindings.createBooleanBinding( Bindings.createBooleanBinding(
() -> { () -> {
return model.getOpenFileSystems().size() == 0 return model.getSessionEntries().size() == 0;
&& !model.getMode().isChooser();
}, },
model.getOpenFileSystems()))); model.getSessionEntries())));
return multi; return multi.createRegion();
} }
private TabPane createTabPane() { private TabPane createTabPane() {
@ -153,16 +63,17 @@ public class BrowserComp extends SimpleComp {
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING); Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE); toggleStyleClass(tabs, DENSE);
var map = new HashMap<OpenFileSystemModel, Tab>(); var map = new HashMap<BrowserSessionTab<?>, Tab>();
// Restore state // Restore state
model.getOpenFileSystems().forEach(v -> { model.getSessionEntries().forEach(v -> {
var t = createTab(tabs, v); var t = createTab(tabs, v);
map.put(v, t); map.put(v, t);
tabs.getTabs().add(t); tabs.getTabs().add(t);
}); });
tabs.getSelectionModel() tabs.getSelectionModel()
.select(model.getOpenFileSystems().indexOf(model.getSelected().getValue())); .select(model.getSessionEntries()
.indexOf(model.getSelectedEntry().getValue()));
// Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually! // Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually!
var modifying = new SimpleBooleanProperty(); var modifying = new SimpleBooleanProperty();
@ -174,7 +85,7 @@ public class BrowserComp extends SimpleComp {
} }
if (newValue == null) { if (newValue == null) {
model.getSelected().setValue(null); model.getSelectedEntry().setValue(null);
return; return;
} }
@ -184,11 +95,11 @@ public class BrowserComp extends SimpleComp {
.findAny() .findAny()
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.orElse(null); .orElse(null);
model.getSelected().setValue(source); model.getSelectedEntry().setValue(source);
}); });
// Handle selection from model // Handle selection from model
model.getSelected().addListener((observable, oldValue, newValue) -> { model.getSelectedEntry().addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
if (newValue == null) { if (newValue == null) {
tabs.getSelectionModel().select(null); tabs.getSelectionModel().select(null);
@ -210,7 +121,7 @@ public class BrowserComp extends SimpleComp {
}); });
}); });
model.getOpenFileSystems().addListener((ListChangeListener<? super OpenFileSystemModel>) c -> { model.getSessionEntries().addListener((ListChangeListener<? super BrowserSessionTab>) c -> {
while (c.next()) { while (c.next()) {
for (var r : c.getRemoved()) { for (var r : c.getRemoved()) {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
@ -247,14 +158,14 @@ public class BrowserComp extends SimpleComp {
continue; continue;
} }
model.closeFileSystemAsync(source.getKey()); model.closeAsync(source.getKey());
} }
} }
}); });
return tabs; return tabs;
} }
private Tab createTab(TabPane tabs, OpenFileSystemModel model) { private Tab createTab(TabPane tabs, BrowserSessionTab<?> model) {
var tab = new Tab(); var tab = new Tab();
var ring = new RingProgressIndicator(0, false); var ring = new RingProgressIndicator(0, false);
@ -279,12 +190,12 @@ public class BrowserComp extends SimpleComp {
PlatformThread.sync(model.getBusy()))); PlatformThread.sync(model.getBusy())));
tab.setText(model.getName()); tab.setText(model.getName());
tab.setContent(new OpenFileSystemComp(model).createSimple()); tab.setContent(model.comp().createRegion());
var id = UUID.randomUUID().toString(); var id = UUID.randomUUID().toString();
tab.setId(id); tab.setId(id);
SimpleChangeListener.apply(tabs.skinProperty(), newValue -> { tabs.skinProperty().subscribe(newValue -> {
if (newValue != null) { if (newValue != null) {
Platform.runLater(() -> { Platform.runLater(() -> {
Label l = (Label) tabs.lookup("#" + id + " .tab-label"); Label l = (Label) tabs.lookup("#" + id + " .tab-label");
@ -303,7 +214,7 @@ public class BrowserComp extends SimpleComp {
if (color != null) { if (color != null) {
c.getStyleClass().add(color.getId()); c.getStyleClass().add(color.getId());
} }
new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c); new TooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c);
c.addEventHandler( c.addEventHandler(
DragEvent.DRAG_ENTERED, DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater( mouseEvent -> Platform.runLater(

View file

@ -7,13 +7,15 @@ import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class AppLayoutComp extends Comp<CompStructure<Pane>> { public class AppLayoutComp extends Comp<CompStructure<Pane>> {
@ -22,18 +24,20 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
@Override @Override
public CompStructure<Pane> createBase() { public CompStructure<Pane> createBase() {
var multi = new MultiContentComp(model.getEntries().stream() Map<Comp<?>, ObservableValue<Boolean>> map = model.getEntries().stream()
.collect(Collectors.toMap( .collect(Collectors.toMap(
entry -> entry.comp(), entry -> entry.comp(),
entry -> PlatformThread.sync(Bindings.createBooleanBinding( entry -> Bindings.createBooleanBinding(
() -> { () -> {
return model.getSelected().getValue().equals(entry); return model.getSelected().getValue().equals(entry);
}, },
model.getSelected()))))); model.getSelected())));
var multi = new MultiContentComp(map);
var pane = new BorderPane(); var pane = new BorderPane();
var sidebar = new SideMenuBarComp(model.getSelectedInternal(), model.getEntries()); var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries());
pane.setCenter(multi.createRegion()); StackPane multiR = (StackPane) multi.createRegion();
pane.setCenter(multiR);
pane.setRight(sidebar.createRegion()); pane.setRight(sidebar.createRegion());
pane.getStyleClass().add("background"); pane.getStyleClass().add("background");
model.getSelected().addListener((c, o, n) -> { model.getSelected().addListener((c, o, n) -> {

View file

@ -3,7 +3,6 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@ -50,7 +49,7 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
var graphic = getGraphic(); var graphic = getGraphic();
if (graphic instanceof FontIcon f) { if (graphic instanceof FontIcon f) {
// f.iconColorProperty().bind(button.textFillProperty()); // f.iconColorProperty().bind(button.textFillProperty());
SimpleChangeListener.apply(button.fontProperty(), c -> { button.fontProperty().subscribe(c -> {
f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels()); f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
}); });
} }

View file

@ -4,8 +4,7 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.css.Size; import javafx.css.Size;
import javafx.css.SizeUnits; import javafx.css.SizeUnits;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -38,12 +37,12 @@ public class DropdownComp extends Comp<CompStructure<Button>> {
.createRegion(); .createRegion();
button.visibleProperty() button.visibleProperty()
.bind(BindingsHelper.anyMatch(cm.getItems().stream() .bind(ListBindingsHelper.anyMatch(cm.getItems().stream()
.map(menuItem -> menuItem.getGraphic().visibleProperty()) .map(menuItem -> menuItem.getGraphic().visibleProperty())
.toList())); .toList()));
var graphic = new FontIcon("mdi2c-chevron-double-down"); var graphic = new FontIcon("mdi2c-chevron-double-down");
SimpleChangeListener.apply(button.fontProperty(), c -> { button.fontProperty().subscribe(c -> {
graphic.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels()); graphic.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
}); });

View file

@ -0,0 +1,47 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.StackPane;
import lombok.AllArgsConstructor;
import lombok.Value;
import org.kordamp.ikonli.javafx.FontIcon;
@AllArgsConstructor
public class FontIconComp extends Comp<FontIconComp.Structure> {
private final ObservableValue<String> icon;
public FontIconComp(String icon) {
this.icon = new SimpleStringProperty(icon);
}
@Override
public FontIconComp.Structure createBase() {
var fi = new FontIcon();
var obs = PlatformThread.sync(icon);
icon.subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
fi.setIconLiteral(val);
});
});
var pane = new StackPane(fi);
return new FontIconComp.Structure(fi, pane);
}
@Value
public static class Structure implements CompStructure<StackPane> {
FontIcon icon;
StackPane pane;
@Override
public StackPane get() {
return pane;
}
}
}

View file

@ -3,7 +3,6 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.TextField; import javafx.scene.control.TextField;
@ -65,7 +64,7 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
sp.prefHeightProperty().bind(r.prefHeightProperty()); sp.prefHeightProperty().bind(r.prefHeightProperty());
r.setDisable(true); r.setDisable(true);
SimpleChangeListener.apply(currentValue, n -> { currentValue.subscribe(n -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
// Check if control value is the same. Then don't set it as that might cause bugs // Check if control value is the same. Then don't set it as that might cause bugs
if (Objects.equals(r.getText(), n) || (n == null && r.getText().isEmpty())) { if (Objects.equals(r.getText(), n) || (n == null && r.getText().isEmpty())) {

View file

@ -3,7 +3,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.collections.ListChangeListener; import javafx.collections.ListChangeListener;
@ -88,7 +88,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
} }
if (!listView.getChildren().equals(newShown)) { if (!listView.getChildren().equals(newShown)) {
BindingsHelper.setContent(listView.getChildren(), newShown); ListBindingsHelper.setContent(listView.getChildren(), newShown);
} }
}; };

View file

@ -6,7 +6,6 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.Hyperlinks;
@ -59,7 +58,7 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow(); var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow();
wv.getEngine().setUserStyleSheetLocation(url.toString()); wv.getEngine().setUserStyleSheetLocation(url.toString());
SimpleChangeListener.apply(PlatformThread.sync(markdown), val -> { PlatformThread.sync(markdown).subscribe(val -> {
// Workaround for https://bugs.openjdk.org/browse/JDK-8199014 // Workaround for https://bugs.openjdk.org/browse/JDK-8199014
try { try {
var file = Files.createTempFile(null, ".html"); var file = Files.createTempFile(null, ".html");

View file

@ -3,8 +3,10 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableMap;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
@ -12,24 +14,54 @@ import java.util.Map;
public class MultiContentComp extends SimpleComp { public class MultiContentComp extends SimpleComp {
private final Map<Comp<?>, ObservableValue<Boolean>> content; private final ObservableMap<Comp<?>, ObservableValue<Boolean>> content;
public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content) { public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content) {
this.content = FXCollections.observableMap(content);
}
public MultiContentComp(ObservableMap<Comp<?>, ObservableValue<Boolean>> content) {
this.content = content; this.content = content;
} }
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var stack = new StackPane(); ObservableMap<Comp<?>, Region> m = FXCollections.observableHashMap();
stack.setPickOnBounds(false); content.addListener((MapChangeListener<? super Comp<?>, ? super ObservableValue<Boolean>>) change -> {
for (Map.Entry<Comp<?>, ObservableValue<Boolean>> entry : content.entrySet()) { if (change.wasAdded()) {
var region = entry.getKey().createRegion(); var r = change.getKey().createRegion();
stack.getChildren().add(region); change.getValueAdded().subscribe(val -> {
SimpleChangeListener.apply(PlatformThread.sync(entry.getValue()), val -> { PlatformThread.runLaterIfNeeded(() -> {
region.setManaged(val); r.setManaged(val);
region.setVisible(val); r.setVisible(val);
}); });
});
m.put(change.getKey(), r);
} else {
m.remove(change.getKey());
} }
});
var stack = new StackPane();
m.addListener((MapChangeListener<? super Comp<?>, Region>) change -> {
if (change.wasAdded()) {
stack.getChildren().add(change.getValueAdded());
} else {
stack.getChildren().remove(change.getValueRemoved());
}
});
for (Map.Entry<Comp<?>, ObservableValue<Boolean>> e : content.entrySet()) {
var r = e.getKey().createRegion();
e.getValue().subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
r.setManaged(val);
r.setVisible(val);
});
});
m.put(e.getKey(), r);
}
return stack; return stack;
} }
} }

View file

@ -37,7 +37,7 @@ public class OsLogoComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var img = BindingsHelper.persist(Bindings.createObjectBinding( var img = Bindings.createObjectBinding(
() -> { () -> {
if (state.getValue() != SystemStateComp.State.SUCCESS) { if (state.getValue() != SystemStateComp.State.SUCCESS) {
return null; return null;
@ -51,7 +51,7 @@ public class OsLogoComp extends SimpleComp {
return getImage(ons.getOsName()); return getImage(ons.getOsName());
}, },
wrapper.getPersistentState(), wrapper.getPersistentState(),
state)); state);
var hide = BindingsHelper.map(img, s -> s != null); var hide = BindingsHelper.map(img, s -> s != null);
return new StackComp( return new StackComp(
List.of(new SystemStateComp(state).hide(hide), new PrettyImageComp(img, 24, 24).visible(hide))) List.of(new SystemStateComp(state).hide(hide), new PrettyImageComp(img, 24, 24).visible(hide)))

View file

@ -7,8 +7,8 @@ import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.UserReportComp; import io.xpipe.app.issue.UserReportComp;
@ -73,7 +73,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
var e = entries.get(i); var e = entries.get(i);
var b = new IconButtonComp(e.icon(), () -> value.setValue(e)); var b = new IconButtonComp(e.icon(), () -> value.setValue(e));
b.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + i])); b.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + i]));
b.apply(new FancyTooltipAugment<>(e.name())); b.apply(new TooltipAugment<>(e.name()));
b.apply(struc -> { b.apply(struc -> {
AppFont.setSize(struc.get(), 2); AppFont.setSize(struc.get(), 2);
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e)); struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
@ -133,7 +133,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
UserReportComp.show(event.build()); UserReportComp.show(event.build());
}) })
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()])) .shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()]))
.apply(new FancyTooltipAugment<>("reportIssue")) .apply(new TooltipAugment<>("reportIssue"))
.apply(simpleBorders) .apply(simpleBorders)
.accessibleTextKey("reportIssue"); .accessibleTextKey("reportIssue");
b.apply(struc -> { b.apply(struc -> {
@ -145,7 +145,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{ {
var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB)) var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1])) .shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1]))
.apply(new FancyTooltipAugment<>("visitGithubRepository")) .apply(new TooltipAugment<>("visitGithubRepository"))
.apply(simpleBorders) .apply(simpleBorders)
.accessibleTextKey("visitGithubRepository"); .accessibleTextKey("visitGithubRepository");
b.apply(struc -> { b.apply(struc -> {
@ -157,7 +157,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{ {
var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD)) var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2])) .shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]))
.apply(new FancyTooltipAugment<>("discord")) .apply(new TooltipAugment<>("discord"))
.apply(simpleBorders) .apply(simpleBorders)
.accessibleTextKey("discord"); .accessibleTextKey("discord");
b.apply(struc -> { b.apply(struc -> {
@ -167,9 +167,20 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
} }
{ {
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded()) var b = new IconButtonComp("mdi2t-translate", () -> Hyperlinks.open(Hyperlinks.TRANSLATE))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 3])) .shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 3]))
.apply(new FancyTooltipAugment<>("updateAvailableTooltip")) .apply(new TooltipAugment<>("translate"))
.apply(simpleBorders)
.accessibleTextKey("translate");
b.apply(struc -> {
AppFont.setSize(struc.get(), 2);
});
vbox.getChildren().add(b.createRegion());
}
{
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded())
.apply(new TooltipAugment<>("updateAvailableTooltip"))
.accessibleTextKey("updateAvailableTooltip"); .accessibleTextKey("updateAvailableTooltip");
b.apply(struc -> { b.apply(struc -> {
AppFont.setSize(struc.get(), 2); AppFont.setSize(struc.get(), 2);

View file

@ -3,7 +3,6 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.comp.store.StoreSection; import io.xpipe.app.comp.store.StoreSection;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -32,13 +31,13 @@ public class StoreToggleComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var disable = section.getWrapper().getValidity().map(state -> state != DataStoreEntry.Validity.COMPLETE); var disable = section.getWrapper().getValidity().map(state -> state != DataStoreEntry.Validity.COMPLETE);
var visible = BindingsHelper.persist(Bindings.createBooleanBinding( var visible = Bindings.createBooleanBinding(
() -> { () -> {
return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE
&& section.getShowDetails().get(); && section.getShowDetails().get();
}, },
section.getWrapper().getValidity(), section.getWrapper().getValidity(),
section.getShowDetails())); section.getShowDetails());
var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey)) var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey))
.visible(visible) .visible(visible)
.disable(disable); .disable(disable);

View file

@ -5,7 +5,6 @@ import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.core.process.ShellStoreState; import io.xpipe.core.process.ShellStoreState;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@ -35,7 +34,7 @@ public class SystemStateComp extends SimpleComp {
state)); state));
var fi = new FontIcon(); var fi = new FontIcon();
fi.getStyleClass().add("inner-icon"); fi.getStyleClass().add("inner-icon");
SimpleChangeListener.apply(icon, val -> fi.setIconLiteral(val)); icon.subscribe(val -> fi.setIconLiteral(val));
var border = new FontIcon("mdi2c-circle-outline"); var border = new FontIcon("mdi2c-circle-outline");
border.getStyleClass().add("outer-icon"); border.getStyleClass().add("outer-icon");
@ -63,10 +62,12 @@ public class SystemStateComp extends SimpleComp {
"""; """;
pane.getStylesheets().add(Styles.toDataURI(dataClass1)); pane.getStylesheets().add(Styles.toDataURI(dataClass1));
SimpleChangeListener.apply(PlatformThread.sync(state), val -> { state.subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
pane.getStylesheets().removeAll(success, failure, other); pane.getStylesheets().removeAll(success, failure, other);
pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other); pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other);
}); });
});
return pane; return pane;
} }

View file

@ -5,7 +5,6 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@ -13,7 +12,6 @@ import javafx.event.ActionEvent;
import javafx.scene.control.Button; import javafx.scene.control.Button;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
@ -56,12 +54,8 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
var text = new VBox(header, desc); var text = new VBox(header, desc);
text.setSpacing(2); text.setSpacing(2);
var fi = new FontIcon(); var fi = new FontIconComp(icon).createStructure();
SimpleChangeListener.apply(PlatformThread.sync(icon), val -> { var pane = fi.getPane();
fi.setIconLiteral(val);
});
var pane = new StackPane(fi);
var hbox = new HBox(pane, text); var hbox = new HBox(pane, text);
hbox.setSpacing(8); hbox.setSpacing(8);
pane.prefWidthProperty() pane.prefWidthProperty()
@ -76,11 +70,11 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
desc.heightProperty())); desc.heightProperty()));
pane.prefHeightProperty().addListener((c, o, n) -> { pane.prefHeightProperty().addListener((c, o, n) -> {
var size = Math.min(n.intValue(), 100); var size = Math.min(n.intValue(), 100);
fi.setIconSize((int) (size * 0.55)); fi.getIcon().setIconSize((int) (size * 0.55));
}); });
bt.setGraphic(hbox); bt.setGraphic(hbox);
return Structure.builder() return Structure.builder()
.graphic(fi) .graphic(fi.getIcon())
.button(bt) .button(bt)
.content(hbox) .content(hbox)
.name(header) .name(header)

View file

@ -1,5 +1,6 @@
package io.xpipe.app.comp.store; package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
@ -80,6 +81,10 @@ public class StoreCategoryWrapper {
private void setupListeners() { private void setupListeners() {
name.addListener((c, o, n) -> { name.addListener((c, o, n) -> {
if (n.equals(translatedName(category.getName()))) {
return;
}
category.setName(n); category.setName(n);
}); });
@ -91,6 +96,10 @@ public class StoreCategoryWrapper {
update(); update();
}); });
AppPrefs.get().language().addListener((observable, oldValue, newValue) -> {
update();
});
sortMode.addListener((observable, oldValue, newValue) -> { sortMode.addListener((observable, oldValue, newValue) -> {
category.setSortMode(newValue); category.setSortMode(newValue);
}); });
@ -112,8 +121,9 @@ public class StoreCategoryWrapper {
public void update() { public void update() {
// Avoid reupdating name when changed from the name property! // Avoid reupdating name when changed from the name property!
if (!category.getName().equals(name.getValue())) { var catName = translatedName(category.getName());
name.setValue(category.getName()); if (!catName.equals(name.getValue())) {
name.setValue(catName);
} }
lastAccess.setValue(category.getLastAccess().minus(Duration.ofMillis(500))); lastAccess.setValue(category.getLastAccess().minus(Duration.ofMillis(500)));
@ -140,18 +150,30 @@ public class StoreCategoryWrapper {
}); });
} }
public String getName() { private String translatedName(String original) {
return name.getValue(); if (original.equals("All connections")) {
return AppI18n.get("allConnections");
}
if (original.equals("All scripts")) {
return AppI18n.get("allScripts");
}
if (original.equals("Predefined")) {
return AppI18n.get("predefined");
}
if (original.equals("Custom")) {
return AppI18n.get("custom");
}
if (original.equals("Default")) {
return AppI18n.get("default");
}
return original;
} }
public Property<String> nameProperty() { public Property<String> nameProperty() {
return name; return name;
} }
public Instant getLastAccess() {
return lastAccess.getValue();
}
public Property<Instant> lastAccessProperty() { public Property<Instant> lastAccessProperty() {
return lastAccess; return lastAccess;
} }

View file

@ -12,7 +12,6 @@ import io.xpipe.app.ext.DataStoreProviders;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.ExceptionConverter; import io.xpipe.app.issue.ExceptionConverter;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
@ -381,7 +380,7 @@ public class StoreCreationComp extends DialogComp {
providerChoice.apply(GrowAugment.create(true, false)); providerChoice.apply(GrowAugment.create(true, false));
providerChoice.onSceneAssign(struc -> struc.get().requestFocus()); providerChoice.onSceneAssign(struc -> struc.get().requestFocus());
SimpleChangeListener.apply(provider, n -> { provider.subscribe(n -> {
if (n != null) { if (n != null) {
var d = n.guiDialog(existingEntry, store); var d = n.guiDialog(existingEntry, store);
var propVal = new SimpleValidator(); var propVal = new SimpleValidator();

View file

@ -26,6 +26,9 @@ public class StoreCreationMenu {
menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh")); menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh"));
menu.getItems()
.add(category("addVisual", "mdi2c-camera-plus", DataStoreProvider.CreationCategory.VISUAL, null));
menu.getItems() menu.getItems()
.add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null)); .add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null));
@ -81,7 +84,8 @@ public class StoreCreationMenu {
event.consume(); event.consume();
}); });
sub.forEach(dataStoreProvider -> { sub.forEach(dataStoreProvider -> {
var item = new MenuItem(dataStoreProvider.getDisplayName()); var item = new MenuItem();
item.textProperty().bind(dataStoreProvider.displayName());
item.setGraphic(PrettyImageHelper.ofFixedSizeSquare(dataStoreProvider.getDisplayIconFileName(null), 16) item.setGraphic(PrettyImageHelper.ofFixedSizeSquare(dataStoreProvider.getDisplayIconFileName(null), 16)
.createRegion()); .createRegion());
item.setOnAction(event -> { item.setOnAction(event -> {

View file

@ -13,9 +13,7 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
@ -97,12 +95,15 @@ public abstract class StoreEntryComp extends SimpleComp {
wrapper.executeDefaultAction(); wrapper.executeDefaultAction();
}); });
}); });
new ContextMenuAugment<>(mouseEvent -> mouseEvent.isSecondaryButtonDown(), null, () -> this.createContextMenu()).augment(new SimpleCompStructure<>(button)); new ContextMenuAugment<>(
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
null,
() -> this.createContextMenu())
.augment(button);
var loading = LoadingOverlayComp.noProgress( var loading = LoadingOverlayComp.noProgress(
Comp.of(() -> button), Comp.of(() -> button),
BindingsHelper.persist( wrapper.getInRefresh().and(wrapper.getObserving().not()));
wrapper.getInRefresh().and(wrapper.getObserving().not())));
return loading.createRegion(); return loading.createRegion();
} }
@ -138,7 +139,7 @@ public abstract class StoreEntryComp extends SimpleComp {
} }
protected void applyState(Node node) { protected void applyState(Node node) {
SimpleChangeListener.apply(PlatformThread.sync(wrapper.getValidity()), val -> { PlatformThread.sync(wrapper.getValidity()).subscribe(val -> {
switch (val) { switch (val) {
case LOAD_FAILED -> { case LOAD_FAILED -> {
node.pseudoClassStateChanged(FAILED, true); node.pseudoClassStateChanged(FAILED, true);
@ -174,8 +175,7 @@ public abstract class StoreEntryComp extends SimpleComp {
var imageComp = PrettyImageHelper.ofFixedSize(img, w, h); var imageComp = PrettyImageHelper.ofFixedSize(img, w, h);
var storeIcon = imageComp.createRegion(); var storeIcon = imageComp.createRegion();
if (wrapper.getValidity().getValue().isUsable()) { if (wrapper.getValidity().getValue().isUsable()) {
new FancyTooltipAugment<>(new SimpleStringProperty( new TooltipAugment<>(wrapper.getEntry().getProvider().displayName())
wrapper.getEntry().getProvider().getDisplayName()))
.augment(storeIcon); .augment(storeIcon);
} }
@ -212,7 +212,7 @@ public abstract class StoreEntryComp extends SimpleComp {
}); });
button.accessibleText( button.accessibleText(
actionProvider.getName(wrapper.getEntry().ref()).getValue()); actionProvider.getName(wrapper.getEntry().ref()).getValue());
button.apply(new FancyTooltipAugment<>( button.apply(new TooltipAugment<>(
actionProvider.getName(wrapper.getEntry().ref()))); actionProvider.getName(wrapper.getEntry().ref())));
if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) { if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) {
button.hide(Bindings.not(p.getValue())); button.hide(Bindings.not(p.getValue()));
@ -247,8 +247,10 @@ public abstract class StoreEntryComp extends SimpleComp {
settingsButton.styleClass("settings"); settingsButton.styleClass("settings");
settingsButton.accessibleText("More"); settingsButton.accessibleText("More");
settingsButton.apply(new ContextMenuAugment<>( settingsButton.apply(new ContextMenuAugment<>(
event -> event.getButton() == MouseButton.PRIMARY, null, () -> StoreEntryComp.this.createContextMenu())); event -> event.getButton() == MouseButton.PRIMARY,
settingsButton.apply(new FancyTooltipAugment<>("more")); null,
() -> StoreEntryComp.this.createContextMenu()));
settingsButton.apply(new TooltipAugment<>("more"));
return settingsButton; return settingsButton;
} }
@ -371,7 +373,8 @@ public abstract class StoreEntryComp extends SimpleComp {
StoreViewState.get() StoreViewState.get()
.getSortedCategories(wrapper.getCategory().getValue().getRoot()) .getSortedCategories(wrapper.getCategory().getValue().getRoot())
.forEach(storeCategoryWrapper -> { .forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem(storeCategoryWrapper.getName()); MenuItem m = new MenuItem();
m.textProperty().bind(storeCategoryWrapper.nameProperty());
m.setOnAction(event -> { m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory()); wrapper.moveTo(storeCategoryWrapper.getCategory());
event.consume(); event.consume();

View file

@ -5,7 +5,6 @@ import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@ -50,16 +49,16 @@ public class StoreEntryListComp extends SimpleComp {
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>(); var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put( map.put(
createList(), createList(),
BindingsHelper.persist(Bindings.not(Bindings.isEmpty( Bindings.not(Bindings.isEmpty(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren())))); StoreViewState.get().getCurrentTopLevelSection().getShownChildren())));
map.put(new StoreIntroComp(), showIntro); map.put(new StoreIntroComp(), showIntro);
map.put( map.put(
new StoreNotFoundComp(), new StoreNotFoundComp(),
BindingsHelper.persist(Bindings.and( Bindings.and(
Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())), Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())),
Bindings.isEmpty( Bindings.isEmpty(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren())))); StoreViewState.get().getCurrentTopLevelSection().getShownChildren())));
return new MultiContentComp(map).createRegion(); return new MultiContentComp(map).createRegion();
} }
} }

View file

@ -5,11 +5,11 @@ import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.impl.FilterComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -36,7 +36,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
public StoreEntryListStatusComp() { public StoreEntryListStatusComp() {
this.sortMode = new SimpleObjectProperty<>(); this.sortMode = new SimpleObjectProperty<>();
SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { StoreViewState.get().getActiveCategory().subscribe(val -> {
sortMode.setValue(val.getSortMode().getValue()); sortMode.setValue(val.getSortMode().getValue());
}); });
sortMode.addListener((observable, oldValue, newValue) -> { sortMode.addListener((observable, oldValue, newValue) -> {
@ -51,21 +51,16 @@ public class StoreEntryListStatusComp extends SimpleComp {
private Region createGroupListHeader() { private Region createGroupListHeader() {
var label = new Label(); var label = new Label();
label.textProperty() var name = BindingsHelper.flatMap(
.bind(Bindings.createStringBinding( StoreViewState.get().getActiveCategory(),
() -> { categoryWrapper -> AppI18n.observable(
return StoreViewState.get() categoryWrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory())
.getActiveCategory() ? "connections"
.getValue() : "scripts"));
.getRoot() label.textProperty().bind(name);
.equals(StoreViewState.get().getAllConnectionsCategory())
? "Connections"
: "Scripts";
},
StoreViewState.get().getActiveCategory()));
label.getStyleClass().add("name"); label.getStyleClass().add("name");
var all = BindingsHelper.filteredContentBinding( var all = ListBindingsHelper.filteredContentBinding(
StoreViewState.get().getAllEntries(), StoreViewState.get().getAllEntries(),
storeEntryWrapper -> { storeEntryWrapper -> {
var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot(); var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot();
@ -76,7 +71,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
.equals(storeRoot); .equals(storeRoot);
}, },
StoreViewState.get().getActiveCategory()); StoreViewState.get().getActiveCategory());
var shownList = BindingsHelper.filteredContentBinding( var shownList = ListBindingsHelper.filteredContentBinding(
all, all,
storeEntryWrapper -> { storeEntryWrapper -> {
return storeEntryWrapper.shouldShow( return storeEntryWrapper.shouldShow(
@ -135,7 +130,8 @@ public class StoreEntryListStatusComp extends SimpleComp {
} }
private Region createButtons() { private Region createButtons() {
var menu = new MenuButton(AppI18n.get("addConnections"), new FontIcon("mdi2p-plus-thick")); var menu = new MenuButton(null, new FontIcon("mdi2p-plus-thick"));
menu.textProperty().bind(AppI18n.observable("addConnections"));
menu.setAlignment(Pos.CENTER); menu.setAlignment(Pos.CENTER);
menu.setTextAlignment(TextAlignment.CENTER); menu.setTextAlignment(TextAlignment.CENTER);
AppFont.medium(menu); AppFont.medium(menu);
@ -188,7 +184,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
sortMode)); sortMode));
}); });
alphabetical.accessibleTextKey("sortAlphabetical"); alphabetical.accessibleTextKey("sortAlphabetical");
alphabetical.apply(new FancyTooltipAugment<>("sortAlphabetical")); alphabetical.apply(new TooltipAugment<>("sortAlphabetical"));
return alphabetical; return alphabetical;
} }
@ -227,7 +223,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
sortMode)); sortMode));
}); });
date.accessibleTextKey("sortLastUsed"); date.accessibleTextKey("sortLastUsed");
date.apply(new FancyTooltipAugment<>("sortLastUsed")); date.apply(new TooltipAugment<>("sortLastUsed"));
return date; return date;
} }

View file

@ -21,15 +21,18 @@ public class StoreIntroComp extends SimpleComp {
@Override @Override
public Region createSimple() { public Region createSimple() {
var title = new Label(AppI18n.get("storeIntroTitle")); var title = new Label();
title.textProperty().bind(AppI18n.observable("storeIntroTitle"));
title.getStyleClass().add(Styles.TEXT_BOLD); title.getStyleClass().add(Styles.TEXT_BOLD);
AppFont.setSize(title, 7); AppFont.setSize(title, 7);
var introDesc = new Label(AppI18n.get("storeIntroDescription")); var introDesc = new Label();
introDesc.textProperty().bind(AppI18n.observable("storeIntroDescription"));
introDesc.setWrapText(true); introDesc.setWrapText(true);
introDesc.setMaxWidth(470); introDesc.setMaxWidth(470);
var scanButton = new Button(AppI18n.get("detectConnections"), new FontIcon("mdi2m-magnify")); var scanButton = new Button(null, new FontIcon("mdi2m-magnify"));
scanButton.textProperty().bind(AppI18n.observable("detectConnections"));
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local())); scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local()));
scanButton.setDefaultButton(true); scanButton.setDefaultButton(true);
var scanPane = new StackPane(scanButton); var scanPane = new StackPane(scanButton);
@ -43,12 +46,7 @@ public class StoreIntroComp extends SimpleComp {
hbox.setSpacing(35); hbox.setSpacing(35);
hbox.setAlignment(Pos.CENTER); hbox.setAlignment(Pos.CENTER);
var v = new VBox( var v = new VBox(hbox, scanPane);
hbox, scanPane
// new Separator(Orientation.HORIZONTAL),
// documentation,
// docLinkPane
);
v.setMinWidth(Region.USE_PREF_SIZE); v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(Region.USE_PREF_SIZE); v.setMaxWidth(Region.USE_PREF_SIZE);
v.setMinHeight(Region.USE_PREF_SIZE); v.setMinHeight(Region.USE_PREF_SIZE);

View file

@ -39,7 +39,7 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
} }
var graphic = provider.getDisplayIconFileName(null); var graphic = provider.getDisplayIconFileName(null);
return JfxHelper.createNamedEntry(provider.getDisplayName(), provider.getDisplayDescription(), graphic); return JfxHelper.createNamedEntry(provider.displayName(), provider.displayDescription(), graphic);
} }
@Override @Override
@ -49,8 +49,13 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
protected void updateItem(DataStoreProvider item, boolean empty) { protected void updateItem(DataStoreProvider item, boolean empty) {
super.updateItem(item, empty); super.updateItem(item, empty);
setGraphic(createGraphic(item)); setGraphic(createGraphic(item));
setAccessibleText(item != null ? item.getDisplayName() : null); if (item != null) {
setAccessibleHelp(item != null ? item.getDisplayDescription() : null); accessibleTextProperty().bind(item.displayName());
accessibleHelpProperty().bind(item.displayDescription());
} else {
accessibleTextProperty().unbind();
accessibleHelpProperty().unbind();
}
} }
}; };
var cb = new ComboBox<DataStoreProvider>(); var cb = new ComboBox<DataStoreProvider>();

View file

@ -42,7 +42,9 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
var graphic = var graphic =
w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore()); w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore());
if (c.isEmpty()) { if (c.isEmpty()) {
var item = ContextMenuHelper.item(PrettyImageHelper.ofFixedSizeSquare(graphic, 16), w.getName().getValue()); var item = ContextMenuHelper.item(
PrettyImageHelper.ofFixedSizeSquare(graphic, 16),
w.getName().getValue());
item.setOnAction(event -> { item.setOnAction(event -> {
action.accept(w); action.accept(w);
contextMenu.hide(); contextMenu.hide();

View file

@ -2,6 +2,7 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
@ -64,10 +65,10 @@ public class StoreSection {
var c = Comparator.<StoreSection>comparingInt( var c = Comparator.<StoreSection>comparingInt(
value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1); value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1);
var mappedSortMode = BindingsHelper.mappedBinding( var mappedSortMode = BindingsHelper.flatMap(
category, category,
storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null); storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null);
return BindingsHelper.orderedContentBinding( return ListBindingsHelper.orderedContentBinding(
list, list,
(o1, o2) -> { (o1, o2) -> {
var current = mappedSortMode.getValue(); var current = mappedSortMode.getValue();
@ -86,16 +87,18 @@ public class StoreSection {
Predicate<StoreEntryWrapper> entryFilter, Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString, ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> category) { ObservableValue<StoreCategoryWrapper> category) {
var topLevel = BindingsHelper.filteredContentBinding( var topLevel = ListBindingsHelper.filteredContentBinding(
all, all,
section -> { section -> {
return DataStorage.get().isRootEntry(section.getEntry()); return DataStorage.get().isRootEntry(section.getEntry());
}, },
category); category);
var cached = BindingsHelper.cachedMappedContentBinding( var cached = ListBindingsHelper.cachedMappedContentBinding(
topLevel, storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category)); topLevel,
topLevel,
storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category); var ordered = sorted(cached, category);
var shown = BindingsHelper.filteredContentBinding( var shown = ListBindingsHelper.filteredContentBinding(
ordered, ordered,
section -> { section -> {
var showFilter = filterString == null || section.shouldShow(filterString.get()); var showFilter = filterString == null || section.shouldShow(filterString.get());
@ -121,7 +124,7 @@ public class StoreSection {
return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth); return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth);
} }
var allChildren = BindingsHelper.filteredContentBinding(all, other -> { var allChildren = ListBindingsHelper.filteredContentBinding(all, other -> {
// Legacy implementation that does not use children caches. Use for testing // Legacy implementation that does not use children caches. Use for testing
// if (true) return DataStorage.get() // if (true) return DataStorage.get()
// .getDisplayParent(other.getEntry()) // .getDisplayParent(other.getEntry())
@ -131,10 +134,12 @@ public class StoreSection {
// This check is fast as the children are cached in the storage // This check is fast as the children are cached in the storage
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()); return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
}); });
var cached = BindingsHelper.cachedMappedContentBinding( var cached = ListBindingsHelper.cachedMappedContentBinding(
allChildren, entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category)); allChildren,
allChildren,
entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category); var ordered = sorted(cached, category);
var filtered = BindingsHelper.filteredContentBinding( var filtered = ListBindingsHelper.filteredContentBinding(
ordered, ordered,
section -> { section -> {
var showFilter = filterString == null || section.shouldShow(filterString.get()); var showFilter = filterString == null || section.shouldShow(filterString.get());

View file

@ -7,8 +7,7 @@ import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -40,11 +39,11 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
} }
private Comp<CompStructure<Button>> createQuickAccessButton() { private Comp<CompStructure<Button>> createQuickAccessButton() {
var quickAccessDisabled = BindingsHelper.persist(Bindings.createBooleanBinding( var quickAccessDisabled = Bindings.createBooleanBinding(
() -> { () -> {
return section.getShownChildren().isEmpty(); return section.getShownChildren().isEmpty();
}, },
section.getShownChildren())); section.getShownChildren());
Consumer<StoreEntryWrapper> quickAccessAction = w -> { Consumer<StoreEntryWrapper> quickAccessAction = w -> {
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
w.executeDefaultAction(); w.executeDefaultAction();
@ -91,8 +90,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
return "Expand " + section.getWrapper().getName().getValue(); return "Expand " + section.getWrapper().getName().getValue();
}, },
section.getWrapper().getName())) section.getWrapper().getName()))
.disable(BindingsHelper.persist( .disable(Bindings.size(section.getShownChildren()).isEqualTo(0))
Bindings.size(section.getShownChildren()).isEqualTo(0)))
.styleClass("expand-button") .styleClass("expand-button")
.maxHeight(100) .maxHeight(100)
.vgrow(); .vgrow();
@ -131,7 +129,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the // Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// section is actually expanded // section is actually expanded
var listSections = BindingsHelper.filteredContentBinding( var listSections = ListBindingsHelper.filteredContentBinding(
section.getShownChildren(), section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20 storeSection -> section.getAllChildren().size() <= 20
|| section.getWrapper().getExpanded().get(), || section.getWrapper().getExpanded().get(),
@ -143,26 +141,26 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
.minHeight(0) .minHeight(0)
.hgrow(); .hgrow();
var expanded = BindingsHelper.persist(Bindings.createBooleanBinding( var expanded = Bindings.createBooleanBinding(
() -> { () -> {
return section.getWrapper().getExpanded().get() return section.getWrapper().getExpanded().get()
&& section.getShownChildren().size() > 0; && section.getShownChildren().size() > 0;
}, },
section.getWrapper().getExpanded(), section.getWrapper().getExpanded(),
section.getShownChildren())); section.getShownChildren());
var full = new VerticalComp(List.of( var full = new VerticalComp(List.of(
topEntryList, topEntryList,
Comp.separator().hide(BindingsHelper.persist(expanded.not())), Comp.separator().hide(expanded.not()),
new HorizontalComp(List.of(content)) new HorizontalComp(List.of(content))
.styleClass("content") .styleClass("content")
.apply(struc -> struc.get().setFillHeight(true)) .apply(struc -> struc.get().setFillHeight(true))
.hide(BindingsHelper.persist(Bindings.or( .hide(Bindings.or(
Bindings.not(section.getWrapper().getExpanded()), Bindings.not(section.getWrapper().getExpanded()),
Bindings.size(section.getShownChildren()).isEqualTo(0)))))); Bindings.size(section.getShownChildren()).isEqualTo(0)))));
return full.styleClass("store-entry-section-comp") return full.styleClass("store-entry-section-comp")
.apply(struc -> { .apply(struc -> {
struc.get().setFillWidth(true); struc.get().setFillWidth(true);
SimpleChangeListener.apply(expanded, val -> { expanded.subscribe(val -> {
struc.get().pseudoClassStateChanged(EXPANDED, val); struc.get().pseudoClassStateChanged(EXPANDED, val);
}); });
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0); struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
@ -170,7 +168,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
struc.get().pseudoClassStateChanged(ROOT, topLevel); struc.get().pseudoClassStateChanged(ROOT, topLevel);
struc.get().pseudoClassStateChanged(SUB, !topLevel); struc.get().pseudoClassStateChanged(SUB, !topLevel);
SimpleChangeListener.apply(section.getWrapper().getColor(), val -> { section.getWrapper().getColor().subscribe(val -> {
if (!topLevel) { if (!topLevel) {
return; return;
} }

View file

@ -8,8 +8,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.IconButtonComp;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
@ -101,16 +100,15 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
+ section.getWrapper().getName().getValue(); + section.getWrapper().getName().getValue();
}, },
section.getWrapper().getName())) section.getWrapper().getName()))
.disable(BindingsHelper.persist( .disable(Bindings.size(section.getAllChildren()).isEqualTo(0))
Bindings.size(section.getAllChildren()).isEqualTo(0)))
.grow(false, true) .grow(false, true)
.styleClass("expand-button"); .styleClass("expand-button");
var quickAccessDisabled = BindingsHelper.persist(Bindings.createBooleanBinding( var quickAccessDisabled = Bindings.createBooleanBinding(
() -> { () -> {
return section.getShownChildren().isEmpty(); return section.getShownChildren().isEmpty();
}, },
section.getShownChildren())); section.getShownChildren());
Consumer<StoreEntryWrapper> quickAccessAction = w -> { Consumer<StoreEntryWrapper> quickAccessAction = w -> {
action.accept(w); action.accept(w);
}; };
@ -134,7 +132,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the // Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// section is actually expanded // section is actually expanded
var listSections = section.getWrapper() != null var listSections = section.getWrapper() != null
? BindingsHelper.filteredContentBinding( ? ListBindingsHelper.filteredContentBinding(
section.getShownChildren(), section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20 || expanded.get(), storeSection -> section.getAllChildren().size() <= 20 || expanded.get(),
expanded, expanded,
@ -149,9 +147,9 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
list.add(new HorizontalComp(List.of(content)) list.add(new HorizontalComp(List.of(content))
.styleClass("content") .styleClass("content")
.apply(struc -> struc.get().setFillHeight(true)) .apply(struc -> struc.get().setFillHeight(true))
.hide(BindingsHelper.persist(Bindings.or( .hide(Bindings.or(
Bindings.not(expanded), Bindings.not(expanded),
Bindings.size(section.getAllChildren()).isEqualTo(0))))); Bindings.size(section.getAllChildren()).isEqualTo(0))));
var vert = new VerticalComp(list); var vert = new VerticalComp(list);
if (condensedStyle) { if (condensedStyle) {
@ -160,7 +158,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
return vert.styleClass("store-section-mini-comp") return vert.styleClass("store-section-mini-comp")
.apply(struc -> { .apply(struc -> {
struc.get().setFillWidth(true); struc.get().setFillWidth(true);
SimpleChangeListener.apply(expanded, val -> { expanded.subscribe(val -> {
struc.get().pseudoClassStateChanged(EXPANDED, val); struc.get().pseudoClassStateChanged(EXPANDED, val);
}); });
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0); struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
@ -171,7 +169,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
}) })
.apply(struc -> { .apply(struc -> {
if (section.getWrapper() != null) { if (section.getWrapper() != null) {
SimpleChangeListener.apply(section.getWrapper().getColor(), val -> { section.getWrapper().getColor().subscribe(val -> {
if (section.getDepth() != 1) { if (section.getDepth() != 1) {
return; return;
} }

View file

@ -1,7 +1,7 @@
package io.xpipe.app.comp.store; package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppCache; import io.xpipe.app.core.AppCache;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
@ -270,10 +270,12 @@ public class StoreViewState {
return parent; return parent;
} }
return o1.getName().compareToIgnoreCase(o2.getName()); return o1.nameProperty()
.getValue()
.compareToIgnoreCase(o2.nameProperty().getValue());
} }
}; };
return BindingsHelper.filteredContentBinding( return ListBindingsHelper.filteredContentBinding(
categories, cat -> root == null || cat.getRoot().equals(root)) categories, cat -> root == null || cat.getRoot().equals(root))
.sorted(comparator); .sorted(comparator);
} }

View file

@ -65,13 +65,13 @@ public class App extends Application {
"XPipe %s (%s)", t.getValue(), AppProperties.get().getVersion()); "XPipe %s (%s)", t.getValue(), AppProperties.get().getVersion());
var prefix = AppProperties.get().isStaging() ? "[Public Test Build, Not a proper release] " : ""; var prefix = AppProperties.get().isStaging() ? "[Public Test Build, Not a proper release] " : "";
var suffix = u.getValue() != null var suffix = u.getValue() != null
? String.format( ? AppI18n.get("updateReadyTitle", u.getValue().getVersion())
" (Update to %s ready)", u.getValue().getVersion())
: ""; : "";
return prefix + base + suffix; return prefix + base + suffix;
}, },
u, u,
t); t,
AppPrefs.get().language());
var appWindow = AppMainWindow.init(stage); var appWindow = AppMainWindow.init(stage);
appWindow.getStage().titleProperty().bind(PlatformThread.sync(titleBinding)); appWindow.getStage().titleProperty().bind(PlatformThread.sync(titleBinding));

View file

@ -1,11 +1,11 @@
package io.xpipe.app.core; package io.xpipe.app.core;
import io.xpipe.app.exchange.MessageExchangeImpls;
import io.xpipe.app.ext.ExtensionException; import io.xpipe.app.ext.ExtensionException;
import io.xpipe.app.ext.XPipeServiceProviders;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.util.ModuleHelper; import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.ModuleLayerLoader;
import io.xpipe.core.util.XPipeInstallation; import io.xpipe.core.util.XPipeInstallation;
import lombok.Getter; import lombok.Getter;
import lombok.Value; import lombok.Value;
@ -47,10 +47,11 @@ public class AppExtensionManager {
} }
if (load) { if (load) {
// INSTANCE.addNativeLibrariesToPath();
try { try {
XPipeServiceProviders.load(INSTANCE.extendedLayer); ProcessControlProvider.init(INSTANCE.extendedLayer);
MessageExchangeImpls.loadAll(); ModuleLayerLoader.loadAll(INSTANCE.extendedLayer, t -> {
ErrorEvent.fromThrowable(t).handle();
});
} catch (Throwable t) { } catch (Throwable t) {
throw new ExtensionException( throw new ExtensionException(
"Service provider initialization failed. Is the installation data corrupt?", t); "Service provider initialization failed. Is the installation data corrupt?", t);

View file

@ -3,7 +3,6 @@ package io.xpipe.app.core;
import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.core.mode.OperationMode; import io.xpipe.app.core.mode.OperationMode;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
@ -98,7 +97,7 @@ public class AppGreetings {
alert.getButtonTypes().add(buttonType); alert.getButtonTypes().add(buttonType);
Button button = (Button) alert.getDialogPane().lookupButton(buttonType); Button button = (Button) alert.getDialogPane().lookupButton(buttonType);
button.disableProperty().bind(BindingsHelper.persist(accepted.not())); button.disableProperty().bind(accepted.not());
} }
alert.getButtonTypes().add(ButtonType.CANCEL); alert.getButtonTypes().add(ButtonType.CANCEL);

View file

@ -2,7 +2,7 @@ package io.xpipe.app.core;
import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
@ -10,112 +10,57 @@ import io.xpipe.app.prefs.SupportedLocale;
import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.OptionsBuilder;
import io.xpipe.app.util.Translatable; import io.xpipe.app.util.Translatable;
import io.xpipe.core.util.ModuleHelper; import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.XPipeInstallation;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding; import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.Value;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.ocpsoft.prettytime.PrettyTime; import org.ocpsoft.prettytime.PrettyTime;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult; import java.nio.file.FileVisitResult;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor; import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration; import java.util.*;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern; import java.util.regex.Pattern;
public class AppI18n { public class AppI18n {
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$"); private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$");
private static final AppI18n INSTANCE = new AppI18n(); private static AppI18n INSTANCE;
private Map<String, String> translations; private final Property<LoadedTranslations> currentLanguage = new SimpleObjectProperty<>();
private Map<String, String> markdownDocumentations; private LoadedTranslations english;
private PrettyTime prettyTime;
public static void init() { public static void init() throws Exception {
var i = INSTANCE; if (INSTANCE == null) {
if (i.translations != null) { INSTANCE = new AppI18n();
return; }
INSTANCE.load();
} }
i.load(); public static AppI18n get() {
if (AppPrefs.get() != null) {
AppPrefs.get().language().addListener((c, o, n) -> {
i.clear();
i.load();
});
}
}
public static AppI18n getInstance() {
return INSTANCE; return INSTANCE;
} }
public static StringBinding readableInstant(String s, ObservableValue<Instant> instant) {
return readableInstant(instant, rs -> getValue(getInstance().getLocalised(s), rs));
}
public static StringBinding readableInstant(ObservableValue<Instant> instant, UnaryOperator<String> op) {
return Bindings.createStringBinding(
() -> {
if (instant.getValue() == null) {
return "null";
}
return op.apply(
getInstance().prettyTime.format(instant.getValue().minus(Duration.ofSeconds(1))));
},
instant);
}
public static StringBinding readableInstant(ObservableValue<Instant> instant) {
return Bindings.createStringBinding(
() -> {
if (instant.getValue() == null) {
return "null";
}
return getInstance().prettyTime.format(instant.getValue().minus(Duration.ofSeconds(1)));
},
instant);
}
public static StringBinding readableDuration(ObservableValue<Duration> duration) {
return Bindings.createStringBinding(
() -> {
if (duration.getValue() == null) {
return "null";
}
return getInstance()
.prettyTime
.formatDuration(getInstance()
.prettyTime
.approximateDuration(Instant.now().plus(duration.getValue())));
},
duration);
}
public static ObservableValue<String> observable(String s, Object... vars) { public static ObservableValue<String> observable(String s, Object... vars) {
if (s == null) { if (s == null) {
return null; return null;
} }
var key = INSTANCE.getKey(s); var key = INSTANCE.getKey(s);
return Bindings.createStringBinding(() -> { return Bindings.createStringBinding(
() -> {
return get(key, vars); return get(key, vars);
}); },
INSTANCE.currentLanguage);
} }
public static String get(String s, Object... vars) { public static String get(String s, Object... vars) {
@ -147,7 +92,7 @@ public class AppI18n {
|| caller.equals(ModuleHelper.class) || caller.equals(ModuleHelper.class)
|| caller.equals(ModalOverlayComp.class) || caller.equals(ModalOverlayComp.class)
|| caller.equals(AppI18n.class) || caller.equals(AppI18n.class)
|| caller.equals(FancyTooltipAugment.class) || caller.equals(TooltipAugment.class)
|| caller.equals(PrefsChoiceValue.class) || caller.equals(PrefsChoiceValue.class)
|| caller.equals(Translatable.class) || caller.equals(Translatable.class)
|| caller.equals(AppWindowHelper.class) || caller.equals(AppWindowHelper.class)
@ -160,9 +105,28 @@ public class AppI18n {
return ""; return "";
} }
private void clear() { private void load() throws Exception {
translations.clear(); if (english == null) {
prettyTime = null; english = load(Locale.ENGLISH);
Locale.setDefault(Locale.ENGLISH);
}
if (currentLanguage.getValue() == null) {
if (AppPrefs.get() != null) {
AppPrefs.get().language().subscribe(n -> {
try {
currentLanguage.setValue(n != null ? load(n.getLocale()) : null);
Locale.setDefault(n != null ? n.getLocale() : Locale.ENGLISH);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
});
}
}
}
private LoadedTranslations getLoaded() {
return currentLanguage.getValue() != null ? currentLanguage.getValue() : english;
} }
public String getKey(String s) { public String getKey(String s) {
@ -173,62 +137,66 @@ public class AppI18n {
return key; return key;
} }
public boolean containsKey(String s) {
var key = getKey(s);
if (translations == null) {
return false;
}
return translations.containsKey(key);
}
public String getLocalised(String s, Object... vars) { public String getLocalised(String s, Object... vars) {
var key = getKey(s); var key = getKey(s);
if (translations == null) { if (english == null) {
TrackEvent.warn("Translations not initialized for " + key); TrackEvent.warn("Translations not initialized for " + key);
return s; return s;
} }
if (!translations.containsKey(key)) { if (currentLanguage.getValue() != null
&& currentLanguage.getValue().getTranslations().containsKey(key)) {
var localisedString = currentLanguage.getValue().getTranslations().get(key);
return getValue(localisedString, vars);
}
if (english.getTranslations().containsKey(key)) {
var localisedString = english.getTranslations().get(key);
return getValue(localisedString, vars);
}
TrackEvent.warn("Translation key not found for " + key); TrackEvent.warn("Translation key not found for " + key);
return key; return key;
} }
var localisedString = translations.get(key); private boolean matchesLocale(Path f, Locale l) {
return getValue(localisedString, vars);
}
public boolean isLoaded() {
return translations != null;
}
private boolean matchesLocale(Path f) {
var l = AppPrefs.get() != null
? AppPrefs.get().language().getValue().getLocale()
: SupportedLocale.ENGLISH.getLocale();
var name = FilenameUtils.getBaseName(f.getFileName().toString()); var name = FilenameUtils.getBaseName(f.getFileName().toString());
var ending = "_" + l.toLanguageTag(); var ending = "_" + l.toLanguageTag();
return name.endsWith(ending); return name.endsWith(ending);
} }
public String getMarkdownDocumentation(String name) { public String getMarkdownDocumentation(String name) {
if (!markdownDocumentations.containsKey(name)) { if (currentLanguage.getValue() != null
&& currentLanguage.getValue().getMarkdownDocumentations().containsKey(name)) {
var localisedString =
currentLanguage.getValue().getMarkdownDocumentations().get(name);
return localisedString;
}
if (english.getMarkdownDocumentations().containsKey(name)) {
var localisedString = english.getMarkdownDocumentations().get(name);
return localisedString;
}
TrackEvent.withWarn("Markdown documentation for key " + name + " not found") TrackEvent.withWarn("Markdown documentation for key " + name + " not found")
.handle(); .handle();
return "";
} }
return markdownDocumentations.getOrDefault(name, ""); private Path getModuleLangPath(String module) {
return XPipeInstallation.getLangPath().resolve(module);
} }
private void load() { private LoadedTranslations load(Locale l) throws Exception {
TrackEvent.info("Loading translations ..."); TrackEvent.info("Loading translations ...");
translations = new HashMap<>(); var translations = new HashMap<String, String>();
for (var module : AppExtensionManager.getInstance().getContentModules()) { for (var module : AppExtensionManager.getInstance().getContentModules()) {
AppResources.with(module.getName(), "lang", basePath -> { var basePath = getModuleLangPath(FilenameUtils.getExtension(module.getName()))
.resolve("strings");
if (!Files.exists(basePath)) { if (!Files.exists(basePath)) {
return; continue;
} }
AtomicInteger fileCounter = new AtomicInteger(); AtomicInteger fileCounter = new AtomicInteger();
@ -238,7 +206,7 @@ public class AppI18n {
Files.walkFileTree(basePath, new SimpleFileVisitor<>() { Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override @Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!matchesLocale(file)) { if (!matchesLocale(file, l)) {
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
@ -249,7 +217,7 @@ public class AppI18n {
fileCounter.incrementAndGet(); fileCounter.incrementAndGet();
try (var in = Files.newInputStream(file)) { try (var in = Files.newInputStream(file)) {
var props = new Properties(); var props = new Properties();
props.load(in); props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
props.forEach((key, value) -> { props.forEach((key, value) -> {
var hasPrefix = key.toString().contains("."); var hasPrefix = key.toString().contains(".");
var usedPrefix = hasPrefix ? "" : defaultPrefix; var usedPrefix = hasPrefix ? "" : defaultPrefix;
@ -267,21 +235,21 @@ public class AppI18n {
.tag("fileCount", fileCounter.get()) .tag("fileCount", fileCounter.get())
.tag("lineCount", lineCounter.get()) .tag("lineCount", lineCounter.get())
.handle(); .handle();
});
} }
markdownDocumentations = new HashMap<>(); var markdownDocumentations = new HashMap<String, String>();
for (var module : AppExtensionManager.getInstance().getContentModules()) { for (var module : AppExtensionManager.getInstance().getContentModules()) {
AppResources.with(module.getName(), "lang", basePath -> { var basePath = getModuleLangPath(FilenameUtils.getExtension(module.getName()))
.resolve("texts");
if (!Files.exists(basePath)) { if (!Files.exists(basePath)) {
return; continue;
} }
var moduleName = FilenameUtils.getExtension(module.getName()); var moduleName = FilenameUtils.getExtension(module.getName());
Files.walkFileTree(basePath, new SimpleFileVisitor<>() { Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override @Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!matchesLocale(file)) { if (!matchesLocale(file, l)) {
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
@ -302,13 +270,23 @@ public class AppI18n {
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
} }
}); });
});
} }
this.prettyTime = new PrettyTime( var prettyTime = new PrettyTime(
AppPrefs.get() != null AppPrefs.get() != null
? AppPrefs.get().language().getValue().getLocale() ? AppPrefs.get().language().getValue().getLocale()
: SupportedLocale.ENGLISH.getLocale()); : SupportedLocale.getEnglish().getLocale());
return new LoadedTranslations(l, translations, markdownDocumentations, prettyTime);
}
@Value
static class LoadedTranslations {
Locale locale;
Map<String, String> translations;
Map<String, String> markdownDocumentations;
PrettyTime prettyTime;
} }
@SuppressWarnings("removal") @SuppressWarnings("removal")

View file

@ -30,11 +30,11 @@ public class AppImages {
TrackEvent.info("Loading images ..."); TrackEvent.info("Loading images ...");
for (var module : AppExtensionManager.getInstance().getContentModules()) { for (var module : AppExtensionManager.getInstance().getContentModules()) {
loadDirectory(module.getName(), "img"); loadDirectory(module.getName(), "img", true, true);
} }
} }
public static void loadDirectory(String module, String dir) { public static void loadDirectory(String module, String dir, boolean loadImages, boolean loadSvgs) {
AppResources.with(module, dir, basePath -> { AppResources.with(module, dir, basePath -> {
if (!Files.exists(basePath)) { if (!Files.exists(basePath)) {
return; return;
@ -48,10 +48,10 @@ public class AppImages {
var relativeFileName = FilenameUtils.separatorsToUnix( var relativeFileName = FilenameUtils.separatorsToUnix(
basePath.relativize(file).toString()); basePath.relativize(file).toString());
try { try {
if (FilenameUtils.getExtension(file.toString()).equals("svg")) { if (FilenameUtils.getExtension(file.toString()).equals("svg") && loadSvgs) {
var s = Files.readString(file); var s = Files.readString(file);
svgImages.put(defaultPrefix + relativeFileName, s); svgImages.put(defaultPrefix + relativeFileName, s);
} else { } else if (loadImages) {
images.put(defaultPrefix + relativeFileName, loadImage(file)); images.put(defaultPrefix + relativeFileName, loadImage(file));
} }
} catch (IOException ex) { } catch (IOException ex) {

View file

@ -1,11 +1,9 @@
package io.xpipe.app.core; package io.xpipe.app.core;
import io.xpipe.app.browser.BrowserComp; import io.xpipe.app.browser.session.BrowserSessionComp;
import io.xpipe.app.browser.BrowserModel; import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.comp.DeveloperTabComp;
import io.xpipe.app.comp.store.StoreLayoutComp; import io.xpipe.app.comp.store.StoreLayoutComp;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefsComp; import io.xpipe.app.prefs.AppPrefsComp;
import io.xpipe.app.util.LicenseProvider; import io.xpipe.app.util.LicenseProvider;
import javafx.beans.property.Property; import javafx.beans.property.Property;
@ -30,13 +28,11 @@ public class AppLayoutModel {
private final List<Entry> entries; private final List<Entry> entries;
private final Property<Entry> selected; private final Property<Entry> selected;
private final ObservableValue<Entry> selectedWrapper;
public AppLayoutModel(SavedState savedState) { public AppLayoutModel(SavedState savedState) {
this.savedState = savedState; this.savedState = savedState;
this.entries = createEntryList(); this.entries = createEntryList();
this.selected = new SimpleObjectProperty<>(entries.get(1)); this.selected = new SimpleObjectProperty<>(entries.get(1));
this.selectedWrapper = PlatformThread.sync(selected);
} }
public static AppLayoutModel get() { public static AppLayoutModel get() {
@ -53,14 +49,10 @@ public class AppLayoutModel {
INSTANCE = null; INSTANCE = null;
} }
public Property<Entry> getSelectedInternal() { public Property<Entry> getSelected() {
return selected; return selected;
} }
public ObservableValue<Entry> getSelected() {
return selectedWrapper;
}
public void selectBrowser() { public void selectBrowser() {
selected.setValue(entries.getFirst()); selected.setValue(entries.getFirst());
} }
@ -79,21 +71,16 @@ public class AppLayoutModel {
private List<Entry> createEntryList() { private List<Entry> createEntryList() {
var l = new ArrayList<>(List.of( var l = new ArrayList<>(List.of(
new Entry(AppI18n.observable("browser"), "mdi2f-file-cabinet", new BrowserComp(BrowserModel.DEFAULT)), new Entry(
AppI18n.observable("browser"),
"mdi2f-file-cabinet",
new BrowserSessionComp(BrowserSessionModel.DEFAULT)),
new Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()), new Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()),
new Entry(AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp()))); new Entry(AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp()),
// new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new new Entry(
// StorageLayoutComp()),
// new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp())
if (AppProperties.get().isDeveloperMode() && !AppProperties.get().isImage()) {
l.add(new Entry(AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
}
l.add(new Entry(
AppI18n.observable("explorePlans"), AppI18n.observable("explorePlans"),
"mdi2p-professional-hexagon", "mdi2p-professional-hexagon",
LicenseProvider.get().overviewPage())); LicenseProvider.get().overviewPage())));
return l; return l;
} }

View file

@ -29,6 +29,7 @@ public class AppProperties {
UUID buildUuid; UUID buildUuid;
String sentryUrl; String sentryUrl;
String arch; String arch;
List<String> languages;
@Getter @Getter
boolean image; boolean image;
@ -53,6 +54,7 @@ public class AppProperties {
.orElse(UUID.randomUUID()); .orElse(UUID.randomUUID());
sentryUrl = System.getProperty("io.xpipe.app.sentryUrl"); sentryUrl = System.getProperty("io.xpipe.app.sentryUrl");
arch = System.getProperty("io.xpipe.app.arch"); arch = System.getProperty("io.xpipe.app.arch");
languages = Arrays.asList(System.getProperty("io.xpipe.app.languages").split(";"));
staging = XPipeInstallation.isStaging(); staging = XPipeInstallation.isStaging();
useVirtualThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.useVirtualThreads")) useVirtualThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.useVirtualThreads"))
.map(Boolean::parseBoolean) .map(Boolean::parseBoolean)

View file

@ -3,25 +3,24 @@ package io.xpipe.app.core;
import atlantafx.base.theme.*; import atlantafx.base.theme.*;
import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType; import io.xpipe.core.process.OsType;
import javafx.animation.Interpolator; import javafx.animation.*;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Application; import javafx.application.Application;
import javafx.application.ColorScheme; import javafx.application.ColorScheme;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane; import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.stage.Window; import javafx.stage.Window;
import javafx.stage.WindowEvent;
import javafx.util.Duration; import javafx.util.Duration;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
@ -39,12 +38,15 @@ public class AppTheme {
private static final PseudoClass PERFORMANCE = PseudoClass.getPseudoClass("performance"); private static final PseudoClass PERFORMANCE = PseudoClass.getPseudoClass("performance");
private static boolean init; private static boolean init;
public static void initThemeHandlers(Window stage) { public static void initThemeHandlers(Stage stage) {
if (AppPrefs.get() == null) { if (AppPrefs.get() == null) {
return; return;
} }
SimpleChangeListener.apply(AppPrefs.get().theme, t -> { initWindowsThemeHandler(stage);
Runnable r = () -> {
AppPrefs.get().theme.subscribe(t -> {
Theme.ALL.forEach( Theme.ALL.forEach(
theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId())); theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId()));
if (t == null) { if (t == null) {
@ -56,10 +58,56 @@ public class AppTheme {
stage.getScene().getRoot().pseudoClassStateChanged(DARK, t.isDark()); stage.getScene().getRoot().pseudoClassStateChanged(DARK, t.isDark());
}); });
SimpleChangeListener.apply(AppPrefs.get().performanceMode(), val -> { AppPrefs.get().performanceMode().subscribe(val -> {
stage.getScene().getRoot().pseudoClassStateChanged(PRETTY, !val); stage.getScene().getRoot().pseudoClassStateChanged(PRETTY, !val);
stage.getScene().getRoot().pseudoClassStateChanged(PERFORMANCE, val); stage.getScene().getRoot().pseudoClassStateChanged(PERFORMANCE, val);
}); });
};
if (stage.getOwner() != null) {
// If we set the theme pseudo classes earlier when the window is not shown
// they do not apply. Is this a bug in JavaFX?
Platform.runLater(r);
} else {
r.run();
}
}
private static void initWindowsThemeHandler(Window stage) {
if (OsType.getLocal() != OsType.WINDOWS) {
return;
}
EventHandler<WindowEvent> windowTheme = new EventHandler<>() {
@Override
public void handle(WindowEvent event) {
if (!stage.isShowing()) {
return;
}
try {
// var c = new WindowControl(stage);
// c.setWindowAttribute(20, AppPrefs.get().theme.getValue().isDark());
} catch (Throwable e) {
ErrorEvent.fromThrowable(e).handle();
}
stage.removeEventFilter(WindowEvent.WINDOW_SHOWN, this);
}
};
if (stage.isShowing()) {
windowTheme.handle(null);
} else {
stage.addEventFilter(WindowEvent.WINDOW_SHOWN, windowTheme);
}
AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
var transition = new PauseTransition(Duration.millis(300));
transition.setOnFinished(e -> {
windowTheme.handle(null);
});
transition.play();
});
});
} }
public static void init() { public static void init() {

View file

@ -53,7 +53,8 @@ public class AppWindowHelper {
// This allows for assigning logos even if AppImages has not been initialized yet // This allows for assigning logos even if AppImages has not been initialized yet
var dir = OsType.getLocal() == OsType.MACOS ? "img/logo/padded" : "img/logo/full"; var dir = OsType.getLocal() == OsType.MACOS ? "img/logo/padded" : "img/logo/full";
AppResources.with(AppResources.XPIPE_MODULE, dir, path -> { AppResources.with(AppResources.XPIPE_MODULE, dir, path -> {
var size = switch (OsType.getLocal()) { var size =
switch (OsType.getLocal()) {
case OsType.Linux linux -> 128; case OsType.Linux linux -> 128;
case OsType.MacOs macOs -> 128; case OsType.MacOs macOs -> 128;
case OsType.Windows windows -> 32; case OsType.Windows windows -> 32;
@ -82,12 +83,7 @@ public class AppWindowHelper {
} }
stage.setOnShown(e -> { stage.setOnShown(e -> {
// If we set the theme pseudo classes earlier when the window is not shown
// they do not apply. Is this a bug in JavaFX?
Platform.runLater(() -> {
AppTheme.initThemeHandlers(stage); AppTheme.initThemeHandlers(stage);
});
centerToMainWindow(stage); centerToMainWindow(stage);
clampWindow(stage).ifPresent(rectangle2D -> { clampWindow(stage).ifPresent(rectangle2D -> {
stage.setX(rectangle2D.getMinX()); stage.setX(rectangle2D.getMinX());

View file

@ -13,7 +13,8 @@ public class AppFontLoadingCheck {
// This can fail if the found system fonts can somehow not be loaded // This can fail if the found system fonts can somehow not be loaded
Font.getDefault(); Font.getDefault();
} catch (Throwable e) { } catch (Throwable e) {
var event = ErrorEvent.fromThrowable("Unable to load fonts", e).build(); var event = ErrorEvent.fromThrowable("Unable to load fonts. Do you have valid font packages installed?", e)
.build();
// We can't use the normal error handling facility // We can't use the normal error handling facility
// as the platform reports as working but opening windows still does not work // as the platform reports as working but opening windows still does not work
new LogErrorHandler().handle(event); new LogErrorHandler().handle(event);

View file

@ -18,7 +18,7 @@ public class AppPtbCheck {
.setContent(AppWindowHelper.alertContentText("You are running a PTB build of XPipe." .setContent(AppWindowHelper.alertContentText("You are running a PTB build of XPipe."
+ " This version is unstable and might contain bugs." + " This version is unstable and might contain bugs."
+ " You should not use it as a daily driver." + " You should not use it as a daily driver."
+ " It will also not receive regular updates." + " It will also not receive regular updates after its testing period."
+ " You will have to install and launch the normal XPipe release for that.")); + " You will have to install and launch the normal XPipe release for that."));
}); });
} }

View file

@ -55,6 +55,7 @@ public class AppShellCheck {
- On Windows, an AntiVirus program might block required programs and commands - On Windows, an AntiVirus program might block required programs and commands
- The system shell is restricted or blocked - The system shell is restricted or blocked
- Some elementary command-line tools are not available or not working correctly - Some elementary command-line tools are not available or not working correctly
- Your PATH environment variable is corrupt / incomplete
%s %s
""" """

View file

@ -1,6 +1,6 @@
package io.xpipe.app.core.mode; package io.xpipe.app.core.mode;
import io.xpipe.app.browser.BrowserModel; import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.comp.store.StoreViewState;
import io.xpipe.app.core.*; import io.xpipe.app.core.*;
import io.xpipe.app.core.check.AppAvCheck; import io.xpipe.app.core.check.AppAvCheck;
@ -47,6 +47,7 @@ public class BaseMode extends OperationMode {
AppI18n.init(); AppI18n.init();
LicenseProvider.get().init(); LicenseProvider.get().init();
AppPrefs.initLocal(); AppPrefs.initLocal();
AppI18n.init();
AppCertutilCheck.check(); AppCertutilCheck.check();
AppAvCheck.check(); AppAvCheck.check();
AppSid.init(); AppSid.init();
@ -74,7 +75,7 @@ public class BaseMode extends OperationMode {
@Override @Override
public void finalTeardown() { public void finalTeardown() {
TrackEvent.info("Background mode shutdown started"); TrackEvent.info("Background mode shutdown started");
BrowserModel.DEFAULT.reset(); BrowserSessionModel.DEFAULT.reset();
StoreViewState.reset(); StoreViewState.reset();
DataStorage.reset(); DataStorage.reset();
AppPrefs.reset(); AppPrefs.reset();

View file

@ -2,7 +2,6 @@ package io.xpipe.app.exchange;
import io.xpipe.beacon.BeaconHandler; import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.LaunchExchange; import io.xpipe.beacon.exchange.LaunchExchange;
import io.xpipe.core.process.TerminalInitScriptConfig;
import io.xpipe.core.store.LaunchableStore; import io.xpipe.core.store.LaunchableStore;
import java.util.Arrays; import java.util.Arrays;
@ -16,9 +15,9 @@ public class LaunchExchangeImpl extends LaunchExchange
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception { public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var store = getStoreEntryById(msg.getId(), false); var store = getStoreEntryById(msg.getId(), false);
if (store.getStore() instanceof LaunchableStore s) { if (store.getStore() instanceof LaunchableStore s) {
var command = s.prepareLaunchCommand() // var command = s.prepareLaunchCommand()
.prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName()), sc -> null); // .prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName()), sc -> null);
return Response.builder().command(split(command)).build(); // return Response.builder().command(split(command)).build();
} }
throw new IllegalArgumentException(store.getName() + " is not launchable"); throw new IllegalArgumentException(store.getName() + " is not launchable");

View file

@ -3,6 +3,7 @@ package io.xpipe.app.exchange;
import io.xpipe.beacon.RequestMessage; import io.xpipe.beacon.RequestMessage;
import io.xpipe.beacon.ResponseMessage; import io.xpipe.beacon.ResponseMessage;
import io.xpipe.beacon.exchange.MessageExchanges; import io.xpipe.beacon.exchange.MessageExchanges;
import io.xpipe.core.util.ModuleLayerLoader;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@ -13,27 +14,6 @@ public class MessageExchangeImpls {
private static List<MessageExchangeImpl<?, ?>> ALL; private static List<MessageExchangeImpl<?, ?>> ALL;
public static void loadAll() {
ALL = ServiceLoader.load(MessageExchangeImpl.class).stream()
.map(s -> {
// TrackEvent.trace("init", "Loaded exchange implementation " + ex.getId());
return (MessageExchangeImpl<?, ?>) s.get();
})
.collect(Collectors.toList());
ALL.forEach(messageExchange -> {
if (MessageExchanges.byId(messageExchange.getId()).isEmpty()) {
throw new AssertionError("Missing base exchange: " + messageExchange.getId());
}
});
MessageExchanges.getAll().forEach(messageExchange -> {
if (MessageExchangeImpls.byId(messageExchange.getId()).isEmpty()) {
throw new AssertionError("Missing exchange implementation: " + messageExchange.getId());
}
});
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static <RQ extends RequestMessage, RS extends ResponseMessage> Optional<MessageExchangeImpl<RQ, RS>> byId( public static <RQ extends RequestMessage, RS extends ResponseMessage> Optional<MessageExchangeImpl<RQ, RS>> byId(
String name) { String name) {
@ -53,4 +33,29 @@ public class MessageExchangeImpls {
public static List<MessageExchangeImpl<?, ?>> getAll() { public static List<MessageExchangeImpl<?, ?>> getAll() {
return ALL; return ALL;
} }
public static class Loader implements ModuleLayerLoader {
@Override
public void init(ModuleLayer layer) {
ALL = ServiceLoader.load(layer, MessageExchangeImpl.class).stream()
.map(s -> {
// TrackEvent.trace("init", "Loaded exchange implementation " + ex.getId());
return (MessageExchangeImpl<?, ?>) s.get();
})
.collect(Collectors.toList());
ALL.forEach(messageExchange -> {
if (MessageExchanges.byId(messageExchange.getId()).isEmpty()) {
throw new AssertionError("Missing base exchange: " + messageExchange.getId());
}
});
MessageExchanges.getAll().forEach(messageExchange -> {
if (MessageExchangeImpls.byId(messageExchange.getId()).isEmpty()) {
throw new AssertionError("Missing exchange implementation: " + messageExchange.getId());
}
});
}
}
} }

View file

@ -1,18 +0,0 @@
package io.xpipe.app.exchange.cli;
import io.xpipe.app.exchange.MessageExchangeImpl;
import io.xpipe.app.update.XPipeInstanceHelper;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.cli.InstanceExchange;
import io.xpipe.core.store.LocalStore;
public class InstanceExchangeImpl extends InstanceExchange
implements MessageExchangeImpl<InstanceExchange.Request, InstanceExchange.Response> {
@Override
public Response handleRequest(BeaconHandler handler, Request msg) {
return Response.builder()
.instance(XPipeInstanceHelper.getInstance(new LocalStore()).orElseThrow())
.build();
}
}

View file

@ -22,7 +22,7 @@ public class StoreProviderListExchangeImpl extends StoreProviderListExchange
.filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory())) .filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()))
.map(p -> ProviderEntry.builder() .map(p -> ProviderEntry.builder()
.id(p.getId()) .id(p.getId())
.description(p.getDisplayDescription()) .description(p.displayDescription().getValue())
.hidden(p.getCreationCategory() == null) .hidden(p.getCreationCategory() == null)
.build()) .build())
.toList())); .toList()));

View file

@ -143,15 +143,5 @@ public interface ActionProvider {
}) })
.toList()); .toList());
} }
@Override
public boolean requiresFullDaemon() {
return true;
}
@Override
public boolean prioritizeLoading() {
return false;
}
} }
} }

View file

@ -1,5 +1,6 @@
package io.xpipe.app.ext; package io.xpipe.app.ext;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.comp.base.MarkdownComp; import io.xpipe.app.comp.base.MarkdownComp;
import io.xpipe.app.comp.store.StoreEntryComp; import io.xpipe.app.comp.store.StoreEntryComp;
import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.comp.store.StoreEntryWrapper;
@ -15,6 +16,7 @@ import io.xpipe.core.dialog.Dialog;
import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStore;
import io.xpipe.core.util.JacksonizedValue; import io.xpipe.core.util.JacksonizedValue;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@ -44,6 +46,14 @@ public interface DataStoreProvider {
} }
} }
default ActionProvider.Action launchAction(DataStoreEntry store) {
return null;
}
default ActionProvider.Action browserAction(BrowserSessionModel sessionModel, DataStoreEntry store, BooleanProperty busy) {
return null;
}
default String browserDisplayName(DataStore store) { default String browserDisplayName(DataStore store) {
var e = DataStorage.get().getStoreDisplayName(store); var e = DataStorage.get().getStoreDisplayName(store);
return e.orElse("?"); return e.orElse("?");
@ -147,19 +157,15 @@ public interface DataStoreProvider {
return new SimpleStringProperty(null); return new SimpleStringProperty(null);
} }
default String i18n(String key) { default ObservableValue<String> i18n(String key) {
return AppI18n.get(getId() + "." + key); return AppI18n.observable(getId() + "." + key);
} }
default String i18nKey(String key) { default ObservableValue<String> displayName() {
return getId() + "." + key;
}
default String getDisplayName() {
return i18n("displayName"); return i18n("displayName");
} }
default String getDisplayDescription() { default ObservableValue<String> displayDescription() {
return i18n("displayDescription"); return i18n("displayDescription");
} }
@ -201,6 +207,7 @@ public interface DataStoreProvider {
COMMAND, COMMAND,
TUNNEL, TUNNEL,
SCRIPT, SCRIPT,
CLUSTER CLUSTER,
VISUAL;
} }
} }

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