mirror of
https://github.com/xpipe-io/xpipe.git
synced 2024-11-21 15:10:23 +00:00
Merge branch vnc into master
This commit is contained in:
parent
6bd105b1de
commit
cbc5ad473a
636 changed files with 16725 additions and 3220 deletions
2
.gitattributes
vendored
2
.gitattributes
vendored
|
@ -1,4 +1,4 @@
|
||||||
* text=auto
|
* text=auto eol=lf
|
||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
*.bat text eol=crlf
|
*.bat text eol=crlf
|
||||||
*.png binary
|
*.png binary
|
||||||
|
|
|
@ -14,19 +14,6 @@ There are no real formal contribution guidelines right now, they will maybe come
|
||||||
- [dist](dist) - Tools to create a distributable package of XPipe
|
- [dist](dist) - Tools to create a distributable package of XPipe
|
||||||
- [ext](ext) - Available XPipe extensions. Essentially every concrete feature implementation is implemented as an extension
|
- [ext](ext) - Available XPipe extensions. Essentially every concrete feature implementation is implemented as an extension
|
||||||
|
|
||||||
## Modularity
|
|
||||||
|
|
||||||
All XPipe components target [Java 21](https://openjdk.java.net/projects/jdk/20/) and make full use of the Java Module System (JPMS).
|
|
||||||
All components are modularized, including all their dependencies.
|
|
||||||
In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
|
|
||||||
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
|
|
||||||
many IDEs still have problems building this project properly.
|
|
||||||
|
|
||||||
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
|
|
||||||
The tested and recommended IDE is IntelliJ.
|
|
||||||
When setting up the project in IntelliJ, make sure that the correct JDK (Java 21)
|
|
||||||
is selected both for the project and for gradle itself.
|
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
You need to have an up-to-date version of XPipe installed on your local system in order to properly
|
You need to have an up-to-date version of XPipe installed on your local system in order to properly
|
||||||
|
@ -39,9 +26,9 @@ Note that in case the current master branch is ahead of the latest release, it m
|
||||||
It is therefore recommended to always check out the matching version tag for your local repository and local XPipe installation.
|
It is therefore recommended to always check out the matching version tag for your local repository and local XPipe installation.
|
||||||
You can find the available version tags at https://github.com/xpipe-io/xpipe/tags
|
You can find the available version tags at https://github.com/xpipe-io/xpipe/tags
|
||||||
|
|
||||||
You need to have GraalVM Community Edition for Java 21 installed as a JDK to compile the project.
|
You need to have JDK for Java 22 installed to compile the project.
|
||||||
If you are on Linux or macOS, you can easily accomplish that by running the `setup.sh` script.
|
If you are on Linux or macOS, you can easily accomplish that by running the `setup.sh` script.
|
||||||
On Windows, you have to manually install the JDK.
|
On Windows, you have to manually install a JDK, e.g. from [Adoptium](https://adoptium.net/temurin/releases/?version=22).
|
||||||
|
|
||||||
## Building and Running
|
## Building and Running
|
||||||
|
|
||||||
|
@ -58,6 +45,19 @@ You are also able to properly debug the built production application through two
|
||||||
Note that when any unit test is run using a debugger, the XPipe daemon process that is started will also attempt
|
Note that when any unit test is run using a debugger, the XPipe daemon process that is started will also attempt
|
||||||
to connect to that debugger through [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme) as well.
|
to connect to that debugger through [AttachMe](https://plugins.jetbrains.com/plugin/13263-attachme) as well.
|
||||||
|
|
||||||
|
## Modularity and IDEs
|
||||||
|
|
||||||
|
All XPipe components target [Java 22](https://openjdk.java.net/projects/jdk/22/) and make full use of the Java Module System (JPMS).
|
||||||
|
All components are modularized, including all their dependencies.
|
||||||
|
In case a dependency is (sadly) not modularized yet, module information is manually added using [extra-java-module-info](https://github.com/gradlex-org/extra-java-module-info).
|
||||||
|
Further, note that as this is a pretty complicated Java project that fully utilizes modularity,
|
||||||
|
many IDEs still have problems building this project properly.
|
||||||
|
|
||||||
|
For example, you can't build this project in eclipse or vscode as it will complain about missing modules.
|
||||||
|
The tested and recommended IDE is IntelliJ.
|
||||||
|
When setting up the project in IntelliJ, make sure that the correct JDK (Java 22)
|
||||||
|
is selected both for the project and for gradle itself.
|
||||||
|
|
||||||
## Contributing guide
|
## Contributing guide
|
||||||
|
|
||||||
Especially when starting out, it might be a good idea to start with easy tasks first. Here's a selection of suitable common tasks that are very easy to implement:
|
Especially when starting out, it might be a good idea to start with easy tasks first. Here's a selection of suitable common tasks that are very easy to implement:
|
||||||
|
@ -96,3 +96,7 @@ The [sample action](https://github.com/xpipe-io/xpipe/blob/master/ext/base/src/m
|
||||||
### Implementing something else
|
### Implementing something else
|
||||||
|
|
||||||
if you want to work on something that was not listed here, you can still do so of course. You can reach out on the [Discord server](https://discord.gg/8y89vS8cRb) to discuss any development plans and get you started.
|
if you want to work on something that was not listed here, you can still do so of course. You can reach out on the [Discord server](https://discord.gg/8y89vS8cRb) to discuss any development plans and get you started.
|
||||||
|
|
||||||
|
### Translations
|
||||||
|
|
||||||
|
See the [translation guide](/lang/README.md) for details.
|
||||||
|
|
|
@ -39,11 +39,12 @@ dependencies {
|
||||||
api 'com.vladsch.flexmark:flexmark-util-visitor:0.64.8'
|
api 'com.vladsch.flexmark:flexmark-util-visitor:0.64.8'
|
||||||
|
|
||||||
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
|
api files("$rootDir/gradle/gradle_scripts/markdowngenerator-1.3.1.1.jar")
|
||||||
|
api files("$rootDir/gradle/gradle_scripts/vernacular-1.16.jar")
|
||||||
api 'info.picocli:picocli:4.7.5'
|
api 'info.picocli:picocli:4.7.5'
|
||||||
api 'org.kohsuke:github-api:1.321'
|
api 'org.kohsuke:github-api:1.321'
|
||||||
api 'io.sentry:sentry:7.6.0'
|
api 'io.sentry:sentry:7.8.0'
|
||||||
api 'org.ocpsoft.prettytime:prettytime:5.0.7.Final'
|
api 'org.ocpsoft.prettytime:prettytime:5.0.7.Final'
|
||||||
api 'commons-io:commons-io:2.15.1'
|
api 'commons-io:commons-io:2.16.1'
|
||||||
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.0"
|
api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: "2.17.0"
|
||||||
api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.17.0"
|
api group: 'com.fasterxml.jackson.module', name: 'jackson-module-parameter-names', version: "2.17.0"
|
||||||
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.0"
|
api group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: "2.17.0"
|
||||||
|
|
|
@ -12,9 +12,11 @@ public class Main {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Since this is not marked as a console application, it will not print anything when you run it in a console on Windows
|
// Since this is not marked as a console application, it will not print anything when you run it in a console on
|
||||||
|
// Windows
|
||||||
if (args.length == 1 && args[0].equals("--help")) {
|
if (args.length == 1 && args[0].equals("--help")) {
|
||||||
System.out.println("""
|
System.out.println(
|
||||||
|
"""
|
||||||
The daemon executable xpiped does not accept any command-line arguments.
|
The daemon executable xpiped does not accept any command-line arguments.
|
||||||
|
|
||||||
For a reference on what you can do from the CLI, take a look at the xpipe CLI executable instead.
|
For a reference on what you can do from the CLI, take a look at the xpipe CLI executable instead.
|
||||||
|
|
|
@ -12,72 +12,46 @@ import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.impl.FilterComp;
|
import io.xpipe.app.fxcomps.impl.FilterComp;
|
||||||
import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.util.BooleanScope;
|
import io.xpipe.app.storage.DataStoreEntry;
|
||||||
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
|
import io.xpipe.app.util.DataStoreCategoryChoiceComp;
|
||||||
import io.xpipe.app.util.FixedHierarchyStore;
|
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
|
||||||
import io.xpipe.core.store.DataStore;
|
|
||||||
import io.xpipe.core.store.ShellStore;
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.css.PseudoClass;
|
import javafx.css.PseudoClass;
|
||||||
import javafx.geometry.Point2D;
|
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.input.DragEvent;
|
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Timer;
|
|
||||||
import java.util.TimerTask;
|
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
final class BrowserBookmarkComp extends SimpleComp {
|
public final class BrowserBookmarkComp extends SimpleComp {
|
||||||
|
|
||||||
public static final Timer DROP_TIMER = new Timer("dnd", true);
|
|
||||||
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
|
||||||
private final BrowserModel model;
|
private final ObservableValue<DataStoreEntry> selected;
|
||||||
private Point2D lastOver = new Point2D(-1, -1);
|
private final Predicate<StoreEntryWrapper> applicable;
|
||||||
private TimerTask activeTask;
|
private final BiConsumer<StoreEntryWrapper, BooleanProperty> action;
|
||||||
|
|
||||||
BrowserBookmarkComp(BrowserModel model) {
|
public BrowserBookmarkComp(
|
||||||
this.model = model;
|
ObservableValue<DataStoreEntry> selected,
|
||||||
|
Predicate<StoreEntryWrapper> applicable,
|
||||||
|
BiConsumer<StoreEntryWrapper, BooleanProperty> action) {
|
||||||
|
this.selected = selected;
|
||||||
|
this.applicable = applicable;
|
||||||
|
this.action = action;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
var filterText = new SimpleStringProperty();
|
var filterText = new SimpleStringProperty();
|
||||||
var open = PlatformThread.sync(model.getSelected());
|
|
||||||
Predicate<StoreEntryWrapper> applicable = storeEntryWrapper -> {
|
|
||||||
return (storeEntryWrapper.getEntry().getStore() instanceof ShellStore
|
|
||||||
|| storeEntryWrapper.getEntry().getStore() instanceof FixedHierarchyStore)
|
|
||||||
&& storeEntryWrapper.getEntry().getValidity().isUsable();
|
|
||||||
};
|
|
||||||
var selectedCategory = new SimpleObjectProperty<>(
|
var selectedCategory = new SimpleObjectProperty<>(
|
||||||
StoreViewState.get().getActiveCategory().getValue());
|
StoreViewState.get().getActiveCategory().getValue());
|
||||||
|
|
||||||
BooleanProperty busy = new SimpleBooleanProperty(false);
|
BooleanProperty busy = new SimpleBooleanProperty(false);
|
||||||
Consumer<StoreEntryWrapper> action = w -> {
|
|
||||||
ThreadHelper.runFailableAsync(() -> {
|
|
||||||
var entry = w.getEntry();
|
|
||||||
if (!entry.getValidity().isUsable()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.getStore() instanceof ShellStore fileSystem) {
|
|
||||||
model.openFileSystemAsync(entry.ref(), null, busy);
|
|
||||||
} else if (entry.getStore() instanceof FixedHierarchyStore) {
|
|
||||||
BooleanScope.execute(busy, () -> {
|
|
||||||
w.refreshChildren();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (s, comp) -> {
|
BiConsumer<StoreSection, Comp<CompStructure<Button>>> augment = (s, comp) -> {
|
||||||
comp.disable(Bindings.createBooleanBinding(
|
comp.disable(Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
|
@ -85,14 +59,15 @@ final class BrowserBookmarkComp extends SimpleComp {
|
||||||
},
|
},
|
||||||
busy));
|
busy));
|
||||||
comp.apply(struc -> {
|
comp.apply(struc -> {
|
||||||
open.addListener((observable, oldValue, newValue) -> {
|
selected.addListener((observable, oldValue, newValue) -> {
|
||||||
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
struc.get()
|
struc.get()
|
||||||
.pseudoClassStateChanged(
|
.pseudoClassStateChanged(
|
||||||
SELECTED,
|
SELECTED,
|
||||||
newValue != null
|
newValue != null
|
||||||
&& newValue.getEntry()
|
&& newValue.equals(
|
||||||
.get()
|
s.getWrapper().getEntry()));
|
||||||
.equals(s.getWrapper().getEntry()));
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -101,7 +76,7 @@ final class BrowserBookmarkComp extends SimpleComp {
|
||||||
StoreSection.createTopLevel(
|
StoreSection.createTopLevel(
|
||||||
StoreViewState.get().getAllEntries(), storeEntryWrapper -> true, filterText, selectedCategory),
|
StoreViewState.get().getAllEntries(), storeEntryWrapper -> true, filterText, selectedCategory),
|
||||||
augment,
|
augment,
|
||||||
action,
|
entryWrapper -> action.accept(entryWrapper, busy),
|
||||||
true);
|
true);
|
||||||
var category = new DataStoreCategoryChoiceComp(
|
var category = new DataStoreCategoryChoiceComp(
|
||||||
StoreViewState.get().getAllConnectionsCategory(),
|
StoreViewState.get().getAllConnectionsCategory(),
|
||||||
|
@ -125,21 +100,4 @@ final class BrowserBookmarkComp extends SimpleComp {
|
||||||
content.getStyleClass().add("bookmark-list");
|
content.getStyleClass().add("bookmark-list");
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleHoverTimer(DataStore store, DragEvent event) {
|
|
||||||
if (lastOver.getX() == event.getX() && lastOver.getY() == event.getY()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastOver = (new Point2D(event.getX(), event.getY()));
|
|
||||||
activeTask = new TimerTask() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
if (activeTask != this) {}
|
|
||||||
|
|
||||||
// Platform.runLater(() -> model.openExistingFileSystemIfPresent(store.asNeeded()));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
DROP_TIMER.schedule(activeTask, 500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
import atlantafx.base.controls.Breadcrumbs;
|
import atlantafx.base.controls.Breadcrumbs;
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import io.xpipe.core.store.FileNames;
|
import io.xpipe.core.store.FileNames;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
@ -40,7 +40,7 @@ public class BrowserBreadcrumbBar extends SimpleComp {
|
||||||
|
|
||||||
var breadcrumbs = new Breadcrumbs<String>();
|
var breadcrumbs = new Breadcrumbs<String>();
|
||||||
breadcrumbs.setMinWidth(0);
|
breadcrumbs.setMinWidth(0);
|
||||||
SimpleChangeListener.apply(PlatformThread.sync(model.getCurrentPath()), val -> {
|
PlatformThread.sync(model.getCurrentPath()).subscribe(val -> {
|
||||||
if (val == null) {
|
if (val == null) {
|
||||||
breadcrumbs.setSelectedCrumb(null);
|
breadcrumbs.setSelectedCrumb(null);
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.file.FileSystemHelper;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import io.xpipe.core.process.ProcessControlProvider;
|
import io.xpipe.core.process.ProcessControlProvider;
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
import atlantafx.base.theme.Styles;
|
import atlantafx.base.theme.Styles;
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.CompStructure;
|
import io.xpipe.app.fxcomps.CompStructure;
|
||||||
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
|
|
||||||
import io.xpipe.app.fxcomps.impl.TextFieldComp;
|
import io.xpipe.app.fxcomps.impl.TextFieldComp;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
||||||
import javafx.beans.property.Property;
|
import javafx.beans.property.Property;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
@ -29,7 +29,7 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
|
||||||
var expanded = new SimpleBooleanProperty();
|
var expanded = new SimpleBooleanProperty();
|
||||||
var text = new TextFieldComp(filterString, false).createRegion();
|
var text = new TextFieldComp(filterString, false).createRegion();
|
||||||
var button = new Button();
|
var button = new Button();
|
||||||
new FancyTooltipAugment<>("app.search").augment(button);
|
new TooltipAugment<>("app.search").augment(button);
|
||||||
text.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
text.focusedProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
if (!newValue && filterString.getValue() == null) {
|
if (!newValue && filterString.getValue() == null) {
|
||||||
if (button.isFocused()) {
|
if (button.isFocused()) {
|
||||||
|
@ -47,7 +47,7 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
|
||||||
text.setMinWidth(0);
|
text.setMinWidth(0);
|
||||||
Styles.toggleStyleClass(text, Styles.LEFT_PILL);
|
Styles.toggleStyleClass(text, Styles.LEFT_PILL);
|
||||||
|
|
||||||
SimpleChangeListener.apply(filterString, val -> {
|
filterString.subscribe(val -> {
|
||||||
if (val == null) {
|
if (val == null) {
|
||||||
text.getStyleClass().remove(Styles.SUCCESS);
|
text.getStyleClass().remove(Styles.SUCCESS);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -2,8 +2,10 @@ package io.xpipe.app.browser;
|
||||||
|
|
||||||
import atlantafx.base.theme.Styles;
|
import atlantafx.base.theme.Styles;
|
||||||
import io.xpipe.app.core.AppFont;
|
import io.xpipe.app.core.AppFont;
|
||||||
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.core.AppLayoutModel;
|
import io.xpipe.app.core.AppLayoutModel;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
|
|
||||||
|
@ -15,8 +17,10 @@ public class BrowserGreetingComp extends SimpleComp {
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
var r = new Label(getText());
|
var r = new Label(getText());
|
||||||
AppLayoutModel.get().getSelected().addListener((observableValue, entry, t1) -> {
|
AppLayoutModel.get().getSelected().addListener((observableValue, entry, t1) -> {
|
||||||
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
r.setText(getText());
|
r.setText(getText());
|
||||||
});
|
});
|
||||||
|
});
|
||||||
AppFont.setSize(r, 7);
|
AppFont.setSize(r, 7);
|
||||||
r.getStyleClass().add(Styles.TEXT_BOLD);
|
r.getStyleClass().add(Styles.TEXT_BOLD);
|
||||||
return r;
|
return r;
|
||||||
|
@ -27,11 +31,11 @@ public class BrowserGreetingComp extends SimpleComp {
|
||||||
var hour = ldt.getHour();
|
var hour = ldt.getHour();
|
||||||
String text;
|
String text;
|
||||||
if (hour > 18 || hour < 5) {
|
if (hour > 18 || hour < 5) {
|
||||||
text = "Good evening";
|
text = AppI18n.get("goodEvening");
|
||||||
} else if (hour < 12) {
|
} else if (hour < 12) {
|
||||||
text = "Good morning";
|
text = AppI18n.get("goodMorning");
|
||||||
} else {
|
} else {
|
||||||
text = "Good afternoon";
|
text = AppI18n.get("goodAfternoon");
|
||||||
}
|
}
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,8 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
import atlantafx.base.theme.Styles;
|
import atlantafx.base.theme.Styles;
|
||||||
|
import io.xpipe.app.browser.file.BrowserContextMenu;
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.browser.icon.FileIconManager;
|
import io.xpipe.app.browser.icon.FileIconManager;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
|
@ -10,7 +12,6 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
||||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||||
import io.xpipe.app.fxcomps.impl.StackComp;
|
import io.xpipe.app.fxcomps.impl.StackComp;
|
||||||
import io.xpipe.app.fxcomps.impl.TextFieldComp;
|
import io.xpipe.app.fxcomps.impl.TextFieldComp;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import io.xpipe.app.util.BooleanScope;
|
import io.xpipe.app.util.BooleanScope;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
@ -42,7 +43,7 @@ public class BrowserNavBar extends SimpleComp {
|
||||||
@Override
|
@Override
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
var path = new SimpleStringProperty(model.getCurrentPath().get());
|
var path = new SimpleStringProperty(model.getCurrentPath().get());
|
||||||
SimpleChangeListener.apply(model.getCurrentPath(), (newValue) -> {
|
model.getCurrentPath().subscribe((newValue) -> {
|
||||||
path.set(newValue);
|
path.set(newValue);
|
||||||
});
|
});
|
||||||
path.addListener((observable, oldValue, newValue) -> {
|
path.addListener((observable, oldValue, newValue) -> {
|
||||||
|
@ -58,7 +59,7 @@ public class BrowserNavBar extends SimpleComp {
|
||||||
.styleClass(Styles.CENTER_PILL)
|
.styleClass(Styles.CENTER_PILL)
|
||||||
.styleClass("path-text")
|
.styleClass("path-text")
|
||||||
.apply(struc -> {
|
.apply(struc -> {
|
||||||
SimpleChangeListener.apply(struc.get().focusedProperty(), val -> {
|
struc.get().focusedProperty().subscribe(val -> {
|
||||||
struc.get()
|
struc.get()
|
||||||
.pseudoClassStateChanged(
|
.pseudoClassStateChanged(
|
||||||
INVISIBLE,
|
INVISIBLE,
|
||||||
|
@ -71,7 +72,7 @@ public class BrowserNavBar extends SimpleComp {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
SimpleChangeListener.apply(model.getInOverview(), val -> {
|
model.getInOverview().subscribe(val -> {
|
||||||
// Pseudo classes do not apply if set instantly before shown
|
// Pseudo classes do not apply if set instantly before shown
|
||||||
// If we start a new tab with a directory set, we have to set the pseudo class one pulse later
|
// If we start a new tab with a directory set, we have to set the pseudo class one pulse later
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.file.BrowserFileOverviewComp;
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.comp.base.SimpleTitledPaneComp;
|
import io.xpipe.app.comp.base.SimpleTitledPaneComp;
|
||||||
import io.xpipe.app.core.AppI18n;
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import io.xpipe.core.process.ShellControl;
|
import io.xpipe.core.process.ShellControl;
|
||||||
|
@ -66,7 +68,7 @@ public class BrowserOverviewComp extends SimpleComp {
|
||||||
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
|
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
|
||||||
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
|
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
|
||||||
|
|
||||||
var recent = BindingsHelper.mappedContentBinding(
|
var recent = ListBindingsHelper.mappedContentBinding(
|
||||||
model.getSavedState().getRecentDirectories(),
|
model.getSavedState().getRecentDirectories(),
|
||||||
s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()));
|
s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()));
|
||||||
var recentOverview = new BrowserFileOverviewComp(model, recent, true);
|
var recentOverview = new BrowserFileOverviewComp(model, recent, true);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
import lombok.extern.jackson.Jacksonized;
|
import lombok.extern.jackson.Jacksonized;
|
||||||
|
@ -18,6 +19,7 @@ public interface BrowserSavedState {
|
||||||
@Value
|
@Value
|
||||||
@Jacksonized
|
@Jacksonized
|
||||||
@Builder
|
@Builder
|
||||||
|
@AllArgsConstructor
|
||||||
class Entry {
|
class Entry {
|
||||||
|
|
||||||
UUID uuid;
|
UUID uuid;
|
||||||
|
|
|
@ -27,7 +27,7 @@ public class BrowserSavedStateImpl implements BrowserSavedState {
|
||||||
this.lastSystems = FXCollections.observableArrayList(lastSystems);
|
this.lastSystems = FXCollections.observableArrayList(lastSystems);
|
||||||
}
|
}
|
||||||
|
|
||||||
static BrowserSavedStateImpl load() {
|
public static BrowserSavedStateImpl load() {
|
||||||
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
|
return AppCache.get("browser-state", BrowserSavedStateImpl.class, () -> {
|
||||||
return new BrowserSavedStateImpl(FXCollections.observableArrayList());
|
return new BrowserSavedStateImpl(FXCollections.observableArrayList());
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,9 +52,9 @@ public class BrowserSelectionListComp extends SimpleComp {
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
var c = new ListBoxViewComp<>(list, list, entry -> {
|
var c = new ListBoxViewComp<>(list, list, entry -> {
|
||||||
return Comp.of(() -> {
|
return Comp.of(() -> {
|
||||||
var wv = PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 20)
|
var image = PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(entry, false), 24)
|
||||||
.createRegion();
|
.createRegion();
|
||||||
var l = new Label(null, wv);
|
var l = new Label(null, image);
|
||||||
l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
l.setTextOverrun(OverrunStyle.CENTER_ELLIPSIS);
|
||||||
l.textProperty().bind(PlatformThread.sync(nameTransformation.apply(entry)));
|
l.textProperty().bind(PlatformThread.sync(nameTransformation.apply(entry)));
|
||||||
return l;
|
return l;
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
import atlantafx.base.controls.Spacer;
|
import atlantafx.base.controls.Spacer;
|
||||||
|
import io.xpipe.app.browser.file.BrowserContextMenu;
|
||||||
|
import io.xpipe.app.browser.file.BrowserFileListCompEntry;
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.core.AppFont;
|
import io.xpipe.app.core.AppFont;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
|
@ -11,6 +14,7 @@ import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||||
import io.xpipe.app.util.HumanReadableFormat;
|
import io.xpipe.app.util.HumanReadableFormat;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.scene.control.ToolBar;
|
import javafx.scene.control.ToolBar;
|
||||||
|
import javafx.scene.input.MouseButton;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
@ -59,7 +63,7 @@ public class BrowserStatusBarComp extends SimpleComp {
|
||||||
|
|
||||||
private Comp<?> createClipboardStatus() {
|
private Comp<?> createClipboardStatus() {
|
||||||
var cc = BrowserClipboard.currentCopyClipboard;
|
var cc = BrowserClipboard.currentCopyClipboard;
|
||||||
var ccCount = (BindingsHelper.persist(Bindings.createStringBinding(
|
var ccCount = Bindings.createStringBinding(
|
||||||
() -> {
|
() -> {
|
||||||
if (cc.getValue() != null && cc.getValue().getEntries().size() > 0) {
|
if (cc.getValue() != null && cc.getValue().getEntries().size() > 0) {
|
||||||
return cc.getValue().getEntries().size() + " file"
|
return cc.getValue().getEntries().size() + " file"
|
||||||
|
@ -68,7 +72,7 @@ public class BrowserStatusBarComp extends SimpleComp {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cc)));
|
cc);
|
||||||
return new LabelComp(ccCount);
|
return new LabelComp(ccCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +90,7 @@ public class BrowserStatusBarComp extends SimpleComp {
|
||||||
.count();
|
.count();
|
||||||
},
|
},
|
||||||
model.getFileList().getAll());
|
model.getFileList().getAll());
|
||||||
var selectedComp = new LabelComp(BindingsHelper.persist(Bindings.createStringBinding(
|
var selectedComp = new LabelComp(Bindings.createStringBinding(
|
||||||
() -> {
|
() -> {
|
||||||
if (selectedCount.getValue().intValue() == 0) {
|
if (selectedCount.getValue().intValue() == 0) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -95,7 +99,7 @@ public class BrowserStatusBarComp extends SimpleComp {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectedCount,
|
selectedCount,
|
||||||
allCount)));
|
allCount));
|
||||||
return selectedComp;
|
return selectedComp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,6 +128,10 @@ public class BrowserStatusBarComp extends SimpleComp {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use status bar as an extension of file list
|
// Use status bar as an extension of file list
|
||||||
new ContextMenuAugment<>(mouseEvent -> mouseEvent.isSecondaryButtonDown(), null, () -> new BrowserContextMenu(model, null)).augment(new SimpleCompStructure<>(r));
|
new ContextMenuAugment<>(
|
||||||
|
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
|
||||||
|
null,
|
||||||
|
() -> new BrowserContextMenu(model, null))
|
||||||
|
.augment(new SimpleCompStructure<>(r));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.comp.base.LoadingOverlayComp;
|
import io.xpipe.app.comp.base.LoadingOverlayComp;
|
||||||
import io.xpipe.app.core.AppI18n;
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
|
import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment;
|
||||||
import io.xpipe.app.fxcomps.impl.*;
|
import io.xpipe.app.fxcomps.impl.*;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.core.process.OsType;
|
import io.xpipe.core.process.OsType;
|
||||||
|
@ -37,18 +38,22 @@ public class BrowserTransferComp extends SimpleComp {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
|
var syncItems = PlatformThread.sync(model.getItems());
|
||||||
|
var syncDownloaded = PlatformThread.sync(model.getDownloading());
|
||||||
|
var syncAllDownloaded = PlatformThread.sync(model.getAllDownloaded());
|
||||||
|
|
||||||
var background = new LabelComp(AppI18n.observable("transferDescription"))
|
var background = new LabelComp(AppI18n.observable("transferDescription"))
|
||||||
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
|
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2d-download-outline")))
|
||||||
.visible(BindingsHelper.persist(Bindings.isEmpty(model.getItems())));
|
.visible(Bindings.isEmpty(syncItems));
|
||||||
var backgroundStack =
|
var backgroundStack =
|
||||||
new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
|
new StackComp(List.of(background)).grow(true, true).styleClass("download-background");
|
||||||
|
|
||||||
var binding = BindingsHelper.mappedContentBinding(model.getItems(), item -> item.getFileEntry());
|
var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getFileEntry());
|
||||||
var list = new BrowserSelectionListComp(
|
var list = new BrowserSelectionListComp(
|
||||||
binding,
|
binding,
|
||||||
entry -> Bindings.createStringBinding(
|
entry -> Bindings.createStringBinding(
|
||||||
() -> {
|
() -> {
|
||||||
var sourceItem = model.getItems().stream()
|
var sourceItem = syncItems.stream()
|
||||||
.filter(item -> item.getFileEntry() == entry)
|
.filter(item -> item.getFileEntry() == entry)
|
||||||
.findAny();
|
.findAny();
|
||||||
if (sourceItem.isEmpty()) {
|
if (sourceItem.isEmpty()) {
|
||||||
|
@ -63,27 +68,27 @@ public class BrowserTransferComp extends SimpleComp {
|
||||||
.orElse("?");
|
.orElse("?");
|
||||||
return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
|
return FileNames.getFileName(entry.getPath()) + " (" + name + ")";
|
||||||
},
|
},
|
||||||
model.getAllDownloaded()))
|
syncAllDownloaded))
|
||||||
.apply(struc -> struc.get().setMinHeight(150))
|
.apply(struc -> struc.get().setMinHeight(150))
|
||||||
.grow(false, true);
|
.grow(false, true);
|
||||||
var dragNotice = new LabelComp(model.getAllDownloaded()
|
var dragNotice = new LabelComp(syncAllDownloaded
|
||||||
.flatMap(aBoolean ->
|
.flatMap(aBoolean ->
|
||||||
aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
|
aBoolean ? AppI18n.observable("dragLocalFiles") : AppI18n.observable("dragFiles")))
|
||||||
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
|
.apply(struc -> struc.get().setGraphic(new FontIcon("mdi2h-hand-left")))
|
||||||
.hide(PlatformThread.sync(BindingsHelper.persist(Bindings.isEmpty(model.getItems()))))
|
.hide(Bindings.isEmpty(syncItems))
|
||||||
.grow(true, false)
|
.grow(true, false)
|
||||||
.apply(struc -> struc.get().setPadding(new Insets(8)));
|
.apply(struc -> struc.get().setPadding(new Insets(8)));
|
||||||
|
|
||||||
var downloadButton = new IconButtonComp("mdi2d-download", () -> {
|
var downloadButton = new IconButtonComp("mdi2d-download", () -> {
|
||||||
model.download();
|
model.download();
|
||||||
})
|
})
|
||||||
.hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems())))
|
.hide(Bindings.isEmpty(syncItems))
|
||||||
.disable(PlatformThread.sync(model.getAllDownloaded()))
|
.disable(syncAllDownloaded)
|
||||||
.apply(new FancyTooltipAugment<>("downloadStageDescription"));
|
.apply(new TooltipAugment<>("downloadStageDescription"));
|
||||||
var clearButton = new IconButtonComp("mdi2c-close", () -> {
|
var clearButton = new IconButtonComp("mdi2c-close", () -> {
|
||||||
model.clear();
|
model.clear();
|
||||||
})
|
})
|
||||||
.hide(BindingsHelper.persist(Bindings.isEmpty(model.getItems())));
|
.hide(Bindings.isEmpty(syncItems));
|
||||||
var clearPane = Comp.derive(
|
var clearPane = Comp.derive(
|
||||||
new HorizontalComp(List.of(downloadButton, clearButton))
|
new HorizontalComp(List.of(downloadButton, clearButton))
|
||||||
.apply(struc -> struc.get().setSpacing(10)),
|
.apply(struc -> struc.get().setSpacing(10)),
|
||||||
|
@ -122,12 +127,15 @@ public class BrowserTransferComp extends SimpleComp {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(model.getBrowserSessionModel()
|
||||||
|
.getSelectedEntry()
|
||||||
|
.getValue()
|
||||||
|
instanceof OpenFileSystemModel fileSystemModel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var files = drag.getEntries();
|
var files = drag.getEntries();
|
||||||
model.drop(
|
model.drop(fileSystemModel, files);
|
||||||
model.getBrowserModel()
|
|
||||||
.getSelected()
|
|
||||||
.getValue(),
|
|
||||||
files);
|
|
||||||
event.setDropCompleted(true);
|
event.setDropCompleted(true);
|
||||||
event.consume();
|
event.consume();
|
||||||
}
|
}
|
||||||
|
@ -140,11 +148,11 @@ public class BrowserTransferComp extends SimpleComp {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
struc.get().setOnDragDetected(event -> {
|
struc.get().setOnDragDetected(event -> {
|
||||||
if (model.getDownloading().get()) {
|
if (syncDownloaded.getValue()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var selected = model.getItems().stream()
|
var selected = syncItems.stream()
|
||||||
.map(BrowserTransferModel.Item::getFileEntry)
|
.map(BrowserTransferModel.Item::getFileEntry)
|
||||||
.toList();
|
.toList();
|
||||||
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
|
Dragboard db = struc.get().startDragAndDrop(TransferMode.COPY);
|
||||||
|
@ -154,7 +162,7 @@ public class BrowserTransferComp extends SimpleComp {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var files = model.getItems().stream()
|
var files = syncItems.stream()
|
||||||
.filter(item -> item.downloadFinished().get())
|
.filter(item -> item.downloadFinished().get())
|
||||||
.map(item -> {
|
.map(item -> {
|
||||||
try {
|
try {
|
||||||
|
@ -191,7 +199,7 @@ public class BrowserTransferComp extends SimpleComp {
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
PlatformThread.sync(model.getDownloading()));
|
syncDownloaded);
|
||||||
return stack.styleClass("transfer").createRegion();
|
return stack.styleClass("transfer").createRegion();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.file.FileSystemHelper;
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
|
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.util.BooleanScope;
|
import io.xpipe.app.util.BooleanScope;
|
||||||
import io.xpipe.app.util.ShellTemp;
|
import io.xpipe.app.util.ShellTemp;
|
||||||
|
@ -36,7 +39,7 @@ public class BrowserTransferModel {
|
||||||
t.setName("file downloader");
|
t.setName("file downloader");
|
||||||
return t;
|
return t;
|
||||||
});
|
});
|
||||||
BrowserModel browserModel;
|
BrowserSessionModel browserSessionModel;
|
||||||
ObservableList<Item> items = FXCollections.observableArrayList();
|
ObservableList<Item> items = FXCollections.observableArrayList();
|
||||||
BooleanProperty downloading = new SimpleBooleanProperty();
|
BooleanProperty downloading = new SimpleBooleanProperty();
|
||||||
BooleanProperty allDownloaded = new SimpleBooleanProperty();
|
BooleanProperty allDownloaded = new SimpleBooleanProperty();
|
||||||
|
|
|
@ -9,7 +9,7 @@ public class BrowserTransferProgress {
|
||||||
long transferred;
|
long transferred;
|
||||||
long total;
|
long total;
|
||||||
|
|
||||||
static BrowserTransferProgress empty() {
|
public static BrowserTransferProgress empty() {
|
||||||
return new BrowserTransferProgress(null, 0, 0);
|
return new BrowserTransferProgress(null, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ public class BrowserTransferProgress {
|
||||||
return new BrowserTransferProgress(name, 0, size);
|
return new BrowserTransferProgress(name, 0, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
static BrowserTransferProgress finished(String name, long size) {
|
public static BrowserTransferProgress finished(String name, long size) {
|
||||||
return new BrowserTransferProgress(name, size, size);
|
return new BrowserTransferProgress(name, size, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser;
|
||||||
|
|
||||||
import atlantafx.base.controls.Spacer;
|
import atlantafx.base.controls.Spacer;
|
||||||
|
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||||
import io.xpipe.app.comp.base.ButtonComp;
|
import io.xpipe.app.comp.base.ButtonComp;
|
||||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||||
import io.xpipe.app.comp.base.TileButtonComp;
|
import io.xpipe.app.comp.base.TileButtonComp;
|
||||||
import io.xpipe.app.core.AppFont;
|
import io.xpipe.app.core.AppFont;
|
||||||
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
||||||
|
@ -12,6 +14,7 @@ import io.xpipe.app.fxcomps.impl.LabelComp;
|
||||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||||
import io.xpipe.app.fxcomps.impl.PrettySvgComp;
|
import io.xpipe.app.fxcomps.impl.PrettySvgComp;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||||
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
@ -31,9 +34,9 @@ import java.util.List;
|
||||||
|
|
||||||
public class BrowserWelcomeComp extends SimpleComp {
|
public class BrowserWelcomeComp extends SimpleComp {
|
||||||
|
|
||||||
private final BrowserModel model;
|
private final BrowserSessionModel model;
|
||||||
|
|
||||||
public BrowserWelcomeComp(BrowserModel model) {
|
public BrowserWelcomeComp(BrowserSessionModel model) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,13 +57,14 @@ public class BrowserWelcomeComp extends SimpleComp {
|
||||||
hbox.setSpacing(15);
|
hbox.setSpacing(15);
|
||||||
|
|
||||||
if (state == null) {
|
if (state == null) {
|
||||||
var header = new Label("Here you will be able to see where you left off last time.");
|
var header = new Label();
|
||||||
|
header.textProperty().bind(AppI18n.observable("browserWelcomeEmpty"));
|
||||||
vbox.getChildren().add(header);
|
vbox.getChildren().add(header);
|
||||||
hbox.setPadding(new Insets(40, 40, 40, 50));
|
hbox.setPadding(new Insets(40, 40, 40, 50));
|
||||||
return new VBox(hbox);
|
return new VBox(hbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = BindingsHelper.filteredContentBinding(state.getEntries(), e -> {
|
var list = ListBindingsHelper.filteredContentBinding(state.getEntries(), e -> {
|
||||||
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
|
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
|
||||||
if (entry.isEmpty()) {
|
if (entry.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -74,14 +78,14 @@ public class BrowserWelcomeComp extends SimpleComp {
|
||||||
});
|
});
|
||||||
var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list);
|
var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list);
|
||||||
|
|
||||||
var header = new LabelComp(Bindings.createStringBinding(
|
var headerBinding = BindingsHelper.flatMap(empty, b -> {
|
||||||
() -> {
|
if (b) {
|
||||||
return !empty.get()
|
return AppI18n.observable("browserWelcomeEmpty");
|
||||||
? "You were recently connected to the following systems:"
|
} else {
|
||||||
: "Here you will be able to see where you left off last time.";
|
return AppI18n.observable("browserWelcomeSystems");
|
||||||
},
|
}
|
||||||
empty))
|
});
|
||||||
.createRegion();
|
var header = new LabelComp(headerBinding).createRegion();
|
||||||
AppFont.setSize(header, 1);
|
AppFont.setSize(header, 1);
|
||||||
vbox.getChildren().add(header);
|
vbox.getChildren().add(header);
|
||||||
|
|
||||||
|
@ -92,7 +96,10 @@ public class BrowserWelcomeComp extends SimpleComp {
|
||||||
var disable = new SimpleBooleanProperty();
|
var disable = new SimpleBooleanProperty();
|
||||||
var entryButton = entryButton(e, disable);
|
var entryButton = entryButton(e, disable);
|
||||||
var dirButton = dirButton(e, disable);
|
var dirButton = dirButton(e, disable);
|
||||||
return new HorizontalComp(List.of(entryButton, dirButton));
|
return new HorizontalComp(List.of(entryButton, dirButton)).apply(struc -> {
|
||||||
|
((Region) struc.get().getChildren().get(0)).prefHeightProperty().bind(struc.get().heightProperty());
|
||||||
|
((Region) struc.get().getChildren().get(1)).prefHeightProperty().bind(struc.get().heightProperty());
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.apply(struc -> {
|
.apply(struc -> {
|
||||||
VBox vBox = (VBox) struc.get().getContent();
|
VBox vBox = (VBox) struc.get().getContent();
|
||||||
|
@ -125,11 +132,13 @@ public class BrowserWelcomeComp extends SimpleComp {
|
||||||
|
|
||||||
private Comp<?> entryButton(BrowserSavedState.Entry e, BooleanProperty disable) {
|
private Comp<?> entryButton(BrowserSavedState.Entry e, BooleanProperty disable) {
|
||||||
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
|
var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid());
|
||||||
var graphic = entry.get()
|
var graphic =
|
||||||
.getProvider()
|
entry.get().getProvider().getDisplayIconFileName(entry.get().getStore());
|
||||||
.getDisplayIconFileName(entry.get().getStore());
|
|
||||||
var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24);
|
var view = PrettyImageHelper.ofFixedSize(graphic, 30, 24);
|
||||||
return new ButtonComp(new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())), view.createRegion(), () -> {
|
return new ButtonComp(
|
||||||
|
new SimpleStringProperty(DataStorage.get().getStoreDisplayName(entry.get())),
|
||||||
|
view.createRegion(),
|
||||||
|
() -> {
|
||||||
ThreadHelper.runAsync(() -> {
|
ThreadHelper.runAsync(() -> {
|
||||||
model.restoreStateAsync(e, disable);
|
model.restoreStateAsync(e, disable);
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
package io.xpipe.app.browser.action;
|
package io.xpipe.app.browser.action;
|
||||||
|
|
||||||
import io.xpipe.app.browser.BrowserEntry;
|
import io.xpipe.app.browser.file.BrowserEntry;
|
||||||
import io.xpipe.app.browser.OpenFileSystemModel;
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package io.xpipe.app.browser.action;
|
package io.xpipe.app.browser.action;
|
||||||
|
|
||||||
import io.xpipe.app.browser.BrowserEntry;
|
import io.xpipe.app.browser.file.BrowserEntry;
|
||||||
import io.xpipe.app.browser.OpenFileSystemModel;
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
package io.xpipe.app.browser.action;
|
package io.xpipe.app.browser.action;
|
||||||
|
|
||||||
import io.xpipe.app.browser.BrowserEntry;
|
import io.xpipe.app.browser.file.BrowserEntry;
|
||||||
import io.xpipe.app.browser.OpenFileSystemModel;
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.core.util.ModuleLayerLoader;
|
import io.xpipe.core.util.ModuleLayerLoader;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.input.KeyCombination;
|
import javafx.scene.input.KeyCombination;
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ public interface BrowserAction {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
String getName(OpenFileSystemModel model, List<BrowserEntry> entries);
|
ObservableValue<String> getName(OpenFileSystemModel model, List<BrowserEntry> entries);
|
||||||
|
|
||||||
default boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
default boolean isApplicable(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -91,15 +92,5 @@ public interface BrowserAction {
|
||||||
})
|
})
|
||||||
.toList());
|
.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean requiresFullDaemon() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean prioritizeLoading() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package io.xpipe.app.browser.action;
|
package io.xpipe.app.browser.action;
|
||||||
|
|
||||||
import io.xpipe.app.browser.BrowserEntry;
|
import io.xpipe.app.browser.file.BrowserEntry;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package io.xpipe.app.browser.action;
|
package io.xpipe.app.browser.action;
|
||||||
|
|
||||||
import io.xpipe.app.browser.BrowserEntry;
|
import io.xpipe.app.browser.file.BrowserEntry;
|
||||||
import io.xpipe.app.browser.OpenFileSystemModel;
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.core.process.ShellControl;
|
import io.xpipe.core.process.ShellControl;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
package io.xpipe.app.browser.action;
|
package io.xpipe.app.browser.action;
|
||||||
|
|
||||||
import io.xpipe.app.browser.BrowserEntry;
|
import io.xpipe.app.browser.file.BrowserEntry;
|
||||||
import io.xpipe.app.browser.OpenFileSystemModel;
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
|
import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
||||||
import io.xpipe.app.fxcomps.util.Shortcuts;
|
import io.xpipe.app.fxcomps.util.Shortcuts;
|
||||||
import io.xpipe.app.util.BooleanScope;
|
import io.xpipe.app.util.BooleanScope;
|
||||||
import io.xpipe.app.util.LicenseProvider;
|
import io.xpipe.app.util.LicenseProvider;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.MenuItem;
|
import javafx.scene.control.MenuItem;
|
||||||
import org.kordamp.ikonli.javafx.FontIcon;
|
import org.kordamp.ikonli.javafx.FontIcon;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.UnaryOperator;
|
|
||||||
|
|
||||||
public interface LeafAction extends BrowserAction {
|
public interface LeafAction extends BrowserAction {
|
||||||
|
|
||||||
|
@ -23,7 +21,7 @@ public interface LeafAction extends BrowserAction {
|
||||||
var b = new Button();
|
var b = new Button();
|
||||||
b.setOnAction(event -> {
|
b.setOnAction(event -> {
|
||||||
// Only accept shortcut actions in the current tab
|
// Only accept shortcut actions in the current tab
|
||||||
if (!model.equals(model.getBrowserModel().getSelected().getValue())) {
|
if (!model.equals(model.getBrowserModel().getSelectedEntry().getValue())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,13 +37,14 @@ public interface LeafAction extends BrowserAction {
|
||||||
if (getShortcut() != null) {
|
if (getShortcut() != null) {
|
||||||
Shortcuts.addShortcut(b, getShortcut());
|
Shortcuts.addShortcut(b, getShortcut());
|
||||||
}
|
}
|
||||||
new FancyTooltipAugment<>(new SimpleStringProperty(getName(model, selected))).augment(b);
|
var name = getName(model, selected);
|
||||||
|
new TooltipAugment<>(name).augment(b);
|
||||||
var graphic = getIcon(model, selected);
|
var graphic = getIcon(model, selected);
|
||||||
if (graphic != null) {
|
if (graphic != null) {
|
||||||
b.setGraphic(graphic);
|
b.setGraphic(graphic);
|
||||||
}
|
}
|
||||||
b.setMnemonicParsing(false);
|
b.setMnemonicParsing(false);
|
||||||
b.setAccessibleText(getName(model, selected));
|
b.accessibleTextProperty().bind(name);
|
||||||
|
|
||||||
b.setDisable(!isActive(model, selected));
|
b.setDisable(!isActive(model, selected));
|
||||||
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
|
model.getCurrentPath().addListener((observable, oldValue, newValue) -> {
|
||||||
|
@ -61,10 +60,10 @@ public interface LeafAction extends BrowserAction {
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
|
|
||||||
default MenuItem toMenuItem(
|
default MenuItem toMenuItem(OpenFileSystemModel model, List<BrowserEntry> selected) {
|
||||||
OpenFileSystemModel model, List<BrowserEntry> selected, UnaryOperator<String> nameFunc) {
|
var name = getName(model, selected);
|
||||||
var name = nameFunc.apply(getName(model, selected));
|
var mi = new MenuItem();
|
||||||
var mi = new MenuItem(name);
|
mi.textProperty().bind(name);
|
||||||
mi.setOnAction(event -> {
|
mi.setOnAction(event -> {
|
||||||
ThreadHelper.runFailableAsync(() -> {
|
ThreadHelper.runFailableAsync(() -> {
|
||||||
BooleanScope.execute(model.getBusy(), () -> {
|
BooleanScope.execute(model.getBusy(), () -> {
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
package io.xpipe.app.browser.action;
|
package io.xpipe.app.browser.action;
|
||||||
|
|
||||||
import io.xpipe.app.browser.BrowserEntry;
|
import io.xpipe.app.browser.file.BrowserEntry;
|
||||||
import io.xpipe.app.browser.OpenFileSystemModel;
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.util.TerminalLauncher;
|
import io.xpipe.app.util.TerminalLauncher;
|
||||||
import io.xpipe.core.process.CommandBuilder;
|
import io.xpipe.core.process.CommandBuilder;
|
||||||
import io.xpipe.core.process.ShellControl;
|
import io.xpipe.core.process.ShellControl;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -39,9 +41,11 @@ public abstract class MultiExecuteAction implements BranchAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
public ObservableValue<String> getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||||
var t = AppPrefs.get().terminalType().getValue();
|
var t = AppPrefs.get().terminalType().getValue();
|
||||||
return "in " + (t != null ? t.toTranslatedString().getValue() : "?");
|
return AppI18n.observable(
|
||||||
|
"executeInTerminal",
|
||||||
|
t != null ? t.toTranslatedString().getValue() : "?");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -66,8 +70,8 @@ public abstract class MultiExecuteAction implements BranchAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
public ObservableValue<String> getName(OpenFileSystemModel model, List<BrowserEntry> entries) {
|
||||||
return "in background";
|
return AppI18n.observable("executeInBackground");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package io.xpipe.app.browser.action;
|
package io.xpipe.app.browser.action;
|
||||||
|
|
||||||
import io.xpipe.app.browser.BrowserEntry;
|
import io.xpipe.app.browser.file.BrowserEntry;
|
||||||
import io.xpipe.app.browser.OpenFileSystemModel;
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.util.FileOpener;
|
import io.xpipe.app.util.FileOpener;
|
||||||
import io.xpipe.core.process.ShellControl;
|
import io.xpipe.core.process.ShellControl;
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.file;
|
||||||
|
|
||||||
import io.xpipe.app.core.AppI18n;
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.core.AppWindowHelper;
|
import io.xpipe.app.core.AppWindowHelper;
|
|
@ -1,8 +1,9 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.file;
|
||||||
|
|
||||||
import io.xpipe.app.browser.action.BranchAction;
|
import io.xpipe.app.browser.action.BranchAction;
|
||||||
import io.xpipe.app.browser.action.BrowserAction;
|
import io.xpipe.app.browser.action.BrowserAction;
|
||||||
import io.xpipe.app.browser.action.LeafAction;
|
import io.xpipe.app.browser.action.LeafAction;
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.core.AppFont;
|
import io.xpipe.app.core.AppFont;
|
||||||
import io.xpipe.app.util.LicenseProvider;
|
import io.xpipe.app.util.LicenseProvider;
|
||||||
import javafx.scene.control.ContextMenu;
|
import javafx.scene.control.ContextMenu;
|
||||||
|
@ -13,7 +14,7 @@ import org.kordamp.ikonli.javafx.FontIcon;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
final class BrowserContextMenu extends ContextMenu {
|
public final class BrowserContextMenu extends ContextMenu {
|
||||||
|
|
||||||
private final OpenFileSystemModel model;
|
private final OpenFileSystemModel model;
|
||||||
private final BrowserEntry source;
|
private final BrowserEntry source;
|
||||||
|
@ -74,17 +75,17 @@ final class BrowserContextMenu extends ContextMenu {
|
||||||
for (BrowserAction a : all) {
|
for (BrowserAction a : all) {
|
||||||
var used = resolveIfNeeded(a, selected);
|
var used = resolveIfNeeded(a, selected);
|
||||||
if (a instanceof LeafAction la) {
|
if (a instanceof LeafAction la) {
|
||||||
getItems().add(la.toMenuItem(model, used, s -> s));
|
getItems().add(la.toMenuItem(model, used));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (a instanceof BranchAction la) {
|
if (a instanceof BranchAction la) {
|
||||||
var m = new Menu(a.getName(model, used) + " ...");
|
var m = new Menu(a.getName(model, used).getValue() + " ...");
|
||||||
for (LeafAction sub : la.getBranchingActions(model, used)) {
|
for (LeafAction sub : la.getBranchingActions(model, used)) {
|
||||||
var subUsed = resolveIfNeeded(sub, selected);
|
var subUsed = resolveIfNeeded(sub, selected);
|
||||||
if (!sub.isApplicable(model, subUsed)) {
|
if (!sub.isApplicable(model, subUsed)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
m.getItems().add(sub.toMenuItem(model, subUsed, s -> s));
|
m.getItems().add(sub.toMenuItem(model, subUsed));
|
||||||
}
|
}
|
||||||
var graphic = a.getIcon(model, used);
|
var graphic = a.getIcon(model, used);
|
||||||
if (graphic != null) {
|
if (graphic != null) {
|
|
@ -1,4 +1,4 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.file;
|
||||||
|
|
||||||
import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
|
import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
|
||||||
import io.xpipe.app.browser.icon.BrowserIconFileType;
|
import io.xpipe.app.browser.icon.BrowserIconFileType;
|
||||||
|
@ -29,7 +29,7 @@ public class BrowserEntry {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var f : BrowserIconFileType.ALL) {
|
for (var f : BrowserIconFileType.getAll()) {
|
||||||
if (f.matches(rawFileEntry)) {
|
if (f.matches(rawFileEntry)) {
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ public class BrowserEntry {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var f : BrowserIconDirectoryType.ALL) {
|
for (var f : BrowserIconDirectoryType.getAll()) {
|
||||||
if (f.matches(rawFileEntry)) {
|
if (f.matches(rawFileEntry)) {
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
|
@ -1,15 +1,15 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.file;
|
||||||
|
|
||||||
import atlantafx.base.controls.Spacer;
|
import atlantafx.base.controls.Spacer;
|
||||||
import atlantafx.base.theme.Styles;
|
import atlantafx.base.theme.Styles;
|
||||||
import io.xpipe.app.browser.action.BrowserAction;
|
import io.xpipe.app.browser.action.BrowserAction;
|
||||||
import io.xpipe.app.browser.icon.FileIconManager;
|
import io.xpipe.app.browser.icon.FileIconManager;
|
||||||
import io.xpipe.app.comp.base.LazyTextFieldComp;
|
import io.xpipe.app.comp.base.LazyTextFieldComp;
|
||||||
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
||||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.util.BooleanScope;
|
import io.xpipe.app.util.BooleanScope;
|
||||||
import io.xpipe.app.util.HumanReadableFormat;
|
import io.xpipe.app.util.HumanReadableFormat;
|
||||||
|
@ -48,7 +48,7 @@ import java.util.Objects;
|
||||||
import static io.xpipe.app.util.HumanReadableFormat.byteCount;
|
import static io.xpipe.app.util.HumanReadableFormat.byteCount;
|
||||||
import static javafx.scene.control.TableColumn.SortType.ASCENDING;
|
import static javafx.scene.control.TableColumn.SortType.ASCENDING;
|
||||||
|
|
||||||
final class BrowserFileListComp extends SimpleComp {
|
public final class BrowserFileListComp extends SimpleComp {
|
||||||
|
|
||||||
private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden");
|
private static final PseudoClass HIDDEN = PseudoClass.getPseudoClass("hidden");
|
||||||
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
private static final PseudoClass EMPTY = PseudoClass.getPseudoClass("empty");
|
||||||
|
@ -71,7 +71,8 @@ final class BrowserFileListComp extends SimpleComp {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private TableView<BrowserEntry> createTable() {
|
private TableView<BrowserEntry> createTable() {
|
||||||
var filenameCol = new TableColumn<BrowserEntry, String>("Name");
|
var filenameCol = new TableColumn<BrowserEntry, String>();
|
||||||
|
filenameCol.textProperty().bind(AppI18n.observable("name"));
|
||||||
filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
|
filenameCol.setCellValueFactory(param -> new SimpleStringProperty(
|
||||||
param.getValue() != null
|
param.getValue() != null
|
||||||
? FileNames.getFileName(
|
? FileNames.getFileName(
|
||||||
|
@ -81,17 +82,20 @@ final class BrowserFileListComp extends SimpleComp {
|
||||||
filenameCol.setSortType(ASCENDING);
|
filenameCol.setSortType(ASCENDING);
|
||||||
filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing()));
|
filenameCol.setCellFactory(col -> new FilenameCell(fileList.getEditing()));
|
||||||
|
|
||||||
var sizeCol = new TableColumn<BrowserEntry, Number>("Size");
|
var sizeCol = new TableColumn<BrowserEntry, Number>();
|
||||||
|
sizeCol.textProperty().bind(AppI18n.observable("size"));
|
||||||
sizeCol.setCellValueFactory(param -> new SimpleLongProperty(
|
sizeCol.setCellValueFactory(param -> new SimpleLongProperty(
|
||||||
param.getValue().getRawFileEntry().resolved().getSize()));
|
param.getValue().getRawFileEntry().resolved().getSize()));
|
||||||
sizeCol.setCellFactory(col -> new FileSizeCell());
|
sizeCol.setCellFactory(col -> new FileSizeCell());
|
||||||
|
|
||||||
var mtimeCol = new TableColumn<BrowserEntry, Instant>("Modified");
|
var mtimeCol = new TableColumn<BrowserEntry, Instant>();
|
||||||
|
mtimeCol.textProperty().bind(AppI18n.observable("modified"));
|
||||||
mtimeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
|
mtimeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
|
||||||
param.getValue().getRawFileEntry().resolved().getDate()));
|
param.getValue().getRawFileEntry().resolved().getDate()));
|
||||||
mtimeCol.setCellFactory(col -> new FileTimeCell());
|
mtimeCol.setCellFactory(col -> new FileTimeCell());
|
||||||
|
|
||||||
var modeCol = new TableColumn<BrowserEntry, String>("Attributes");
|
var modeCol = new TableColumn<BrowserEntry, String>();
|
||||||
|
modeCol.textProperty().bind(AppI18n.observable("attributes"));
|
||||||
modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
|
modeCol.setCellValueFactory(param -> new SimpleObjectProperty<>(
|
||||||
param.getValue().getRawFileEntry().resolved().getMode()));
|
param.getValue().getRawFileEntry().resolved().getMode()));
|
||||||
modeCol.setCellFactory(col -> new FileModeCell());
|
modeCol.setCellFactory(col -> new FileModeCell());
|
||||||
|
@ -122,7 +126,7 @@ final class BrowserFileListComp extends SimpleComp {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareTableSelectionModel(TableView<BrowserEntry> table) {
|
private void prepareTableSelectionModel(TableView<BrowserEntry> table) {
|
||||||
if (!fileList.getMode().isMultiple()) {
|
if (!fileList.getSelectionMode().isMultiple()) {
|
||||||
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
|
||||||
} else {
|
} else {
|
||||||
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||||
|
@ -142,9 +146,9 @@ final class BrowserFileListComp extends SimpleComp {
|
||||||
.getPath()));
|
.getPath()));
|
||||||
// Remove unsuitable selection
|
// Remove unsuitable selection
|
||||||
toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY
|
toSelect.removeIf(browserEntry -> (browserEntry.getRawFileEntry().getKind() == FileKind.DIRECTORY
|
||||||
&& !fileList.getMode().isAcceptsDirectories())
|
&& !fileList.getSelectionMode().isAcceptsDirectories())
|
||||||
|| (browserEntry.getRawFileEntry().getKind() != FileKind.DIRECTORY
|
|| (browserEntry.getRawFileEntry().getKind() != FileKind.DIRECTORY
|
||||||
&& !fileList.getMode().isAcceptsFiles()));
|
&& !fileList.getSelectionMode().isAcceptsFiles()));
|
||||||
fileList.getSelection().setAll(toSelect);
|
fileList.getSelection().setAll(toSelect);
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
@ -505,16 +509,21 @@ final class BrowserFileListComp extends SimpleComp {
|
||||||
.get();
|
.get();
|
||||||
var quickAccess = new BrowserQuickAccessButtonComp(
|
var quickAccess = new BrowserQuickAccessButtonComp(
|
||||||
() -> getTableRow().getItem(), fileList.getFileSystemModel())
|
() -> getTableRow().getItem(), fileList.getFileSystemModel())
|
||||||
.hide(BindingsHelper.persist(Bindings.createBooleanBinding(
|
.hide(Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
var item = getTableRow().getItem();
|
var item = getTableRow().getItem();
|
||||||
var notDir = item.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY;
|
var notDir = item.getRawFileEntry()
|
||||||
var isParentLink = item
|
.resolved()
|
||||||
.getRawFileEntry()
|
.getKind()
|
||||||
.equals(fileList.getFileSystemModel().getCurrentParentDirectory());
|
!= FileKind.DIRECTORY;
|
||||||
|
var isParentLink = item.getRawFileEntry()
|
||||||
|
.equals(fileList.getFileSystemModel()
|
||||||
|
.getCurrentParentDirectory());
|
||||||
return notDir || isParentLink;
|
return notDir || isParentLink;
|
||||||
},
|
},
|
||||||
itemProperty())))
|
itemProperty())
|
||||||
|
.not()
|
||||||
|
.not())
|
||||||
.createRegion();
|
.createRegion();
|
||||||
|
|
||||||
editing.addListener((observable, oldValue, newValue) -> {
|
editing.addListener((observable, oldValue, newValue) -> {
|
|
@ -1,5 +1,7 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.file;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.BrowserClipboard;
|
||||||
|
import io.xpipe.app.browser.BrowserSelectionListComp;
|
||||||
import io.xpipe.core.store.FileKind;
|
import io.xpipe.core.store.FileKind;
|
||||||
import javafx.geometry.Point2D;
|
import javafx.geometry.Point2D;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
|
@ -1,6 +1,7 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.file;
|
||||||
|
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.core.store.FileKind;
|
import io.xpipe.core.store.FileKind;
|
||||||
import io.xpipe.core.store.FileNames;
|
import io.xpipe.core.store.FileNames;
|
||||||
|
@ -25,6 +26,8 @@ public final class BrowserFileListModel {
|
||||||
static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR =
|
static final Comparator<BrowserEntry> FILE_TYPE_COMPARATOR =
|
||||||
Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
Comparator.comparing(path -> path.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY);
|
||||||
|
|
||||||
|
private final OpenFileSystemModel.SelectionMode selectionMode;
|
||||||
|
|
||||||
private final OpenFileSystemModel fileSystemModel;
|
private final OpenFileSystemModel fileSystemModel;
|
||||||
private final Property<Comparator<BrowserEntry>> comparatorProperty =
|
private final Property<Comparator<BrowserEntry>> comparatorProperty =
|
||||||
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
|
new SimpleObjectProperty<>(FILE_TYPE_COMPARATOR);
|
||||||
|
@ -33,13 +36,14 @@ public final class BrowserFileListModel {
|
||||||
private final ObservableList<BrowserEntry> previousSelection = FXCollections.observableArrayList();
|
private final ObservableList<BrowserEntry> previousSelection = FXCollections.observableArrayList();
|
||||||
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
|
private final ObservableList<BrowserEntry> selection = FXCollections.observableArrayList();
|
||||||
private final ObservableList<FileSystem.FileEntry> selectedRaw =
|
private final ObservableList<FileSystem.FileEntry> selectedRaw =
|
||||||
BindingsHelper.mappedContentBinding(selection, entry -> entry.getRawFileEntry());
|
ListBindingsHelper.mappedContentBinding(selection, entry -> entry.getRawFileEntry());
|
||||||
|
|
||||||
private final Property<BrowserEntry> draggedOverDirectory = new SimpleObjectProperty<>();
|
private final Property<BrowserEntry> draggedOverDirectory = new SimpleObjectProperty<>();
|
||||||
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
|
private final Property<Boolean> draggedOverEmpty = new SimpleBooleanProperty();
|
||||||
private final Property<BrowserEntry> editing = new SimpleObjectProperty<>();
|
private final Property<BrowserEntry> editing = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
public BrowserFileListModel(OpenFileSystemModel fileSystemModel) {
|
public BrowserFileListModel(OpenFileSystemModel.SelectionMode selectionMode, OpenFileSystemModel fileSystemModel) {
|
||||||
|
this.selectionMode = selectionMode;
|
||||||
this.fileSystemModel = fileSystemModel;
|
this.fileSystemModel = fileSystemModel;
|
||||||
|
|
||||||
fileSystemModel.getFilter().addListener((observable, oldValue, newValue) -> {
|
fileSystemModel.getFilter().addListener((observable, oldValue, newValue) -> {
|
||||||
|
@ -51,10 +55,6 @@ public final class BrowserFileListModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public BrowserModel.Mode getMode() {
|
|
||||||
return fileSystemModel.getBrowserModel().getMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAll(Stream<FileSystem.FileEntry> newFiles) {
|
public void setAll(Stream<FileSystem.FileEntry> newFiles) {
|
||||||
try (var s = newFiles) {
|
try (var s = newFiles) {
|
||||||
var parent = fileSystemModel.getCurrentParentDirectory();
|
var parent = fileSystemModel.getCurrentParentDirectory();
|
||||||
|
@ -135,12 +135,6 @@ public final class BrowserFileListModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onDoubleClick(BrowserEntry entry) {
|
public void onDoubleClick(BrowserEntry entry) {
|
||||||
if (entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY
|
|
||||||
&& getMode().equals(BrowserModel.Mode.SINGLE_FILE_CHOOSER)) {
|
|
||||||
getFileSystemModel().getBrowserModel().finishChooser();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
|
if (entry.getRawFileEntry().resolved().getKind() == FileKind.DIRECTORY) {
|
||||||
fileSystemModel.cdAsync(entry.getRawFileEntry().resolved().getPath());
|
fileSystemModel.cdAsync(entry.getRawFileEntry().resolved().getPath());
|
||||||
}
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.file;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.browser.icon.BrowserIcons;
|
import io.xpipe.app.browser.icon.BrowserIcons;
|
||||||
import io.xpipe.app.comp.base.ListBoxViewComp;
|
import io.xpipe.app.comp.base.ListBoxViewComp;
|
||||||
import io.xpipe.app.comp.base.VBoxViewComp;
|
import io.xpipe.app.comp.base.VBoxViewComp;
|
||||||
|
@ -31,8 +32,10 @@ public class BrowserFileOverviewComp extends SimpleComp {
|
||||||
Function<FileSystem.FileEntry, Comp<?>> factory = entry -> {
|
Function<FileSystem.FileEntry, Comp<?>> factory = entry -> {
|
||||||
return Comp.of(() -> {
|
return Comp.of(() -> {
|
||||||
var icon = BrowserIcons.createIcon(entry);
|
var icon = BrowserIcons.createIcon(entry);
|
||||||
var graphic = new HorizontalComp(List.of(icon,
|
var graphic = new HorizontalComp(List.of(
|
||||||
new BrowserQuickAccessButtonComp(() -> new BrowserEntry(entry, model.getFileList(),false),model)));
|
icon,
|
||||||
|
new BrowserQuickAccessButtonComp(
|
||||||
|
() -> new BrowserEntry(entry, model.getFileList(), false), model)));
|
||||||
var l = new Button(entry.getPath(), graphic.createRegion());
|
var l = new Button(entry.getPath(), graphic.createRegion());
|
||||||
l.setGraphicTextGap(1);
|
l.setGraphicTextGap(1);
|
||||||
l.setOnAction(event -> {
|
l.setOnAction(event -> {
|
|
@ -1,5 +1,6 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.file;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
||||||
import io.xpipe.app.util.InputHelper;
|
import io.xpipe.app.util.InputHelper;
|
|
@ -1,5 +1,6 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.file;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.browser.icon.FileIconManager;
|
import io.xpipe.app.browser.icon.FileIconManager;
|
||||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||||
import io.xpipe.app.util.BooleanAnimationTimer;
|
import io.xpipe.app.util.BooleanAnimationTimer;
|
||||||
|
@ -28,18 +29,105 @@ import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class BrowserQuickAccessContextMenu extends ContextMenu {
|
public class BrowserQuickAccessContextMenu extends ContextMenu {
|
||||||
|
|
||||||
|
private final Supplier<BrowserEntry> base;
|
||||||
|
private final OpenFileSystemModel model;
|
||||||
|
private ContextMenu shownBrowserActionsMenu;
|
||||||
|
private boolean expandBrowserActionMenuKey;
|
||||||
|
private boolean keyBasedNavigation;
|
||||||
|
private boolean closeBrowserActionMenuKey;
|
||||||
|
public BrowserQuickAccessContextMenu(Supplier<BrowserEntry> base, OpenFileSystemModel model) {
|
||||||
|
this.base = base;
|
||||||
|
this.model = model;
|
||||||
|
|
||||||
|
addEventFilter(Menu.ON_SHOWING, e -> {
|
||||||
|
Node content = getSkin().getNode();
|
||||||
|
if (content instanceof Region r) {
|
||||||
|
r.setMaxWidth(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
addEventFilter(Menu.ON_SHOWN, e -> {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
getItems().getFirst().getStyleableNode().requestFocus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
InputHelper.onLeft(this, false, e -> {
|
||||||
|
hide();
|
||||||
|
e.consume();
|
||||||
|
});
|
||||||
|
setAutoHide(true);
|
||||||
|
getStyleClass().add("condensed");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showMenu(Node anchor) {
|
||||||
|
getItems().clear();
|
||||||
|
ThreadHelper.runFailableAsync(() -> {
|
||||||
|
var entry = base.get();
|
||||||
|
if (entry.getRawFileEntry().resolved().getKind() != FileKind.DIRECTORY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actionsMenu = new AtomicReference<ContextMenu>();
|
||||||
|
var r = new Menu();
|
||||||
|
var newItems = updateMenuItems(r, entry, true);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
getItems().addAll(r.getItems());
|
||||||
|
show(anchor, Side.RIGHT, 0, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private MenuItem createItem(BrowserEntry browserEntry) {
|
||||||
|
return new QuickAccessMenu(browserEntry).getMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<MenuItem> updateMenuItems(Menu m, BrowserEntry entry, boolean updateInstantly) throws Exception {
|
||||||
|
var newFiles = model.getFileSystem()
|
||||||
|
.listFiles(entry.getRawFileEntry().resolved().getPath());
|
||||||
|
try (var s = newFiles) {
|
||||||
|
var list = s.map(fileEntry -> fileEntry.resolved()).toList();
|
||||||
|
// Wait until all files are listed, i.e. do not skip the stream elements
|
||||||
|
list = list.subList(0, Math.min(list.size(), 150));
|
||||||
|
|
||||||
|
var newItems = new ArrayList<MenuItem>();
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
var empty = new Menu("<empty>");
|
||||||
|
empty.getStyleClass().add("leaf");
|
||||||
|
newItems.add(empty);
|
||||||
|
} else {
|
||||||
|
var browserEntries = list.stream()
|
||||||
|
.map(fileEntry -> new BrowserEntry(fileEntry, model.getFileList(), false))
|
||||||
|
.toList();
|
||||||
|
var menus = browserEntries.stream()
|
||||||
|
.sorted(model.getFileList().order())
|
||||||
|
.collect(Collectors.toMap(e -> e, e -> createItem(e), (v1, v2) -> v2, LinkedHashMap::new));
|
||||||
|
var dirs = browserEntries.stream()
|
||||||
|
.filter(e -> e.getRawFileEntry().getKind() == FileKind.DIRECTORY)
|
||||||
|
.toList();
|
||||||
|
if (dirs.size() == 1) {
|
||||||
|
updateMenuItems((Menu) menus.get(dirs.getFirst()), dirs.getFirst(), true);
|
||||||
|
}
|
||||||
|
newItems.addAll(menus.values());
|
||||||
|
}
|
||||||
|
if (updateInstantly) {
|
||||||
|
m.getItems().setAll(newItems);
|
||||||
|
}
|
||||||
|
return newItems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
class QuickAccessMenu {
|
class QuickAccessMenu {
|
||||||
private final BrowserEntry browserEntry;
|
private final BrowserEntry browserEntry;
|
||||||
private ContextMenu browserActionMenu;
|
|
||||||
private final Menu menu;
|
private final Menu menu;
|
||||||
|
private ContextMenu browserActionMenu;
|
||||||
|
|
||||||
public QuickAccessMenu(BrowserEntry browserEntry) {
|
public QuickAccessMenu(BrowserEntry browserEntry) {
|
||||||
this.browserEntry = browserEntry;
|
this.browserEntry = browserEntry;
|
||||||
this.menu = new Menu(
|
this.menu = new Menu(
|
||||||
// Use original name, not the link target
|
// Use original name, not the link target
|
||||||
browserEntry.getRawFileEntry().getName(),
|
browserEntry.getRawFileEntry().getName(),
|
||||||
PrettyImageHelper.ofFixedSizeSquare(FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24)
|
PrettyImageHelper.ofFixedSizeSquare(
|
||||||
|
FileIconManager.getFileIcon(browserEntry.getRawFileEntry(), false), 24)
|
||||||
.createRegion());
|
.createRegion());
|
||||||
createMenu();
|
createMenu();
|
||||||
addInputListeners();
|
addInputListeners();
|
||||||
|
@ -141,7 +229,8 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
|
||||||
});
|
});
|
||||||
new BooleanAnimationTimer(hover, 100, () -> {
|
new BooleanAnimationTimer(hover, 100, () -> {
|
||||||
expandDirectoryMenu(empty);
|
expandDirectoryMenu(empty);
|
||||||
}).start();
|
})
|
||||||
|
.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addInputListeners() {
|
private void addInputListeners() {
|
||||||
|
@ -154,13 +243,15 @@ public class BrowserQuickAccessContextMenu extends ContextMenu {
|
||||||
} else {
|
} else {
|
||||||
expandBrowserActionMenuKey = false;
|
expandBrowserActionMenuKey = false;
|
||||||
}
|
}
|
||||||
if (event.getCode().equals(KeyCode.LEFT) && browserActionMenu != null && browserActionMenu.isShowing()) {
|
if (event.getCode().equals(KeyCode.LEFT)
|
||||||
|
&& browserActionMenu != null
|
||||||
|
&& browserActionMenu.isShowing()) {
|
||||||
closeBrowserActionMenuKey = true;
|
closeBrowserActionMenuKey = true;
|
||||||
} else {
|
} else {
|
||||||
closeBrowserActionMenuKey = false;
|
closeBrowserActionMenuKey = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
contextMenu.addEventFilter(MouseEvent.ANY,event -> {
|
contextMenu.addEventFilter(MouseEvent.ANY, event -> {
|
||||||
keyBasedNavigation = false;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,17 +1,17 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.file;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.BrowserTransferProgress;
|
||||||
|
import io.xpipe.app.browser.fs.OpenFileSystemModel;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.core.process.OsType;
|
import io.xpipe.core.process.OsType;
|
||||||
import io.xpipe.core.store.FileKind;
|
import io.xpipe.core.store.*;
|
||||||
import io.xpipe.core.store.FileNames;
|
|
||||||
import io.xpipe.core.store.FileSystem;
|
|
||||||
import io.xpipe.core.store.LocalStore;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.time.Instant;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
|
@ -151,6 +151,18 @@ public class FileSystemHelper {
|
||||||
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
|
Files.isDirectory(file) ? FileKind.DIRECTORY : FileKind.FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static FileSystem.FileEntry getRemoteWrapper(FileSystem fileSystem, String file) throws Exception {
|
||||||
|
return new FileSystem.FileEntry(
|
||||||
|
fileSystem,
|
||||||
|
file,
|
||||||
|
Instant.now(),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
fileSystem.getFileSize(file),
|
||||||
|
null,
|
||||||
|
fileSystem.directoryExists(file) ? FileKind.DIRECTORY : FileKind.FILE);
|
||||||
|
}
|
||||||
|
|
||||||
public static void dropLocalFilesInto(
|
public static void dropLocalFilesInto(
|
||||||
FileSystem.FileEntry entry,
|
FileSystem.FileEntry entry,
|
||||||
List<Path> files,
|
List<Path> files,
|
||||||
|
@ -278,7 +290,8 @@ public class FileSystemHelper {
|
||||||
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
|
var baseRelative = FileNames.toDirectory(FileNames.getParent(source.getPath()));
|
||||||
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
|
List<FileSystem.FileEntry> list = source.getFileSystem().listFilesRecursively(source.getPath());
|
||||||
for (FileSystem.FileEntry fileEntry : list) {
|
for (FileSystem.FileEntry fileEntry : list) {
|
||||||
flatFiles.put(fileEntry, FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath())));
|
var rel = FileNames.toUnix(FileNames.relativize(baseRelative, fileEntry.getPath()));
|
||||||
|
flatFiles.put(fileEntry, rel);
|
||||||
if (fileEntry.getKind() == FileKind.FILE) {
|
if (fileEntry.getKind() == FileKind.FILE) {
|
||||||
// This one is up-to-date and does not need to be recalculated
|
// This one is up-to-date and does not need to be recalculated
|
||||||
totalSize.addAndGet(fileEntry.getSize());
|
totalSize.addAndGet(fileEntry.getSize());
|
||||||
|
@ -293,7 +306,8 @@ public class FileSystemHelper {
|
||||||
AtomicLong transferred = new AtomicLong();
|
AtomicLong transferred = new AtomicLong();
|
||||||
for (var e : flatFiles.entrySet()) {
|
for (var e : flatFiles.entrySet()) {
|
||||||
var sourceFile = e.getKey();
|
var sourceFile = e.getKey();
|
||||||
var targetFile = FileNames.join(target.getPath(), e.getValue());
|
var fixedRelPath = new FilePath(e.getValue()).fileSystemCompatible(target.getFileSystem().getShell().orElseThrow().getOsType());
|
||||||
|
var targetFile = FileNames.join(target.getPath(), fixedRelPath.toString());
|
||||||
if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
|
if (sourceFile.getFileSystem().equals(target.getFileSystem())) {
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.fs;
|
||||||
|
|
||||||
import io.xpipe.app.util.ShellControlCache;
|
import io.xpipe.app.util.ShellControlCache;
|
||||||
import io.xpipe.core.process.ShellControl;
|
import io.xpipe.core.process.ShellControl;
|
|
@ -1,7 +1,13 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.fs;
|
||||||
|
|
||||||
import atlantafx.base.controls.Spacer;
|
import atlantafx.base.controls.Spacer;
|
||||||
|
import io.xpipe.app.browser.BrowserFilterComp;
|
||||||
|
import io.xpipe.app.browser.BrowserNavBar;
|
||||||
|
import io.xpipe.app.browser.BrowserOverviewComp;
|
||||||
|
import io.xpipe.app.browser.BrowserStatusBarComp;
|
||||||
import io.xpipe.app.browser.action.BrowserAction;
|
import io.xpipe.app.browser.action.BrowserAction;
|
||||||
|
import io.xpipe.app.browser.file.BrowserContextMenu;
|
||||||
|
import io.xpipe.app.browser.file.BrowserFileListComp;
|
||||||
import io.xpipe.app.comp.base.ModalOverlayComp;
|
import io.xpipe.app.comp.base.ModalOverlayComp;
|
||||||
import io.xpipe.app.comp.base.MultiContentComp;
|
import io.xpipe.app.comp.base.MultiContentComp;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
|
@ -9,7 +15,6 @@ import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
||||||
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
|
||||||
import io.xpipe.app.fxcomps.util.Shortcuts;
|
import io.xpipe.app.fxcomps.util.Shortcuts;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
@ -55,7 +60,9 @@ public class OpenFileSystemComp extends SimpleComp {
|
||||||
|
|
||||||
var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open"));
|
var menuButton = new MenuButton(null, new FontIcon("mdral-folder_open"));
|
||||||
new ContextMenuAugment<>(
|
new ContextMenuAugment<>(
|
||||||
event -> event.getButton() == MouseButton.PRIMARY, null, () -> new BrowserContextMenu(model, null))
|
event -> event.getButton() == MouseButton.PRIMARY,
|
||||||
|
null,
|
||||||
|
() -> new BrowserContextMenu(model, null))
|
||||||
.augment(new SimpleCompStructure<>(menuButton));
|
.augment(new SimpleCompStructure<>(menuButton));
|
||||||
menuButton.disableProperty().bind(model.getInOverview());
|
menuButton.disableProperty().bind(model.getInOverview());
|
||||||
menuButton.setAccessibleText("Directory options");
|
menuButton.setAccessibleText("Directory options");
|
||||||
|
@ -97,7 +104,7 @@ public class OpenFileSystemComp extends SimpleComp {
|
||||||
home,
|
home,
|
||||||
model.getCurrentPath().isNull(),
|
model.getCurrentPath().isNull(),
|
||||||
fileList,
|
fileList,
|
||||||
BindingsHelper.persist(model.getCurrentPath().isNull().not())));
|
model.getCurrentPath().isNull().not()));
|
||||||
return stack.createRegion();
|
return stack.createRegion();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.fs;
|
||||||
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanBinding;
|
|
@ -1,7 +1,15 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.fs;
|
||||||
|
|
||||||
|
import io.xpipe.app.browser.BrowserSavedState;
|
||||||
|
import io.xpipe.app.browser.BrowserTransferProgress;
|
||||||
import io.xpipe.app.browser.action.BrowserAction;
|
import io.xpipe.app.browser.action.BrowserAction;
|
||||||
|
import io.xpipe.app.browser.file.BrowserFileListModel;
|
||||||
|
import io.xpipe.app.browser.file.FileSystemHelper;
|
||||||
|
import io.xpipe.app.browser.session.BrowserAbstractSessionModel;
|
||||||
|
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||||
|
import io.xpipe.app.browser.session.BrowserSessionTab;
|
||||||
import io.xpipe.app.comp.base.ModalOverlayComp;
|
import io.xpipe.app.comp.base.ModalOverlayComp;
|
||||||
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.app.storage.DataStoreEntryRef;
|
import io.xpipe.app.storage.DataStoreEntryRef;
|
||||||
|
@ -27,45 +35,90 @@ import java.util.Optional;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
public final class OpenFileSystemModel {
|
public final class OpenFileSystemModel extends BrowserSessionTab<FileSystemStore> {
|
||||||
|
|
||||||
private final DataStoreEntryRef<? extends FileSystemStore> entry;
|
|
||||||
private final Property<String> filter = new SimpleStringProperty();
|
private final Property<String> filter = new SimpleStringProperty();
|
||||||
private final BrowserFileListModel fileList;
|
private final BrowserFileListModel fileList;
|
||||||
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
|
private final ReadOnlyObjectWrapper<String> currentPath = new ReadOnlyObjectWrapper<>();
|
||||||
private final OpenFileSystemHistory history = new OpenFileSystemHistory();
|
private final OpenFileSystemHistory history = new OpenFileSystemHistory();
|
||||||
private final BooleanProperty busy = new SimpleBooleanProperty();
|
|
||||||
private final BrowserModel browserModel;
|
|
||||||
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
|
private final Property<ModalOverlayComp.OverlayContent> overlay = new SimpleObjectProperty<>();
|
||||||
private final BooleanProperty inOverview = new SimpleBooleanProperty();
|
private final BooleanProperty inOverview = new SimpleBooleanProperty();
|
||||||
private final String name;
|
|
||||||
private final String tooltip;
|
|
||||||
private final Property<BrowserTransferProgress> progress =
|
private final Property<BrowserTransferProgress> progress =
|
||||||
new SimpleObjectProperty<>(BrowserTransferProgress.empty());
|
new SimpleObjectProperty<>(BrowserTransferProgress.empty());
|
||||||
private FileSystem fileSystem;
|
private FileSystem fileSystem;
|
||||||
private OpenFileSystemSavedState savedState;
|
private OpenFileSystemSavedState savedState;
|
||||||
private OpenFileSystemCache cache;
|
private OpenFileSystemCache cache;
|
||||||
|
|
||||||
public OpenFileSystemModel(BrowserModel browserModel, DataStoreEntryRef<? extends FileSystemStore> entry) {
|
public OpenFileSystemModel(
|
||||||
this.browserModel = browserModel;
|
BrowserAbstractSessionModel<?> model,
|
||||||
this.entry = entry;
|
DataStoreEntryRef<? extends FileSystemStore> entry,
|
||||||
this.name = DataStorage.get().getStoreDisplayName(entry.get());
|
SelectionMode selectionMode) {
|
||||||
this.tooltip = DataStorage.get().getId(entry.getEntry()).toString();
|
super(model, entry);
|
||||||
this.inOverview.bind(Bindings.createBooleanBinding(
|
this.inOverview.bind(Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
return currentPath.get() == null;
|
return currentPath.get() == null;
|
||||||
},
|
},
|
||||||
currentPath));
|
currentPath));
|
||||||
fileList = new BrowserFileListModel(this);
|
fileList = new BrowserFileListModel(selectionMode, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isBusy() {
|
@Override
|
||||||
|
public Comp<?> comp() {
|
||||||
|
return new OpenFileSystemComp(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canImmediatelyClose() {
|
||||||
return !progress.getValue().done()
|
return !progress.getValue().done()
|
||||||
|| (fileSystem != null
|
|| (fileSystem != null
|
||||||
&& fileSystem.getShell().isPresent()
|
&& fileSystem.getShell().isPresent()
|
||||||
&& fileSystem.getShell().get().getLock().isLocked());
|
&& fileSystem.getShell().get().getLock().isLocked());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() throws Exception {
|
||||||
|
BooleanScope.execute(busy, () -> {
|
||||||
|
var fs = entry.getStore().createFileSystem();
|
||||||
|
if (fs.getShell().isPresent()) {
|
||||||
|
ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());
|
||||||
|
fs.getShell().get().onKill(() -> {
|
||||||
|
browserModel.closeAsync(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
fs.open();
|
||||||
|
this.fileSystem = fs;
|
||||||
|
|
||||||
|
this.cache = new OpenFileSystemCache(this);
|
||||||
|
for (BrowserAction b : BrowserAction.ALL) {
|
||||||
|
b.init(this);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.savedState = OpenFileSystemSavedState.loadForStore(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
if (fileSystem == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DataStorage.get().getStoreEntries().contains(getEntry().get())
|
||||||
|
&& savedState != null
|
||||||
|
&& getCurrentPath().get() != null) {
|
||||||
|
if (getBrowserModel() instanceof BrowserSessionModel bm) {
|
||||||
|
bm.getSavedState()
|
||||||
|
.add(new BrowserSavedState.Entry(
|
||||||
|
getEntry().get().getUuid(), getCurrentPath().get()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fileSystem.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
ErrorEvent.fromThrowable(e).handle();
|
||||||
|
}
|
||||||
|
fileSystem = null;
|
||||||
|
}
|
||||||
|
|
||||||
private void startIfNeeded() throws Exception {
|
private void startIfNeeded() throws Exception {
|
||||||
if (fileSystem == null) {
|
if (fileSystem == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -185,7 +238,7 @@ public final class OpenFileSystemModel {
|
||||||
var name = adjustedPath + " - " + entry.get().getName();
|
var name = adjustedPath + " - " + entry.get().getName();
|
||||||
ThreadHelper.runFailableAsync(() -> {
|
ThreadHelper.runFailableAsync(() -> {
|
||||||
if (ShellDialects.getStartableDialects().stream()
|
if (ShellDialects.getStartableDialects().stream()
|
||||||
.anyMatch(dialect -> adjustedPath.startsWith(dialect.getOpenCommand(null)))) {
|
.anyMatch(dialect -> adjustedPath.toLowerCase().startsWith(dialect.getExecutableName().toLowerCase()))) {
|
||||||
TerminalLauncher.open(
|
TerminalLauncher.open(
|
||||||
entry.getEntry(),
|
entry.getEntry(),
|
||||||
name,
|
name,
|
||||||
|
@ -369,42 +422,10 @@ public final class OpenFileSystemModel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void closeSync() {
|
|
||||||
if (fileSystem == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
fileSystem.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
ErrorEvent.fromThrowable(e).handle();
|
|
||||||
}
|
|
||||||
fileSystem = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isClosed() {
|
public boolean isClosed() {
|
||||||
return fileSystem == null;
|
return fileSystem == null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initFileSystem() throws Exception {
|
|
||||||
BooleanScope.execute(busy, () -> {
|
|
||||||
var fs = entry.getStore().createFileSystem();
|
|
||||||
if (fs.getShell().isPresent()) {
|
|
||||||
ProcessControlProvider.get().withDefaultScripts(fs.getShell().get());
|
|
||||||
fs.getShell().get().onKill(() -> {
|
|
||||||
browserModel.closeFileSystemAsync(this);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
fs.open();
|
|
||||||
this.fileSystem = fs;
|
|
||||||
|
|
||||||
this.cache = new OpenFileSystemCache(this);
|
|
||||||
for (BrowserAction b : BrowserAction.ALL) {
|
|
||||||
b.init(this);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initWithGivenDirectory(String dir) throws Exception {
|
public void initWithGivenDirectory(String dir) throws Exception {
|
||||||
cdSyncWithoutCheck(dir);
|
cdSyncWithoutCheck(dir);
|
||||||
}
|
}
|
||||||
|
@ -414,10 +435,6 @@ public final class OpenFileSystemModel {
|
||||||
history.updateCurrent(null);
|
history.updateCurrent(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
void initSavedState() {
|
|
||||||
this.savedState = OpenFileSystemSavedState.loadForStore(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void openTerminalAsync(String directory) {
|
public void openTerminalAsync(String directory) {
|
||||||
ThreadHelper.runFailableAsync(() -> {
|
ThreadHelper.runFailableAsync(() -> {
|
||||||
if (fileSystem == null) {
|
if (fileSystem == null) {
|
||||||
|
@ -445,4 +462,23 @@ public final class OpenFileSystemModel {
|
||||||
public void forthSync(int i) throws Exception {
|
public void forthSync(int i) throws Exception {
|
||||||
cdSyncWithoutCheck(history.forth(i));
|
cdSyncWithoutCheck(history.forth(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum SelectionMode {
|
||||||
|
SINGLE_FILE(false, true, false),
|
||||||
|
MULTIPLE_FILE(true, true, false),
|
||||||
|
SINGLE_DIRECTORY(false, false, true),
|
||||||
|
MULTIPLE_DIRECTORY(true, false, true),
|
||||||
|
ALL(true, true, true);
|
||||||
|
|
||||||
|
private final boolean multiple;
|
||||||
|
private final boolean acceptsFiles;
|
||||||
|
private final boolean acceptsDirectories;
|
||||||
|
|
||||||
|
SelectionMode(boolean multiple, boolean acceptsFiles, boolean acceptsDirectories) {
|
||||||
|
this.multiple = multiple;
|
||||||
|
this.acceptsFiles = acceptsFiles;
|
||||||
|
this.acceptsDirectories = acceptsDirectories;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.fs;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
import com.fasterxml.jackson.core.JsonParser;
|
import com.fasterxml.jackson.core.JsonParser;
|
|
@ -15,18 +15,18 @@ import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public interface BrowserIconDirectoryType {
|
public abstract class BrowserIconDirectoryType {
|
||||||
|
|
||||||
List<BrowserIconDirectoryType> ALL = new ArrayList<>();
|
private static final List<BrowserIconDirectoryType> ALL = new ArrayList<>();
|
||||||
|
|
||||||
static BrowserIconDirectoryType byId(String id) {
|
public static synchronized BrowserIconDirectoryType byId(String id) {
|
||||||
return ALL.stream()
|
return ALL.stream()
|
||||||
.filter(fileType -> fileType.getId().equals(id))
|
.filter(fileType -> fileType.getId().equals(id))
|
||||||
.findAny()
|
.findAny()
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void loadDefinitions() {
|
public static synchronized void loadDefinitions() {
|
||||||
ALL.add(new BrowserIconDirectoryType() {
|
ALL.add(new BrowserIconDirectoryType() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -74,13 +74,17 @@ public interface BrowserIconDirectoryType {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String getId();
|
public static synchronized List<BrowserIconDirectoryType> getAll() {
|
||||||
|
return ALL;
|
||||||
|
}
|
||||||
|
|
||||||
boolean matches(FileSystem.FileEntry entry);
|
public abstract String getId();
|
||||||
|
|
||||||
String getIcon(FileSystem.FileEntry entry, boolean open);
|
public abstract boolean matches(FileSystem.FileEntry entry);
|
||||||
|
|
||||||
class Simple implements BrowserIconDirectoryType {
|
public abstract String getIcon(FileSystem.FileEntry entry, boolean open);
|
||||||
|
|
||||||
|
public static class Simple extends BrowserIconDirectoryType {
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private final String id;
|
private final String id;
|
||||||
|
|
|
@ -12,18 +12,18 @@ import java.nio.file.Files;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public interface BrowserIconFileType {
|
public abstract class BrowserIconFileType {
|
||||||
|
|
||||||
List<BrowserIconFileType> ALL = new ArrayList<>();
|
private static final List<BrowserIconFileType> ALL = new ArrayList<>();
|
||||||
|
|
||||||
static BrowserIconFileType byId(String id) {
|
public static synchronized BrowserIconFileType byId(String id) {
|
||||||
return ALL.stream()
|
return ALL.stream()
|
||||||
.filter(fileType -> fileType.getId().equals(id))
|
.filter(fileType -> fileType.getId().equals(id))
|
||||||
.findAny()
|
.findAny()
|
||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void loadDefinitions() {
|
public static synchronized void loadDefinitions() {
|
||||||
AppResources.with(AppResources.XPIPE_MODULE, "file_list.txt", path -> {
|
AppResources.with(AppResources.XPIPE_MODULE, "file_list.txt", path -> {
|
||||||
try (var reader =
|
try (var reader =
|
||||||
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
|
new BufferedReader(new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
|
||||||
|
@ -53,14 +53,18 @@ public interface BrowserIconFileType {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
String getId();
|
public static synchronized List<BrowserIconFileType> getAll() {
|
||||||
|
return ALL;
|
||||||
|
}
|
||||||
|
|
||||||
boolean matches(FileSystem.FileEntry entry);
|
public abstract String getId();
|
||||||
|
|
||||||
String getIcon();
|
public abstract boolean matches(FileSystem.FileEntry entry);
|
||||||
|
|
||||||
|
public abstract String getIcon();
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
class Simple implements BrowserIconFileType {
|
public static class Simple extends BrowserIconFileType {
|
||||||
|
|
||||||
private final String id;
|
private final String id;
|
||||||
private final IconVariant icon;
|
private final IconVariant icon;
|
||||||
|
|
|
@ -11,29 +11,27 @@ public class FileIconManager {
|
||||||
|
|
||||||
public static synchronized void loadIfNecessary() {
|
public static synchronized void loadIfNecessary() {
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons");
|
AppImages.loadDirectory(AppResources.XPIPE_MODULE, "browser_icons", true, false);
|
||||||
loaded = true;
|
loaded = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getFileIcon(FileSystem.FileEntry entry, boolean open) {
|
public static synchronized String getFileIcon(FileSystem.FileEntry entry, boolean open) {
|
||||||
if (entry == null) {
|
if (entry == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadIfNecessary();
|
|
||||||
|
|
||||||
var r = entry.resolved();
|
var r = entry.resolved();
|
||||||
if (r.getKind() != FileKind.DIRECTORY) {
|
if (r.getKind() != FileKind.DIRECTORY) {
|
||||||
for (var f : BrowserIconFileType.ALL) {
|
for (var f : BrowserIconFileType.getAll()) {
|
||||||
if (f.matches(r)) {
|
if (f.matches(r)) {
|
||||||
return getIconPath(f.getIcon());
|
return f.getIcon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (var f : BrowserIconDirectoryType.ALL) {
|
for (var f : BrowserIconDirectoryType.getAll()) {
|
||||||
if (f.matches(r)) {
|
if (f.matches(r)) {
|
||||||
return getIconPath(f.getIcon(r, open));
|
return f.getIcon(r, open);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,8 +40,4 @@ public class FileIconManager {
|
||||||
? (open ? "default_folder_opened.svg" : "default_folder.svg")
|
? (open ? "default_folder_opened.svg" : "default_folder.svg")
|
||||||
: "default_file.svg";
|
: "default_file.svg";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getIconPath(String name) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() {}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -1,40 +1,31 @@
|
||||||
package io.xpipe.app.browser;
|
package io.xpipe.app.browser.session;
|
||||||
|
|
||||||
import atlantafx.base.controls.RingProgressIndicator;
|
import atlantafx.base.controls.RingProgressIndicator;
|
||||||
import atlantafx.base.controls.Spacer;
|
|
||||||
import atlantafx.base.theme.Styles;
|
import atlantafx.base.theme.Styles;
|
||||||
import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
|
import io.xpipe.app.browser.BrowserWelcomeComp;
|
||||||
import io.xpipe.app.browser.icon.BrowserIconFileType;
|
|
||||||
import io.xpipe.app.browser.icon.FileIconManager;
|
|
||||||
import io.xpipe.app.comp.base.MultiContentComp;
|
import io.xpipe.app.comp.base.MultiContentComp;
|
||||||
import io.xpipe.app.comp.base.SideSplitPaneComp;
|
|
||||||
import io.xpipe.app.core.AppLayoutModel;
|
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
|
|
||||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||||
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.app.util.BooleanScope;
|
import io.xpipe.app.util.BooleanScope;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.geometry.Insets;
|
|
||||||
import javafx.geometry.Orientation;
|
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.Tab;
|
||||||
|
import javafx.scene.control.TabPane;
|
||||||
import javafx.scene.input.DragEvent;
|
import javafx.scene.input.DragEvent;
|
||||||
import javafx.scene.layout.*;
|
import javafx.scene.layout.Region;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@ -42,106 +33,25 @@ import static atlantafx.base.theme.Styles.DENSE;
|
||||||
import static atlantafx.base.theme.Styles.toggleStyleClass;
|
import static atlantafx.base.theme.Styles.toggleStyleClass;
|
||||||
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
|
import static javafx.scene.control.TabPane.TabClosingPolicy.ALL_TABS;
|
||||||
|
|
||||||
public class BrowserComp extends SimpleComp {
|
public class BrowserSessionTabsComp extends SimpleComp {
|
||||||
|
|
||||||
private final BrowserModel model;
|
private final BrowserSessionModel model;
|
||||||
|
|
||||||
public BrowserComp(BrowserModel model) {
|
public BrowserSessionTabsComp(BrowserSessionModel model) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
public Region createSimple() {
|
||||||
protected Region createSimple() {
|
|
||||||
BrowserIconFileType.loadDefinitions();
|
|
||||||
BrowserIconDirectoryType.loadDefinitions();
|
|
||||||
ThreadHelper.runAsync(() -> {
|
|
||||||
FileIconManager.loadIfNecessary();
|
|
||||||
});
|
|
||||||
|
|
||||||
var bookmarksList = new BrowserBookmarkComp(model).vgrow();
|
|
||||||
var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage())
|
|
||||||
.hide(PlatformThread.sync(Bindings.createBooleanBinding(
|
|
||||||
() -> {
|
|
||||||
if (model.getOpenFileSystems().size() == 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model.getMode().isChooser()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
model.getOpenFileSystems(),
|
|
||||||
model.getSelected())));
|
|
||||||
localDownloadStage.prefHeight(200);
|
|
||||||
localDownloadStage.maxHeight(200);
|
|
||||||
var vertical = new VerticalComp(List.of(bookmarksList, localDownloadStage));
|
|
||||||
|
|
||||||
var splitPane = new SideSplitPaneComp(vertical, createTabs())
|
|
||||||
.withInitialWidth(AppLayoutModel.get().getSavedState().getBrowserConnectionsWidth())
|
|
||||||
.withOnDividerChange(AppLayoutModel.get().getSavedState()::setBrowserConnectionsWidth)
|
|
||||||
.apply(struc -> {
|
|
||||||
struc.getLeft().setMinWidth(200);
|
|
||||||
struc.getLeft().setMaxWidth(500);
|
|
||||||
});
|
|
||||||
var r = addBottomBar(splitPane.createRegion());
|
|
||||||
r.getStyleClass().add("browser");
|
|
||||||
// AppFont.small(r);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Region addBottomBar(Region r) {
|
|
||||||
if (!model.getMode().isChooser()) {
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectedLabel = new Label("Selected: ");
|
|
||||||
selectedLabel.setAlignment(Pos.CENTER);
|
|
||||||
var selected = new HBox();
|
|
||||||
selected.setAlignment(Pos.CENTER_LEFT);
|
|
||||||
selected.setSpacing(10);
|
|
||||||
model.getSelection().addListener((ListChangeListener<? super BrowserEntry>) c -> {
|
|
||||||
PlatformThread.runLaterIfNeeded(() -> {
|
|
||||||
selected.getChildren()
|
|
||||||
.setAll(c.getList().stream()
|
|
||||||
.map(s -> {
|
|
||||||
var field =
|
|
||||||
new TextField(s.getRawFileEntry().getPath());
|
|
||||||
field.setEditable(false);
|
|
||||||
field.setPrefWidth(500);
|
|
||||||
return field;
|
|
||||||
})
|
|
||||||
.toList());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
var spacer = new Spacer(Orientation.HORIZONTAL);
|
|
||||||
var button = new Button("Select");
|
|
||||||
button.setPadding(new Insets(5, 10, 5, 10));
|
|
||||||
button.setOnAction(event -> model.finishChooser());
|
|
||||||
button.setDefaultButton(true);
|
|
||||||
var bottomBar = new HBox(selectedLabel, selected, spacer, button);
|
|
||||||
HBox.setHgrow(selected, Priority.ALWAYS);
|
|
||||||
bottomBar.setAlignment(Pos.CENTER);
|
|
||||||
bottomBar.getStyleClass().add("chooser-bar");
|
|
||||||
|
|
||||||
var layout = new VBox(r, bottomBar);
|
|
||||||
VBox.setVgrow(r, Priority.ALWAYS);
|
|
||||||
return layout;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Comp<?> createTabs() {
|
|
||||||
var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of(
|
var multi = new MultiContentComp(Map.<Comp<?>, ObservableValue<Boolean>>of(
|
||||||
Comp.of(() -> createTabPane()),
|
Comp.of(() -> createTabPane()),
|
||||||
BindingsHelper.persist(Bindings.isNotEmpty(model.getOpenFileSystems())),
|
Bindings.isNotEmpty(model.getSessionEntries()),
|
||||||
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
|
new BrowserWelcomeComp(model).apply(struc -> StackPane.setAlignment(struc.get(), Pos.CENTER_LEFT)),
|
||||||
Bindings.createBooleanBinding(
|
Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
return model.getOpenFileSystems().size() == 0
|
return model.getSessionEntries().size() == 0;
|
||||||
&& !model.getMode().isChooser();
|
|
||||||
},
|
},
|
||||||
model.getOpenFileSystems())));
|
model.getSessionEntries())));
|
||||||
return multi;
|
return multi.createRegion();
|
||||||
}
|
}
|
||||||
|
|
||||||
private TabPane createTabPane() {
|
private TabPane createTabPane() {
|
||||||
|
@ -153,16 +63,17 @@ public class BrowserComp extends SimpleComp {
|
||||||
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
|
Styles.toggleStyleClass(tabs, TabPane.STYLE_CLASS_FLOATING);
|
||||||
toggleStyleClass(tabs, DENSE);
|
toggleStyleClass(tabs, DENSE);
|
||||||
|
|
||||||
var map = new HashMap<OpenFileSystemModel, Tab>();
|
var map = new HashMap<BrowserSessionTab<?>, Tab>();
|
||||||
|
|
||||||
// Restore state
|
// Restore state
|
||||||
model.getOpenFileSystems().forEach(v -> {
|
model.getSessionEntries().forEach(v -> {
|
||||||
var t = createTab(tabs, v);
|
var t = createTab(tabs, v);
|
||||||
map.put(v, t);
|
map.put(v, t);
|
||||||
tabs.getTabs().add(t);
|
tabs.getTabs().add(t);
|
||||||
});
|
});
|
||||||
tabs.getSelectionModel()
|
tabs.getSelectionModel()
|
||||||
.select(model.getOpenFileSystems().indexOf(model.getSelected().getValue()));
|
.select(model.getSessionEntries()
|
||||||
|
.indexOf(model.getSelectedEntry().getValue()));
|
||||||
|
|
||||||
// Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually!
|
// Used for ignoring changes by the tabpane when new tabs are added. We want to perform the selections manually!
|
||||||
var modifying = new SimpleBooleanProperty();
|
var modifying = new SimpleBooleanProperty();
|
||||||
|
@ -174,7 +85,7 @@ public class BrowserComp extends SimpleComp {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValue == null) {
|
if (newValue == null) {
|
||||||
model.getSelected().setValue(null);
|
model.getSelectedEntry().setValue(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,11 +95,11 @@ public class BrowserComp extends SimpleComp {
|
||||||
.findAny()
|
.findAny()
|
||||||
.map(Map.Entry::getKey)
|
.map(Map.Entry::getKey)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
model.getSelected().setValue(source);
|
model.getSelectedEntry().setValue(source);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle selection from model
|
// Handle selection from model
|
||||||
model.getSelected().addListener((observable, oldValue, newValue) -> {
|
model.getSelectedEntry().addListener((observable, oldValue, newValue) -> {
|
||||||
PlatformThread.runLaterIfNeeded(() -> {
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
if (newValue == null) {
|
if (newValue == null) {
|
||||||
tabs.getSelectionModel().select(null);
|
tabs.getSelectionModel().select(null);
|
||||||
|
@ -210,7 +121,7 @@ public class BrowserComp extends SimpleComp {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
model.getOpenFileSystems().addListener((ListChangeListener<? super OpenFileSystemModel>) c -> {
|
model.getSessionEntries().addListener((ListChangeListener<? super BrowserSessionTab>) c -> {
|
||||||
while (c.next()) {
|
while (c.next()) {
|
||||||
for (var r : c.getRemoved()) {
|
for (var r : c.getRemoved()) {
|
||||||
PlatformThread.runLaterIfNeeded(() -> {
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
|
@ -247,14 +158,14 @@ public class BrowserComp extends SimpleComp {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
model.closeFileSystemAsync(source.getKey());
|
model.closeAsync(source.getKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return tabs;
|
return tabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Tab createTab(TabPane tabs, OpenFileSystemModel model) {
|
private Tab createTab(TabPane tabs, BrowserSessionTab<?> model) {
|
||||||
var tab = new Tab();
|
var tab = new Tab();
|
||||||
|
|
||||||
var ring = new RingProgressIndicator(0, false);
|
var ring = new RingProgressIndicator(0, false);
|
||||||
|
@ -279,12 +190,12 @@ public class BrowserComp extends SimpleComp {
|
||||||
PlatformThread.sync(model.getBusy())));
|
PlatformThread.sync(model.getBusy())));
|
||||||
tab.setText(model.getName());
|
tab.setText(model.getName());
|
||||||
|
|
||||||
tab.setContent(new OpenFileSystemComp(model).createSimple());
|
tab.setContent(model.comp().createRegion());
|
||||||
|
|
||||||
var id = UUID.randomUUID().toString();
|
var id = UUID.randomUUID().toString();
|
||||||
tab.setId(id);
|
tab.setId(id);
|
||||||
|
|
||||||
SimpleChangeListener.apply(tabs.skinProperty(), newValue -> {
|
tabs.skinProperty().subscribe(newValue -> {
|
||||||
if (newValue != null) {
|
if (newValue != null) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
Label l = (Label) tabs.lookup("#" + id + " .tab-label");
|
Label l = (Label) tabs.lookup("#" + id + " .tab-label");
|
||||||
|
@ -303,7 +214,7 @@ public class BrowserComp extends SimpleComp {
|
||||||
if (color != null) {
|
if (color != null) {
|
||||||
c.getStyleClass().add(color.getId());
|
c.getStyleClass().add(color.getId());
|
||||||
}
|
}
|
||||||
new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c);
|
new TooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c);
|
||||||
c.addEventHandler(
|
c.addEventHandler(
|
||||||
DragEvent.DRAG_ENTERED,
|
DragEvent.DRAG_ENTERED,
|
||||||
mouseEvent -> Platform.runLater(
|
mouseEvent -> Platform.runLater(
|
|
@ -7,13 +7,15 @@ import io.xpipe.app.core.AppLayoutModel;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.CompStructure;
|
import io.xpipe.app.fxcomps.CompStructure;
|
||||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.Pane;
|
import javafx.scene.layout.Pane;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class AppLayoutComp extends Comp<CompStructure<Pane>> {
|
public class AppLayoutComp extends Comp<CompStructure<Pane>> {
|
||||||
|
@ -22,18 +24,20 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompStructure<Pane> createBase() {
|
public CompStructure<Pane> createBase() {
|
||||||
var multi = new MultiContentComp(model.getEntries().stream()
|
Map<Comp<?>, ObservableValue<Boolean>> map = model.getEntries().stream()
|
||||||
.collect(Collectors.toMap(
|
.collect(Collectors.toMap(
|
||||||
entry -> entry.comp(),
|
entry -> entry.comp(),
|
||||||
entry -> PlatformThread.sync(Bindings.createBooleanBinding(
|
entry -> Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
return model.getSelected().getValue().equals(entry);
|
return model.getSelected().getValue().equals(entry);
|
||||||
},
|
},
|
||||||
model.getSelected())))));
|
model.getSelected())));
|
||||||
|
var multi = new MultiContentComp(map);
|
||||||
|
|
||||||
var pane = new BorderPane();
|
var pane = new BorderPane();
|
||||||
var sidebar = new SideMenuBarComp(model.getSelectedInternal(), model.getEntries());
|
var sidebar = new SideMenuBarComp(model.getSelected(), model.getEntries());
|
||||||
pane.setCenter(multi.createRegion());
|
StackPane multiR = (StackPane) multi.createRegion();
|
||||||
|
pane.setCenter(multiR);
|
||||||
pane.setRight(sidebar.createRegion());
|
pane.setRight(sidebar.createRegion());
|
||||||
pane.getStyleClass().add("background");
|
pane.getStyleClass().add("background");
|
||||||
model.getSelected().addListener((c, o, n) -> {
|
model.getSelected().addListener((c, o, n) -> {
|
||||||
|
|
|
@ -3,7 +3,6 @@ package io.xpipe.app.comp.base;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.CompStructure;
|
import io.xpipe.app.fxcomps.CompStructure;
|
||||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
@ -50,7 +49,7 @@ public class ButtonComp extends Comp<CompStructure<Button>> {
|
||||||
var graphic = getGraphic();
|
var graphic = getGraphic();
|
||||||
if (graphic instanceof FontIcon f) {
|
if (graphic instanceof FontIcon f) {
|
||||||
// f.iconColorProperty().bind(button.textFillProperty());
|
// f.iconColorProperty().bind(button.textFillProperty());
|
||||||
SimpleChangeListener.apply(button.fontProperty(), c -> {
|
button.fontProperty().subscribe(c -> {
|
||||||
f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
|
f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,7 @@ import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.CompStructure;
|
import io.xpipe.app.fxcomps.CompStructure;
|
||||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import javafx.css.Size;
|
import javafx.css.Size;
|
||||||
import javafx.css.SizeUnits;
|
import javafx.css.SizeUnits;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
@ -38,12 +37,12 @@ public class DropdownComp extends Comp<CompStructure<Button>> {
|
||||||
.createRegion();
|
.createRegion();
|
||||||
|
|
||||||
button.visibleProperty()
|
button.visibleProperty()
|
||||||
.bind(BindingsHelper.anyMatch(cm.getItems().stream()
|
.bind(ListBindingsHelper.anyMatch(cm.getItems().stream()
|
||||||
.map(menuItem -> menuItem.getGraphic().visibleProperty())
|
.map(menuItem -> menuItem.getGraphic().visibleProperty())
|
||||||
.toList()));
|
.toList()));
|
||||||
|
|
||||||
var graphic = new FontIcon("mdi2c-chevron-double-down");
|
var graphic = new FontIcon("mdi2c-chevron-double-down");
|
||||||
SimpleChangeListener.apply(button.fontProperty(), c -> {
|
button.fontProperty().subscribe(c -> {
|
||||||
graphic.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
|
graphic.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
47
app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java
Normal file
47
app/src/main/java/io/xpipe/app/comp/base/FontIconComp.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package io.xpipe.app.comp.base;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.CompStructure;
|
import io.xpipe.app.fxcomps.CompStructure;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import javafx.beans.property.Property;
|
import javafx.beans.property.Property;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
|
@ -65,7 +64,7 @@ public class LazyTextFieldComp extends Comp<LazyTextFieldComp.Structure> {
|
||||||
sp.prefHeightProperty().bind(r.prefHeightProperty());
|
sp.prefHeightProperty().bind(r.prefHeightProperty());
|
||||||
r.setDisable(true);
|
r.setDisable(true);
|
||||||
|
|
||||||
SimpleChangeListener.apply(currentValue, n -> {
|
currentValue.subscribe(n -> {
|
||||||
PlatformThread.runLaterIfNeeded(() -> {
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
// Check if control value is the same. Then don't set it as that might cause bugs
|
// Check if control value is the same. Then don't set it as that might cause bugs
|
||||||
if (Objects.equals(r.getText(), n) || (n == null && r.getText().isEmpty())) {
|
if (Objects.equals(r.getText(), n) || (n == null && r.getText().isEmpty())) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ package io.xpipe.app.comp.base;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.CompStructure;
|
import io.xpipe.app.fxcomps.CompStructure;
|
||||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
|
@ -88,7 +88,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!listView.getChildren().equals(newShown)) {
|
if (!listView.getChildren().equals(newShown)) {
|
||||||
BindingsHelper.setContent(listView.getChildren(), newShown);
|
ListBindingsHelper.setContent(listView.getChildren(), newShown);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.CompStructure;
|
import io.xpipe.app.fxcomps.CompStructure;
|
||||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.util.Hyperlinks;
|
import io.xpipe.app.util.Hyperlinks;
|
||||||
|
@ -59,7 +58,7 @@ public class MarkdownComp extends Comp<CompStructure<StackPane>> {
|
||||||
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow();
|
var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow();
|
||||||
wv.getEngine().setUserStyleSheetLocation(url.toString());
|
wv.getEngine().setUserStyleSheetLocation(url.toString());
|
||||||
|
|
||||||
SimpleChangeListener.apply(PlatformThread.sync(markdown), val -> {
|
PlatformThread.sync(markdown).subscribe(val -> {
|
||||||
// Workaround for https://bugs.openjdk.org/browse/JDK-8199014
|
// Workaround for https://bugs.openjdk.org/browse/JDK-8199014
|
||||||
try {
|
try {
|
||||||
var file = Files.createTempFile(null, ".html");
|
var file = Files.createTempFile(null, ".html");
|
||||||
|
|
|
@ -3,8 +3,10 @@ package io.xpipe.app.comp.base;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.collections.MapChangeListener;
|
||||||
|
import javafx.collections.ObservableMap;
|
||||||
import javafx.scene.layout.Region;
|
import javafx.scene.layout.Region;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
|
||||||
|
@ -12,24 +14,54 @@ import java.util.Map;
|
||||||
|
|
||||||
public class MultiContentComp extends SimpleComp {
|
public class MultiContentComp extends SimpleComp {
|
||||||
|
|
||||||
private final Map<Comp<?>, ObservableValue<Boolean>> content;
|
private final ObservableMap<Comp<?>, ObservableValue<Boolean>> content;
|
||||||
|
|
||||||
public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content) {
|
public MultiContentComp(Map<Comp<?>, ObservableValue<Boolean>> content) {
|
||||||
|
this.content = FXCollections.observableMap(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public MultiContentComp(ObservableMap<Comp<?>, ObservableValue<Boolean>> content) {
|
||||||
this.content = content;
|
this.content = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
var stack = new StackPane();
|
ObservableMap<Comp<?>, Region> m = FXCollections.observableHashMap();
|
||||||
stack.setPickOnBounds(false);
|
content.addListener((MapChangeListener<? super Comp<?>, ? super ObservableValue<Boolean>>) change -> {
|
||||||
for (Map.Entry<Comp<?>, ObservableValue<Boolean>> entry : content.entrySet()) {
|
if (change.wasAdded()) {
|
||||||
var region = entry.getKey().createRegion();
|
var r = change.getKey().createRegion();
|
||||||
stack.getChildren().add(region);
|
change.getValueAdded().subscribe(val -> {
|
||||||
SimpleChangeListener.apply(PlatformThread.sync(entry.getValue()), val -> {
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
region.setManaged(val);
|
r.setManaged(val);
|
||||||
region.setVisible(val);
|
r.setVisible(val);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
m.put(change.getKey(), r);
|
||||||
|
} else {
|
||||||
|
m.remove(change.getKey());
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var stack = new StackPane();
|
||||||
|
m.addListener((MapChangeListener<? super Comp<?>, Region>) change -> {
|
||||||
|
if (change.wasAdded()) {
|
||||||
|
stack.getChildren().add(change.getValueAdded());
|
||||||
|
} else {
|
||||||
|
stack.getChildren().remove(change.getValueRemoved());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (Map.Entry<Comp<?>, ObservableValue<Boolean>> e : content.entrySet()) {
|
||||||
|
var r = e.getKey().createRegion();
|
||||||
|
e.getValue().subscribe(val -> {
|
||||||
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
|
r.setManaged(val);
|
||||||
|
r.setVisible(val);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
m.put(e.getKey(), r);
|
||||||
|
}
|
||||||
|
|
||||||
return stack;
|
return stack;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class OsLogoComp extends SimpleComp {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
var img = BindingsHelper.persist(Bindings.createObjectBinding(
|
var img = Bindings.createObjectBinding(
|
||||||
() -> {
|
() -> {
|
||||||
if (state.getValue() != SystemStateComp.State.SUCCESS) {
|
if (state.getValue() != SystemStateComp.State.SUCCESS) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -51,7 +51,7 @@ public class OsLogoComp extends SimpleComp {
|
||||||
return getImage(ons.getOsName());
|
return getImage(ons.getOsName());
|
||||||
},
|
},
|
||||||
wrapper.getPersistentState(),
|
wrapper.getPersistentState(),
|
||||||
state));
|
state);
|
||||||
var hide = BindingsHelper.map(img, s -> s != null);
|
var hide = BindingsHelper.map(img, s -> s != null);
|
||||||
return new StackComp(
|
return new StackComp(
|
||||||
List.of(new SystemStateComp(state).hide(hide), new PrettyImageComp(img, 24, 24).visible(hide)))
|
List.of(new SystemStateComp(state).hide(hide), new PrettyImageComp(img, 24, 24).visible(hide)))
|
||||||
|
|
|
@ -7,8 +7,8 @@ import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.CompStructure;
|
import io.xpipe.app.fxcomps.CompStructure;
|
||||||
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.augment.Augment;
|
import io.xpipe.app.fxcomps.augment.Augment;
|
||||||
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
|
|
||||||
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
||||||
|
import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.issue.UserReportComp;
|
import io.xpipe.app.issue.UserReportComp;
|
||||||
|
@ -73,7 +73,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||||
var e = entries.get(i);
|
var e = entries.get(i);
|
||||||
var b = new IconButtonComp(e.icon(), () -> value.setValue(e));
|
var b = new IconButtonComp(e.icon(), () -> value.setValue(e));
|
||||||
b.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + i]));
|
b.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + i]));
|
||||||
b.apply(new FancyTooltipAugment<>(e.name()));
|
b.apply(new TooltipAugment<>(e.name()));
|
||||||
b.apply(struc -> {
|
b.apply(struc -> {
|
||||||
AppFont.setSize(struc.get(), 2);
|
AppFont.setSize(struc.get(), 2);
|
||||||
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
|
struc.get().pseudoClassStateChanged(selected, value.getValue().equals(e));
|
||||||
|
@ -133,7 +133,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||||
UserReportComp.show(event.build());
|
UserReportComp.show(event.build());
|
||||||
})
|
})
|
||||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()]))
|
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size()]))
|
||||||
.apply(new FancyTooltipAugment<>("reportIssue"))
|
.apply(new TooltipAugment<>("reportIssue"))
|
||||||
.apply(simpleBorders)
|
.apply(simpleBorders)
|
||||||
.accessibleTextKey("reportIssue");
|
.accessibleTextKey("reportIssue");
|
||||||
b.apply(struc -> {
|
b.apply(struc -> {
|
||||||
|
@ -145,7 +145,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||||
{
|
{
|
||||||
var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB))
|
var b = new IconButtonComp("mdi2g-github", () -> Hyperlinks.open(Hyperlinks.GITHUB))
|
||||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1]))
|
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 1]))
|
||||||
.apply(new FancyTooltipAugment<>("visitGithubRepository"))
|
.apply(new TooltipAugment<>("visitGithubRepository"))
|
||||||
.apply(simpleBorders)
|
.apply(simpleBorders)
|
||||||
.accessibleTextKey("visitGithubRepository");
|
.accessibleTextKey("visitGithubRepository");
|
||||||
b.apply(struc -> {
|
b.apply(struc -> {
|
||||||
|
@ -157,7 +157,7 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||||
{
|
{
|
||||||
var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD))
|
var b = new IconButtonComp("mdi2d-discord", () -> Hyperlinks.open(Hyperlinks.DISCORD))
|
||||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]))
|
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 2]))
|
||||||
.apply(new FancyTooltipAugment<>("discord"))
|
.apply(new TooltipAugment<>("discord"))
|
||||||
.apply(simpleBorders)
|
.apply(simpleBorders)
|
||||||
.accessibleTextKey("discord");
|
.accessibleTextKey("discord");
|
||||||
b.apply(struc -> {
|
b.apply(struc -> {
|
||||||
|
@ -167,9 +167,20 @@ public class SideMenuBarComp extends Comp<CompStructure<VBox>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded())
|
var b = new IconButtonComp("mdi2t-translate", () -> Hyperlinks.open(Hyperlinks.TRANSLATE))
|
||||||
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 3]))
|
.shortcut(new KeyCodeCombination(KeyCode.values()[KeyCode.DIGIT1.ordinal() + entries.size() + 3]))
|
||||||
.apply(new FancyTooltipAugment<>("updateAvailableTooltip"))
|
.apply(new TooltipAugment<>("translate"))
|
||||||
|
.apply(simpleBorders)
|
||||||
|
.accessibleTextKey("translate");
|
||||||
|
b.apply(struc -> {
|
||||||
|
AppFont.setSize(struc.get(), 2);
|
||||||
|
});
|
||||||
|
vbox.getChildren().add(b.createRegion());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var b = new IconButtonComp("mdi2u-update", () -> UpdateAvailableAlert.showIfNeeded())
|
||||||
|
.apply(new TooltipAugment<>("updateAvailableTooltip"))
|
||||||
.accessibleTextKey("updateAvailableTooltip");
|
.accessibleTextKey("updateAvailableTooltip");
|
||||||
b.apply(struc -> {
|
b.apply(struc -> {
|
||||||
AppFont.setSize(struc.get(), 2);
|
AppFont.setSize(struc.get(), 2);
|
||||||
|
|
|
@ -3,7 +3,6 @@ package io.xpipe.app.comp.base;
|
||||||
import io.xpipe.app.comp.store.StoreSection;
|
import io.xpipe.app.comp.store.StoreSection;
|
||||||
import io.xpipe.app.core.AppI18n;
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
|
||||||
import io.xpipe.app.storage.DataStoreEntry;
|
import io.xpipe.app.storage.DataStoreEntry;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
@ -32,13 +31,13 @@ public class StoreToggleComp extends SimpleComp {
|
||||||
@Override
|
@Override
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
var disable = section.getWrapper().getValidity().map(state -> state != DataStoreEntry.Validity.COMPLETE);
|
var disable = section.getWrapper().getValidity().map(state -> state != DataStoreEntry.Validity.COMPLETE);
|
||||||
var visible = BindingsHelper.persist(Bindings.createBooleanBinding(
|
var visible = Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE
|
return section.getWrapper().getValidity().getValue() == DataStoreEntry.Validity.COMPLETE
|
||||||
&& section.getShowDetails().get();
|
&& section.getShowDetails().get();
|
||||||
},
|
},
|
||||||
section.getWrapper().getValidity(),
|
section.getWrapper().getValidity(),
|
||||||
section.getShowDetails()));
|
section.getShowDetails());
|
||||||
var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey))
|
var t = new ToggleSwitchComp(value, AppI18n.observable(nameKey))
|
||||||
.visible(visible)
|
.visible(visible)
|
||||||
.disable(disable);
|
.disable(disable);
|
||||||
|
|
|
@ -5,7 +5,6 @@ import io.xpipe.app.comp.store.StoreEntryWrapper;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import io.xpipe.core.process.ShellStoreState;
|
import io.xpipe.core.process.ShellStoreState;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
@ -35,7 +34,7 @@ public class SystemStateComp extends SimpleComp {
|
||||||
state));
|
state));
|
||||||
var fi = new FontIcon();
|
var fi = new FontIcon();
|
||||||
fi.getStyleClass().add("inner-icon");
|
fi.getStyleClass().add("inner-icon");
|
||||||
SimpleChangeListener.apply(icon, val -> fi.setIconLiteral(val));
|
icon.subscribe(val -> fi.setIconLiteral(val));
|
||||||
|
|
||||||
var border = new FontIcon("mdi2c-circle-outline");
|
var border = new FontIcon("mdi2c-circle-outline");
|
||||||
border.getStyleClass().add("outer-icon");
|
border.getStyleClass().add("outer-icon");
|
||||||
|
@ -63,10 +62,12 @@ public class SystemStateComp extends SimpleComp {
|
||||||
""";
|
""";
|
||||||
pane.getStylesheets().add(Styles.toDataURI(dataClass1));
|
pane.getStylesheets().add(Styles.toDataURI(dataClass1));
|
||||||
|
|
||||||
SimpleChangeListener.apply(PlatformThread.sync(state), val -> {
|
state.subscribe(val -> {
|
||||||
|
PlatformThread.runLaterIfNeeded(() -> {
|
||||||
pane.getStylesheets().removeAll(success, failure, other);
|
pane.getStylesheets().removeAll(success, failure, other);
|
||||||
pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other);
|
pane.getStylesheets().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return pane;
|
return pane;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.CompStructure;
|
import io.xpipe.app.fxcomps.CompStructure;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
|
@ -13,7 +12,6 @@ import javafx.event.ActionEvent;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.StackPane;
|
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
|
@ -56,12 +54,8 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
|
||||||
var text = new VBox(header, desc);
|
var text = new VBox(header, desc);
|
||||||
text.setSpacing(2);
|
text.setSpacing(2);
|
||||||
|
|
||||||
var fi = new FontIcon();
|
var fi = new FontIconComp(icon).createStructure();
|
||||||
SimpleChangeListener.apply(PlatformThread.sync(icon), val -> {
|
var pane = fi.getPane();
|
||||||
fi.setIconLiteral(val);
|
|
||||||
});
|
|
||||||
|
|
||||||
var pane = new StackPane(fi);
|
|
||||||
var hbox = new HBox(pane, text);
|
var hbox = new HBox(pane, text);
|
||||||
hbox.setSpacing(8);
|
hbox.setSpacing(8);
|
||||||
pane.prefWidthProperty()
|
pane.prefWidthProperty()
|
||||||
|
@ -76,11 +70,11 @@ public class TileButtonComp extends Comp<TileButtonComp.Structure> {
|
||||||
desc.heightProperty()));
|
desc.heightProperty()));
|
||||||
pane.prefHeightProperty().addListener((c, o, n) -> {
|
pane.prefHeightProperty().addListener((c, o, n) -> {
|
||||||
var size = Math.min(n.intValue(), 100);
|
var size = Math.min(n.intValue(), 100);
|
||||||
fi.setIconSize((int) (size * 0.55));
|
fi.getIcon().setIconSize((int) (size * 0.55));
|
||||||
});
|
});
|
||||||
bt.setGraphic(hbox);
|
bt.setGraphic(hbox);
|
||||||
return Structure.builder()
|
return Structure.builder()
|
||||||
.graphic(fi)
|
.graphic(fi.getIcon())
|
||||||
.button(bt)
|
.button(bt)
|
||||||
.content(hbox)
|
.content(hbox)
|
||||||
.name(header)
|
.name(header)
|
||||||
|
|
|
@ -22,7 +22,7 @@ public class ToggleSwitchComp extends SimpleComp {
|
||||||
@Override
|
@Override
|
||||||
protected Region createSimple() {
|
protected Region createSimple() {
|
||||||
var s = new ToggleSwitch();
|
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) {
|
if (event.getCode() == KeyCode.SPACE || event.getCode() == KeyCode.ENTER) {
|
||||||
s.setSelected(!s.isSelected());
|
s.setSelected(!s.isSelected());
|
||||||
event.consume();
|
event.consume();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package io.xpipe.app.comp.store;
|
package io.xpipe.app.comp.store;
|
||||||
|
|
||||||
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
|
@ -80,6 +81,10 @@ public class StoreCategoryWrapper {
|
||||||
|
|
||||||
private void setupListeners() {
|
private void setupListeners() {
|
||||||
name.addListener((c, o, n) -> {
|
name.addListener((c, o, n) -> {
|
||||||
|
if (n.equals(translatedName(category.getName()))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
category.setName(n);
|
category.setName(n);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -91,6 +96,10 @@ public class StoreCategoryWrapper {
|
||||||
update();
|
update();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AppPrefs.get().language().addListener((observable, oldValue, newValue) -> {
|
||||||
|
update();
|
||||||
|
});
|
||||||
|
|
||||||
sortMode.addListener((observable, oldValue, newValue) -> {
|
sortMode.addListener((observable, oldValue, newValue) -> {
|
||||||
category.setSortMode(newValue);
|
category.setSortMode(newValue);
|
||||||
});
|
});
|
||||||
|
@ -112,8 +121,9 @@ public class StoreCategoryWrapper {
|
||||||
|
|
||||||
public void update() {
|
public void update() {
|
||||||
// Avoid reupdating name when changed from the name property!
|
// Avoid reupdating name when changed from the name property!
|
||||||
if (!category.getName().equals(name.getValue())) {
|
var catName = translatedName(category.getName());
|
||||||
name.setValue(category.getName());
|
if (!catName.equals(name.getValue())) {
|
||||||
|
name.setValue(catName);
|
||||||
}
|
}
|
||||||
|
|
||||||
lastAccess.setValue(category.getLastAccess().minus(Duration.ofMillis(500)));
|
lastAccess.setValue(category.getLastAccess().minus(Duration.ofMillis(500)));
|
||||||
|
@ -140,18 +150,30 @@ public class StoreCategoryWrapper {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
private String translatedName(String original) {
|
||||||
return name.getValue();
|
if (original.equals("All connections")) {
|
||||||
|
return AppI18n.get("allConnections");
|
||||||
|
}
|
||||||
|
if (original.equals("All scripts")) {
|
||||||
|
return AppI18n.get("allScripts");
|
||||||
|
}
|
||||||
|
if (original.equals("Predefined")) {
|
||||||
|
return AppI18n.get("predefined");
|
||||||
|
}
|
||||||
|
if (original.equals("Custom")) {
|
||||||
|
return AppI18n.get("custom");
|
||||||
|
}
|
||||||
|
if (original.equals("Default")) {
|
||||||
|
return AppI18n.get("default");
|
||||||
|
}
|
||||||
|
|
||||||
|
return original;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Property<String> nameProperty() {
|
public Property<String> nameProperty() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Instant getLastAccess() {
|
|
||||||
return lastAccess.getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Property<Instant> lastAccessProperty() {
|
public Property<Instant> lastAccessProperty() {
|
||||||
return lastAccess;
|
return lastAccess;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,6 @@ import io.xpipe.app.ext.DataStoreProviders;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.issue.ExceptionConverter;
|
import io.xpipe.app.issue.ExceptionConverter;
|
||||||
import io.xpipe.app.issue.TrackEvent;
|
import io.xpipe.app.issue.TrackEvent;
|
||||||
|
@ -381,7 +380,7 @@ public class StoreCreationComp extends DialogComp {
|
||||||
providerChoice.apply(GrowAugment.create(true, false));
|
providerChoice.apply(GrowAugment.create(true, false));
|
||||||
providerChoice.onSceneAssign(struc -> struc.get().requestFocus());
|
providerChoice.onSceneAssign(struc -> struc.get().requestFocus());
|
||||||
|
|
||||||
SimpleChangeListener.apply(provider, n -> {
|
provider.subscribe(n -> {
|
||||||
if (n != null) {
|
if (n != null) {
|
||||||
var d = n.guiDialog(existingEntry, store);
|
var d = n.guiDialog(existingEntry, store);
|
||||||
var propVal = new SimpleValidator();
|
var propVal = new SimpleValidator();
|
||||||
|
|
|
@ -26,6 +26,9 @@ public class StoreCreationMenu {
|
||||||
|
|
||||||
menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh"));
|
menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh"));
|
||||||
|
|
||||||
|
menu.getItems()
|
||||||
|
.add(category("addVisual", "mdi2c-camera-plus", DataStoreProvider.CreationCategory.VISUAL, null));
|
||||||
|
|
||||||
menu.getItems()
|
menu.getItems()
|
||||||
.add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null));
|
.add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null));
|
||||||
|
|
||||||
|
@ -81,7 +84,8 @@ public class StoreCreationMenu {
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
sub.forEach(dataStoreProvider -> {
|
sub.forEach(dataStoreProvider -> {
|
||||||
var item = new MenuItem(dataStoreProvider.getDisplayName());
|
var item = new MenuItem();
|
||||||
|
item.textProperty().bind(dataStoreProvider.displayName());
|
||||||
item.setGraphic(PrettyImageHelper.ofFixedSizeSquare(dataStoreProvider.getDisplayIconFileName(null), 16)
|
item.setGraphic(PrettyImageHelper.ofFixedSizeSquare(dataStoreProvider.getDisplayIconFileName(null), 16)
|
||||||
.createRegion());
|
.createRegion());
|
||||||
item.setOnAction(event -> {
|
item.setOnAction(event -> {
|
||||||
|
|
|
@ -13,9 +13,7 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
|
||||||
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
|
||||||
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||||
import io.xpipe.app.fxcomps.impl.*;
|
import io.xpipe.app.fxcomps.impl.*;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.app.storage.DataStoreColor;
|
import io.xpipe.app.storage.DataStoreColor;
|
||||||
|
@ -97,12 +95,15 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
wrapper.executeDefaultAction();
|
wrapper.executeDefaultAction();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
new ContextMenuAugment<>(mouseEvent -> mouseEvent.isSecondaryButtonDown(), null, () -> this.createContextMenu()).augment(new SimpleCompStructure<>(button));
|
new ContextMenuAugment<>(
|
||||||
|
mouseEvent -> mouseEvent.getButton() == MouseButton.SECONDARY,
|
||||||
|
null,
|
||||||
|
() -> this.createContextMenu())
|
||||||
|
.augment(button);
|
||||||
|
|
||||||
var loading = LoadingOverlayComp.noProgress(
|
var loading = LoadingOverlayComp.noProgress(
|
||||||
Comp.of(() -> button),
|
Comp.of(() -> button),
|
||||||
BindingsHelper.persist(
|
wrapper.getInRefresh().and(wrapper.getObserving().not()));
|
||||||
wrapper.getInRefresh().and(wrapper.getObserving().not())));
|
|
||||||
return loading.createRegion();
|
return loading.createRegion();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +139,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void applyState(Node node) {
|
protected void applyState(Node node) {
|
||||||
SimpleChangeListener.apply(PlatformThread.sync(wrapper.getValidity()), val -> {
|
PlatformThread.sync(wrapper.getValidity()).subscribe(val -> {
|
||||||
switch (val) {
|
switch (val) {
|
||||||
case LOAD_FAILED -> {
|
case LOAD_FAILED -> {
|
||||||
node.pseudoClassStateChanged(FAILED, true);
|
node.pseudoClassStateChanged(FAILED, true);
|
||||||
|
@ -174,8 +175,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
var imageComp = PrettyImageHelper.ofFixedSize(img, w, h);
|
var imageComp = PrettyImageHelper.ofFixedSize(img, w, h);
|
||||||
var storeIcon = imageComp.createRegion();
|
var storeIcon = imageComp.createRegion();
|
||||||
if (wrapper.getValidity().getValue().isUsable()) {
|
if (wrapper.getValidity().getValue().isUsable()) {
|
||||||
new FancyTooltipAugment<>(new SimpleStringProperty(
|
new TooltipAugment<>(wrapper.getEntry().getProvider().displayName())
|
||||||
wrapper.getEntry().getProvider().getDisplayName()))
|
|
||||||
.augment(storeIcon);
|
.augment(storeIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,7 +212,7 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
});
|
});
|
||||||
button.accessibleText(
|
button.accessibleText(
|
||||||
actionProvider.getName(wrapper.getEntry().ref()).getValue());
|
actionProvider.getName(wrapper.getEntry().ref()).getValue());
|
||||||
button.apply(new FancyTooltipAugment<>(
|
button.apply(new TooltipAugment<>(
|
||||||
actionProvider.getName(wrapper.getEntry().ref())));
|
actionProvider.getName(wrapper.getEntry().ref())));
|
||||||
if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) {
|
if (actionProvider.activeType() == ActionProvider.DataStoreCallSite.ActiveType.ONLY_SHOW_IF_ENABLED) {
|
||||||
button.hide(Bindings.not(p.getValue()));
|
button.hide(Bindings.not(p.getValue()));
|
||||||
|
@ -247,8 +247,10 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
settingsButton.styleClass("settings");
|
settingsButton.styleClass("settings");
|
||||||
settingsButton.accessibleText("More");
|
settingsButton.accessibleText("More");
|
||||||
settingsButton.apply(new ContextMenuAugment<>(
|
settingsButton.apply(new ContextMenuAugment<>(
|
||||||
event -> event.getButton() == MouseButton.PRIMARY, null, () -> StoreEntryComp.this.createContextMenu()));
|
event -> event.getButton() == MouseButton.PRIMARY,
|
||||||
settingsButton.apply(new FancyTooltipAugment<>("more"));
|
null,
|
||||||
|
() -> StoreEntryComp.this.createContextMenu()));
|
||||||
|
settingsButton.apply(new TooltipAugment<>("more"));
|
||||||
return settingsButton;
|
return settingsButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -371,7 +373,8 @@ public abstract class StoreEntryComp extends SimpleComp {
|
||||||
StoreViewState.get()
|
StoreViewState.get()
|
||||||
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
|
.getSortedCategories(wrapper.getCategory().getValue().getRoot())
|
||||||
.forEach(storeCategoryWrapper -> {
|
.forEach(storeCategoryWrapper -> {
|
||||||
MenuItem m = new MenuItem(storeCategoryWrapper.getName());
|
MenuItem m = new MenuItem();
|
||||||
|
m.textProperty().bind(storeCategoryWrapper.nameProperty());
|
||||||
m.setOnAction(event -> {
|
m.setOnAction(event -> {
|
||||||
wrapper.moveTo(storeCategoryWrapper.getCategory());
|
wrapper.moveTo(storeCategoryWrapper.getCategory());
|
||||||
event.consume();
|
event.consume();
|
||||||
|
|
|
@ -5,7 +5,6 @@ import io.xpipe.app.comp.base.MultiContentComp;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
|
@ -50,16 +49,16 @@ public class StoreEntryListComp extends SimpleComp {
|
||||||
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
|
var map = new LinkedHashMap<Comp<?>, ObservableValue<Boolean>>();
|
||||||
map.put(
|
map.put(
|
||||||
createList(),
|
createList(),
|
||||||
BindingsHelper.persist(Bindings.not(Bindings.isEmpty(
|
Bindings.not(Bindings.isEmpty(
|
||||||
StoreViewState.get().getCurrentTopLevelSection().getShownChildren()))));
|
StoreViewState.get().getCurrentTopLevelSection().getShownChildren())));
|
||||||
|
|
||||||
map.put(new StoreIntroComp(), showIntro);
|
map.put(new StoreIntroComp(), showIntro);
|
||||||
map.put(
|
map.put(
|
||||||
new StoreNotFoundComp(),
|
new StoreNotFoundComp(),
|
||||||
BindingsHelper.persist(Bindings.and(
|
Bindings.and(
|
||||||
Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())),
|
Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())),
|
||||||
Bindings.isEmpty(
|
Bindings.isEmpty(
|
||||||
StoreViewState.get().getCurrentTopLevelSection().getShownChildren()))));
|
StoreViewState.get().getCurrentTopLevelSection().getShownChildren())));
|
||||||
return new MultiContentComp(map).createRegion();
|
return new MultiContentComp(map).createRegion();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,11 @@ import io.xpipe.app.core.AppFont;
|
||||||
import io.xpipe.app.core.AppI18n;
|
import io.xpipe.app.core.AppI18n;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.SimpleComp;
|
import io.xpipe.app.fxcomps.SimpleComp;
|
||||||
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
|
|
||||||
import io.xpipe.app.fxcomps.impl.FilterComp;
|
import io.xpipe.app.fxcomps.impl.FilterComp;
|
||||||
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
||||||
|
import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import io.xpipe.core.process.OsType;
|
import io.xpipe.core.process.OsType;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
@ -36,7 +36,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
|
||||||
|
|
||||||
public StoreEntryListStatusComp() {
|
public StoreEntryListStatusComp() {
|
||||||
this.sortMode = new SimpleObjectProperty<>();
|
this.sortMode = new SimpleObjectProperty<>();
|
||||||
SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> {
|
StoreViewState.get().getActiveCategory().subscribe(val -> {
|
||||||
sortMode.setValue(val.getSortMode().getValue());
|
sortMode.setValue(val.getSortMode().getValue());
|
||||||
});
|
});
|
||||||
sortMode.addListener((observable, oldValue, newValue) -> {
|
sortMode.addListener((observable, oldValue, newValue) -> {
|
||||||
|
@ -51,21 +51,16 @@ public class StoreEntryListStatusComp extends SimpleComp {
|
||||||
|
|
||||||
private Region createGroupListHeader() {
|
private Region createGroupListHeader() {
|
||||||
var label = new Label();
|
var label = new Label();
|
||||||
label.textProperty()
|
var name = BindingsHelper.flatMap(
|
||||||
.bind(Bindings.createStringBinding(
|
StoreViewState.get().getActiveCategory(),
|
||||||
() -> {
|
categoryWrapper -> AppI18n.observable(
|
||||||
return StoreViewState.get()
|
categoryWrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory())
|
||||||
.getActiveCategory()
|
? "connections"
|
||||||
.getValue()
|
: "scripts"));
|
||||||
.getRoot()
|
label.textProperty().bind(name);
|
||||||
.equals(StoreViewState.get().getAllConnectionsCategory())
|
|
||||||
? "Connections"
|
|
||||||
: "Scripts";
|
|
||||||
},
|
|
||||||
StoreViewState.get().getActiveCategory()));
|
|
||||||
label.getStyleClass().add("name");
|
label.getStyleClass().add("name");
|
||||||
|
|
||||||
var all = BindingsHelper.filteredContentBinding(
|
var all = ListBindingsHelper.filteredContentBinding(
|
||||||
StoreViewState.get().getAllEntries(),
|
StoreViewState.get().getAllEntries(),
|
||||||
storeEntryWrapper -> {
|
storeEntryWrapper -> {
|
||||||
var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot();
|
var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot();
|
||||||
|
@ -76,7 +71,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
|
||||||
.equals(storeRoot);
|
.equals(storeRoot);
|
||||||
},
|
},
|
||||||
StoreViewState.get().getActiveCategory());
|
StoreViewState.get().getActiveCategory());
|
||||||
var shownList = BindingsHelper.filteredContentBinding(
|
var shownList = ListBindingsHelper.filteredContentBinding(
|
||||||
all,
|
all,
|
||||||
storeEntryWrapper -> {
|
storeEntryWrapper -> {
|
||||||
return storeEntryWrapper.shouldShow(
|
return storeEntryWrapper.shouldShow(
|
||||||
|
@ -135,7 +130,8 @@ public class StoreEntryListStatusComp extends SimpleComp {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Region createButtons() {
|
private Region createButtons() {
|
||||||
var menu = new MenuButton(AppI18n.get("addConnections"), new FontIcon("mdi2p-plus-thick"));
|
var menu = new MenuButton(null, new FontIcon("mdi2p-plus-thick"));
|
||||||
|
menu.textProperty().bind(AppI18n.observable("addConnections"));
|
||||||
menu.setAlignment(Pos.CENTER);
|
menu.setAlignment(Pos.CENTER);
|
||||||
menu.setTextAlignment(TextAlignment.CENTER);
|
menu.setTextAlignment(TextAlignment.CENTER);
|
||||||
AppFont.medium(menu);
|
AppFont.medium(menu);
|
||||||
|
@ -188,7 +184,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
|
||||||
sortMode));
|
sortMode));
|
||||||
});
|
});
|
||||||
alphabetical.accessibleTextKey("sortAlphabetical");
|
alphabetical.accessibleTextKey("sortAlphabetical");
|
||||||
alphabetical.apply(new FancyTooltipAugment<>("sortAlphabetical"));
|
alphabetical.apply(new TooltipAugment<>("sortAlphabetical"));
|
||||||
return alphabetical;
|
return alphabetical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -227,7 +223,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
|
||||||
sortMode));
|
sortMode));
|
||||||
});
|
});
|
||||||
date.accessibleTextKey("sortLastUsed");
|
date.accessibleTextKey("sortLastUsed");
|
||||||
date.apply(new FancyTooltipAugment<>("sortLastUsed"));
|
date.apply(new TooltipAugment<>("sortLastUsed"));
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,15 +21,18 @@ public class StoreIntroComp extends SimpleComp {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Region createSimple() {
|
public Region createSimple() {
|
||||||
var title = new Label(AppI18n.get("storeIntroTitle"));
|
var title = new Label();
|
||||||
|
title.textProperty().bind(AppI18n.observable("storeIntroTitle"));
|
||||||
title.getStyleClass().add(Styles.TEXT_BOLD);
|
title.getStyleClass().add(Styles.TEXT_BOLD);
|
||||||
AppFont.setSize(title, 7);
|
AppFont.setSize(title, 7);
|
||||||
|
|
||||||
var introDesc = new Label(AppI18n.get("storeIntroDescription"));
|
var introDesc = new Label();
|
||||||
|
introDesc.textProperty().bind(AppI18n.observable("storeIntroDescription"));
|
||||||
introDesc.setWrapText(true);
|
introDesc.setWrapText(true);
|
||||||
introDesc.setMaxWidth(470);
|
introDesc.setMaxWidth(470);
|
||||||
|
|
||||||
var scanButton = new Button(AppI18n.get("detectConnections"), new FontIcon("mdi2m-magnify"));
|
var scanButton = new Button(null, new FontIcon("mdi2m-magnify"));
|
||||||
|
scanButton.textProperty().bind(AppI18n.observable("detectConnections"));
|
||||||
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local()));
|
scanButton.setOnAction(event -> ScanAlert.showAsync(DataStorage.get().local()));
|
||||||
scanButton.setDefaultButton(true);
|
scanButton.setDefaultButton(true);
|
||||||
var scanPane = new StackPane(scanButton);
|
var scanPane = new StackPane(scanButton);
|
||||||
|
@ -43,12 +46,7 @@ public class StoreIntroComp extends SimpleComp {
|
||||||
hbox.setSpacing(35);
|
hbox.setSpacing(35);
|
||||||
hbox.setAlignment(Pos.CENTER);
|
hbox.setAlignment(Pos.CENTER);
|
||||||
|
|
||||||
var v = new VBox(
|
var v = new VBox(hbox, scanPane);
|
||||||
hbox, scanPane
|
|
||||||
// new Separator(Orientation.HORIZONTAL),
|
|
||||||
// documentation,
|
|
||||||
// docLinkPane
|
|
||||||
);
|
|
||||||
v.setMinWidth(Region.USE_PREF_SIZE);
|
v.setMinWidth(Region.USE_PREF_SIZE);
|
||||||
v.setMaxWidth(Region.USE_PREF_SIZE);
|
v.setMaxWidth(Region.USE_PREF_SIZE);
|
||||||
v.setMinHeight(Region.USE_PREF_SIZE);
|
v.setMinHeight(Region.USE_PREF_SIZE);
|
||||||
|
|
|
@ -39,7 +39,7 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
|
||||||
}
|
}
|
||||||
|
|
||||||
var graphic = provider.getDisplayIconFileName(null);
|
var graphic = provider.getDisplayIconFileName(null);
|
||||||
return JfxHelper.createNamedEntry(provider.getDisplayName(), provider.getDisplayDescription(), graphic);
|
return JfxHelper.createNamedEntry(provider.displayName(), provider.displayDescription(), graphic);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -49,8 +49,13 @@ public class StoreProviderChoiceComp extends Comp<CompStructure<ComboBox<DataSto
|
||||||
protected void updateItem(DataStoreProvider item, boolean empty) {
|
protected void updateItem(DataStoreProvider item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
setGraphic(createGraphic(item));
|
setGraphic(createGraphic(item));
|
||||||
setAccessibleText(item != null ? item.getDisplayName() : null);
|
if (item != null) {
|
||||||
setAccessibleHelp(item != null ? item.getDisplayDescription() : null);
|
accessibleTextProperty().bind(item.displayName());
|
||||||
|
accessibleHelpProperty().bind(item.displayDescription());
|
||||||
|
} else {
|
||||||
|
accessibleTextProperty().unbind();
|
||||||
|
accessibleHelpProperty().unbind();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var cb = new ComboBox<DataStoreProvider>();
|
var cb = new ComboBox<DataStoreProvider>();
|
||||||
|
|
|
@ -42,7 +42,9 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
|
||||||
var graphic =
|
var graphic =
|
||||||
w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore());
|
w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore());
|
||||||
if (c.isEmpty()) {
|
if (c.isEmpty()) {
|
||||||
var item = ContextMenuHelper.item(PrettyImageHelper.ofFixedSizeSquare(graphic, 16), w.getName().getValue());
|
var item = ContextMenuHelper.item(
|
||||||
|
PrettyImageHelper.ofFixedSizeSquare(graphic, 16),
|
||||||
|
w.getName().getValue());
|
||||||
item.setOnAction(event -> {
|
item.setOnAction(event -> {
|
||||||
action.accept(w);
|
action.accept(w);
|
||||||
contextMenu.hide();
|
contextMenu.hide();
|
||||||
|
@ -83,7 +85,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
struc.get().setOnAction(event -> {
|
struc.get().setOnAction(event -> {
|
||||||
ContextMenuHelper.toggleShow(cm,struc.get(), Side.RIGHT);
|
ContextMenuHelper.toggleShow(cm, struc.get(), Side.RIGHT);
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,6 +2,7 @@ package io.xpipe.app.comp.store;
|
||||||
|
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
||||||
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
import io.xpipe.app.storage.DataStoreEntry;
|
import io.xpipe.app.storage.DataStoreEntry;
|
||||||
|
@ -64,10 +65,10 @@ public class StoreSection {
|
||||||
|
|
||||||
var c = Comparator.<StoreSection>comparingInt(
|
var c = Comparator.<StoreSection>comparingInt(
|
||||||
value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1);
|
value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1);
|
||||||
var mappedSortMode = BindingsHelper.mappedBinding(
|
var mappedSortMode = BindingsHelper.flatMap(
|
||||||
category,
|
category,
|
||||||
storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null);
|
storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null);
|
||||||
return BindingsHelper.orderedContentBinding(
|
return ListBindingsHelper.orderedContentBinding(
|
||||||
list,
|
list,
|
||||||
(o1, o2) -> {
|
(o1, o2) -> {
|
||||||
var current = mappedSortMode.getValue();
|
var current = mappedSortMode.getValue();
|
||||||
|
@ -86,16 +87,18 @@ public class StoreSection {
|
||||||
Predicate<StoreEntryWrapper> entryFilter,
|
Predicate<StoreEntryWrapper> entryFilter,
|
||||||
ObservableStringValue filterString,
|
ObservableStringValue filterString,
|
||||||
ObservableValue<StoreCategoryWrapper> category) {
|
ObservableValue<StoreCategoryWrapper> category) {
|
||||||
var topLevel = BindingsHelper.filteredContentBinding(
|
var topLevel = ListBindingsHelper.filteredContentBinding(
|
||||||
all,
|
all,
|
||||||
section -> {
|
section -> {
|
||||||
return DataStorage.get().isRootEntry(section.getEntry());
|
return DataStorage.get().isRootEntry(section.getEntry());
|
||||||
},
|
},
|
||||||
category);
|
category);
|
||||||
var cached = BindingsHelper.cachedMappedContentBinding(
|
var cached = ListBindingsHelper.cachedMappedContentBinding(
|
||||||
topLevel, storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category));
|
topLevel,
|
||||||
|
topLevel,
|
||||||
|
storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category));
|
||||||
var ordered = sorted(cached, category);
|
var ordered = sorted(cached, category);
|
||||||
var shown = BindingsHelper.filteredContentBinding(
|
var shown = ListBindingsHelper.filteredContentBinding(
|
||||||
ordered,
|
ordered,
|
||||||
section -> {
|
section -> {
|
||||||
var showFilter = filterString == null || section.shouldShow(filterString.get());
|
var showFilter = filterString == null || section.shouldShow(filterString.get());
|
||||||
|
@ -121,7 +124,7 @@ public class StoreSection {
|
||||||
return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth);
|
return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
var allChildren = BindingsHelper.filteredContentBinding(all, other -> {
|
var allChildren = ListBindingsHelper.filteredContentBinding(all, other -> {
|
||||||
// Legacy implementation that does not use children caches. Use for testing
|
// Legacy implementation that does not use children caches. Use for testing
|
||||||
// if (true) return DataStorage.get()
|
// if (true) return DataStorage.get()
|
||||||
// .getDisplayParent(other.getEntry())
|
// .getDisplayParent(other.getEntry())
|
||||||
|
@ -131,10 +134,12 @@ public class StoreSection {
|
||||||
// This check is fast as the children are cached in the storage
|
// This check is fast as the children are cached in the storage
|
||||||
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
|
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
|
||||||
});
|
});
|
||||||
var cached = BindingsHelper.cachedMappedContentBinding(
|
var cached = ListBindingsHelper.cachedMappedContentBinding(
|
||||||
allChildren, entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category));
|
allChildren,
|
||||||
|
allChildren,
|
||||||
|
entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category));
|
||||||
var ordered = sorted(cached, category);
|
var ordered = sorted(cached, category);
|
||||||
var filtered = BindingsHelper.filteredContentBinding(
|
var filtered = ListBindingsHelper.filteredContentBinding(
|
||||||
ordered,
|
ordered,
|
||||||
section -> {
|
section -> {
|
||||||
var showFilter = filterString == null || section.shouldShow(filterString.get());
|
var showFilter = filterString == null || section.shouldShow(filterString.get());
|
||||||
|
|
|
@ -7,8 +7,7 @@ import io.xpipe.app.fxcomps.augment.GrowAugment;
|
||||||
import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
||||||
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
||||||
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import io.xpipe.app.storage.DataStoreColor;
|
import io.xpipe.app.storage.DataStoreColor;
|
||||||
import io.xpipe.app.util.ThreadHelper;
|
import io.xpipe.app.util.ThreadHelper;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
|
@ -40,11 +39,11 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Comp<CompStructure<Button>> createQuickAccessButton() {
|
private Comp<CompStructure<Button>> createQuickAccessButton() {
|
||||||
var quickAccessDisabled = BindingsHelper.persist(Bindings.createBooleanBinding(
|
var quickAccessDisabled = Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
return section.getShownChildren().isEmpty();
|
return section.getShownChildren().isEmpty();
|
||||||
},
|
},
|
||||||
section.getShownChildren()));
|
section.getShownChildren());
|
||||||
Consumer<StoreEntryWrapper> quickAccessAction = w -> {
|
Consumer<StoreEntryWrapper> quickAccessAction = w -> {
|
||||||
ThreadHelper.runFailableAsync(() -> {
|
ThreadHelper.runFailableAsync(() -> {
|
||||||
w.executeDefaultAction();
|
w.executeDefaultAction();
|
||||||
|
@ -91,8 +90,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
|
||||||
return "Expand " + section.getWrapper().getName().getValue();
|
return "Expand " + section.getWrapper().getName().getValue();
|
||||||
},
|
},
|
||||||
section.getWrapper().getName()))
|
section.getWrapper().getName()))
|
||||||
.disable(BindingsHelper.persist(
|
.disable(Bindings.size(section.getShownChildren()).isEqualTo(0))
|
||||||
Bindings.size(section.getShownChildren()).isEqualTo(0)))
|
|
||||||
.styleClass("expand-button")
|
.styleClass("expand-button")
|
||||||
.maxHeight(100)
|
.maxHeight(100)
|
||||||
.vgrow();
|
.vgrow();
|
||||||
|
@ -131,7 +129,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
|
||||||
|
|
||||||
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
|
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
|
||||||
// section is actually expanded
|
// section is actually expanded
|
||||||
var listSections = BindingsHelper.filteredContentBinding(
|
var listSections = ListBindingsHelper.filteredContentBinding(
|
||||||
section.getShownChildren(),
|
section.getShownChildren(),
|
||||||
storeSection -> section.getAllChildren().size() <= 20
|
storeSection -> section.getAllChildren().size() <= 20
|
||||||
|| section.getWrapper().getExpanded().get(),
|
|| section.getWrapper().getExpanded().get(),
|
||||||
|
@ -143,26 +141,26 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
|
||||||
.minHeight(0)
|
.minHeight(0)
|
||||||
.hgrow();
|
.hgrow();
|
||||||
|
|
||||||
var expanded = BindingsHelper.persist(Bindings.createBooleanBinding(
|
var expanded = Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
return section.getWrapper().getExpanded().get()
|
return section.getWrapper().getExpanded().get()
|
||||||
&& section.getShownChildren().size() > 0;
|
&& section.getShownChildren().size() > 0;
|
||||||
},
|
},
|
||||||
section.getWrapper().getExpanded(),
|
section.getWrapper().getExpanded(),
|
||||||
section.getShownChildren()));
|
section.getShownChildren());
|
||||||
var full = new VerticalComp(List.of(
|
var full = new VerticalComp(List.of(
|
||||||
topEntryList,
|
topEntryList,
|
||||||
Comp.separator().hide(BindingsHelper.persist(expanded.not())),
|
Comp.separator().hide(expanded.not()),
|
||||||
new HorizontalComp(List.of(content))
|
new HorizontalComp(List.of(content))
|
||||||
.styleClass("content")
|
.styleClass("content")
|
||||||
.apply(struc -> struc.get().setFillHeight(true))
|
.apply(struc -> struc.get().setFillHeight(true))
|
||||||
.hide(BindingsHelper.persist(Bindings.or(
|
.hide(Bindings.or(
|
||||||
Bindings.not(section.getWrapper().getExpanded()),
|
Bindings.not(section.getWrapper().getExpanded()),
|
||||||
Bindings.size(section.getShownChildren()).isEqualTo(0))))));
|
Bindings.size(section.getShownChildren()).isEqualTo(0)))));
|
||||||
return full.styleClass("store-entry-section-comp")
|
return full.styleClass("store-entry-section-comp")
|
||||||
.apply(struc -> {
|
.apply(struc -> {
|
||||||
struc.get().setFillWidth(true);
|
struc.get().setFillWidth(true);
|
||||||
SimpleChangeListener.apply(expanded, val -> {
|
expanded.subscribe(val -> {
|
||||||
struc.get().pseudoClassStateChanged(EXPANDED, val);
|
struc.get().pseudoClassStateChanged(EXPANDED, val);
|
||||||
});
|
});
|
||||||
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
|
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
|
||||||
|
@ -170,7 +168,7 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
|
||||||
struc.get().pseudoClassStateChanged(ROOT, topLevel);
|
struc.get().pseudoClassStateChanged(ROOT, topLevel);
|
||||||
struc.get().pseudoClassStateChanged(SUB, !topLevel);
|
struc.get().pseudoClassStateChanged(SUB, !topLevel);
|
||||||
|
|
||||||
SimpleChangeListener.apply(section.getWrapper().getColor(), val -> {
|
section.getWrapper().getColor().subscribe(val -> {
|
||||||
if (!topLevel) {
|
if (!topLevel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp;
|
||||||
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
import io.xpipe.app.fxcomps.impl.IconButtonComp;
|
||||||
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
import io.xpipe.app.fxcomps.impl.PrettyImageHelper;
|
||||||
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
import io.xpipe.app.fxcomps.impl.VerticalComp;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import io.xpipe.app.storage.DataStoreColor;
|
import io.xpipe.app.storage.DataStoreColor;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.property.BooleanProperty;
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
@ -101,16 +100,15 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
||||||
+ section.getWrapper().getName().getValue();
|
+ section.getWrapper().getName().getValue();
|
||||||
},
|
},
|
||||||
section.getWrapper().getName()))
|
section.getWrapper().getName()))
|
||||||
.disable(BindingsHelper.persist(
|
.disable(Bindings.size(section.getAllChildren()).isEqualTo(0))
|
||||||
Bindings.size(section.getAllChildren()).isEqualTo(0)))
|
|
||||||
.grow(false, true)
|
.grow(false, true)
|
||||||
.styleClass("expand-button");
|
.styleClass("expand-button");
|
||||||
|
|
||||||
var quickAccessDisabled = BindingsHelper.persist(Bindings.createBooleanBinding(
|
var quickAccessDisabled = Bindings.createBooleanBinding(
|
||||||
() -> {
|
() -> {
|
||||||
return section.getShownChildren().isEmpty();
|
return section.getShownChildren().isEmpty();
|
||||||
},
|
},
|
||||||
section.getShownChildren()));
|
section.getShownChildren());
|
||||||
Consumer<StoreEntryWrapper> quickAccessAction = w -> {
|
Consumer<StoreEntryWrapper> quickAccessAction = w -> {
|
||||||
action.accept(w);
|
action.accept(w);
|
||||||
};
|
};
|
||||||
|
@ -134,7 +132,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
||||||
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
|
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
|
||||||
// section is actually expanded
|
// section is actually expanded
|
||||||
var listSections = section.getWrapper() != null
|
var listSections = section.getWrapper() != null
|
||||||
? BindingsHelper.filteredContentBinding(
|
? ListBindingsHelper.filteredContentBinding(
|
||||||
section.getShownChildren(),
|
section.getShownChildren(),
|
||||||
storeSection -> section.getAllChildren().size() <= 20 || expanded.get(),
|
storeSection -> section.getAllChildren().size() <= 20 || expanded.get(),
|
||||||
expanded,
|
expanded,
|
||||||
|
@ -149,9 +147,9 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
||||||
list.add(new HorizontalComp(List.of(content))
|
list.add(new HorizontalComp(List.of(content))
|
||||||
.styleClass("content")
|
.styleClass("content")
|
||||||
.apply(struc -> struc.get().setFillHeight(true))
|
.apply(struc -> struc.get().setFillHeight(true))
|
||||||
.hide(BindingsHelper.persist(Bindings.or(
|
.hide(Bindings.or(
|
||||||
Bindings.not(expanded),
|
Bindings.not(expanded),
|
||||||
Bindings.size(section.getAllChildren()).isEqualTo(0)))));
|
Bindings.size(section.getAllChildren()).isEqualTo(0))));
|
||||||
|
|
||||||
var vert = new VerticalComp(list);
|
var vert = new VerticalComp(list);
|
||||||
if (condensedStyle) {
|
if (condensedStyle) {
|
||||||
|
@ -160,7 +158,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
||||||
return vert.styleClass("store-section-mini-comp")
|
return vert.styleClass("store-section-mini-comp")
|
||||||
.apply(struc -> {
|
.apply(struc -> {
|
||||||
struc.get().setFillWidth(true);
|
struc.get().setFillWidth(true);
|
||||||
SimpleChangeListener.apply(expanded, val -> {
|
expanded.subscribe(val -> {
|
||||||
struc.get().pseudoClassStateChanged(EXPANDED, val);
|
struc.get().pseudoClassStateChanged(EXPANDED, val);
|
||||||
});
|
});
|
||||||
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
|
struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0);
|
||||||
|
@ -171,7 +169,7 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
|
||||||
})
|
})
|
||||||
.apply(struc -> {
|
.apply(struc -> {
|
||||||
if (section.getWrapper() != null) {
|
if (section.getWrapper() != null) {
|
||||||
SimpleChangeListener.apply(section.getWrapper().getColor(), val -> {
|
section.getWrapper().getColor().subscribe(val -> {
|
||||||
if (section.getDepth() != 1) {
|
if (section.getDepth() != 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package io.xpipe.app.comp.store;
|
package io.xpipe.app.comp.store;
|
||||||
|
|
||||||
import io.xpipe.app.core.AppCache;
|
import io.xpipe.app.core.AppCache;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
import io.xpipe.app.fxcomps.util.ListBindingsHelper;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.app.storage.DataStorage;
|
import io.xpipe.app.storage.DataStorage;
|
||||||
|
@ -270,10 +270,12 @@ public class StoreViewState {
|
||||||
return parent;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
return o1.getName().compareToIgnoreCase(o2.getName());
|
return o1.nameProperty()
|
||||||
|
.getValue()
|
||||||
|
.compareToIgnoreCase(o2.nameProperty().getValue());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return BindingsHelper.filteredContentBinding(
|
return ListBindingsHelper.filteredContentBinding(
|
||||||
categories, cat -> root == null || cat.getRoot().equals(root))
|
categories, cat -> root == null || cat.getRoot().equals(root))
|
||||||
.sorted(comparator);
|
.sorted(comparator);
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,13 +65,13 @@ public class App extends Application {
|
||||||
"XPipe %s (%s)", t.getValue(), AppProperties.get().getVersion());
|
"XPipe %s (%s)", t.getValue(), AppProperties.get().getVersion());
|
||||||
var prefix = AppProperties.get().isStaging() ? "[Public Test Build, Not a proper release] " : "";
|
var prefix = AppProperties.get().isStaging() ? "[Public Test Build, Not a proper release] " : "";
|
||||||
var suffix = u.getValue() != null
|
var suffix = u.getValue() != null
|
||||||
? String.format(
|
? AppI18n.get("updateReadyTitle", u.getValue().getVersion())
|
||||||
" (Update to %s ready)", u.getValue().getVersion())
|
|
||||||
: "";
|
: "";
|
||||||
return prefix + base + suffix;
|
return prefix + base + suffix;
|
||||||
},
|
},
|
||||||
u,
|
u,
|
||||||
t);
|
t,
|
||||||
|
AppPrefs.get().language());
|
||||||
|
|
||||||
var appWindow = AppMainWindow.init(stage);
|
var appWindow = AppMainWindow.init(stage);
|
||||||
appWindow.getStage().titleProperty().bind(PlatformThread.sync(titleBinding));
|
appWindow.getStage().titleProperty().bind(PlatformThread.sync(titleBinding));
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package io.xpipe.app.core;
|
package io.xpipe.app.core;
|
||||||
|
|
||||||
import io.xpipe.app.exchange.MessageExchangeImpls;
|
|
||||||
import io.xpipe.app.ext.ExtensionException;
|
import io.xpipe.app.ext.ExtensionException;
|
||||||
import io.xpipe.app.ext.XPipeServiceProviders;
|
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.issue.TrackEvent;
|
import io.xpipe.app.issue.TrackEvent;
|
||||||
|
import io.xpipe.core.process.ProcessControlProvider;
|
||||||
import io.xpipe.core.util.ModuleHelper;
|
import io.xpipe.core.util.ModuleHelper;
|
||||||
|
import io.xpipe.core.util.ModuleLayerLoader;
|
||||||
import io.xpipe.core.util.XPipeInstallation;
|
import io.xpipe.core.util.XPipeInstallation;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
@ -47,10 +47,11 @@ public class AppExtensionManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (load) {
|
if (load) {
|
||||||
// INSTANCE.addNativeLibrariesToPath();
|
|
||||||
try {
|
try {
|
||||||
XPipeServiceProviders.load(INSTANCE.extendedLayer);
|
ProcessControlProvider.init(INSTANCE.extendedLayer);
|
||||||
MessageExchangeImpls.loadAll();
|
ModuleLayerLoader.loadAll(INSTANCE.extendedLayer, t -> {
|
||||||
|
ErrorEvent.fromThrowable(t).handle();
|
||||||
|
});
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
throw new ExtensionException(
|
throw new ExtensionException(
|
||||||
"Service provider initialization failed. Is the installation data corrupt?", t);
|
"Service provider initialization failed. Is the installation data corrupt?", t);
|
||||||
|
|
|
@ -3,7 +3,6 @@ package io.xpipe.app.core;
|
||||||
import io.xpipe.app.comp.base.MarkdownComp;
|
import io.xpipe.app.comp.base.MarkdownComp;
|
||||||
import io.xpipe.app.core.mode.OperationMode;
|
import io.xpipe.app.core.mode.OperationMode;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.util.BindingsHelper;
|
|
||||||
import javafx.beans.property.SimpleBooleanProperty;
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.geometry.Pos;
|
import javafx.geometry.Pos;
|
||||||
|
@ -98,7 +97,7 @@ public class AppGreetings {
|
||||||
alert.getButtonTypes().add(buttonType);
|
alert.getButtonTypes().add(buttonType);
|
||||||
|
|
||||||
Button button = (Button) alert.getDialogPane().lookupButton(buttonType);
|
Button button = (Button) alert.getDialogPane().lookupButton(buttonType);
|
||||||
button.disableProperty().bind(BindingsHelper.persist(accepted.not()));
|
button.disableProperty().bind(accepted.not());
|
||||||
}
|
}
|
||||||
|
|
||||||
alert.getButtonTypes().add(ButtonType.CANCEL);
|
alert.getButtonTypes().add(ButtonType.CANCEL);
|
||||||
|
|
|
@ -2,7 +2,7 @@ package io.xpipe.app.core;
|
||||||
|
|
||||||
import io.xpipe.app.comp.base.ModalOverlayComp;
|
import io.xpipe.app.comp.base.ModalOverlayComp;
|
||||||
import io.xpipe.app.ext.PrefsChoiceValue;
|
import io.xpipe.app.ext.PrefsChoiceValue;
|
||||||
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment;
|
import io.xpipe.app.fxcomps.impl.TooltipAugment;
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.issue.TrackEvent;
|
import io.xpipe.app.issue.TrackEvent;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
|
@ -10,112 +10,57 @@ import io.xpipe.app.prefs.SupportedLocale;
|
||||||
import io.xpipe.app.util.OptionsBuilder;
|
import io.xpipe.app.util.OptionsBuilder;
|
||||||
import io.xpipe.app.util.Translatable;
|
import io.xpipe.app.util.Translatable;
|
||||||
import io.xpipe.core.util.ModuleHelper;
|
import io.xpipe.core.util.ModuleHelper;
|
||||||
|
import io.xpipe.core.util.XPipeInstallation;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.StringBinding;
|
import javafx.beans.property.Property;
|
||||||
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import lombok.Value;
|
||||||
import org.apache.commons.io.FilenameUtils;
|
import org.apache.commons.io.FilenameUtils;
|
||||||
import org.ocpsoft.prettytime.PrettyTime;
|
import org.ocpsoft.prettytime.PrettyTime;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileVisitResult;
|
import java.nio.file.FileVisitResult;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.SimpleFileVisitor;
|
import java.nio.file.SimpleFileVisitor;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
import java.time.Duration;
|
import java.util.*;
|
||||||
import java.time.Instant;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
import java.util.Properties;
|
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.function.UnaryOperator;
|
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class AppI18n {
|
public class AppI18n {
|
||||||
|
|
||||||
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$");
|
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$");
|
||||||
private static final AppI18n INSTANCE = new AppI18n();
|
private static AppI18n INSTANCE;
|
||||||
private Map<String, String> translations;
|
private final Property<LoadedTranslations> currentLanguage = new SimpleObjectProperty<>();
|
||||||
private Map<String, String> markdownDocumentations;
|
private LoadedTranslations english;
|
||||||
private PrettyTime prettyTime;
|
|
||||||
|
|
||||||
public static void init() {
|
public static void init() throws Exception {
|
||||||
var i = INSTANCE;
|
if (INSTANCE == null) {
|
||||||
if (i.translations != null) {
|
INSTANCE = new AppI18n();
|
||||||
return;
|
}
|
||||||
|
INSTANCE.load();
|
||||||
}
|
}
|
||||||
|
|
||||||
i.load();
|
public static AppI18n get() {
|
||||||
|
|
||||||
if (AppPrefs.get() != null) {
|
|
||||||
AppPrefs.get().language().addListener((c, o, n) -> {
|
|
||||||
i.clear();
|
|
||||||
i.load();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static AppI18n getInstance() {
|
|
||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static StringBinding readableInstant(String s, ObservableValue<Instant> instant) {
|
|
||||||
return readableInstant(instant, rs -> getValue(getInstance().getLocalised(s), rs));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StringBinding readableInstant(ObservableValue<Instant> instant, UnaryOperator<String> op) {
|
|
||||||
return Bindings.createStringBinding(
|
|
||||||
() -> {
|
|
||||||
if (instant.getValue() == null) {
|
|
||||||
return "null";
|
|
||||||
}
|
|
||||||
|
|
||||||
return op.apply(
|
|
||||||
getInstance().prettyTime.format(instant.getValue().minus(Duration.ofSeconds(1))));
|
|
||||||
},
|
|
||||||
instant);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StringBinding readableInstant(ObservableValue<Instant> instant) {
|
|
||||||
return Bindings.createStringBinding(
|
|
||||||
() -> {
|
|
||||||
if (instant.getValue() == null) {
|
|
||||||
return "null";
|
|
||||||
}
|
|
||||||
|
|
||||||
return getInstance().prettyTime.format(instant.getValue().minus(Duration.ofSeconds(1)));
|
|
||||||
},
|
|
||||||
instant);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static StringBinding readableDuration(ObservableValue<Duration> duration) {
|
|
||||||
return Bindings.createStringBinding(
|
|
||||||
() -> {
|
|
||||||
if (duration.getValue() == null) {
|
|
||||||
return "null";
|
|
||||||
}
|
|
||||||
|
|
||||||
return getInstance()
|
|
||||||
.prettyTime
|
|
||||||
.formatDuration(getInstance()
|
|
||||||
.prettyTime
|
|
||||||
.approximateDuration(Instant.now().plus(duration.getValue())));
|
|
||||||
},
|
|
||||||
duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ObservableValue<String> observable(String s, Object... vars) {
|
public static ObservableValue<String> observable(String s, Object... vars) {
|
||||||
if (s == null) {
|
if (s == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var key = INSTANCE.getKey(s);
|
var key = INSTANCE.getKey(s);
|
||||||
return Bindings.createStringBinding(() -> {
|
return Bindings.createStringBinding(
|
||||||
|
() -> {
|
||||||
return get(key, vars);
|
return get(key, vars);
|
||||||
});
|
},
|
||||||
|
INSTANCE.currentLanguage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String get(String s, Object... vars) {
|
public static String get(String s, Object... vars) {
|
||||||
|
@ -147,7 +92,7 @@ public class AppI18n {
|
||||||
|| caller.equals(ModuleHelper.class)
|
|| caller.equals(ModuleHelper.class)
|
||||||
|| caller.equals(ModalOverlayComp.class)
|
|| caller.equals(ModalOverlayComp.class)
|
||||||
|| caller.equals(AppI18n.class)
|
|| caller.equals(AppI18n.class)
|
||||||
|| caller.equals(FancyTooltipAugment.class)
|
|| caller.equals(TooltipAugment.class)
|
||||||
|| caller.equals(PrefsChoiceValue.class)
|
|| caller.equals(PrefsChoiceValue.class)
|
||||||
|| caller.equals(Translatable.class)
|
|| caller.equals(Translatable.class)
|
||||||
|| caller.equals(AppWindowHelper.class)
|
|| caller.equals(AppWindowHelper.class)
|
||||||
|
@ -160,9 +105,28 @@ public class AppI18n {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clear() {
|
private void load() throws Exception {
|
||||||
translations.clear();
|
if (english == null) {
|
||||||
prettyTime = null;
|
english = load(Locale.ENGLISH);
|
||||||
|
Locale.setDefault(Locale.ENGLISH);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLanguage.getValue() == null) {
|
||||||
|
if (AppPrefs.get() != null) {
|
||||||
|
AppPrefs.get().language().subscribe(n -> {
|
||||||
|
try {
|
||||||
|
currentLanguage.setValue(n != null ? load(n.getLocale()) : null);
|
||||||
|
Locale.setDefault(n != null ? n.getLocale() : Locale.ENGLISH);
|
||||||
|
} catch (Exception e) {
|
||||||
|
ErrorEvent.fromThrowable(e).handle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoadedTranslations getLoaded() {
|
||||||
|
return currentLanguage.getValue() != null ? currentLanguage.getValue() : english;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getKey(String s) {
|
public String getKey(String s) {
|
||||||
|
@ -173,62 +137,66 @@ public class AppI18n {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean containsKey(String s) {
|
|
||||||
var key = getKey(s);
|
|
||||||
if (translations == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return translations.containsKey(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLocalised(String s, Object... vars) {
|
public String getLocalised(String s, Object... vars) {
|
||||||
var key = getKey(s);
|
var key = getKey(s);
|
||||||
|
|
||||||
if (translations == null) {
|
if (english == null) {
|
||||||
TrackEvent.warn("Translations not initialized for " + key);
|
TrackEvent.warn("Translations not initialized for " + key);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!translations.containsKey(key)) {
|
if (currentLanguage.getValue() != null
|
||||||
|
&& currentLanguage.getValue().getTranslations().containsKey(key)) {
|
||||||
|
var localisedString = currentLanguage.getValue().getTranslations().get(key);
|
||||||
|
return getValue(localisedString, vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (english.getTranslations().containsKey(key)) {
|
||||||
|
var localisedString = english.getTranslations().get(key);
|
||||||
|
return getValue(localisedString, vars);
|
||||||
|
}
|
||||||
|
|
||||||
TrackEvent.warn("Translation key not found for " + key);
|
TrackEvent.warn("Translation key not found for " + key);
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
var localisedString = translations.get(key);
|
private boolean matchesLocale(Path f, Locale l) {
|
||||||
return getValue(localisedString, vars);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isLoaded() {
|
|
||||||
return translations != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean matchesLocale(Path f) {
|
|
||||||
var l = AppPrefs.get() != null
|
|
||||||
? AppPrefs.get().language().getValue().getLocale()
|
|
||||||
: SupportedLocale.ENGLISH.getLocale();
|
|
||||||
var name = FilenameUtils.getBaseName(f.getFileName().toString());
|
var name = FilenameUtils.getBaseName(f.getFileName().toString());
|
||||||
var ending = "_" + l.toLanguageTag();
|
var ending = "_" + l.toLanguageTag();
|
||||||
return name.endsWith(ending);
|
return name.endsWith(ending);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getMarkdownDocumentation(String name) {
|
public String getMarkdownDocumentation(String name) {
|
||||||
if (!markdownDocumentations.containsKey(name)) {
|
if (currentLanguage.getValue() != null
|
||||||
|
&& currentLanguage.getValue().getMarkdownDocumentations().containsKey(name)) {
|
||||||
|
var localisedString =
|
||||||
|
currentLanguage.getValue().getMarkdownDocumentations().get(name);
|
||||||
|
return localisedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (english.getMarkdownDocumentations().containsKey(name)) {
|
||||||
|
var localisedString = english.getMarkdownDocumentations().get(name);
|
||||||
|
return localisedString;
|
||||||
|
}
|
||||||
|
|
||||||
TrackEvent.withWarn("Markdown documentation for key " + name + " not found")
|
TrackEvent.withWarn("Markdown documentation for key " + name + " not found")
|
||||||
.handle();
|
.handle();
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return markdownDocumentations.getOrDefault(name, "");
|
private Path getModuleLangPath(String module) {
|
||||||
|
return XPipeInstallation.getLangPath().resolve(module);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void load() {
|
private LoadedTranslations load(Locale l) throws Exception {
|
||||||
TrackEvent.info("Loading translations ...");
|
TrackEvent.info("Loading translations ...");
|
||||||
|
|
||||||
translations = new HashMap<>();
|
var translations = new HashMap<String, String>();
|
||||||
for (var module : AppExtensionManager.getInstance().getContentModules()) {
|
for (var module : AppExtensionManager.getInstance().getContentModules()) {
|
||||||
AppResources.with(module.getName(), "lang", basePath -> {
|
var basePath = getModuleLangPath(FilenameUtils.getExtension(module.getName()))
|
||||||
|
.resolve("strings");
|
||||||
if (!Files.exists(basePath)) {
|
if (!Files.exists(basePath)) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
AtomicInteger fileCounter = new AtomicInteger();
|
AtomicInteger fileCounter = new AtomicInteger();
|
||||||
|
@ -238,7 +206,7 @@ public class AppI18n {
|
||||||
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
|
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||||
if (!matchesLocale(file)) {
|
if (!matchesLocale(file, l)) {
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,7 +217,7 @@ public class AppI18n {
|
||||||
fileCounter.incrementAndGet();
|
fileCounter.incrementAndGet();
|
||||||
try (var in = Files.newInputStream(file)) {
|
try (var in = Files.newInputStream(file)) {
|
||||||
var props = new Properties();
|
var props = new Properties();
|
||||||
props.load(in);
|
props.load(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||||
props.forEach((key, value) -> {
|
props.forEach((key, value) -> {
|
||||||
var hasPrefix = key.toString().contains(".");
|
var hasPrefix = key.toString().contains(".");
|
||||||
var usedPrefix = hasPrefix ? "" : defaultPrefix;
|
var usedPrefix = hasPrefix ? "" : defaultPrefix;
|
||||||
|
@ -267,21 +235,21 @@ public class AppI18n {
|
||||||
.tag("fileCount", fileCounter.get())
|
.tag("fileCount", fileCounter.get())
|
||||||
.tag("lineCount", lineCounter.get())
|
.tag("lineCount", lineCounter.get())
|
||||||
.handle();
|
.handle();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
markdownDocumentations = new HashMap<>();
|
var markdownDocumentations = new HashMap<String, String>();
|
||||||
for (var module : AppExtensionManager.getInstance().getContentModules()) {
|
for (var module : AppExtensionManager.getInstance().getContentModules()) {
|
||||||
AppResources.with(module.getName(), "lang", basePath -> {
|
var basePath = getModuleLangPath(FilenameUtils.getExtension(module.getName()))
|
||||||
|
.resolve("texts");
|
||||||
if (!Files.exists(basePath)) {
|
if (!Files.exists(basePath)) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var moduleName = FilenameUtils.getExtension(module.getName());
|
var moduleName = FilenameUtils.getExtension(module.getName());
|
||||||
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
|
Files.walkFileTree(basePath, new SimpleFileVisitor<>() {
|
||||||
@Override
|
@Override
|
||||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
|
||||||
if (!matchesLocale(file)) {
|
if (!matchesLocale(file, l)) {
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,13 +270,23 @@ public class AppI18n {
|
||||||
return FileVisitResult.CONTINUE;
|
return FileVisitResult.CONTINUE;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.prettyTime = new PrettyTime(
|
var prettyTime = new PrettyTime(
|
||||||
AppPrefs.get() != null
|
AppPrefs.get() != null
|
||||||
? AppPrefs.get().language().getValue().getLocale()
|
? AppPrefs.get().language().getValue().getLocale()
|
||||||
: SupportedLocale.ENGLISH.getLocale());
|
: SupportedLocale.getEnglish().getLocale());
|
||||||
|
|
||||||
|
return new LoadedTranslations(l, translations, markdownDocumentations, prettyTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value
|
||||||
|
static class LoadedTranslations {
|
||||||
|
|
||||||
|
Locale locale;
|
||||||
|
Map<String, String> translations;
|
||||||
|
Map<String, String> markdownDocumentations;
|
||||||
|
PrettyTime prettyTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("removal")
|
@SuppressWarnings("removal")
|
||||||
|
|
|
@ -30,11 +30,11 @@ public class AppImages {
|
||||||
|
|
||||||
TrackEvent.info("Loading images ...");
|
TrackEvent.info("Loading images ...");
|
||||||
for (var module : AppExtensionManager.getInstance().getContentModules()) {
|
for (var module : AppExtensionManager.getInstance().getContentModules()) {
|
||||||
loadDirectory(module.getName(), "img");
|
loadDirectory(module.getName(), "img", true, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void loadDirectory(String module, String dir) {
|
public static void loadDirectory(String module, String dir, boolean loadImages, boolean loadSvgs) {
|
||||||
AppResources.with(module, dir, basePath -> {
|
AppResources.with(module, dir, basePath -> {
|
||||||
if (!Files.exists(basePath)) {
|
if (!Files.exists(basePath)) {
|
||||||
return;
|
return;
|
||||||
|
@ -48,10 +48,10 @@ public class AppImages {
|
||||||
var relativeFileName = FilenameUtils.separatorsToUnix(
|
var relativeFileName = FilenameUtils.separatorsToUnix(
|
||||||
basePath.relativize(file).toString());
|
basePath.relativize(file).toString());
|
||||||
try {
|
try {
|
||||||
if (FilenameUtils.getExtension(file.toString()).equals("svg")) {
|
if (FilenameUtils.getExtension(file.toString()).equals("svg") && loadSvgs) {
|
||||||
var s = Files.readString(file);
|
var s = Files.readString(file);
|
||||||
svgImages.put(defaultPrefix + relativeFileName, s);
|
svgImages.put(defaultPrefix + relativeFileName, s);
|
||||||
} else {
|
} else if (loadImages) {
|
||||||
images.put(defaultPrefix + relativeFileName, loadImage(file));
|
images.put(defaultPrefix + relativeFileName, loadImage(file));
|
||||||
}
|
}
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
package io.xpipe.app.core;
|
package io.xpipe.app.core;
|
||||||
|
|
||||||
import io.xpipe.app.browser.BrowserComp;
|
import io.xpipe.app.browser.session.BrowserSessionComp;
|
||||||
import io.xpipe.app.browser.BrowserModel;
|
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||||
import io.xpipe.app.comp.DeveloperTabComp;
|
|
||||||
import io.xpipe.app.comp.store.StoreLayoutComp;
|
import io.xpipe.app.comp.store.StoreLayoutComp;
|
||||||
import io.xpipe.app.fxcomps.Comp;
|
import io.xpipe.app.fxcomps.Comp;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
|
||||||
import io.xpipe.app.prefs.AppPrefsComp;
|
import io.xpipe.app.prefs.AppPrefsComp;
|
||||||
import io.xpipe.app.util.LicenseProvider;
|
import io.xpipe.app.util.LicenseProvider;
|
||||||
import javafx.beans.property.Property;
|
import javafx.beans.property.Property;
|
||||||
|
@ -30,13 +28,11 @@ public class AppLayoutModel {
|
||||||
private final List<Entry> entries;
|
private final List<Entry> entries;
|
||||||
|
|
||||||
private final Property<Entry> selected;
|
private final Property<Entry> selected;
|
||||||
private final ObservableValue<Entry> selectedWrapper;
|
|
||||||
|
|
||||||
public AppLayoutModel(SavedState savedState) {
|
public AppLayoutModel(SavedState savedState) {
|
||||||
this.savedState = savedState;
|
this.savedState = savedState;
|
||||||
this.entries = createEntryList();
|
this.entries = createEntryList();
|
||||||
this.selected = new SimpleObjectProperty<>(entries.get(1));
|
this.selected = new SimpleObjectProperty<>(entries.get(1));
|
||||||
this.selectedWrapper = PlatformThread.sync(selected);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AppLayoutModel get() {
|
public static AppLayoutModel get() {
|
||||||
|
@ -53,14 +49,10 @@ public class AppLayoutModel {
|
||||||
INSTANCE = null;
|
INSTANCE = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Property<Entry> getSelectedInternal() {
|
public Property<Entry> getSelected() {
|
||||||
return selected;
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableValue<Entry> getSelected() {
|
|
||||||
return selectedWrapper;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void selectBrowser() {
|
public void selectBrowser() {
|
||||||
selected.setValue(entries.getFirst());
|
selected.setValue(entries.getFirst());
|
||||||
}
|
}
|
||||||
|
@ -79,21 +71,16 @@ public class AppLayoutModel {
|
||||||
|
|
||||||
private List<Entry> createEntryList() {
|
private List<Entry> createEntryList() {
|
||||||
var l = new ArrayList<>(List.of(
|
var l = new ArrayList<>(List.of(
|
||||||
new Entry(AppI18n.observable("browser"), "mdi2f-file-cabinet", new BrowserComp(BrowserModel.DEFAULT)),
|
new Entry(
|
||||||
|
AppI18n.observable("browser"),
|
||||||
|
"mdi2f-file-cabinet",
|
||||||
|
new BrowserSessionComp(BrowserSessionModel.DEFAULT)),
|
||||||
new Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()),
|
new Entry(AppI18n.observable("connections"), "mdi2c-connection", new StoreLayoutComp()),
|
||||||
new Entry(AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp())));
|
new Entry(AppI18n.observable("settings"), "mdsmz-miscellaneous_services", new AppPrefsComp()),
|
||||||
// new SideMenuBarComp.Entry(AppI18n.observable("help"), "mdi2b-book-open-variant", new
|
new Entry(
|
||||||
// StorageLayoutComp()),
|
|
||||||
// new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp())
|
|
||||||
if (AppProperties.get().isDeveloperMode() && !AppProperties.get().isImage()) {
|
|
||||||
l.add(new Entry(AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
|
|
||||||
}
|
|
||||||
|
|
||||||
l.add(new Entry(
|
|
||||||
AppI18n.observable("explorePlans"),
|
AppI18n.observable("explorePlans"),
|
||||||
"mdi2p-professional-hexagon",
|
"mdi2p-professional-hexagon",
|
||||||
LicenseProvider.get().overviewPage()));
|
LicenseProvider.get().overviewPage())));
|
||||||
|
|
||||||
return l;
|
return l;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ public class AppProperties {
|
||||||
UUID buildUuid;
|
UUID buildUuid;
|
||||||
String sentryUrl;
|
String sentryUrl;
|
||||||
String arch;
|
String arch;
|
||||||
|
List<String> languages;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
boolean image;
|
boolean image;
|
||||||
|
@ -53,6 +54,7 @@ public class AppProperties {
|
||||||
.orElse(UUID.randomUUID());
|
.orElse(UUID.randomUUID());
|
||||||
sentryUrl = System.getProperty("io.xpipe.app.sentryUrl");
|
sentryUrl = System.getProperty("io.xpipe.app.sentryUrl");
|
||||||
arch = System.getProperty("io.xpipe.app.arch");
|
arch = System.getProperty("io.xpipe.app.arch");
|
||||||
|
languages = Arrays.asList(System.getProperty("io.xpipe.app.languages").split(";"));
|
||||||
staging = XPipeInstallation.isStaging();
|
staging = XPipeInstallation.isStaging();
|
||||||
useVirtualThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.useVirtualThreads"))
|
useVirtualThreads = Optional.ofNullable(System.getProperty("io.xpipe.app.useVirtualThreads"))
|
||||||
.map(Boolean::parseBoolean)
|
.map(Boolean::parseBoolean)
|
||||||
|
|
|
@ -3,25 +3,24 @@ package io.xpipe.app.core;
|
||||||
import atlantafx.base.theme.*;
|
import atlantafx.base.theme.*;
|
||||||
import io.xpipe.app.ext.PrefsChoiceValue;
|
import io.xpipe.app.ext.PrefsChoiceValue;
|
||||||
import io.xpipe.app.fxcomps.util.PlatformThread;
|
import io.xpipe.app.fxcomps.util.PlatformThread;
|
||||||
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
|
|
||||||
import io.xpipe.app.issue.ErrorEvent;
|
import io.xpipe.app.issue.ErrorEvent;
|
||||||
import io.xpipe.app.issue.TrackEvent;
|
import io.xpipe.app.issue.TrackEvent;
|
||||||
import io.xpipe.app.prefs.AppPrefs;
|
import io.xpipe.app.prefs.AppPrefs;
|
||||||
import io.xpipe.core.process.OsType;
|
import io.xpipe.core.process.OsType;
|
||||||
import javafx.animation.Interpolator;
|
import javafx.animation.*;
|
||||||
import javafx.animation.KeyFrame;
|
|
||||||
import javafx.animation.KeyValue;
|
|
||||||
import javafx.animation.Timeline;
|
|
||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.application.ColorScheme;
|
import javafx.application.ColorScheme;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.css.PseudoClass;
|
import javafx.css.PseudoClass;
|
||||||
|
import javafx.event.EventHandler;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
import javafx.scene.image.ImageView;
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.layout.Pane;
|
import javafx.scene.layout.Pane;
|
||||||
|
import javafx.stage.Stage;
|
||||||
import javafx.stage.Window;
|
import javafx.stage.Window;
|
||||||
|
import javafx.stage.WindowEvent;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -39,12 +38,15 @@ public class AppTheme {
|
||||||
private static final PseudoClass PERFORMANCE = PseudoClass.getPseudoClass("performance");
|
private static final PseudoClass PERFORMANCE = PseudoClass.getPseudoClass("performance");
|
||||||
private static boolean init;
|
private static boolean init;
|
||||||
|
|
||||||
public static void initThemeHandlers(Window stage) {
|
public static void initThemeHandlers(Stage stage) {
|
||||||
if (AppPrefs.get() == null) {
|
if (AppPrefs.get() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SimpleChangeListener.apply(AppPrefs.get().theme, t -> {
|
initWindowsThemeHandler(stage);
|
||||||
|
|
||||||
|
Runnable r = () -> {
|
||||||
|
AppPrefs.get().theme.subscribe(t -> {
|
||||||
Theme.ALL.forEach(
|
Theme.ALL.forEach(
|
||||||
theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId()));
|
theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId()));
|
||||||
if (t == null) {
|
if (t == null) {
|
||||||
|
@ -56,10 +58,56 @@ public class AppTheme {
|
||||||
stage.getScene().getRoot().pseudoClassStateChanged(DARK, t.isDark());
|
stage.getScene().getRoot().pseudoClassStateChanged(DARK, t.isDark());
|
||||||
});
|
});
|
||||||
|
|
||||||
SimpleChangeListener.apply(AppPrefs.get().performanceMode(), val -> {
|
AppPrefs.get().performanceMode().subscribe(val -> {
|
||||||
stage.getScene().getRoot().pseudoClassStateChanged(PRETTY, !val);
|
stage.getScene().getRoot().pseudoClassStateChanged(PRETTY, !val);
|
||||||
stage.getScene().getRoot().pseudoClassStateChanged(PERFORMANCE, val);
|
stage.getScene().getRoot().pseudoClassStateChanged(PERFORMANCE, val);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
if (stage.getOwner() != null) {
|
||||||
|
// If we set the theme pseudo classes earlier when the window is not shown
|
||||||
|
// they do not apply. Is this a bug in JavaFX?
|
||||||
|
Platform.runLater(r);
|
||||||
|
} else {
|
||||||
|
r.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void initWindowsThemeHandler(Window stage) {
|
||||||
|
if (OsType.getLocal() != OsType.WINDOWS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventHandler<WindowEvent> windowTheme = new EventHandler<>() {
|
||||||
|
@Override
|
||||||
|
public void handle(WindowEvent event) {
|
||||||
|
if (!stage.isShowing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// var c = new WindowControl(stage);
|
||||||
|
// c.setWindowAttribute(20, AppPrefs.get().theme.getValue().isDark());
|
||||||
|
} catch (Throwable e) {
|
||||||
|
ErrorEvent.fromThrowable(e).handle();
|
||||||
|
}
|
||||||
|
stage.removeEventFilter(WindowEvent.WINDOW_SHOWN, this);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (stage.isShowing()) {
|
||||||
|
windowTheme.handle(null);
|
||||||
|
} else {
|
||||||
|
stage.addEventFilter(WindowEvent.WINDOW_SHOWN, windowTheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
var transition = new PauseTransition(Duration.millis(300));
|
||||||
|
transition.setOnFinished(e -> {
|
||||||
|
windowTheme.handle(null);
|
||||||
|
});
|
||||||
|
transition.play();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void init() {
|
public static void init() {
|
||||||
|
|
|
@ -53,7 +53,8 @@ public class AppWindowHelper {
|
||||||
// This allows for assigning logos even if AppImages has not been initialized yet
|
// This allows for assigning logos even if AppImages has not been initialized yet
|
||||||
var dir = OsType.getLocal() == OsType.MACOS ? "img/logo/padded" : "img/logo/full";
|
var dir = OsType.getLocal() == OsType.MACOS ? "img/logo/padded" : "img/logo/full";
|
||||||
AppResources.with(AppResources.XPIPE_MODULE, dir, path -> {
|
AppResources.with(AppResources.XPIPE_MODULE, dir, path -> {
|
||||||
var size = switch (OsType.getLocal()) {
|
var size =
|
||||||
|
switch (OsType.getLocal()) {
|
||||||
case OsType.Linux linux -> 128;
|
case OsType.Linux linux -> 128;
|
||||||
case OsType.MacOs macOs -> 128;
|
case OsType.MacOs macOs -> 128;
|
||||||
case OsType.Windows windows -> 32;
|
case OsType.Windows windows -> 32;
|
||||||
|
@ -82,12 +83,7 @@ public class AppWindowHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
stage.setOnShown(e -> {
|
stage.setOnShown(e -> {
|
||||||
// If we set the theme pseudo classes earlier when the window is not shown
|
|
||||||
// they do not apply. Is this a bug in JavaFX?
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
AppTheme.initThemeHandlers(stage);
|
AppTheme.initThemeHandlers(stage);
|
||||||
});
|
|
||||||
|
|
||||||
centerToMainWindow(stage);
|
centerToMainWindow(stage);
|
||||||
clampWindow(stage).ifPresent(rectangle2D -> {
|
clampWindow(stage).ifPresent(rectangle2D -> {
|
||||||
stage.setX(rectangle2D.getMinX());
|
stage.setX(rectangle2D.getMinX());
|
||||||
|
|
|
@ -13,7 +13,8 @@ public class AppFontLoadingCheck {
|
||||||
// This can fail if the found system fonts can somehow not be loaded
|
// This can fail if the found system fonts can somehow not be loaded
|
||||||
Font.getDefault();
|
Font.getDefault();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
var event = ErrorEvent.fromThrowable("Unable to load fonts", e).build();
|
var event = ErrorEvent.fromThrowable("Unable to load fonts. Do you have valid font packages installed?", e)
|
||||||
|
.build();
|
||||||
// We can't use the normal error handling facility
|
// We can't use the normal error handling facility
|
||||||
// as the platform reports as working but opening windows still does not work
|
// as the platform reports as working but opening windows still does not work
|
||||||
new LogErrorHandler().handle(event);
|
new LogErrorHandler().handle(event);
|
||||||
|
|
|
@ -18,7 +18,7 @@ public class AppPtbCheck {
|
||||||
.setContent(AppWindowHelper.alertContentText("You are running a PTB build of XPipe."
|
.setContent(AppWindowHelper.alertContentText("You are running a PTB build of XPipe."
|
||||||
+ " This version is unstable and might contain bugs."
|
+ " This version is unstable and might contain bugs."
|
||||||
+ " You should not use it as a daily driver."
|
+ " You should not use it as a daily driver."
|
||||||
+ " It will also not receive regular updates."
|
+ " It will also not receive regular updates after its testing period."
|
||||||
+ " You will have to install and launch the normal XPipe release for that."));
|
+ " You will have to install and launch the normal XPipe release for that."));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,6 +55,7 @@ public class AppShellCheck {
|
||||||
- On Windows, an AntiVirus program might block required programs and commands
|
- On Windows, an AntiVirus program might block required programs and commands
|
||||||
- The system shell is restricted or blocked
|
- The system shell is restricted or blocked
|
||||||
- Some elementary command-line tools are not available or not working correctly
|
- Some elementary command-line tools are not available or not working correctly
|
||||||
|
- Your PATH environment variable is corrupt / incomplete
|
||||||
|
|
||||||
%s
|
%s
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
package io.xpipe.app.core.mode;
|
package io.xpipe.app.core.mode;
|
||||||
|
|
||||||
import io.xpipe.app.browser.BrowserModel;
|
import io.xpipe.app.browser.session.BrowserSessionModel;
|
||||||
import io.xpipe.app.comp.store.StoreViewState;
|
import io.xpipe.app.comp.store.StoreViewState;
|
||||||
import io.xpipe.app.core.*;
|
import io.xpipe.app.core.*;
|
||||||
import io.xpipe.app.core.check.AppAvCheck;
|
import io.xpipe.app.core.check.AppAvCheck;
|
||||||
|
@ -47,6 +47,7 @@ public class BaseMode extends OperationMode {
|
||||||
AppI18n.init();
|
AppI18n.init();
|
||||||
LicenseProvider.get().init();
|
LicenseProvider.get().init();
|
||||||
AppPrefs.initLocal();
|
AppPrefs.initLocal();
|
||||||
|
AppI18n.init();
|
||||||
AppCertutilCheck.check();
|
AppCertutilCheck.check();
|
||||||
AppAvCheck.check();
|
AppAvCheck.check();
|
||||||
AppSid.init();
|
AppSid.init();
|
||||||
|
@ -74,7 +75,7 @@ public class BaseMode extends OperationMode {
|
||||||
@Override
|
@Override
|
||||||
public void finalTeardown() {
|
public void finalTeardown() {
|
||||||
TrackEvent.info("Background mode shutdown started");
|
TrackEvent.info("Background mode shutdown started");
|
||||||
BrowserModel.DEFAULT.reset();
|
BrowserSessionModel.DEFAULT.reset();
|
||||||
StoreViewState.reset();
|
StoreViewState.reset();
|
||||||
DataStorage.reset();
|
DataStorage.reset();
|
||||||
AppPrefs.reset();
|
AppPrefs.reset();
|
||||||
|
|
|
@ -2,7 +2,6 @@ package io.xpipe.app.exchange;
|
||||||
|
|
||||||
import io.xpipe.beacon.BeaconHandler;
|
import io.xpipe.beacon.BeaconHandler;
|
||||||
import io.xpipe.beacon.exchange.LaunchExchange;
|
import io.xpipe.beacon.exchange.LaunchExchange;
|
||||||
import io.xpipe.core.process.TerminalInitScriptConfig;
|
|
||||||
import io.xpipe.core.store.LaunchableStore;
|
import io.xpipe.core.store.LaunchableStore;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
@ -16,9 +15,9 @@ public class LaunchExchangeImpl extends LaunchExchange
|
||||||
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
|
public Response handleRequest(BeaconHandler handler, Request msg) throws Exception {
|
||||||
var store = getStoreEntryById(msg.getId(), false);
|
var store = getStoreEntryById(msg.getId(), false);
|
||||||
if (store.getStore() instanceof LaunchableStore s) {
|
if (store.getStore() instanceof LaunchableStore s) {
|
||||||
var command = s.prepareLaunchCommand()
|
// var command = s.prepareLaunchCommand()
|
||||||
.prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName()), sc -> null);
|
// .prepareTerminalOpen(TerminalInitScriptConfig.ofName(store.getName()), sc -> null);
|
||||||
return Response.builder().command(split(command)).build();
|
// return Response.builder().command(split(command)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalArgumentException(store.getName() + " is not launchable");
|
throw new IllegalArgumentException(store.getName() + " is not launchable");
|
||||||
|
|
|
@ -3,6 +3,7 @@ package io.xpipe.app.exchange;
|
||||||
import io.xpipe.beacon.RequestMessage;
|
import io.xpipe.beacon.RequestMessage;
|
||||||
import io.xpipe.beacon.ResponseMessage;
|
import io.xpipe.beacon.ResponseMessage;
|
||||||
import io.xpipe.beacon.exchange.MessageExchanges;
|
import io.xpipe.beacon.exchange.MessageExchanges;
|
||||||
|
import io.xpipe.core.util.ModuleLayerLoader;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
@ -13,27 +14,6 @@ public class MessageExchangeImpls {
|
||||||
|
|
||||||
private static List<MessageExchangeImpl<?, ?>> ALL;
|
private static List<MessageExchangeImpl<?, ?>> ALL;
|
||||||
|
|
||||||
public static void loadAll() {
|
|
||||||
ALL = ServiceLoader.load(MessageExchangeImpl.class).stream()
|
|
||||||
.map(s -> {
|
|
||||||
// TrackEvent.trace("init", "Loaded exchange implementation " + ex.getId());
|
|
||||||
return (MessageExchangeImpl<?, ?>) s.get();
|
|
||||||
})
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
|
|
||||||
ALL.forEach(messageExchange -> {
|
|
||||||
if (MessageExchanges.byId(messageExchange.getId()).isEmpty()) {
|
|
||||||
throw new AssertionError("Missing base exchange: " + messageExchange.getId());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
MessageExchanges.getAll().forEach(messageExchange -> {
|
|
||||||
if (MessageExchangeImpls.byId(messageExchange.getId()).isEmpty()) {
|
|
||||||
throw new AssertionError("Missing exchange implementation: " + messageExchange.getId());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public static <RQ extends RequestMessage, RS extends ResponseMessage> Optional<MessageExchangeImpl<RQ, RS>> byId(
|
public static <RQ extends RequestMessage, RS extends ResponseMessage> Optional<MessageExchangeImpl<RQ, RS>> byId(
|
||||||
String name) {
|
String name) {
|
||||||
|
@ -53,4 +33,29 @@ public class MessageExchangeImpls {
|
||||||
public static List<MessageExchangeImpl<?, ?>> getAll() {
|
public static List<MessageExchangeImpl<?, ?>> getAll() {
|
||||||
return ALL;
|
return ALL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class Loader implements ModuleLayerLoader {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init(ModuleLayer layer) {
|
||||||
|
ALL = ServiceLoader.load(layer, MessageExchangeImpl.class).stream()
|
||||||
|
.map(s -> {
|
||||||
|
// TrackEvent.trace("init", "Loaded exchange implementation " + ex.getId());
|
||||||
|
return (MessageExchangeImpl<?, ?>) s.get();
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
ALL.forEach(messageExchange -> {
|
||||||
|
if (MessageExchanges.byId(messageExchange.getId()).isEmpty()) {
|
||||||
|
throw new AssertionError("Missing base exchange: " + messageExchange.getId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
MessageExchanges.getAll().forEach(messageExchange -> {
|
||||||
|
if (MessageExchangeImpls.byId(messageExchange.getId()).isEmpty()) {
|
||||||
|
throw new AssertionError("Missing exchange implementation: " + messageExchange.getId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -22,7 +22,7 @@ public class StoreProviderListExchangeImpl extends StoreProviderListExchange
|
||||||
.filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()))
|
.filter(dataStoreProvider -> category.equals(dataStoreProvider.getCreationCategory()))
|
||||||
.map(p -> ProviderEntry.builder()
|
.map(p -> ProviderEntry.builder()
|
||||||
.id(p.getId())
|
.id(p.getId())
|
||||||
.description(p.getDisplayDescription())
|
.description(p.displayDescription().getValue())
|
||||||
.hidden(p.getCreationCategory() == null)
|
.hidden(p.getCreationCategory() == null)
|
||||||
.build())
|
.build())
|
||||||
.toList()));
|
.toList()));
|
||||||
|
|
|
@ -143,15 +143,5 @@ public interface ActionProvider {
|
||||||
})
|
})
|
||||||
.toList());
|
.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
Loading…
Reference in a new issue