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
*.bat text eol=crlf
*.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
- [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
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.
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.
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
@ -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
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
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
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 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 '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 '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.module', name: 'jackson-module-parameter-names', version: "2.17.0"
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.0"

View file

@ -12,11 +12,13 @@ public class Main {
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")) {
System.out.println("""
System.out.println(
"""
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.
""");
return;

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.HorizontalComp;
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.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.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.geometry.Point2D;
import javafx.scene.control.Button;
import javafx.scene.input.DragEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
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 final BrowserModel model;
private Point2D lastOver = new Point2D(-1, -1);
private TimerTask activeTask;
private final ObservableValue<DataStoreEntry> selected;
private final Predicate<StoreEntryWrapper> applicable;
private final BiConsumer<StoreEntryWrapper, BooleanProperty> action;
BrowserBookmarkComp(BrowserModel model) {
this.model = model;
public BrowserBookmarkComp(
ObservableValue<DataStoreEntry> selected,
Predicate<StoreEntryWrapper> applicable,
BiConsumer<StoreEntryWrapper, BooleanProperty> action) {
this.selected = selected;
this.applicable = applicable;
this.action = action;
}
@Override
protected Region createSimple() {
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<>(
StoreViewState.get().getActiveCategory().getValue());
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) -> {
comp.disable(Bindings.createBooleanBinding(
() -> {
@ -85,14 +59,15 @@ final class BrowserBookmarkComp extends SimpleComp {
},
busy));
comp.apply(struc -> {
open.addListener((observable, oldValue, newValue) -> {
struc.get()
.pseudoClassStateChanged(
SELECTED,
newValue != null
&& newValue.getEntry()
.get()
.equals(s.getWrapper().getEntry()));
selected.addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
struc.get()
.pseudoClassStateChanged(
SELECTED,
newValue != null
&& newValue.equals(
s.getWrapper().getEntry()));
});
});
});
};
@ -101,7 +76,7 @@ final class BrowserBookmarkComp extends SimpleComp {
StoreSection.createTopLevel(
StoreViewState.get().getAllEntries(), storeEntryWrapper -> true, filterText, selectedCategory),
augment,
action,
entryWrapper -> action.accept(entryWrapper, busy),
true);
var category = new DataStoreCategoryChoiceComp(
StoreViewState.get().getAllConnectionsCategory(),
@ -125,21 +100,4 @@ final class BrowserBookmarkComp extends SimpleComp {
content.getStyleClass().add("bookmark-list");
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;
import atlantafx.base.controls.Breadcrumbs;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.core.store.FileNames;
import javafx.scene.Node;
import javafx.scene.control.Button;
@ -40,7 +40,7 @@ public class BrowserBreadcrumbBar extends SimpleComp {
var breadcrumbs = new Breadcrumbs<String>();
breadcrumbs.setMinWidth(0);
SimpleChangeListener.apply(PlatformThread.sync(model.getCurrentPath()), val -> {
PlatformThread.sync(model.getCurrentPath()).subscribe(val -> {
if (val == null) {
breadcrumbs.setSelectedCrumb(null);
return;

View file

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

View file

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

View file

@ -2,8 +2,10 @@ package io.xpipe.app.browser;
import atlantafx.base.theme.Styles;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.scene.control.Label;
import javafx.scene.layout.Region;
@ -15,7 +17,9 @@ public class BrowserGreetingComp extends SimpleComp {
protected Region createSimple() {
var r = new Label(getText());
AppLayoutModel.get().getSelected().addListener((observableValue, entry, t1) -> {
r.setText(getText());
PlatformThread.runLaterIfNeeded(() -> {
r.setText(getText());
});
});
AppFont.setSize(r, 7);
r.getStyleClass().add(Styles.TEXT_BOLD);
@ -27,11 +31,11 @@ public class BrowserGreetingComp extends SimpleComp {
var hour = ldt.getHour();
String text;
if (hour > 18 || hour < 5) {
text = "Good evening";
text = AppI18n.get("goodEvening");
} else if (hour < 12) {
text = "Good morning";
text = AppI18n.get("goodMorning");
} else {
text = "Good afternoon";
text = AppI18n.get("goodAfternoon");
}
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;
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.fxcomps.Comp;
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.StackComp;
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.ThreadHelper;
import javafx.application.Platform;
@ -42,7 +43,7 @@ public class BrowserNavBar extends SimpleComp {
@Override
protected Region createSimple() {
var path = new SimpleStringProperty(model.getCurrentPath().get());
SimpleChangeListener.apply(model.getCurrentPath(), (newValue) -> {
model.getCurrentPath().subscribe((newValue) -> {
path.set(newValue);
});
path.addListener((observable, oldValue, newValue) -> {
@ -58,7 +59,7 @@ public class BrowserNavBar extends SimpleComp {
.styleClass(Styles.CENTER_PILL)
.styleClass("path-text")
.apply(struc -> {
SimpleChangeListener.apply(struc.get().focusedProperty(), val -> {
struc.get().focusedProperty().subscribe(val -> {
struc.get()
.pseudoClassStateChanged(
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
// If we start a new tab with a directory set, we have to set the pseudo class one pulse later
Platform.runLater(() -> {

View file

@ -1,10 +1,12 @@
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.core.AppI18n;
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.ListBindingsHelper;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.ThreadHelper;
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 rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
var recent = BindingsHelper.mappedContentBinding(
var recent = ListBindingsHelper.mappedContentBinding(
model.getSavedState().getRecentDirectories(),
s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()));
var recentOverview = new BrowserFileOverviewComp(model, recent, true);

View file

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

View file

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

View file

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

View file

@ -1,6 +1,9 @@
package io.xpipe.app.browser;
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.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
@ -11,6 +14,7 @@ import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.util.HumanReadableFormat;
import javafx.beans.binding.Bindings;
import javafx.scene.control.ToolBar;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Region;
import lombok.EqualsAndHashCode;
import lombok.Value;
@ -59,7 +63,7 @@ public class BrowserStatusBarComp extends SimpleComp {
private Comp<?> createClipboardStatus() {
var cc = BrowserClipboard.currentCopyClipboard;
var ccCount = (BindingsHelper.persist(Bindings.createStringBinding(
var ccCount = Bindings.createStringBinding(
() -> {
if (cc.getValue() != null && cc.getValue().getEntries().size() > 0) {
return cc.getValue().getEntries().size() + " file"
@ -68,7 +72,7 @@ public class BrowserStatusBarComp extends SimpleComp {
return null;
}
},
cc)));
cc);
return new LabelComp(ccCount);
}
@ -86,7 +90,7 @@ public class BrowserStatusBarComp extends SimpleComp {
.count();
},
model.getFileList().getAll());
var selectedComp = new LabelComp(BindingsHelper.persist(Bindings.createStringBinding(
var selectedComp = new LabelComp(Bindings.createStringBinding(
() -> {
if (selectedCount.getValue().intValue() == 0) {
return null;
@ -95,7 +99,7 @@ public class BrowserStatusBarComp extends SimpleComp {
}
},
selectedCount,
allCount)));
allCount));
return selectedComp;
}
@ -124,6 +128,10 @@ public class BrowserStatusBarComp extends SimpleComp {
});
// 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;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.comp.base.LoadingOverlayComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.core.process.OsType;
@ -37,18 +38,22 @@ public class BrowserTransferComp extends SimpleComp {
@Override
protected Region createSimple() {
var syncItems = PlatformThread.sync(model.getItems());
var syncDownloaded = PlatformThread.sync(model.getDownloading());
var syncAllDownloaded = PlatformThread.sync(model.getAllDownloaded());
var background = new LabelComp(AppI18n.observable("transferDescription"))
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
.visible(BindingsHelper.persist(Bindings.isEmpty(model.getItems())));
.visible(Bindings.isEmpty(syncItems));
var backgroundStack =
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(
binding,
entry -> Bindings.createStringBinding(
() -> {
var sourceItem = model.getItems().stream()
var sourceItem = syncItems.stream()
.filter(item -> item.getFileEntry() == entry)
.findAny();
if (sourceItem.isEmpty()) {
@ -63,27 +68,27 @@ public class BrowserTransferComp extends SimpleComp {
.orElse("?");
return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
},
model.getAllDownloaded()))
syncAllDownloaded))
.apply(struc -> struc.get().setMinHeight(150))
.grow(false, true);
var dragNotice = new LabelComp(model.getAllDownloaded()
var dragNotice = new LabelComp(syncAllDownloaded
.flatMap(aBoolean ->
aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
.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)
.apply(struc -> struc.get().setPadding(new Insets(8)));
var downloadButton = new IconButtonComp("mdi2d-download", () -> {
model.download();
})
.hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems())))
.disable(PlatformThread.sync(model.getAllDownloaded()))
.apply(new FancyTooltipAugment<>("downloadStageDescription"));
.hide(Bindings.isEmpty(syncItems))
.disable(syncAllDownloaded)
.apply(new TooltipAugment<>("downloadStageDescription"));
var clearButton = new IconButtonComp("mdi2c-close", () -> {
model.clear();
})
.hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems())));
.hide(Bindings.isEmpty(syncItems));
var clearPane = Comp.derive(
new HorizontalComp(List.of(downloadButton, clearButton))
.apply(struc -> struc.get().setSpacing(10)),
@ -122,12 +127,15 @@ public class BrowserTransferComp extends SimpleComp {
return;
}
if (!(model.getBrowserSessionModel()
.getSelectedEntry()
.getValue()
instanceof OpenFileSystemModel fileSystemModel)) {
return;
}
var files = drag.getEntries();
model.drop(
model.getBrowserModel()
.getSelected()
.getValue(),
files);
model.drop(fileSystemModel, files);
event.setDropCompleted(true);
event.consume();
}
@ -140,11 +148,11 @@ public class BrowserTransferComp extends SimpleComp {
}
});
struc.get().setOnDragDetected(event -> {
if (model.getDownloading().get()) {
if (syncDownloaded.getValue()) {
return;
}
var selected = model.getItems().stream()
var selected = syncItems.stream()
.map(BrowserTransferModel.Item::getFileEntry)
.toList();
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
@ -154,7 +162,7 @@ public class BrowserTransferComp extends SimpleComp {
return;
}
var files = model.getItems().stream()
var files = syncItems.stream()
.filter(item -> item.downloadFinished().get())
.map(item -> {
try {
@ -191,7 +199,7 @@ public class BrowserTransferComp extends SimpleComp {
event.consume();
});
}),
PlatformThread.sync(model.getDownloading()));
syncDownloaded);
return stack.styleClass("transfer").createRegion();
}
}

View file

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

View file

@ -9,7 +9,7 @@ public class BrowserTransferProgress {
long transferred;
long total;
static BrowserTransferProgress empty() {
public static BrowserTransferProgress empty() {
return new BrowserTransferProgress(null, 0, 0);
}
@ -17,7 +17,7 @@ public class BrowserTransferProgress {
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);
}

View file

@ -1,10 +1,12 @@
package io.xpipe.app.browser;
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.ListBoxViewComp;
import io.xpipe.app.comp.base.TileButtonComp;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp;
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.PrettySvgComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
@ -31,9 +34,9 @@ import java.util.List;
public class BrowserWelcomeComp extends SimpleComp {
private final BrowserModel model;
private final BrowserSessionModel model;
public BrowserWelcomeComp(BrowserModel model) {
public BrowserWelcomeComp(BrowserSessionModel model) {
this.model = model;
}
@ -54,13 +57,14 @@ public class BrowserWelcomeComp extends SimpleComp {
hbox.setSpacing(15);
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);
hbox.setPadding(new Insets(40, 40, 40, 50));
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());
if (entry.isEmpty()) {
return false;
@ -74,14 +78,14 @@ public class BrowserWelcomeComp extends SimpleComp {
});
var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list);
var header = new LabelComp(Bindings.createStringBinding(
() -> {
return !empty.get()
? "You were recently connected to the following systems:"
: "Here you will be able to see where you left off last time.";
},
empty))
.createRegion();
var headerBinding = BindingsHelper.flatMap(empty, b -> {
if (b) {
return AppI18n.observable("browserWelcomeEmpty");
} else {
return AppI18n.observable("browserWelcomeSystems");
}
});
var header = new LabelComp(headerBinding).createRegion();
AppFont.setSize(header, 1);
vbox.getChildren().add(header);
@ -89,10 +93,13 @@ public class BrowserWelcomeComp extends SimpleComp {
storeList.setSpacing(8);
var listBox = new ListBoxViewComp<>(list, list, e -> {
var disable = new SimpleBooleanProperty();
var disable = new SimpleBooleanProperty();
var entryButton = entryButton(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 -> {
VBox vBox = (VBox) struc.get().getContent();
@ -125,15 +132,17 @@ public class BrowserWelcomeComp extends SimpleComp {
private Comp<?> entryButton(BrowserSavedState.Entry e, BooleanProperty disable) {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
var graphic = entry.get()
.getProvider()
.getDisplayIconFileName(entry.get().getStore());
var graphic =
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24);
return new ButtonComp(new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())), view.createRegion(), () -> {
ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable);
});
})
return new ButtonComp(
new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())),
view.createRegion(),
() -> {
ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable);
});
})
.minWidth(250)
.accessibleText(DataStorage.get().getStoreDisplayName(entry.get()))
.disable(disable)
@ -144,10 +153,10 @@ public class BrowserWelcomeComp extends SimpleComp {
private Comp<?> dirButton(BrowserSavedState.Entry e, BooleanProperty disable) {
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
return new ButtonComp(new SimpleStringProperty(e.getPath()), null, () -> {
ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable);
});
})
ThreadHelper.runAsync(() -> {
model.restoreStateAsync(e, disable);
});
})
.accessibleText(e.getPath())
.disable(disable)
.styleClass("directory-button")

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;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import java.util.List;

View file

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

View file

@ -1,9 +1,10 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.core.util.ModuleLayerLoader;
import javafx.beans.value.ObservableValue;
import javafx.scene.Node;
import javafx.scene.input.KeyCombination;
@ -53,7 +54,7 @@ public interface BrowserAction {
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) {
return true;
@ -91,15 +92,5 @@ public interface BrowserAction {
})
.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;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.file.BrowserEntry;
import java.util.List;

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel;
import io.xpipe.app.browser.file.BrowserEntry;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.util.FileOpener;
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.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.BrowserAction;
import io.xpipe.app.browser.action.LeafAction;
import io.xpipe.app.browser.fs.OpenFileSystemModel;
import io.xpipe.app.core.AppFont;
import io.xpipe.app.util.LicenseProvider;
import javafx.scene.control.ContextMenu;
@ -13,7 +14,7 @@ import org.kordamp.ikonli.javafx.FontIcon;
import java.util.ArrayList;
import java.util.List;
final class BrowserContextMenu extends ContextMenu {
public final class BrowserContextMenu extends ContextMenu {
private final OpenFileSystemModel model;
private final BrowserEntry source;
@ -74,17 +75,17 @@ final class BrowserContextMenu extends ContextMenu {
for (BrowserAction a : all) {
var used = resolveIfNeeded(a, selected);
if (a instanceof LeafAction la) {
getItems().add(la.toMenuItem(model, used, s -> s));
getItems().add(la.toMenuItem(model, used));
}
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)) {
var subUsed = resolveIfNeeded(sub, selected);
if (!sub.isApplicable(model, subUsed)) {
continue;
}
m.getItems().add(sub.toMenuItem(model, subUsed, s -> s));
m.getItems().add(sub.toMenuItem(model, subUsed));
}
var graphic = a.getIcon(model, used);
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.BrowserIconFileType;
@ -29,7 +29,7 @@ public class BrowserEntry {
return null;
}
for (var f : BrowserIconFileType.ALL) {
for (var f : BrowserIconFileType.getAll()) {
if (f.matches(rawFileEntry)) {
return f;
}
@ -43,7 +43,7 @@ public class BrowserEntry {
return null;
}
for (var f : BrowserIconDirectoryType.ALL) {
for (var f : BrowserIconDirectoryType.getAll()) {
if (f.matches(rawFileEntry)) {
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.theme.Styles;
import io.xpipe.app.browser.action.BrowserAction;
import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.comp.base.LazyTextFieldComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.HumanReadableFormat;
@ -48,7 +48,7 @@ import java.util.Objects;
import static io.xpipe.app.util.HumanReadableFormat.byteCount;
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 EMPTY = PseudoClass.getPseudoClass("empty");
@ -71,7 +71,8 @@ final class BrowserFileListComp extends SimpleComp {
@SuppressWarnings("unchecked")
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(
param.getValue() != null
? FileNames.getFileName(
@ -81,17 +82,20 @@ final class BrowserFileListComp extends SimpleComp {
filenameCol.setSortType(ASCENDING);
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(
param.getValue().getRawFileEntry().resolved().getSize()));
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<>(
param.getValue().getRawFileEntry().resolved().getDate()));
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<>(
param.getValue().getRawFileEntry().resolved().getMode()));
modeCol.setCellFactory(col -> new FileModeCell());
@ -122,7 +126,7 @@ final class BrowserFileListComp extends SimpleComp {
}
private void prepareTableSelectionModel(TableView<BrowserEntry> table) {
if (!fileList.getMode().isMultiple()) {
if (!fileList.getSelectionMode().isMultiple()) {
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
} else {
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
@ -142,9 +146,9 @@ final class BrowserFileListComp extends SimpleComp {
.getPath()));
// Remove unsuitable selection
toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY
&& !fileList.getMode().isAcceptsDirectories())
&& !fileList.getSelectionMode().isAcceptsDirectories())
|| (browserEntry.getRawFileEntry().getKind() != FileKind.DIRECTORY
&& !fileList.getMode().isAcceptsFiles()));
&& !fileList.getSelectionMode().isAcceptsFiles()));
fileList.getSelection().setAll(toSelect);
Platform.runLater(() -> {
@ -268,7 +272,7 @@ final class BrowserFileListComp extends SimpleComp {
return false;
},
null,
null,
() -> {
if (row.getItem() != null && row.getItem().isSynthetic()) {
return null;
@ -505,16 +509,21 @@ final class BrowserFileListComp extends SimpleComp {
.get();
var quickAccess = new BrowserQuickAccessButtonComp(
() -> getTableRow().getItem(), fileList.getFileSystemModel())
.hide(BindingsHelper.persist(Bindings.createBooleanBinding(
() -> {
var item = getTableRow().getItem();
var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY;
var isParentLink = item
.getRawFileEntry()
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
return notDir || isParentLink;
},
itemProperty())))
.hide(Bindings.createBooleanBinding(
() -> {
var item = getTableRow().getItem();
var notDir = item.getRawFileEntry()
.resolved()
.getKind()
!= FileKind.DIRECTORY;
var isParentLink = item.getRawFileEntry()
.equals(fileList.getFileSystemModel()
.getCurrentParentDirectory());
return notDir || isParentLink;
},
itemProperty())
.not()
.not())
.createRegion();
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 javafx.geometry.Point2D;
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.core.store.FileKind;
import io.xpipe.core.store.FileNames;
@ -25,6 +26,8 @@ public final class BrowserFileListModel {
static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR =
Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
private final OpenFileSystemModel.SelectionMode selectionMode;
private final OpenFileSystemModel fileSystemModel;
private final Property<Comparator<BrowserEntry>> comparatorProperty =
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
@ -33,13 +36,14 @@ public final class BrowserFileListModel {
private final ObservableList<BrowserEntry> previousSelection = FXCollections.observableArrayList();
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
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<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
private final Property<BrowserEntry> editing = new SimpleObjectProperty<>();
public BrowserFileListModel(OpenFileSystemModel fileSystemModel) {
public BrowserFileListModel(OpenFileSystemModel.SelectionMode selectionMode, OpenFileSystemModel fileSystemModel) {
this.selectionMode = selectionMode;
this.fileSystemModel = fileSystemModel;
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) {
try (var s = newFiles) {
var parent = fileSystemModel.getCurrentParentDirectory();
@ -135,12 +135,6 @@ public final class BrowserFileListModel {
}
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) {
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.comp.base.ListBoxViewComp;
import io.xpipe.app.comp.base.VBoxViewComp;
@ -31,8 +32,10 @@ public class BrowserFileOverviewComp extends SimpleComp {
Function<FileSystem.FileEntry, Comp<?>> factory = entry -> {
return Comp.of(() -> {
var icon = BrowserIcons.createIcon(entry);
var graphic = new HorizontalComp(List.of(icon,
new BrowserQuickAccessButtonComp(() -> new BrowserEntry(entry, model.getFileList(),false),model)));
var graphic = new HorizontalComp(List.of(
icon,
new BrowserQuickAccessButtonComp(
() -> new BrowserEntry(entry, model.getFileList(), false), model)));
var l = new Button(entry.getPath(), graphic.createRegion());
l.setGraphicTextGap(1);
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.impl.IconButtonComp;
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.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.util.BooleanAnimationTimer;
@ -28,18 +29,105 @@ import java.util.stream.Collectors;
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
class QuickAccessMenu {
private final BrowserEntry browserEntry;
private ContextMenu browserActionMenu;
private final Menu menu;
private ContextMenu browserActionMenu;
public QuickAccessMenu(BrowserEntry browserEntry) {
this.browserEntry = browserEntry;
this.menu = new Menu(
// Use original name, not the link target
browserEntry.getRawFileEntry().getName(),
PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24)
PrettyImageHelper.ofFixedSizeSquare(
FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24)
.createRegion());
createMenu();
addInputListeners();
@ -48,7 +136,7 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
private void createMenu() {
var fileEntry = browserEntry.getRawFileEntry();
if (fileEntry.resolved().getKind() != FileKind.DIRECTORY) {
createFileMenu();
createFileMenu();
} else {
createDirectoryMenu();
}
@ -140,8 +228,9 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
}
});
new BooleanAnimationTimer(hover, 100, () -> {
expandDirectoryMenu(empty);
}).start();
expandDirectoryMenu(empty);
})
.start();
}
private void addInputListeners() {
@ -154,13 +243,15 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
} else {
expandBrowserActionMenuKey = false;
}
if (event.getCode().equals(KeyCode.LEFT) && browserActionMenu != null && browserActionMenu.isShowing()) {
if (event.getCode().equals(KeyCode.LEFT)
&& browserActionMenu != null
&& browserActionMenu.isShowing()) {
closeBrowserActionMenuKey = true;
} else {
closeBrowserActionMenuKey = false;
}
});
contextMenu.addEventFilter(MouseEvent.ANY,event -> {
contextMenu.addEventFilter(MouseEvent.ANY, event -> {
keyBasedNavigation = 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.core.process.OsType;
import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames;
import io.xpipe.core.store.FileSystem;
import io.xpipe.core.store.LocalStore;
import io.xpipe.core.store.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
@ -151,6 +151,18 @@ public class FileSystemHelper {
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(
FileSystem.FileEntry entry,
List<Path> files,
@ -278,7 +290,8 @@ public class FileSystemHelper {
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
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) {
// This one is up-to-date and does not need to be recalculated
totalSize.addAndGet(fileEntry.getSize());
@ -293,7 +306,8 @@ public class FileSystemHelper {
AtomicLong transferred = new AtomicLong();
for (var e : flatFiles.entrySet()) {
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())) {
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.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 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.file.BrowserContextMenu;
import io.xpipe.app.browser.file.BrowserFileListComp;
import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.comp.base.MultiContentComp;
import io.xpipe.app.fxcomps.Comp;
@ -9,7 +15,6 @@ import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -55,7 +60,9 @@ public class OpenFileSystemComp extends SimpleComp {
var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open"));
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));
menuButton.disableProperty().bind(model.getInOverview());
menuButton.setAccessibleText("Directory options");
@ -97,7 +104,7 @@ public class OpenFileSystemComp extends SimpleComp {
home,
model.getCurrentPath().isNull(),
fileList,
BindingsHelper.persist(model.getCurrentPath().isNull().not())));
model.getCurrentPath().isNull().not()));
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.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.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.fxcomps.Comp;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef;
@ -27,45 +35,90 @@ import java.util.Optional;
import java.util.stream.Stream;
@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 BrowserFileListModel fileList;
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
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 BooleanProperty inOverview = new SimpleBooleanProperty();
private final String name;
private final String tooltip;
private final Property<BrowserTransferProgress> progress =
new SimpleObjectProperty<>(BrowserTransferProgress.empty());
private FileSystem fileSystem;
private OpenFileSystemSavedState savedState;
private OpenFileSystemCache cache;
public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef<? extends FileSystemStore> entry) {
this.browserModel = browserModel;
this.entry = entry;
this.name = DataStorage.get().getStoreDisplayName(entry.get());
this.tooltip = DataStorage.get().getId(entry.getEntry()).toString();
public OpenFileSystemModel(
BrowserAbstractSessionModel<?> model,
DataStoreEntryRef<? extends FileSystemStore> entry,
SelectionMode selectionMode) {
super(model, entry);
this.inOverview.bind(Bindings.createBooleanBinding(
() -> {
return currentPath.get() == null;
},
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()
|| (fileSystem != null
&& fileSystem.getShell().isPresent()
&& 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 {
if (fileSystem == null) {
return;
@ -185,7 +238,7 @@ public final class OpenFileSystemModel {
var name = adjustedPath + " - " + entry.get().getName();
ThreadHelper.runFailableAsync(() -> {
if (ShellDialects.getStartableDialects().stream()
.anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand(null)))) {
.anyMatch(dialect -> adjustedPath.toLowerCase().startsWith(dialect.getExecutableName().toLowerCase()))) {
TerminalLauncher.open(
entry.getEntry(),
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() {
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 {
cdSyncWithoutCheck(dir);
}
@ -414,10 +435,6 @@ public final class OpenFileSystemModel {
history.updateCurrent(null);
}
void initSavedState() {
this.savedState = OpenFileSystemSavedState.loadForStore(this);
}
public void openTerminalAsync(String directory) {
ThreadHelper.runFailableAsync(() -> {
if (fileSystem == null) {
@ -445,4 +462,23 @@ public final class OpenFileSystemModel {
public void forthSync(int i) throws Exception {
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.JsonParser;

View file

@ -15,18 +15,18 @@ import java.util.List;
import java.util.Set;
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()
.filter(fileType -> fileType.getId().equals(id))
.findAny()
.orElseThrow();
}
static void loadDefinitions() {
public static synchronized void loadDefinitions() {
ALL.add(new BrowserIconDirectoryType() {
@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
private final String id;

View file

@ -12,18 +12,18 @@ import java.nio.file.Files;
import java.util.*;
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()
.filter(fileType -> fileType.getId().equals(id))
.findAny()
.orElseThrow();
}
static void loadDefinitions() {
public static synchronized void loadDefinitions() {
AppResources.with(AppResources.XPIPE_MODULE, "file_list.txt", path -> {
try (var reader =
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
class Simple implements BrowserIconFileType {
public static class Simple extends BrowserIconFileType {
private final String id;
private final IconVariant icon;

View file

@ -11,29 +11,27 @@ public class FileIconManager {
public static synchronized void loadIfNecessary() {
if (!loaded) {
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons");
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons", true, false);
loaded = true;
}
}
public static String getFileIcon(FileSystem.FileEntry entry, boolean open) {
public static synchronized String getFileIcon(FileSystem.FileEntry entry, boolean open) {
if (entry == null) {
return null;
}
loadIfNecessary();
var r = entry.resolved();
if (r.getKind() != FileKind.DIRECTORY) {
for (var f : BrowserIconFileType.ALL) {
for (var f : BrowserIconFileType.getAll()) {
if (f.matches(r)) {
return getIconPath(f.getIcon());
return f.getIcon();
}
}
} else {
for (var f : BrowserIconDirectoryType.ALL) {
for (var f : BrowserIconDirectoryType.getAll()) {
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")
: "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.Spacer;
import atlantafx.base.theme.Styles;
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.browser.BrowserWelcomeComp;
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.SimpleComp;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.VerticalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.util.BooleanScope;
import io.xpipe.app.util.ThreadHelper;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
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.layout.*;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -42,106 +33,25 @@ import static atlantafx.base.theme.Styles.DENSE;
import static atlantafx.base.theme.Styles.toggleStyleClass;
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;
}
@Override
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() {
public Region createSimple() {
var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of(
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)),
Bindings.createBooleanBinding(
() -> {
return model.getOpenFileSystems().size() == 0
&& !model.getMode().isChooser();
return model.getSessionEntries().size() == 0;
},
model.getOpenFileSystems())));
return multi;
model.getSessionEntries())));
return multi.createRegion();
}
private TabPane createTabPane() {
@ -153,16 +63,17 @@ public class BrowserComp extends SimpleComp {
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
toggleStyleClass(tabs, DENSE);
var map = new HashMap<OpenFileSystemModel, Tab>();
var map = new HashMap<BrowserSessionTab<?>, Tab>();
// Restore state
model.getOpenFileSystems().forEach(v -> {
model.getSessionEntries().forEach(v -> {
var t = createTab(tabs, v);
map.put(v, t);
tabs.getTabs().add(t);
});
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!
var modifying = new SimpleBooleanProperty();
@ -174,7 +85,7 @@ public class BrowserComp extends SimpleComp {
}
if (newValue == null) {
model.getSelected().setValue(null);
model.getSelectedEntry().setValue(null);
return;
}
@ -184,11 +95,11 @@ public class BrowserComp extends SimpleComp {
.findAny()
.map(Map.Entry::getKey)
.orElse(null);
model.getSelected().setValue(source);
model.getSelectedEntry().setValue(source);
});
// Handle selection from model
model.getSelected().addListener((observable, oldValue, newValue) -> {
model.getSelectedEntry().addListener((observable, oldValue, newValue) -> {
PlatformThread.runLaterIfNeeded(() -> {
if (newValue == 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()) {
for (var r : c.getRemoved()) {
PlatformThread.runLaterIfNeeded(() -> {
@ -247,14 +158,14 @@ public class BrowserComp extends SimpleComp {
continue;
}
model.closeFileSystemAsync(source.getKey());
model.closeAsync(source.getKey());
}
}
});
return tabs;
}
private Tab createTab(TabPane tabs, OpenFileSystemModel model) {
private Tab createTab(TabPane tabs, BrowserSessionTab<?> model) {
var tab = new Tab();
var ring = new RingProgressIndicator(0, false);
@ -279,12 +190,12 @@ public class BrowserComp extends SimpleComp {
PlatformThread.sync(model.getBusy())));
tab.setText(model.getName());
tab.setContent(new OpenFileSystemComp(model).createSimple());
tab.setContent(model.comp().createRegion());
var id = UUID.randomUUID().toString();
tab.setId(id);
SimpleChangeListener.apply(tabs.skinProperty(), newValue -> {
tabs.skinProperty().subscribe(newValue -> {
if (newValue != null) {
Platform.runLater(() -> {
Label l = (Label) tabs.lookup("#" + id + " .tab-label");
@ -303,7 +214,7 @@ public class BrowserComp extends SimpleComp {
if (color != null) {
c.getStyleClass().add(color.getId());
}
new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c);
new TooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c);
c.addEventHandler(
DragEvent.DRAG_ENTERED,
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.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import java.util.Map;
import java.util.stream.Collectors;
public class AppLayoutComp extends Comp<CompStructure<Pane>> {
@ -22,18 +24,20 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
@Override
public CompStructure<Pane> createBase() {
var multi = new MultiContentComp(model.getEntries().stream()
Map<Comp<?>, ObservableValue<Boolean>> map = model.getEntries().stream()
.collect(Collectors.toMap(
entry -> entry.comp(),
entry -> PlatformThread.sync(Bindings.createBooleanBinding(
entry -> Bindings.createBooleanBinding(
() -> {
return model.getSelected().getValue().equals(entry);
},
model.getSelected())))));
model.getSelected())));
var multi = new MultiContentComp(map);
var pane = new BorderPane();
var sidebar = new SideMenuBarComp(model.getSelectedInternal(), model.getEntries());
pane.setCenter(multi.createRegion());
var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries());
StackPane multiR = (StackPane) multi.createRegion();
pane.setCenter(multiR);
pane.setRight(sidebar.createRegion());
pane.getStyleClass().add("background");
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.CompStructure;
import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
@ -50,7 +49,7 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
var graphic = getGraphic();
if (graphic instanceof FontIcon f) {
// f.iconColorProperty().bind(button.textFillProperty());
SimpleChangeListener.apply(button.fontProperty(), c -> {
button.fontProperty().subscribe(c -> {
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.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import javafx.css.Size;
import javafx.css.SizeUnits;
import javafx.scene.control.Button;
@ -38,12 +37,12 @@ public class DropdownComp extends Comp<CompStructure<Button>> {
.createRegion();
button.visibleProperty()
.bind(BindingsHelper.anyMatch(cm.getItems().stream()
.bind(ListBindingsHelper.anyMatch(cm.getItems().stream()
.map(menuItem -> menuItem.getGraphic().visibleProperty())
.toList()));
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());
});

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.CompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.control.TextField;
@ -65,7 +64,7 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
sp.prefHeightProperty().bind(r.prefHeightProperty());
r.setDisable(true);
SimpleChangeListener.apply(currentValue, n -> {
currentValue.subscribe(n -> {
PlatformThread.runLaterIfNeeded(() -> {
// 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())) {

View file

@ -3,7 +3,7 @@ package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
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 javafx.application.Platform;
import javafx.collections.ListChangeListener;
@ -88,7 +88,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
}
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.SimpleCompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.prefs.AppPrefs;
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();
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
try {
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.SimpleComp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
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.StackPane;
@ -12,24 +14,54 @@ import java.util.Map;
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) {
this.content = FXCollections.observableMap(content);
}
public MultiContentComp(ObservableMap<Comp<?>, ObservableValue<Boolean>> content) {
this.content = content;
}
@Override
protected Region createSimple() {
ObservableMap<Comp<?>, Region> m = FXCollections.observableHashMap();
content.addListener((MapChangeListener<? super Comp<?>, ? super ObservableValue<Boolean>>) change -> {
if (change.wasAdded()) {
var r = change.getKey().createRegion();
change.getValueAdded().subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
r.setManaged(val);
r.setVisible(val);
});
});
m.put(change.getKey(), r);
} else {
m.remove(change.getKey());
}
});
var stack = new StackPane();
stack.setPickOnBounds(false);
for (Map.Entry<Comp<?>, ObservableValue<Boolean>> entry : content.entrySet()) {
var region = entry.getKey().createRegion();
stack.getChildren().add(region);
SimpleChangeListener.apply(PlatformThread.sync(entry.getValue()), val -> {
region.setManaged(val);
region.setVisible(val);
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;
}
}

View file

@ -37,7 +37,7 @@ public class OsLogoComp extends SimpleComp {
@Override
protected Region createSimple() {
var img = BindingsHelper.persist(Bindings.createObjectBinding(
var img = Bindings.createObjectBinding(
() -> {
if (state.getValue() != SystemStateComp.State.SUCCESS) {
return null;
@ -51,7 +51,7 @@ public class OsLogoComp extends SimpleComp {
return getImage(ons.getOsName());
},
wrapper.getPersistentState(),
state));
state);
var hide = BindingsHelper.map(img, s -> s != null);
return new StackComp(
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.SimpleCompStructure;
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.TooltipAugment;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.UserReportComp;
@ -73,7 +73,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
var e = entries.get(i);
var b = new IconButtonComp(e.icon(), () -> value.setValue(e));
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 -> {
AppFont.setSize(struc.get(), 2);
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
@ -133,7 +133,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
UserReportComp.show(event.build());
})
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()]))
.apply(new FancyTooltipAugment<>("reportIssue"))
.apply(new TooltipAugment<>("reportIssue"))
.apply(simpleBorders)
.accessibleTextKey("reportIssue");
b.apply(struc -> {
@ -145,7 +145,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{
var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1]))
.apply(new FancyTooltipAugment<>("visitGithubRepository"))
.apply(new TooltipAugment<>("visitGithubRepository"))
.apply(simpleBorders)
.accessibleTextKey("visitGithubRepository");
b.apply(struc -> {
@ -157,7 +157,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
{
var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD))
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]))
.apply(new FancyTooltipAugment<>("discord"))
.apply(new TooltipAugment<>("discord"))
.apply(simpleBorders)
.accessibleTextKey("discord");
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]))
.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");
b.apply(struc -> {
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.core.AppI18n;
import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
@ -32,13 +31,13 @@ public class StoreToggleComp extends SimpleComp {
@Override
protected Region createSimple() {
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
&& section.getShowDetails().get();
},
section.getWrapper().getValidity(),
section.getShowDetails()));
section.getShowDetails());
var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey))
.visible(visible)
.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.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.core.process.ShellStoreState;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
@ -35,7 +34,7 @@ public class SystemStateComp extends SimpleComp {
state));
var fi = new FontIcon();
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");
border.getStyleClass().add("outer-icon");
@ -63,9 +62,11 @@ public class SystemStateComp extends SimpleComp {
""";
pane.getStylesheets().add(Styles.toDataURI(dataClass1));
SimpleChangeListener.apply(PlatformThread.sync(state), val -> {
pane.getStylesheets().removeAll(success, failure, other);
pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other);
state.subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> {
pane.getStylesheets().removeAll(success, failure, other);
pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other);
});
});
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.CompStructure;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
@ -13,7 +12,6 @@ import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import lombok.AllArgsConstructor;
import lombok.Builder;
@ -56,12 +54,8 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
var text = new VBox(header, desc);
text.setSpacing(2);
var fi = new FontIcon();
SimpleChangeListener.apply(PlatformThread.sync(icon), val -> {
fi.setIconLiteral(val);
});
var pane = new StackPane(fi);
var fi = new FontIconComp(icon).createStructure();
var pane = fi.getPane();
var hbox = new HBox(pane, text);
hbox.setSpacing(8);
pane.prefWidthProperty()
@ -76,11 +70,11 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
desc.heightProperty()));
pane.prefHeightProperty().addListener((c, o, n) -> {
var size = Math.min(n.intValue(), 100);
fi.setIconSize((int) (size * 0.55));
fi.getIcon().setIconSize((int) (size * 0.55));
});
bt.setGraphic(hbox);
return Structure.builder()
.graphic(fi)
.graphic(fi.getIcon())
.button(bt)
.content(hbox)
.name(header)

View file

@ -22,7 +22,7 @@ public class ToggleSwitchComp extends SimpleComp {
@Override
protected Region createSimple() {
var s = new ToggleSwitch();
s.addEventFilter(KeyEvent.KEY_PRESSED,event -> {
s.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.SPACE || event.getCode() == KeyCode.ENTER) {
s.setSelected(!s.isSelected());
event.consume();

View file

@ -1,5 +1,6 @@
package io.xpipe.app.comp.store;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
@ -80,6 +81,10 @@ public class StoreCategoryWrapper {
private void setupListeners() {
name.addListener((c, o, n) -> {
if (n.equals(translatedName(category.getName()))) {
return;
}
category.setName(n);
});
@ -91,6 +96,10 @@ public class StoreCategoryWrapper {
update();
});
AppPrefs.get().language().addListener((observable, oldValue, newValue) -> {
update();
});
sortMode.addListener((observable, oldValue, newValue) -> {
category.setSortMode(newValue);
});
@ -112,8 +121,9 @@ public class StoreCategoryWrapper {
public void update() {
// Avoid reupdating name when changed from the name property!
if (!category.getName().equals(name.getValue())) {
name.setValue(category.getName());
var catName = translatedName(category.getName());
if (!catName.equals(name.getValue())) {
name.setValue(catName);
}
lastAccess.setValue(category.getLastAccess().minus(Duration.ofMillis(500)));
@ -140,18 +150,30 @@ public class StoreCategoryWrapper {
});
}
public String getName() {
return name.getValue();
private String translatedName(String original) {
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() {
return name;
}
public Instant getLastAccess() {
return lastAccess.getValue();
}
public Property<Instant> lastAccessProperty() {
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.augment.GrowAugment;
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.ExceptionConverter;
import io.xpipe.app.issue.TrackEvent;
@ -381,7 +380,7 @@ public class StoreCreationComp extends DialogComp {
providerChoice.apply(GrowAugment.create(true, false));
providerChoice.onSceneAssign(struc -> struc.get().requestFocus());
SimpleChangeListener.apply(provider, n -> {
provider.subscribe(n -> {
if (n != null) {
var d = n.guiDialog(existingEntry, store);
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("addVisual", "mdi2c-camera-plus", DataStoreProvider.CreationCategory.VISUAL, null));
menu.getItems()
.add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null));
@ -81,7 +84,8 @@ public class StoreCreationMenu {
event.consume();
});
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)
.createRegion());
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.GrowAugment;
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.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreColor;
@ -97,12 +95,15 @@ public abstract class StoreEntryComp extends SimpleComp {
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(
Comp.of(() -> button),
BindingsHelper.persist(
wrapper.getInRefresh().and(wrapper.getObserving().not())));
wrapper.getInRefresh().and(wrapper.getObserving().not()));
return loading.createRegion();
}
@ -138,7 +139,7 @@ public abstract class StoreEntryComp extends SimpleComp {
}
protected void applyState(Node node) {
SimpleChangeListener.apply(PlatformThread.sync(wrapper.getValidity()), val -> {
PlatformThread.sync(wrapper.getValidity()).subscribe(val -> {
switch (val) {
case LOAD_FAILED -> {
node.pseudoClassStateChanged(FAILED, true);
@ -174,8 +175,7 @@ public abstract class StoreEntryComp extends SimpleComp {
var imageComp = PrettyImageHelper.ofFixedSize(img, w, h);
var storeIcon = imageComp.createRegion();
if (wrapper.getValidity().getValue().isUsable()) {
new FancyTooltipAugment<>(new SimpleStringProperty(
wrapper.getEntry().getProvider().getDisplayName()))
new TooltipAugment<>(wrapper.getEntry().getProvider().displayName())
.augment(storeIcon);
}
@ -212,7 +212,7 @@ public abstract class StoreEntryComp extends SimpleComp {
});
button.accessibleText(
actionProvider.getName(wrapper.getEntry().ref()).getValue());
button.apply(new FancyTooltipAugment<>(
button.apply(new TooltipAugment<>(
actionProvider.getName(wrapper.getEntry().ref())));
if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) {
button.hide(Bindings.not(p.getValue()));
@ -247,8 +247,10 @@ public abstract class StoreEntryComp extends SimpleComp {
settingsButton.styleClass("settings");
settingsButton.accessibleText("More");
settingsButton.apply(new ContextMenuAugment<>(
event -> event.getButton() == MouseButton.PRIMARY, null, () -> StoreEntryComp.this.createContextMenu()));
settingsButton.apply(new FancyTooltipAugment<>("more"));
event -> event.getButton() == MouseButton.PRIMARY,
null,
() -> StoreEntryComp.this.createContextMenu()));
settingsButton.apply(new TooltipAugment<>("more"));
return settingsButton;
}
@ -371,7 +373,8 @@ public abstract class StoreEntryComp extends SimpleComp {
StoreViewState.get()
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
.forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem(storeCategoryWrapper.getName());
MenuItem m = new MenuItem();
m.textProperty().bind(storeCategoryWrapper.nameProperty());
m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory());
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.SimpleComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
@ -50,16 +49,16 @@ public class StoreEntryListComp extends SimpleComp {
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
map.put(
createList(),
BindingsHelper.persist(Bindings.not(Bindings.isEmpty(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren()))));
Bindings.not(Bindings.isEmpty(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren())));
map.put(new StoreIntroComp(), showIntro);
map.put(
new StoreNotFoundComp(),
BindingsHelper.persist(Bindings.and(
Bindings.and(
Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())),
Bindings.isEmpty(
StoreViewState.get().getCurrentTopLevelSection().getShownChildren()))));
StoreViewState.get().getCurrentTopLevelSection().getShownChildren())));
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.fxcomps.Comp;
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.IconButtonComp;
import io.xpipe.app.fxcomps.impl.TooltipAugment;
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.core.process.OsType;
import javafx.beans.binding.Bindings;
@ -36,7 +36,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
public StoreEntryListStatusComp() {
this.sortMode = new SimpleObjectProperty<>();
SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> {
StoreViewState.get().getActiveCategory().subscribe(val -> {
sortMode.setValue(val.getSortMode().getValue());
});
sortMode.addListener((observable, oldValue, newValue) -> {
@ -51,21 +51,16 @@ public class StoreEntryListStatusComp extends SimpleComp {
private Region createGroupListHeader() {
var label = new Label();
label.textProperty()
.bind(Bindings.createStringBinding(
() -> {
return StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.equals(StoreViewState.get().getAllConnectionsCategory())
? "Connections"
: "Scripts";
},
StoreViewState.get().getActiveCategory()));
var name = BindingsHelper.flatMap(
StoreViewState.get().getActiveCategory(),
categoryWrapper -> AppI18n.observable(
categoryWrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory())
? "connections"
: "scripts"));
label.textProperty().bind(name);
label.getStyleClass().add("name");
var all = BindingsHelper.filteredContentBinding(
var all = ListBindingsHelper.filteredContentBinding(
StoreViewState.get().getAllEntries(),
storeEntryWrapper -> {
var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot();
@ -76,7 +71,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
.equals(storeRoot);
},
StoreViewState.get().getActiveCategory());
var shownList = BindingsHelper.filteredContentBinding(
var shownList = ListBindingsHelper.filteredContentBinding(
all,
storeEntryWrapper -> {
return storeEntryWrapper.shouldShow(
@ -135,7 +130,8 @@ public class StoreEntryListStatusComp extends SimpleComp {
}
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.setTextAlignment(TextAlignment.CENTER);
AppFont.medium(menu);
@ -188,7 +184,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
sortMode));
});
alphabetical.accessibleTextKey("sortAlphabetical");
alphabetical.apply(new FancyTooltipAugment<>("sortAlphabetical"));
alphabetical.apply(new TooltipAugment<>("sortAlphabetical"));
return alphabetical;
}
@ -227,7 +223,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
sortMode));
});
date.accessibleTextKey("sortLastUsed");
date.apply(new FancyTooltipAugment<>("sortLastUsed"));
date.apply(new TooltipAugment<>("sortLastUsed"));
return date;
}

View file

@ -21,15 +21,18 @@ public class StoreIntroComp extends SimpleComp {
@Override
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);
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.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.setDefaultButton(true);
var scanPane = new StackPane(scanButton);
@ -43,12 +46,7 @@ public class StoreIntroComp extends SimpleComp {
hbox.setSpacing(35);
hbox.setAlignment(Pos.CENTER);
var v = new VBox(
hbox, scanPane
// new Separator(Orientation.HORIZONTAL),
// documentation,
// docLinkPane
);
var v = new VBox(hbox, scanPane);
v.setMinWidth(Region.USE_PREF_SIZE);
v.setMaxWidth(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);
return JfxHelper.createNamedEntry(provider.getDisplayName(), provider.getDisplayDescription(), graphic);
return JfxHelper.createNamedEntry(provider.displayName(), provider.displayDescription(), graphic);
}
@Override
@ -49,8 +49,13 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
protected void updateItem(DataStoreProvider item, boolean empty) {
super.updateItem(item, empty);
setGraphic(createGraphic(item));
setAccessibleText(item != null ? item.getDisplayName() : null);
setAccessibleHelp(item != null ? item.getDisplayDescription() : null);
if (item != null) {
accessibleTextProperty().bind(item.displayName());
accessibleHelpProperty().bind(item.displayDescription());
} else {
accessibleTextProperty().unbind();
accessibleHelpProperty().unbind();
}
}
};
var cb = new ComboBox<DataStoreProvider>();

View file

@ -42,7 +42,9 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
var graphic =
w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore());
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 -> {
action.accept(w);
contextMenu.hide();
@ -83,7 +85,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
}
struc.get().setOnAction(event -> {
ContextMenuHelper.toggleShow(cm,struc.get(), Side.RIGHT);
ContextMenuHelper.toggleShow(cm, struc.get(), Side.RIGHT);
event.consume();
});
});

View file

@ -2,6 +2,7 @@ package io.xpipe.app.comp.store;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntry;
@ -64,10 +65,10 @@ public class StoreSection {
var c = Comparator.<StoreSection>comparingInt(
value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1);
var mappedSortMode = BindingsHelper.mappedBinding(
var mappedSortMode = BindingsHelper.flatMap(
category,
storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null);
return BindingsHelper.orderedContentBinding(
return ListBindingsHelper.orderedContentBinding(
list,
(o1, o2) -> {
var current = mappedSortMode.getValue();
@ -86,16 +87,18 @@ public class StoreSection {
Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> category) {
var topLevel = BindingsHelper.filteredContentBinding(
var topLevel = ListBindingsHelper.filteredContentBinding(
all,
section -> {
return DataStorage.get().isRootEntry(section.getEntry());
},
category);
var cached = BindingsHelper.cachedMappedContentBinding(
topLevel, storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category));
var cached = ListBindingsHelper.cachedMappedContentBinding(
topLevel,
topLevel,
storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category);
var shown = BindingsHelper.filteredContentBinding(
var shown = ListBindingsHelper.filteredContentBinding(
ordered,
section -> {
var showFilter = filterString == null || section.shouldShow(filterString.get());
@ -121,7 +124,7 @@ public class StoreSection {
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
// if (true) return DataStorage.get()
// .getDisplayParent(other.getEntry())
@ -131,10 +134,12 @@ public class StoreSection {
// This check is fast as the children are cached in the storage
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
});
var cached = BindingsHelper.cachedMappedContentBinding(
allChildren, entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category));
var cached = ListBindingsHelper.cachedMappedContentBinding(
allChildren,
allChildren,
entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category);
var filtered = BindingsHelper.filteredContentBinding(
var filtered = ListBindingsHelper.filteredContentBinding(
ordered,
section -> {
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.IconButtonComp;
import io.xpipe.app.fxcomps.impl.VerticalComp;
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.storage.DataStoreColor;
import io.xpipe.app.util.ThreadHelper;
import javafx.beans.binding.Bindings;
@ -40,11 +39,11 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
}
private Comp<CompStructure<Button>> createQuickAccessButton() {
var quickAccessDisabled = BindingsHelper.persist(Bindings.createBooleanBinding(
var quickAccessDisabled = Bindings.createBooleanBinding(
() -> {
return section.getShownChildren().isEmpty();
},
section.getShownChildren()));
section.getShownChildren());
Consumer<StoreEntryWrapper> quickAccessAction = w -> {
ThreadHelper.runFailableAsync(() -> {
w.executeDefaultAction();
@ -91,8 +90,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
return "Expand " + section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(BindingsHelper.persist(
Bindings.size(section.getShownChildren()).isEqualTo(0)))
.disable(Bindings.size(section.getShownChildren()).isEqualTo(0))
.styleClass("expand-button")
.maxHeight(100)
.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
// section is actually expanded
var listSections = BindingsHelper.filteredContentBinding(
var listSections = ListBindingsHelper.filteredContentBinding(
section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20
|| section.getWrapper().getExpanded().get(),
@ -143,26 +141,26 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
.minHeight(0)
.hgrow();
var expanded = BindingsHelper.persist(Bindings.createBooleanBinding(
var expanded = Bindings.createBooleanBinding(
() -> {
return section.getWrapper().getExpanded().get()
&& section.getShownChildren().size() > 0;
},
section.getWrapper().getExpanded(),
section.getShownChildren()));
section.getShownChildren());
var full = new VerticalComp(List.of(
topEntryList,
Comp.separator().hide(BindingsHelper.persist(expanded.not())),
Comp.separator().hide(expanded.not()),
new HorizontalComp(List.of(content))
.styleClass("content")
.apply(struc -> struc.get().setFillHeight(true))
.hide(BindingsHelper.persist(Bindings.or(
.hide(Bindings.or(
Bindings.not(section.getWrapper().getExpanded()),
Bindings.size(section.getShownChildren()).isEqualTo(0))))));
Bindings.size(section.getShownChildren()).isEqualTo(0)))));
return full.styleClass("store-entry-section-comp")
.apply(struc -> {
struc.get().setFillWidth(true);
SimpleChangeListener.apply(expanded, val -> {
expanded.subscribe(val -> {
struc.get().pseudoClassStateChanged(EXPANDED, val);
});
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(SUB, !topLevel);
SimpleChangeListener.apply(section.getWrapper().getColor(), val -> {
section.getWrapper().getColor().subscribe(val -> {
if (!topLevel) {
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.PrettyImageHelper;
import io.xpipe.app.fxcomps.impl.VerticalComp;
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.storage.DataStoreColor;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
@ -101,16 +100,15 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
+ section.getWrapper().getName().getValue();
},
section.getWrapper().getName()))
.disable(BindingsHelper.persist(
Bindings.size(section.getAllChildren()).isEqualTo(0)))
.disable(Bindings.size(section.getAllChildren()).isEqualTo(0))
.grow(false, true)
.styleClass("expand-button");
var quickAccessDisabled = BindingsHelper.persist(Bindings.createBooleanBinding(
var quickAccessDisabled = Bindings.createBooleanBinding(
() -> {
return section.getShownChildren().isEmpty();
},
section.getShownChildren()));
section.getShownChildren());
Consumer<StoreEntryWrapper> quickAccessAction = 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
// section is actually expanded
var listSections = section.getWrapper() != null
? BindingsHelper.filteredContentBinding(
? ListBindingsHelper.filteredContentBinding(
section.getShownChildren(),
storeSection -> section.getAllChildren().size() <= 20 || expanded.get(),
expanded,
@ -149,9 +147,9 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
list.add(new HorizontalComp(List.of(content))
.styleClass("content")
.apply(struc -> struc.get().setFillHeight(true))
.hide(BindingsHelper.persist(Bindings.or(
.hide(Bindings.or(
Bindings.not(expanded),
Bindings.size(section.getAllChildren()).isEqualTo(0)))));
Bindings.size(section.getAllChildren()).isEqualTo(0))));
var vert = new VerticalComp(list);
if (condensedStyle) {
@ -160,7 +158,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
return vert.styleClass("store-section-mini-comp")
.apply(struc -> {
struc.get().setFillWidth(true);
SimpleChangeListener.apply(expanded, val -> {
expanded.subscribe(val -> {
struc.get().pseudoClassStateChanged(EXPANDED, val);
});
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
@ -171,7 +169,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
})
.apply(struc -> {
if (section.getWrapper() != null) {
SimpleChangeListener.apply(section.getWrapper().getColor(), val -> {
section.getWrapper().getColor().subscribe(val -> {
if (section.getDepth() != 1) {
return;
}

View file

@ -1,7 +1,7 @@
package io.xpipe.app.comp.store;
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.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage;
@ -270,10 +270,12 @@ public class StoreViewState {
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))
.sorted(comparator);
}

View file

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

View file

@ -1,11 +1,11 @@
package io.xpipe.app.core;
import io.xpipe.app.exchange.MessageExchangeImpls;
import io.xpipe.app.ext.ExtensionException;
import io.xpipe.app.ext.XPipeServiceProviders;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent;
import io.xpipe.core.process.ProcessControlProvider;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.ModuleLayerLoader;
import io.xpipe.core.util.XPipeInstallation;
import lombok.Getter;
import lombok.Value;
@ -47,10 +47,11 @@ public class AppExtensionManager {
}
if (load) {
// INSTANCE.addNativeLibrariesToPath();
try {
XPipeServiceProviders.load(INSTANCE.extendedLayer);
MessageExchangeImpls.loadAll();
ProcessControlProvider.init(INSTANCE.extendedLayer);
ModuleLayerLoader.loadAll(INSTANCE.extendedLayer, t -> {
ErrorEvent.fromThrowable(t).handle();
});
} catch (Throwable t) {
throw new ExtensionException(
"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.core.mode.OperationMode;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
@ -98,7 +97,7 @@ public class AppGreetings {
alert.getButtonTypes().add(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);

View file

@ -2,7 +2,7 @@ package io.xpipe.app.core;
import io.xpipe.app.comp.base.ModalOverlayComp;
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.TrackEvent;
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.Translatable;
import io.xpipe.core.util.ModuleHelper;
import io.xpipe.core.util.XPipeInstallation;
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 lombok.SneakyThrows;
import lombok.Value;
import org.apache.commons.io.FilenameUtils;
import org.ocpsoft.prettytime.PrettyTime;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
public class AppI18n {
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$");
private static final AppI18n INSTANCE = new AppI18n();
private Map<String, String> translations;
private Map<String, String> markdownDocumentations;
private PrettyTime prettyTime;
private static AppI18n INSTANCE;
private final Property<LoadedTranslations> currentLanguage = new SimpleObjectProperty<>();
private LoadedTranslations english;
public static void init() {
var i = INSTANCE;
if (i.translations != null) {
return;
}
i.load();
if (AppPrefs.get() != null) {
AppPrefs.get().language().addListener((c, o, n) -> {
i.clear();
i.load();
});
public static void init() throws Exception {
if (INSTANCE == null) {
INSTANCE = new AppI18n();
}
INSTANCE.load();
}
public static AppI18n getInstance() {
public static AppI18n get() {
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) {
if (s == null) {
return null;
}
var key = INSTANCE.getKey(s);
return Bindings.createStringBinding(() -> {
return get(key, vars);
});
return Bindings.createStringBinding(
() -> {
return get(key, vars);
},
INSTANCE.currentLanguage);
}
public static String get(String s, Object... vars) {
@ -147,7 +92,7 @@ public class AppI18n {
|| caller.equals(ModuleHelper.class)
|| caller.equals(ModalOverlayComp.class)
|| caller.equals(AppI18n.class)
|| caller.equals(FancyTooltipAugment.class)
|| caller.equals(TooltipAugment.class)
|| caller.equals(PrefsChoiceValue.class)
|| caller.equals(Translatable.class)
|| caller.equals(AppWindowHelper.class)
@ -160,9 +105,28 @@ public class AppI18n {
return "";
}
private void clear() {
translations.clear();
prettyTime = null;
private void load() throws Exception {
if (english == 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) {
@ -173,142 +137,156 @@ public class AppI18n {
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) {
var key = getKey(s);
if (translations == null) {
if (english == null) {
TrackEvent.warn("Translations not initialized for " + key);
return s;
}
if (!translations.containsKey(key)) {
TrackEvent.warn("Translation key not found for " + key);
return key;
if (currentLanguage.getValue() != null
&& currentLanguage.getValue().getTranslations().containsKey(key)) {
var localisedString = currentLanguage.getValue().getTranslations().get(key);
return getValue(localisedString, vars);
}
var localisedString = translations.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);
return key;
}
public boolean isLoaded() {
return translations != null;
}
private boolean matchesLocale(Path f) {
var l = AppPrefs.get() != null
? AppPrefs.get().language().getValue().getLocale()
: SupportedLocale.ENGLISH.getLocale();
private boolean matchesLocale(Path f, Locale l) {
var name = FilenameUtils.getBaseName(f.getFileName().toString());
var ending = "_" + l.toLanguageTag();
return name.endsWith(ending);
}
public String getMarkdownDocumentation(String name) {
if (!markdownDocumentations.containsKey(name)) {
TrackEvent.withWarn("Markdown documentation for key " + name + " not found")
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")
.handle();
return "";
}
private Path getModuleLangPath(String module) {
return XPipeInstallation.getLangPath().resolve(module);
}
private LoadedTranslations load(Locale l) throws Exception {
TrackEvent.info("Loading translations ...");
var translations = new HashMap<String, String>();
for (var module : AppExtensionManager.getInstance().getContentModules()) {
var basePath = getModuleLangPath(FilenameUtils.getExtension(module.getName()))
.resolve("strings");
if (!Files.exists(basePath)) {
continue;
}
AtomicInteger fileCounter = new AtomicInteger();
AtomicInteger lineCounter = new AtomicInteger();
var simpleName = FilenameUtils.getExtension(module.getName());
String defaultPrefix = simpleName.equals("app") ? "app." : simpleName + ".";
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!matchesLocale(file, l)) {
return FileVisitResult.CONTINUE;
}
if (!file.getFileName().toString().endsWith(".properties")) {
return FileVisitResult.CONTINUE;
}
fileCounter.incrementAndGet();
try (var in = Files.newInputStream(file)) {
var props = new Properties();
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
props.forEach((key, value) -> {
var hasPrefix = key.toString().contains(".");
var usedPrefix = hasPrefix ? "" : defaultPrefix;
translations.put(usedPrefix + key, value.toString());
lineCounter.incrementAndGet();
});
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
return FileVisitResult.CONTINUE;
}
});
TrackEvent.withDebug("Loading translations for module " + simpleName)
.tag("fileCount", fileCounter.get())
.tag("lineCount", lineCounter.get())
.handle();
}
return markdownDocumentations.getOrDefault(name, "");
}
private void load() {
TrackEvent.info("Loading translations ...");
translations = new HashMap<>();
var markdownDocumentations = new HashMap<String, String>();
for (var module : AppExtensionManager.getInstance().getContentModules()) {
AppResources.with(module.getName(), "lang", basePath -> {
if (!Files.exists(basePath)) {
return;
}
var basePath = getModuleLangPath(FilenameUtils.getExtension(module.getName()))
.resolve("texts");
if (!Files.exists(basePath)) {
continue;
}
AtomicInteger fileCounter = new AtomicInteger();
AtomicInteger lineCounter = new AtomicInteger();
var simpleName = FilenameUtils.getExtension(module.getName());
String defaultPrefix = simpleName.equals("app") ? "app." : simpleName + ".";
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!matchesLocale(file)) {
return FileVisitResult.CONTINUE;
}
if (!file.getFileName().toString().endsWith(".properties")) {
return FileVisitResult.CONTINUE;
}
fileCounter.incrementAndGet();
try (var in = Files.newInputStream(file)) {
var props = new Properties();
props.load(in);
props.forEach((key, value) -> {
var hasPrefix = key.toString().contains(".");
var usedPrefix = hasPrefix ? "" : defaultPrefix;
translations.put(usedPrefix + key, value.toString());
lineCounter.incrementAndGet();
});
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
var moduleName = FilenameUtils.getExtension(module.getName());
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!matchesLocale(file, l)) {
return FileVisitResult.CONTINUE;
}
});
TrackEvent.withDebug("Loading translations for module " + simpleName)
.tag("fileCount", fileCounter.get())
.tag("lineCount", lineCounter.get())
.handle();
if (!file.getFileName().toString().endsWith(".md")) {
return FileVisitResult.CONTINUE;
}
var name = file.getFileName()
.toString()
.substring(0, file.getFileName().toString().lastIndexOf("_"));
try (var in = Files.newInputStream(file)) {
var usedPrefix = moduleName + ":";
markdownDocumentations.put(
usedPrefix + name, new String(in.readAllBytes(), StandardCharsets.UTF_8));
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
return FileVisitResult.CONTINUE;
}
});
}
markdownDocumentations = new HashMap<>();
for (var module : AppExtensionManager.getInstance().getContentModules()) {
AppResources.with(module.getName(), "lang", basePath -> {
if (!Files.exists(basePath)) {
return;
}
var moduleName = FilenameUtils.getExtension(module.getName());
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (!matchesLocale(file)) {
return FileVisitResult.CONTINUE;
}
if (!file.getFileName().toString().endsWith(".md")) {
return FileVisitResult.CONTINUE;
}
var name = file.getFileName()
.toString()
.substring(0, file.getFileName().toString().lastIndexOf("_"));
try (var in = Files.newInputStream(file)) {
var usedPrefix = moduleName + ":";
markdownDocumentations.put(
usedPrefix + name, new String(in.readAllBytes(), StandardCharsets.UTF_8));
} catch (IOException ex) {
ErrorEvent.fromThrowable(ex).omitted(true).build().handle();
}
return FileVisitResult.CONTINUE;
}
});
});
}
this.prettyTime = new PrettyTime(
var prettyTime = new PrettyTime(
AppPrefs.get() != null
? 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")

View file

@ -30,11 +30,11 @@ public class AppImages {
TrackEvent.info("Loading images ...");
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 -> {
if (!Files.exists(basePath)) {
return;
@ -48,10 +48,10 @@ public class AppImages {
var relativeFileName = FilenameUtils.separatorsToUnix(
basePath.relativize(file).toString());
try {
if (FilenameUtils.getExtension(file.toString()).equals("svg")) {
if (FilenameUtils.getExtension(file.toString()).equals("svg") && loadSvgs) {
var s = Files.readString(file);
svgImages.put(defaultPrefix + relativeFileName, s);
} else {
} else if (loadImages) {
images.put(defaultPrefix + relativeFileName, loadImage(file));
}
} catch (IOException ex) {

View file

@ -1,11 +1,9 @@
package io.xpipe.app.core;
import io.xpipe.app.browser.BrowserComp;
import io.xpipe.app.browser.BrowserModel;
import io.xpipe.app.comp.DeveloperTabComp;
import io.xpipe.app.browser.session.BrowserSessionComp;
import io.xpipe.app.browser.session.BrowserSessionModel;
import io.xpipe.app.comp.store.StoreLayoutComp;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.prefs.AppPrefsComp;
import io.xpipe.app.util.LicenseProvider;
import javafx.beans.property.Property;
@ -30,13 +28,11 @@ public class AppLayoutModel {
private final List<Entry> entries;
private final Property<Entry> selected;
private final ObservableValue<Entry> selectedWrapper;
public AppLayoutModel(SavedState savedState) {
this.savedState = savedState;
this.entries = createEntryList();
this.selected = new SimpleObjectProperty<>(entries.get(1));
this.selectedWrapper = PlatformThread.sync(selected);
}
public static AppLayoutModel get() {
@ -53,14 +49,10 @@ public class AppLayoutModel {
INSTANCE = null;
}
public Property<Entry> getSelectedInternal() {
public Property<Entry> getSelected() {
return selected;
}
public ObservableValue<Entry> getSelected() {
return selectedWrapper;
}
public void selectBrowser() {
selected.setValue(entries.getFirst());
}
@ -79,21 +71,16 @@ public class AppLayoutModel {
private List<Entry> createEntryList() {
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("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp())));
// new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new
// 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"),
"mdi2p-professional-hexagon",
LicenseProvider.get().overviewPage()));
new Entry(AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp()),
new Entry(
AppI18n.observable("explorePlans"),
"mdi2p-professional-hexagon",
LicenseProvider.get().overviewPage())));
return l;
}

View file

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

View file

@ -3,25 +3,24 @@ package io.xpipe.app.core;
import atlantafx.base.theme.*;
import io.xpipe.app.ext.PrefsChoiceValue;
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.TrackEvent;
import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.process.OsType;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.animation.*;
import javafx.application.Application;
import javafx.application.ColorScheme;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.stage.Window;
import javafx.stage.WindowEvent;
import javafx.util.Duration;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -39,26 +38,75 @@ public class AppTheme {
private static final PseudoClass PERFORMANCE = PseudoClass.getPseudoClass("performance");
private static boolean init;
public static void initThemeHandlers(Window stage) {
public static void initThemeHandlers(Stage stage) {
if (AppPrefs.get() == null) {
return;
}
SimpleChangeListener.apply(AppPrefs.get().theme, t -> {
Theme.ALL.forEach(
theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId()));
if (t == null) {
return;
initWindowsThemeHandler(stage);
Runnable r = () -> {
AppPrefs.get().theme.subscribe(t -> {
Theme.ALL.forEach(
theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId()));
if (t == null) {
return;
}
stage.getScene().getRoot().getStyleClass().add(t.getCssId());
stage.getScene().getRoot().pseudoClassStateChanged(LIGHT, !t.isDark());
stage.getScene().getRoot().pseudoClassStateChanged(DARK, t.isDark());
});
AppPrefs.get().performanceMode().subscribe(val -> {
stage.getScene().getRoot().pseudoClassStateChanged(PRETTY, !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);
}
stage.getScene().getRoot().getStyleClass().add(t.getCssId());
stage.getScene().getRoot().pseudoClassStateChanged(LIGHT, !t.isDark());
stage.getScene().getRoot().pseudoClassStateChanged(DARK, t.isDark());
});
SimpleChangeListener.apply(AppPrefs.get().performanceMode(), val -> {
stage.getScene().getRoot().pseudoClassStateChanged(PRETTY, !val);
stage.getScene().getRoot().pseudoClassStateChanged(PERFORMANCE, val);
AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> {
var transition = new PauseTransition(Duration.millis(300));
transition.setOnFinished(e -> {
windowTheme.handle(null);
});
transition.play();
});
});
}

View file

@ -53,11 +53,12 @@ public class AppWindowHelper {
// 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";
AppResources.with(AppResources.XPIPE_MODULE, dir, path -> {
var size = switch (OsType.getLocal()) {
case OsType.Linux linux -> 128;
case OsType.MacOs macOs -> 128;
case OsType.Windows windows -> 32;
};
var size =
switch (OsType.getLocal()) {
case OsType.Linux linux -> 128;
case OsType.MacOs macOs -> 128;
case OsType.Windows windows -> 32;
};
stage.getIcons().add(AppImages.loadImage(path.resolve("logo_" + size + "x" + size + ".png")));
});
}
@ -82,12 +83,7 @@ public class AppWindowHelper {
}
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);
clampWindow(stage).ifPresent(rectangle2D -> {
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
Font.getDefault();
} 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
// as the platform reports as working but opening windows still does not work
new LogErrorHandler().handle(event);

View file

@ -18,7 +18,7 @@ public class AppPtbCheck {
.setContent(AppWindowHelper.alertContentText("You are running a PTB build of XPipe."
+ " This version is unstable and might contain bugs."
+ " 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."));
});
}

View file

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

View file

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

View file

@ -2,7 +2,6 @@ package io.xpipe.app.exchange;
import io.xpipe.beacon.BeaconHandler;
import io.xpipe.beacon.exchange.LaunchExchange;
import io.xpipe.core.process.TerminalInitScriptConfig;
import io.xpipe.core.store.LaunchableStore;
import java.util.Arrays;
@ -16,9 +15,9 @@ public class LaunchExchangeImpl extends LaunchExchange
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
var store = getStoreEntryById(msg.getId(), false);
if (store.getStore() instanceof LaunchableStore s) {
var command = s.prepareLaunchCommand()
.prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName()), sc -> null);
return Response.builder().command(split(command)).build();
// var command = s.prepareLaunchCommand()
// .prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName()), sc -> null);
// return Response.builder().command(split(command)).build();
}
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.ResponseMessage;
import io.xpipe.beacon.exchange.MessageExchanges;
import io.xpipe.core.util.ModuleLayerLoader;
import java.util.List;
import java.util.Optional;
@ -13,27 +14,6 @@ public class MessageExchangeImpls {
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")
public static <RQ extends RequestMessage, RS extends ResponseMessage> Optional<MessageExchangeImpl<RQ, RS>> byId(
String name) {
@ -53,4 +33,29 @@ public class MessageExchangeImpls {
public static List<MessageExchangeImpl<?, ?>> getAll() {
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()))
.map(p -> ProviderEntry.builder()
.id(p.getId())
.description(p.getDisplayDescription())
.description(p.displayDescription().getValue())
.hidden(p.getCreationCategory() == null)
.build())
.toList()));

View file

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

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