More rework

This commit is contained in:
crschnick 2024-04-04 16:45:27 +00:00
parent 6bd105b1de
commit 6d88d3926d
172 changed files with 3068 additions and 1164 deletions

View file

@ -39,6 +39,7 @@ 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.6.0'

View file

@ -3,7 +3,6 @@ package io.xpipe.app.browser;
import atlantafx.base.controls.Breadcrumbs; import atlantafx.base.controls.Breadcrumbs;
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 +39,7 @@ public class BrowserBreadcrumbBar extends SimpleComp {
var breadcrumbs = new Breadcrumbs<String>(); var breadcrumbs = new Breadcrumbs<String>();
breadcrumbs.setMinWidth(0); breadcrumbs.setMinWidth(0);
SimpleChangeListener.apply(PlatformThread.sync(model.getCurrentPath()), val -> { PlatformThread.sync(model.getCurrentPath()).subscribe( val -> {
if (val == null) { if (val == null) {
breadcrumbs.setSelectedCrumb(null); breadcrumbs.setSelectedCrumb(null);
return; return;

View file

@ -4,22 +4,19 @@ import atlantafx.base.controls.RingProgressIndicator;
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.icon.BrowserIconDirectoryType; import io.xpipe.app.browser.icon.BrowserIconDirectoryType;
import io.xpipe.app.browser.icon.BrowserIconFileType;
import io.xpipe.app.browser.icon.FileIconManager; import io.xpipe.app.browser.icon.FileIconManager;
import io.xpipe.app.browser.icon.BrowserIconFileType;
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.comp.base.SideSplitPaneComp;
import io.xpipe.app.core.AppLayoutModel; 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.TooltipAugment;
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.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;
@ -52,12 +49,6 @@ public class BrowserComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
BrowserIconFileType.loadDefinitions();
BrowserIconDirectoryType.loadDefinitions();
ThreadHelper.runAsync(() -> {
FileIconManager.loadIfNecessary();
});
var bookmarksList = new BrowserBookmarkComp(model).vgrow(); var bookmarksList = new BrowserBookmarkComp(model).vgrow();
var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage()) var localDownloadStage = new BrowserTransferComp(model.getLocalTransfersStage())
.hide(PlatformThread.sync(Bindings.createBooleanBinding( .hide(PlatformThread.sync(Bindings.createBooleanBinding(
@ -133,7 +124,7 @@ public class BrowserComp extends SimpleComp {
private Comp<?> createTabs() { 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.getOpenFileSystems()),
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(
() -> { () -> {
@ -284,7 +275,7 @@ public class BrowserComp extends SimpleComp {
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 +294,7 @@ public class BrowserComp extends SimpleComp {
if (color != null) { if (color != null) {
c.getStyleClass().add(color.getId()); c.getStyleClass().add(color.getId());
} }
new FancyTooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c); new TooltipAugment<>(new SimpleStringProperty(model.getTooltip())).augment(c);
c.addEventHandler( c.addEventHandler(
DragEvent.DRAG_ENTERED, DragEvent.DRAG_ENTERED,
mouseEvent -> Platform.runLater( mouseEvent -> Platform.runLater(

View file

@ -29,7 +29,7 @@ public class BrowserEntry {
return null; return null;
} }
for (var f : BrowserIconFileType.ALL) { for (var f : BrowserIconFileType.getAll()) {
if (f.matches(rawFileEntry)) { if (f.matches(rawFileEntry)) {
return f; return f;
} }
@ -43,7 +43,7 @@ public class BrowserEntry {
return null; return null;
} }
for (var f : BrowserIconDirectoryType.ALL) { for (var f : BrowserIconDirectoryType.getAll()) {
if (f.matches(rawFileEntry)) { if (f.matches(rawFileEntry)) {
return f; return f;
} }

View file

@ -9,7 +9,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.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;
@ -505,7 +504,7 @@ 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().resolved().getKind() != FileKind.DIRECTORY;
@ -514,7 +513,7 @@ final class BrowserFileListComp extends SimpleComp {
.equals(fileList.getFileSystemModel().getCurrentParentDirectory()); .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) -> {
@ -558,7 +557,7 @@ final class BrowserFileListComp extends SimpleComp {
// Don't set image as that would trigger image comp update // Don't set image as that would trigger image comp update
// and cells are emptied on each change, leading to unnecessary changes // and cells are emptied on each change, leading to unnecessary changes
// img.set(null); // img.set(null);
// Visibility seems to be bugged, so use opacity // Visibility seems to be bugged, so use opacity
setOpacity(0.0); setOpacity(0.0);
} else { } else {

View file

@ -1,6 +1,6 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
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.core.store.FileKind; import io.xpipe.core.store.FileKind;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
@ -33,7 +33,7 @@ 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();

View file

@ -3,9 +3,8 @@ package io.xpipe.app.browser;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
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.TooltipAugment;
import io.xpipe.app.fxcomps.impl.TextFieldComp; import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
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 +28,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 +46,7 @@ public class BrowserFilterComp extends Comp<BrowserFilterComp.Structure> {
text.setMinWidth(0); text.setMinWidth(0);
Styles.toggleStyleClass(text, Styles.LEFT_PILL); Styles.toggleStyleClass(text, Styles.LEFT_PILL);
SimpleChangeListener.apply(filterString, val -> { filterString.subscribe(val -> {
if (val == null) { if (val == null) {
text.getStyleClass().remove(Styles.SUCCESS); text.getStyleClass().remove(Styles.SUCCESS);
} else { } else {

View file

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

View file

@ -1,6 +1,9 @@
package io.xpipe.app.browser; package io.xpipe.app.browser;
import io.xpipe.app.fxcomps.util.BindingsHelper; 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.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.storage.DataStoreEntryRef;
import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.BooleanScope;
@ -47,7 +50,7 @@ public class BrowserModel {
return; return;
} }
BindingsHelper.bindContent(selection, newValue.getFileList().getSelection()); ListBindingsHelper.bindContent(selection, newValue.getFileList().getSelection());
}); });
} }
@ -139,6 +142,13 @@ public class BrowserModel {
return; return;
} }
// Only load icons when a file system is opened
ThreadHelper.runAsync(() -> {
BrowserIconFileType.loadDefinitions();
BrowserIconDirectoryType.loadDefinitions();
FileIconManager.loadIfNecessary();
});
ThreadHelper.runFailableAsync(() -> { ThreadHelper.runFailableAsync(() -> {
OpenFileSystemModel model; OpenFileSystemModel model;

View file

@ -10,7 +10,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 +41,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 +57,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 +70,7 @@ public class BrowserNavBar extends SimpleComp {
} }
}); });
SimpleChangeListener.apply(model.getInOverview(), val -> { model.getInOverview().subscribe(val -> {
// Pseudo classes do not apply if set instantly before shown // Pseudo classes do not apply if set instantly before shown
// If we start a new tab with a directory set, we have to set the pseudo class one pulse later // If we start a new tab with a directory set, we have to set the pseudo class one pulse later
Platform.runLater(() -> { Platform.runLater(() -> {

View file

@ -4,7 +4,7 @@ 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 +66,7 @@ public class BrowserOverviewComp extends SimpleComp {
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false); var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview); var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview);
var recent = BindingsHelper.mappedContentBinding( var recent = ListBindingsHelper.mappedContentBinding(
model.getSavedState().getRecentDirectories(), model.getSavedState().getRecentDirectories(),
s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())); s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()));
var recentOverview = new BrowserFileOverviewComp(model, recent, true); var recentOverview = new BrowserFileOverviewComp(model, recent, true);

View file

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

View file

@ -59,7 +59,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 +68,7 @@ public class BrowserStatusBarComp extends SimpleComp {
return null; return null;
} }
}, },
cc))); cc);
return new LabelComp(ccCount); return new LabelComp(ccCount);
} }
@ -86,7 +86,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 +95,7 @@ public class BrowserStatusBarComp extends SimpleComp {
} }
}, },
selectedCount, selectedCount,
allCount))); allCount));
return selectedComp; return selectedComp;
} }

View file

@ -6,7 +6,7 @@ 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;
@ -39,11 +39,11 @@ public class BrowserTransferComp extends SimpleComp {
protected Region createSimple() { protected Region createSimple() {
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(model.getItems()));
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(model.getItems(), item -> item.getFileEntry());
var list = new BrowserSelectionListComp( var list = new BrowserSelectionListComp(
binding, binding,
entry -> Bindings.createStringBinding( entry -> Bindings.createStringBinding(
@ -70,20 +70,20 @@ public class BrowserTransferComp extends SimpleComp {
.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(PlatformThread.sync(Bindings.isEmpty(model.getItems())))
.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(model.getItems()))
.disable(PlatformThread.sync(model.getAllDownloaded())) .disable(PlatformThread.sync(model.getAllDownloaded()))
.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(model.getItems()));
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)),

View file

@ -5,6 +5,7 @@ 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 +13,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;
@ -54,13 +56,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,13 +77,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)) });
var header = new LabelComp(headerBinding)
.createRegion(); .createRegion();
AppFont.setSize(header, 1); AppFont.setSize(header, 1);
vbox.getChildren().add(header); vbox.getChildren().add(header);

View file

@ -9,7 +9,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;
@ -97,7 +96,7 @@ public class OpenFileSystemComp extends SimpleComp {
home, home,
model.getCurrentPath().isNull(), model.getCurrentPath().isNull(),
fileList, fileList,
BindingsHelper.persist(model.getCurrentPath().isNull().not()))); model.getCurrentPath().isNull().not()));
return stack.createRegion(); return stack.createRegion();
} }
} }

View file

@ -2,7 +2,7 @@ package io.xpipe.app.browser.action;
import io.xpipe.app.browser.BrowserEntry; import io.xpipe.app.browser.BrowserEntry;
import io.xpipe.app.browser.OpenFileSystemModel; import io.xpipe.app.browser.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;
@ -39,7 +39,7 @@ 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); new TooltipAugment<>(new SimpleStringProperty(getName(model, selected))).augment(b);
var graphic = getIcon(model, selected); var graphic = getIcon(model, selected);
if (graphic != null) { if (graphic != null) {
b.setGraphic(graphic); b.setGraphic(graphic);

View file

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

View file

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

View file

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

View file

@ -7,7 +7,6 @@ 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;
@ -25,14 +24,15 @@ public class AppLayoutComp extends Comp<CompStructure<Pane>> {
var multi = new MultiContentComp(model.getEntries().stream() var multi = new MultiContentComp(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 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()); pane.setCenter(multi.createRegion());
pane.setRight(sidebar.createRegion()); pane.setRight(sidebar.createRegion());
pane.getStyleClass().add("background"); pane.getStyleClass().add("background");

View file

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

View file

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

View file

@ -0,0 +1,48 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.util.BindingsHelper;
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>{
@Value
public static class Structure implements CompStructure<StackPane> {
FontIcon icon;
StackPane pane;
@Override
public StackPane get() {
return pane;
}
}
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);
}
}

View file

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

View file

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

View file

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

View file

@ -3,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.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.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
@ -25,9 +24,11 @@ public class MultiContentComp extends SimpleComp {
for (Map.Entry<Comp<?>, ObservableValue<Boolean>> entry : content.entrySet()) { for (Map.Entry<Comp<?>, ObservableValue<Boolean>> entry : content.entrySet()) {
var region = entry.getKey().createRegion(); var region = entry.getKey().createRegion();
stack.getChildren().add(region); stack.getChildren().add(region);
SimpleChangeListener.apply(PlatformThread.sync(entry.getValue()), val -> { entry.getValue().subscribe(val -> {
region.setManaged(val); PlatformThread.runLaterIfNeeded(() -> {
region.setVisible(val); region.setManaged(val);
region.setVisible(val);
});
}); });
} }
return stack; return stack;

View file

@ -37,7 +37,7 @@ public class OsLogoComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
var img = BindingsHelper.persist(Bindings.createObjectBinding( var img = Bindings.createObjectBinding(
() -> { () -> {
if (state.getValue() != SystemStateComp.State.SUCCESS) { if (state.getValue() != SystemStateComp.State.SUCCESS) {
return null; return null;
@ -51,10 +51,11 @@ 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(
List.of(new SystemStateComp(state).hide(hide), new PrettyImageComp(img, 24, 24).visible(hide))) new SystemStateComp(state).hide(hide),
new PrettyImageComp(img, 24, 24).visible(hide)))
.createRegion(); .createRegion();
} }

View file

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

View file

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

View file

@ -5,7 +5,6 @@ import io.xpipe.app.comp.store.StoreEntryWrapper;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.core.process.ShellStoreState; import io.xpipe.core.process.ShellStoreState;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@ -35,7 +34,7 @@ public class SystemStateComp extends SimpleComp {
state)); state));
var fi = new FontIcon(); var fi = new FontIcon();
fi.getStyleClass().add("inner-icon"); fi.getStyleClass().add("inner-icon");
SimpleChangeListener.apply(icon, val -> fi.setIconLiteral(val)); icon.subscribe(val -> fi.setIconLiteral(val));
var border = new FontIcon("mdi2c-circle-outline"); var border = new FontIcon("mdi2c-circle-outline");
border.getStyleClass().add("outer-icon"); border.getStyleClass().add("outer-icon");
@ -63,7 +62,7 @@ public class SystemStateComp extends SimpleComp {
"""; """;
pane.getStylesheets().add(Styles.toDataURI(dataClass1)); pane.getStylesheets().add(Styles.toDataURI(dataClass1));
SimpleChangeListener.apply(PlatformThread.sync(state), val -> { PlatformThread.sync(state).subscribe(val -> {
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);
}); });

View file

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

View file

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

View file

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

View file

@ -26,6 +26,9 @@ public class StoreCreationMenu {
menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh")); menu.getItems().add(category("addHost", "mdi2h-home-plus", DataStoreProvider.CreationCategory.HOST, "ssh"));
menu.getItems()
.add(category("addVisual", "mdi2c-camera-plus", DataStoreProvider.CreationCategory.VISUAL, null));
menu.getItems() menu.getItems()
.add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null)); .add(category("addShell", "mdi2t-text-box-multiple", DataStoreProvider.CreationCategory.SHELL, null));

View file

@ -13,9 +13,7 @@ import io.xpipe.app.fxcomps.SimpleCompStructure;
import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.augment.ContextMenuAugment;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.impl.*;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreColor;
@ -101,8 +99,7 @@ public abstract class StoreEntryComp extends SimpleComp {
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 +135,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,7 +171,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<>(new SimpleStringProperty(
wrapper.getEntry().getProvider().getDisplayName())) wrapper.getEntry().getProvider().getDisplayName()))
.augment(storeIcon); .augment(storeIcon);
} }
@ -212,7 +209,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()));
@ -248,7 +245,7 @@ public abstract class StoreEntryComp extends SimpleComp {
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, null, () -> StoreEntryComp.this.createContextMenu()));
settingsButton.apply(new FancyTooltipAugment<>("more")); settingsButton.apply(new TooltipAugment<>("more"));
return settingsButton; return settingsButton;
} }
@ -371,7 +368,8 @@ public abstract class StoreEntryComp extends SimpleComp {
StoreViewState.get() StoreViewState.get()
.getSortedCategories(wrapper.getCategory().getValue().getRoot()) .getSortedCategories(wrapper.getCategory().getValue().getRoot())
.forEach(storeCategoryWrapper -> { .forEach(storeCategoryWrapper -> {
MenuItem m = new MenuItem(storeCategoryWrapper.getName()); MenuItem m = new MenuItem();
m.textProperty().bind(storeCategoryWrapper.nameProperty());
m.setOnAction(event -> { m.setOnAction(event -> {
wrapper.moveTo(storeCategoryWrapper.getCategory()); wrapper.moveTo(storeCategoryWrapper.getCategory());
event.consume(); event.consume();

View file

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

View file

@ -5,11 +5,11 @@ import io.xpipe.app.core.AppFont;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.SimpleComp;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
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.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,12 @@ 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(StoreViewState.get().getActiveCategory(),
.bind(Bindings.createStringBinding( categoryWrapper -> AppI18n.observable(categoryWrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory()) ? "connections" : "scripts"));
() -> { label.textProperty().bind(name);
return StoreViewState.get()
.getActiveCategory()
.getValue()
.getRoot()
.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 +67,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 +126,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 +180,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 +219,7 @@ public class StoreEntryListStatusComp extends SimpleComp {
sortMode)); sortMode));
}); });
date.accessibleTextKey("sortLastUsed"); date.accessibleTextKey("sortLastUsed");
date.apply(new FancyTooltipAugment<>("sortLastUsed")); date.apply(new TooltipAugment<>("sortLastUsed"));
return date; return date;
} }

View file

@ -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,16 @@ 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 +122,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 +132,10 @@ public class StoreSection {
// This check is fast as the children are cached in the storage // This check is fast as the children are cached in the storage
return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()); return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry());
}); });
var cached = BindingsHelper.cachedMappedContentBinding( var cached = ListBindingsHelper.cachedMappedContentBinding(
allChildren, entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category)); allChildren, allChildren, entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category));
var ordered = sorted(cached, category); var ordered = sorted(cached, category);
var filtered = BindingsHelper.filteredContentBinding( var filtered = ListBindingsHelper.filteredContentBinding(
ordered, ordered,
section -> { section -> {
var showFilter = filterString == null || section.shouldShow(filterString.get()); var showFilter = filterString == null || section.shouldShow(filterString.get());

View file

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

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@ package io.xpipe.app.core;
import io.xpipe.app.comp.base.ModalOverlayComp; import io.xpipe.app.comp.base.ModalOverlayComp;
import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.issue.TrackEvent;
import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
@ -10,101 +10,70 @@ 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+?\\$"); @Value
private static final AppI18n INSTANCE = new AppI18n(); static class LoadedTranslations {
private Map<String, String> translations;
private Map<String, String> markdownDocumentations;
private PrettyTime prettyTime;
public static void init() { Map<String, String> translations;
var i = INSTANCE; Map<String, String> markdownDocumentations;
if (i.translations != null) { PrettyTime prettyTime;
return; }
private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$");
private static AppI18n INSTANCE;
private LoadedTranslations english;
private final Property<LoadedTranslations> currentLanguage = new SimpleObjectProperty<>();
public static void init() throws Exception {
INSTANCE = new AppI18n();
INSTANCE.load();
}
private void load() throws Exception {
if (english == null) {
english = load(Locale.ENGLISH);
} }
i.load();
if (AppPrefs.get() != null) { if (AppPrefs.get() != null) {
AppPrefs.get().language().addListener((c, o, n) -> { AppPrefs.get().language().subscribe(n -> {
i.clear(); try {
i.load(); currentLanguage.setValue(n != null ? load(n.getLocale()) : null);
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
}
}); });
} }
} }
public static AppI18n getInstance() { public static AppI18n get() {
return INSTANCE; return INSTANCE;
} }
public static StringBinding readableInstant(String s, ObservableValue<Instant> instant) { private LoadedTranslations getLoaded() {
return readableInstant(instant, rs -> getValue(getInstance().getLocalised(s), rs)); return currentLanguage.getValue() != null ? currentLanguage.getValue() : english;
}
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) {
@ -115,7 +84,7 @@ public class AppI18n {
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 +116,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,11 +129,6 @@ public class AppI18n {
return ""; return "";
} }
private void clear() {
translations.clear();
prettyTime = null;
}
public String getKey(String s) { public String getKey(String s) {
var key = s; var key = s;
if (!s.contains(".")) { if (!s.contains(".")) {
@ -173,62 +137,62 @@ 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)) {
TrackEvent.warn("Translation key not found for " + key); var localisedString = currentLanguage.getValue().getTranslations().get(key);
return key; return getValue(localisedString, vars);
} }
var localisedString = translations.get(key); if (english.getTranslations().containsKey(key)) {
return getValue(localisedString, vars); var localisedString = english.getTranslations().get(key);
return getValue(localisedString, vars);
}
TrackEvent.warn("Translation key not found for " + key);
return key;
} }
public boolean isLoaded() { private boolean matchesLocale(Path f, Locale l) {
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)) {
TrackEvent.withWarn("Markdown documentation for key " + name + " not found") var localisedString = currentLanguage.getValue().getMarkdownDocumentations().get(name);
.handle(); return localisedString;
} }
return markdownDocumentations.getOrDefault(name, ""); if (english.getMarkdownDocumentations().containsKey(name)) {
var localisedString = english.getMarkdownDocumentations().get(name);
return localisedString;
}
TrackEvent.withWarn("Markdown documentation for key " + name + " not found")
.handle();
return "";
} }
private void load() { private Path getModuleLangPath(String module) {
return XPipeInstallation.getLangPath().resolve(module);
}
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 +202,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 +213,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 +231,20 @@ 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 +265,14 @@ 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.ENGLISH.getLocale());
return new LoadedTranslations(translations,markdownDocumentations, prettyTime);
} }
@SuppressWarnings("removal") @SuppressWarnings("removal")

View file

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

View file

@ -2,10 +2,8 @@ package io.xpipe.app.core;
import io.xpipe.app.browser.BrowserComp; import io.xpipe.app.browser.BrowserComp;
import io.xpipe.app.browser.BrowserModel; import io.xpipe.app.browser.BrowserModel;
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());
} }
@ -81,19 +73,11 @@ public class AppLayoutModel {
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 BrowserComp(BrowserModel.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()), AppI18n.observable("explorePlans"),
// new SideMenuBarComp.Entry(AppI18n.observable("account"), "mdi2a-account", new StorageLayoutComp()) "mdi2p-professional-hexagon",
if (AppProperties.get().isDeveloperMode() && !AppProperties.get().isImage()) { LicenseProvider.get().overviewPage())));
l.add(new Entry(AppI18n.observable("developer"), "mdi2b-book-open-variant", new DeveloperTabComp()));
}
l.add(new Entry(
AppI18n.observable("explorePlans"),
"mdi2p-professional-hexagon",
LicenseProvider.get().overviewPage()));
return l; return l;
} }

View file

@ -3,7 +3,6 @@ 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;
@ -44,7 +43,7 @@ public class AppTheme {
return; return;
} }
SimpleChangeListener.apply(AppPrefs.get().theme, t -> { 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,7 +55,7 @@ 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);
}); });

View file

@ -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();

View file

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

View file

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

View file

@ -201,6 +201,7 @@ public interface DataStoreProvider {
COMMAND, COMMAND,
TUNNEL, TUNNEL,
SCRIPT, SCRIPT,
CLUSTER CLUSTER,
VISUAL;
} }
} }

View file

@ -4,9 +4,9 @@ import atlantafx.base.controls.Spacer;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.augment.GrowAugment;
import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.TooltipAugment;
import io.xpipe.app.fxcomps.util.BindingsHelper;
import io.xpipe.app.fxcomps.util.Shortcuts; import io.xpipe.app.fxcomps.util.Shortcuts;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets; import javafx.geometry.Insets;
@ -144,7 +144,8 @@ public abstract class Comp<S extends CompStructure<?>> {
public Comp<S> hide(ObservableValue<Boolean> o) { public Comp<S> hide(ObservableValue<Boolean> o) {
return apply(struc -> { return apply(struc -> {
var region = struc.get(); var region = struc.get();
SimpleChangeListener.apply(o, n -> { BindingsHelper.preserve(region, o);
o.subscribe(n -> {
if (!n) { if (!n) {
region.setVisible(true); region.setVisible(true);
region.setManaged(true); region.setManaged(true);
@ -189,11 +190,11 @@ public abstract class Comp<S extends CompStructure<?>> {
} }
public Comp<S> tooltip(ObservableValue<String> text) { public Comp<S> tooltip(ObservableValue<String> text) {
return apply(new FancyTooltipAugment<>(text)); return apply(new TooltipAugment<>(text));
} }
public Comp<S> tooltipKey(String key) { public Comp<S> tooltipKey(String key) {
return apply(new FancyTooltipAugment<>(key)); return apply(new TooltipAugment<>(key));
} }
public Region createRegion() { public Region createRegion() {
@ -202,6 +203,8 @@ public abstract class Comp<S extends CompStructure<?>> {
public S createStructure() { public S createStructure() {
S struc = createBase(); S struc = createBase();
// Make comp last at least as long as region
BindingsHelper.preserve(struc.get(), this);
if (augments != null) { if (augments != null) {
for (var a : augments) { for (var a : augments) {
a.augment(struc); a.augment(struc);

View file

@ -4,9 +4,8 @@ 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.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 io.xpipe.app.fxcomps.util.SimpleChangeListener;
import io.xpipe.app.util.Translatable; import io.xpipe.app.util.Translatable;
import javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
@ -72,19 +71,19 @@ public class ChoiceComp<T> extends Comp<CompStructure<ComboBox<T>>> {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
}); });
SimpleChangeListener.apply(range, c -> { range.subscribe(c -> {
var list = FXCollections.observableArrayList(c.keySet()); var list = FXCollections.observableArrayList(c.keySet());
if (!list.contains(null) && includeNone) { if (!list.contains(null) && includeNone) {
list.add(null); list.add(null);
} }
BindingsHelper.setContent(cb.getItems(), list); ListBindingsHelper.setContent(cb.getItems(), list);
}); });
cb.valueProperty().addListener((observable, oldValue, newValue) -> { cb.valueProperty().addListener((observable, oldValue, newValue) -> {
value.setValue(newValue); value.setValue(newValue);
}); });
SimpleChangeListener.apply(value, val -> { value.subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> cb.valueProperty().set(val)); PlatformThread.runLaterIfNeeded(() -> cb.valueProperty().set(val));
}); });

View file

@ -4,7 +4,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 javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections; import javafx.collections.FXCollections;
@ -58,7 +57,7 @@ public class ChoicePaneComp extends Comp<CompStructure<VBox>> {
var vbox = new VBox(transformer.apply(cb)); var vbox = new VBox(transformer.apply(cb));
vbox.setFillWidth(true); vbox.setFillWidth(true);
cb.prefWidthProperty().bind(vbox.widthProperty()); cb.prefWidthProperty().bind(vbox.widthProperty());
SimpleChangeListener.apply(cb.valueProperty(), n -> { cb.valueProperty().subscribe(n -> {
if (n == null) { if (n == null) {
if (vbox.getChildren().size() > 1) { if (vbox.getChildren().size() > 1) {
vbox.getChildren().remove(1); vbox.getChildren().remove(1);
@ -82,7 +81,7 @@ public class ChoicePaneComp extends Comp<CompStructure<VBox>> {
cb.valueProperty().addListener((observable, oldValue, newValue) -> { cb.valueProperty().addListener((observable, oldValue, newValue) -> {
selected.setValue(newValue); selected.setValue(newValue);
}); });
SimpleChangeListener.apply(selected, val -> { selected.subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> cb.valueProperty().set(val)); PlatformThread.runLaterIfNeeded(() -> cb.valueProperty().set(val));
}); });

View file

@ -4,10 +4,9 @@ import atlantafx.base.theme.Styles;
import io.xpipe.app.browser.StandaloneFileBrowser; import io.xpipe.app.browser.StandaloneFileBrowser;
import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppLayoutModel;
import io.xpipe.app.core.AppWindowHelper; import io.xpipe.app.core.AppWindowHelper;
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.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.storage.ContextualFileReference; import io.xpipe.app.storage.ContextualFileReference;
@ -39,7 +38,7 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp {
public <T extends FileSystemStore> ContextualFileReferenceChoiceComp( public <T extends FileSystemStore> ContextualFileReferenceChoiceComp(
ObservableValue<DataStoreEntryRef<T>> fileSystem, Property<String> filePath) { ObservableValue<DataStoreEntryRef<T>> fileSystem, Property<String> filePath) {
this.fileSystem = new SimpleObjectProperty<>(); this.fileSystem = new SimpleObjectProperty<>();
SimpleChangeListener.apply(fileSystem, val -> { fileSystem.subscribe(val -> {
this.fileSystem.setValue(val); this.fileSystem.setValue(val);
}); });
this.filePath = filePath; this.filePath = filePath;
@ -66,7 +65,7 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp {
.styleClass(Styles.CENTER_PILL) .styleClass(Styles.CENTER_PILL)
.grow(false, true); .grow(false, true);
var canGitShare = BindingsHelper.persist(Bindings.createBooleanBinding( var canGitShare = Bindings.createBooleanBinding(
() -> { () -> {
if (!AppPrefs.get().enableGitStorage().get() if (!AppPrefs.get().enableGitStorage().get()
|| filePath.getValue() == null || filePath.getValue() == null
@ -77,8 +76,18 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp {
return true; return true;
}, },
filePath, filePath,
AppPrefs.get().enableGitStorage())); AppPrefs.get().enableGitStorage());
var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> { var gitShareButton = new ButtonComp(null, new FontIcon("mdi2g-git"), () -> {
if (!AppPrefs.get().enableGitStorage().get()) {
AppLayoutModel.get().selectSettings();
AppPrefs.get().selectCategory(3);
return;
}
if (filePath.getValue() == null || ContextualFileReference.of(filePath.getValue()).isInDataDirectory()) {
return;
}
if (filePath.getValue() == null || filePath.getValue().isBlank() || !canGitShare.get()) { if (filePath.getValue() == null || filePath.getValue().isBlank() || !canGitShare.get()) {
return; return;
} }
@ -108,7 +117,7 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp {
ErrorEvent.fromThrowable(e).handle(); ErrorEvent.fromThrowable(e).handle();
} }
}); });
gitShareButton.apply(new FancyTooltipAugment<>("gitShareFileTooltip")); gitShareButton.apply(new TooltipAugment<>("gitShareFileTooltip"));
gitShareButton.styleClass(Styles.RIGHT_PILL).grow(false, true); gitShareButton.styleClass(Styles.RIGHT_PILL).grow(false, true);
var layout = new HorizontalComp(List.of(fileNameComp, fileBrowseButton, gitShareButton)) var layout = new HorizontalComp(List.of(fileNameComp, fileBrowseButton, gitShareButton))

View file

@ -27,11 +27,11 @@ public class DataStoreFlowChoiceComp extends SimpleComp {
map.put(DataFlow.INPUT_OUTPUT, AppI18n.observable("app.inout")); map.put(DataFlow.INPUT_OUTPUT, AppI18n.observable("app.inout"));
return new ToggleGroupComp<>(selected, new SimpleObjectProperty<>(map)) return new ToggleGroupComp<>(selected, new SimpleObjectProperty<>(map))
.apply(struc -> { .apply(struc -> {
new FancyTooltipAugment<>("app.inputDescription") new TooltipAugment<>("app.inputDescription")
.augment(struc.get().getChildren().get(0)); .augment(struc.get().getChildren().get(0));
new FancyTooltipAugment<>("app.outputDescription") new TooltipAugment<>("app.outputDescription")
.augment(struc.get().getChildren().get(1)); .augment(struc.get().getChildren().get(1));
new FancyTooltipAugment<>("app.inoutDescription") new TooltipAugment<>("app.inoutDescription")
.augment(struc.get().getChildren().get(2)); .augment(struc.get().getChildren().get(2));
}) })
.createRegion(); .createRegion();

View file

@ -1,10 +1,10 @@
package io.xpipe.app.fxcomps.impl; package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppActionLinkDetector; import io.xpipe.app.core.AppActionLinkDetector;
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.Property; import javafx.beans.property.Property;
import javafx.scene.Node; import javafx.scene.Node;
@ -28,12 +28,13 @@ public class FilterComp extends Comp<FilterComp.Structure> {
@Override @Override
public Structure createBase() { public Structure createBase() {
var fi = new FontIcon("mdi2m-magnify"); var fi = new FontIcon("mdi2m-magnify");
var bgLabel = new Label("Search", fi); var bgLabel = new Label(null, fi);
bgLabel.textProperty().bind(AppI18n.observable("searchFilter"));
bgLabel.getStyleClass().add("filter-background"); bgLabel.getStyleClass().add("filter-background");
var filter = new TextField(); var filter = new TextField();
filter.setAccessibleText("Filter"); filter.setAccessibleText("Filter");
SimpleChangeListener.apply(filterText, val -> { filterText.subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
if (!Objects.equals(filter.getText(), val)) { if (!Objects.equals(filter.getText(), val)) {
filter.setText(val); filter.setText(val);

View file

@ -3,6 +3,7 @@ package io.xpipe.app.fxcomps.impl;
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.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;

View file

@ -3,7 +3,6 @@ package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppImages; import io.xpipe.app.core.AppImages;
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.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.store.FileNames; import io.xpipe.core.store.FileNames;
@ -106,7 +105,7 @@ public class PrettyImageComp extends SimpleComp {
} }
}; };
SimpleChangeListener.apply(PlatformThread.sync(value), val -> update.accept(val)); PlatformThread.sync(value).subscribe(val -> update.accept(val));
AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> { AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> {
update.accept(value.getValue()); update.accept(value.getValue());
}); });

View file

@ -3,7 +3,6 @@ package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppImages; import io.xpipe.app.core.AppImages;
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.app.prefs.AppPrefs; import io.xpipe.app.prefs.AppPrefs;
import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileNames;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@ -92,7 +91,7 @@ public class PrettySvgComp extends SimpleComp {
image.set(fixed); image.set(fixed);
}; };
SimpleChangeListener.apply(syncValue, val -> update.accept(val)); syncValue.subscribe(val -> update.accept(val));
AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> { AppPrefs.get().theme.addListener((observable, oldValue, newValue) -> {
update.accept(syncValue.getValue()); update.accept(syncValue.getValue());
}); });

View file

@ -11,8 +11,7 @@ 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.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 io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.util.ContextMenuHelper; import io.xpipe.app.util.ContextMenuHelper;
@ -77,7 +76,7 @@ public class StoreCategoryComp extends SimpleComp {
showing.bind(cm.showingProperty()); showing.bind(cm.showingProperty());
return cm; return cm;
})); }));
var shownList = BindingsHelper.filteredContentBinding( var shownList = ListBindingsHelper.filteredContentBinding(
category.getContainedEntries(), category.getContainedEntries(),
storeEntryWrapper -> { storeEntryWrapper -> {
return storeEntryWrapper.shouldShow( return storeEntryWrapper.shouldShow(
@ -92,9 +91,8 @@ public class StoreCategoryComp extends SimpleComp {
Comp.hspacer(4), Comp.hspacer(4),
Comp.of(() -> name), Comp.of(() -> name),
Comp.hspacer(), Comp.hspacer(),
count.hide(BindingsHelper.persist(hover.or(showing).or(focus))), count.hide(hover.or(showing).or(focus)),
settings.hide( settings.hide(hover.not().and(showing.not()).and(focus.not()))));
BindingsHelper.persist(hover.not().and(showing.not()).and(focus.not())))));
h.padding(new Insets(0, 10, 0, (category.getDepth() * 10))); h.padding(new Insets(0, 10, 0, (category.getDepth() * 10)));
var categoryButton = new ButtonComp(null, h.createRegion(), category::select) var categoryButton = new ButtonComp(null, h.createRegion(), category::select)
@ -108,14 +106,14 @@ public class StoreCategoryComp extends SimpleComp {
var l = category.getChildren() var l = category.getChildren()
.sorted(Comparator.comparing( .sorted(Comparator.comparing(
storeCategoryWrapper -> storeCategoryWrapper.getName().toLowerCase(Locale.ROOT))); storeCategoryWrapper -> storeCategoryWrapper.nameProperty().getValue().toLowerCase(Locale.ROOT)));
var children = new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper)); var children = new ListBoxViewComp<>(l, l, storeCategoryWrapper -> new StoreCategoryComp(storeCategoryWrapper));
var emptyBinding = Bindings.isEmpty(category.getChildren()); var emptyBinding = Bindings.isEmpty(category.getChildren());
var v = new VerticalComp(List.of(categoryButton, children.hide(emptyBinding))); var v = new VerticalComp(List.of(categoryButton, children.hide(emptyBinding)));
v.styleClass("category"); v.styleClass("category");
v.apply(struc -> { v.apply(struc -> {
SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { StoreViewState.get().getActiveCategory().subscribe(val -> {
struc.get().pseudoClassStateChanged(SELECTED, val.equals(category)); struc.get().pseudoClassStateChanged(SELECTED, val.equals(category));
}); });
}); });

View file

@ -3,7 +3,6 @@ package io.xpipe.app.fxcomps.impl;
import io.xpipe.app.core.AppProperties; import io.xpipe.app.core.AppProperties;
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.SimpleIntegerProperty; import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
@ -37,7 +36,7 @@ public class SvgView {
public static SvgView create(ObservableValue<String> content) { public static SvgView create(ObservableValue<String> content) {
var widthProperty = new SimpleIntegerProperty(); var widthProperty = new SimpleIntegerProperty();
var heightProperty = new SimpleIntegerProperty(); var heightProperty = new SimpleIntegerProperty();
SimpleChangeListener.apply(content, val -> { content.subscribe(val -> {
if (val == null || val.isBlank()) { if (val == null || val.isBlank()) {
return; return;
} }
@ -69,7 +68,7 @@ public class SvgView {
wv.setDisable(true); wv.setDisable(true);
wv.getEngine().loadContent(svgContent.getValue() != null ? getHtml(svgContent.getValue()) : null); wv.getEngine().loadContent(svgContent.getValue() != null ? getHtml(svgContent.getValue()) : null);
SimpleChangeListener.apply(svgContent, n -> { svgContent.subscribe( n -> {
if (n == null) { if (n == null) {
wv.setOpacity(0.0); wv.setOpacity(0.0);
return; return;

View file

@ -3,7 +3,6 @@ package io.xpipe.app.fxcomps.impl;
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.Property; import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.SimpleStringProperty;
@ -28,7 +27,7 @@ public class TextAreaComp extends Comp<TextAreaComp.Structure> {
this.lastAppliedValue = value; this.lastAppliedValue = value;
this.currentValue = new SimpleStringProperty(value.getValue()); this.currentValue = new SimpleStringProperty(value.getValue());
this.lazy = lazy; this.lazy = lazy;
SimpleChangeListener.apply(value, val -> { value.subscribe(val -> {
this.currentValue.setValue(val); this.currentValue.setValue(val);
}); });
} }

View file

@ -4,7 +4,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 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;
@ -27,7 +26,7 @@ public class TextFieldComp extends Comp<CompStructure<TextField>> {
this.currentValue = new SimpleStringProperty(value.getValue()); this.currentValue = new SimpleStringProperty(value.getValue());
this.lazy = lazy; this.lazy = lazy;
if (!lazy) { if (!lazy) {
SimpleChangeListener.apply(currentValue, val -> { currentValue.subscribe(val -> {
value.setValue(val); value.setValue(val);
}); });
} }

View file

@ -5,7 +5,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 javafx.beans.property.Property; import javafx.beans.property.Property;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.control.ToggleButton; import javafx.scene.control.ToggleButton;
@ -29,7 +28,7 @@ public class ToggleGroupComp<T> extends Comp<CompStructure<HBox>> {
var box = new HBox(); var box = new HBox();
box.getStyleClass().add("toggle-group-comp"); box.getStyleClass().add("toggle-group-comp");
ToggleGroup group = new ToggleGroup(); ToggleGroup group = new ToggleGroup();
SimpleChangeListener.apply(PlatformThread.sync(range), val -> { PlatformThread.sync(range).subscribe(val -> {
if (!val.containsKey(value.getValue())) { if (!val.containsKey(value.getValue())) {
this.value.setValue(null); this.value.setValue(null);
} }

View file

@ -5,18 +5,19 @@ import io.xpipe.app.fxcomps.CompStructure;
import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.augment.Augment;
import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.Shortcuts; import io.xpipe.app.fxcomps.util.Shortcuts;
import javafx.beans.binding.Bindings;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.scene.control.Tooltip; import javafx.scene.control.Tooltip;
public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<S> { public class TooltipAugment<S extends CompStructure<?>> implements Augment<S> {
private final ObservableValue<String> text; private final ObservableValue<String> text;
public FancyTooltipAugment(ObservableValue<String> text) { public TooltipAugment(ObservableValue<String> text) {
this.text = PlatformThread.sync(text); this.text = PlatformThread.sync(text);
} }
public FancyTooltipAugment(String key) { public TooltipAugment(String key) {
this.text = AppI18n.observable(key); this.text = AppI18n.observable(key);
} }
@ -24,11 +25,15 @@ public class FancyTooltipAugment<S extends CompStructure<?>> implements Augment<
public void augment(S struc) { public void augment(S struc) {
var region = struc.get(); var region = struc.get();
var tt = new Tooltip(); var tt = new Tooltip();
var toDisplay = text.getValue();
if (Shortcuts.getDisplayShortcut(region) != null) { if (Shortcuts.getDisplayShortcut(region) != null) {
toDisplay = toDisplay + "\n\nShortcut: " + Shortcuts.getDisplayShortcut(region).getDisplayText(); var s = AppI18n.observable("shortcut");
var binding = Bindings.createStringBinding(() -> {
return text.getValue() + "\n\n" + s.getValue() + ": " + Shortcuts.getDisplayShortcut(region).getDisplayText();
}, text, s);
tt.textProperty().bind(binding);
} else {
tt.textProperty().bind(text);
} }
tt.textProperty().setValue(toDisplay);
tt.setStyle("-fx-font-size: 11pt;"); tt.setStyle("-fx-font-size: 11pt;");
tt.setWrapText(true); tt.setWrapText(true);
tt.setMaxWidth(400); tt.setMaxWidth(400);

View file

@ -1,98 +1,50 @@
package io.xpipe.app.fxcomps.util; package io.xpipe.app.fxcomps.util;
import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.ThreadHelper;
import javafx.beans.Observable;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.binding.ListBinding;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import lombok.Value; import lombok.Value;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.*; import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate;
@SuppressWarnings("InfiniteLoopStatement") @SuppressWarnings("InfiniteLoopStatement")
public class BindingsHelper { public class BindingsHelper {
private static final Set<ReferenceEntry> REFERENCES = Collections.newSetFromMap(new ConcurrentHashMap<>()); private static final Set<ReferenceEntry> REFERENCES = Collections.newSetFromMap(new ConcurrentHashMap<>());
/*
TODO: Proper cleanup. Maybe with a separate thread?
*/
private static final Map<WeakReference<Object>, Set<javafx.beans.Observable>> BINDINGS = new ConcurrentHashMap<>();
static { static {
ThreadHelper.createPlatformThread("referenceGC", true, () -> { ThreadHelper.createPlatformThread("referenceGC", true, () -> {
while (true) { while (true) {
for (ReferenceEntry reference : REFERENCES) { for (ReferenceEntry reference : REFERENCES) {
if (reference.canGc()) { if (reference.canGc()) {
/* REFERENCES.remove(reference);
TODO: Figure out why some bindings are garbage collected, even if they shouldn't
*/
// REFERENCES.remove(reference);
} }
} }
ThreadHelper.sleep(1000); ThreadHelper.sleep(1000);
// Use for testing
// System.gc();
} }
}) })
.start(); .start();
} }
public static <T, V> void bindExclusive( public static void preserve(Object source, Object target) {
Property<V> selected, Map<V, ? extends Property<T>> map, Property<T> toBind) {
selected.addListener((c, o, n) -> {
toBind.unbind();
toBind.bind(map.get(n));
});
toBind.bind(map.get(selected.getValue()));
}
public static void linkPersistently(Object source, Object target) {
REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target)); REFERENCES.add(new ReferenceEntry(new WeakReference<>(source), target));
} }
public static <T extends Binding<?>> T persist(T binding) {
var dependencies = new HashSet<javafx.beans.Observable>();
while (dependencies.addAll(binding.getDependencies().stream()
.map(o -> (javafx.beans.Observable) o)
.toList())) {}
dependencies.add(binding);
BINDINGS.put(new WeakReference<>(binding), dependencies);
return binding;
}
public static <T extends ListBinding<?>> T persist(T binding) {
var dependencies = new HashSet<javafx.beans.Observable>();
while (dependencies.addAll(binding.getDependencies().stream()
.map(o -> (javafx.beans.Observable) o)
.toList())) {}
dependencies.add(binding);
BINDINGS.put(new WeakReference<>(binding), dependencies);
return binding;
}
public static <T> void bindContent(ObservableList<T> l1, ObservableList<? extends T> l2) {
setContent(l1, l2);
l2.addListener((ListChangeListener<? super T>) c -> {
setContent(l1, l2);
});
}
public static <T, U> ObservableValue<U> map( public static <T, U> ObservableValue<U> map(
ObservableValue<T> observableValue, Function<? super T, ? extends U> mapper) { ObservableValue<T> observableValue, Function<? super T, ? extends U> mapper) {
return persist(Bindings.createObjectBinding( return Bindings.createObjectBinding(
() -> { () -> {
return mapper.apply(observableValue.getValue()); return mapper.apply(observableValue.getValue());
}, },
observableValue)); observableValue);
} }
public static <T, U> ObservableValue<U> flatMap( public static <T, U> ObservableValue<U> flatMap(
@ -105,229 +57,10 @@ public class BindingsHelper {
observableValue.addListener((observable, oldValue, newValue) -> { observableValue.addListener((observable, oldValue, newValue) -> {
runnable.run(); runnable.run();
}); });
linkPersistently(observableValue, prop); preserve(prop, observableValue);
return prop; return prop;
} }
public static <T, U> ObservableValue<Boolean> anyMatch(List<? extends ObservableValue<Boolean>> l) {
return BindingsHelper.persist(Bindings.createBooleanBinding(
() -> {
return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue());
},
l.toArray(ObservableValue[]::new)));
}
public static <T, V> void bindMappedContent(ObservableList<T> l1, ObservableList<V> l2, Function<V, T> map) {
Runnable runnable = () -> {
setContent(l1, l2.stream().map(map).toList());
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
}
public static <T, V> ObservableList<T> mappedContentBinding(ObservableList<V> l2, Function<V, T> map) {
ObservableList<T> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(l1, l2.stream().map(map).toList());
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
linkPersistently(l2, l1);
return l1;
}
public static <T, V> ObservableList<T> cachedMappedContentBinding(ObservableList<V> l2, Function<V, T> map) {
var cache = new HashMap<V, T>();
ObservableList<T> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
cache.keySet().removeIf(t -> !l2.contains(t));
setContent(
l1,
l2.stream()
.map(v -> {
if (!cache.containsKey(v)) {
cache.put(v, map.apply(v));
}
return cache.get(v);
})
.toList());
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
linkPersistently(l2, l1);
return l1;
}
public static <T, V> ObservableList<T> cachedMappedContentBinding(
ObservableList<V> all, ObservableList<V> shown, Function<V, T> map) {
var cache = new HashMap<V, T>();
ObservableList<T> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
cache.keySet().removeIf(t -> !all.contains(t));
setContent(
l1,
shown.stream()
.map(v -> {
if (!cache.containsKey(v)) {
cache.put(v, map.apply(v));
}
return cache.get(v);
})
.toList());
};
runnable.run();
shown.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
linkPersistently(all, l1);
linkPersistently(shown, l1);
return l1;
}
public static <T, U> ObservableValue<U> mappedBinding(
ObservableValue<T> observableValue, Function<? super T, ? extends ObservableValue<? extends U>> mapper) {
var binding = (Binding<U>) observableValue.flatMap(mapper);
return persist(binding);
}
public static <V> ObservableList<V> orderedContentBinding(
ObservableList<V> l2, Comparator<V> comp, Observable... observables) {
return orderedContentBinding(
l2,
Bindings.createObjectBinding(
() -> {
return new Comparator<>() {
@Override
public int compare(V o1, V o2) {
return comp.compare(o1, o2);
}
};
},
observables));
}
// public static <T,U> ObservableValue<U> mappedBinding(ObservableValue<T> observableValue, Function<? super T, ?
// extends ObservableValue<? extends U>> mapper) {
// var v = new SimpleObjectProperty<U>();
// SimpleChangeListener.apply(observableValue, val -> {
// v.unbind();
// v.bind(mapper.apply(val));
// });
// return v;
// }
public static <V> ObservableList<V> orderedContentBinding(
ObservableList<V> l2, ObservableValue<Comparator<V>> comp) {
ObservableList<V> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(l1, l2.stream().sorted(comp.getValue()).toList());
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
comp.addListener((observable, oldValue, newValue) -> {
runnable.run();
});
linkPersistently(l2, l1);
return l1;
}
public static <V> ObservableList<V> filteredContentBinding(ObservableList<V> l2, Predicate<V> predicate) {
return filteredContentBinding(l2, new SimpleObjectProperty<>(predicate));
}
public static <V> ObservableList<V> filteredContentBinding(
ObservableList<V> l2, Predicate<V> predicate, Observable... observables) {
return filteredContentBinding(
l2,
Bindings.createObjectBinding(
() -> {
return new Predicate<>() {
@Override
public boolean test(V v) {
return predicate.test(v);
}
};
},
Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new)));
}
public static <V> ObservableList<V> filteredContentBinding(
ObservableList<V> l2, ObservableValue<Predicate<V>> predicate) {
ObservableList<V> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(
l1,
predicate.getValue() != null
? l2.stream().filter(predicate.getValue()).toList()
: l2);
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
predicate.addListener((c, o, n) -> {
runnable.run();
});
linkPersistently(l2, l1);
return l1;
}
public static <T> void setContent(ObservableList<T> target, List<? extends T> newList) {
if (target.equals(newList)) {
return;
}
if (target.size() == 0) {
target.setAll(newList);
return;
}
if (newList.size() == 0) {
target.clear();
return;
}
var targetSet = new HashSet<>(target);
var newSet = new HashSet<>(newList);
// Only add missing element
if (target.size() + 1 == newList.size() && newSet.containsAll(targetSet)) {
var l = new HashSet<>(newSet);
l.removeAll(targetSet);
if (l.size() > 0) {
var found = l.iterator().next();
var index = newList.indexOf(found);
target.add(index, found);
return;
}
}
// Only remove not needed element
if (target.size() - 1 == newList.size() && targetSet.containsAll(newSet)) {
var l = new HashSet<>(targetSet);
l.removeAll(newSet);
if (l.size() > 0) {
target.remove(l.iterator().next());
return;
}
}
// Other cases are more difficult
target.setAll(newList);
}
@Value @Value
private static class ReferenceEntry { private static class ReferenceEntry {

View file

@ -0,0 +1,190 @@
package io.xpipe.app.fxcomps.util;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
public class ListBindingsHelper {
public static <T> void bindContent(ObservableList<T> l1, ObservableList<? extends T> l2) {
setContent(l1, l2);
l2.addListener((ListChangeListener<? super T>) c -> {
setContent(l1, l2);
});
}
public static <T, U> ObservableValue<Boolean> anyMatch(List<? extends ObservableValue<Boolean>> l) {
return Bindings.createBooleanBinding(
() -> {
return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue());
},
l.toArray(ObservableValue[]::new));
}
public static <T, V> ObservableList<T> mappedContentBinding(ObservableList<V> l2, Function<V, T> map) {
ObservableList<T> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(l1, l2.stream().map(map).toList());
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
BindingsHelper.preserve(l1, l2);
return l1;
}
public static <T, V> ObservableList<T> cachedMappedContentBinding(
ObservableList<V> all, ObservableList<V> shown, Function<V, T> map) {
var cache = new HashMap<V, T>();
ObservableList<T> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
cache.keySet().removeIf(t -> !all.contains(t));
setContent(
l1,
shown.stream()
.map(v -> {
if (!cache.containsKey(v)) {
cache.put(v, map.apply(v));
}
return cache.get(v);
})
.toList());
};
runnable.run();
shown.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
BindingsHelper.preserve(l1, all);
BindingsHelper.preserve(l1, shown);
return l1;
}
public static <V> ObservableList<V> orderedContentBinding(
ObservableList<V> l2, Comparator<V> comp, Observable... observables) {
return orderedContentBinding(
l2,
Bindings.createObjectBinding(
() -> {
return new Comparator<>() {
@Override
public int compare(V o1, V o2) {
return comp.compare(o1, o2);
}
};
},
observables));
}
public static <V> ObservableList<V> orderedContentBinding(
ObservableList<V> l2, ObservableValue<Comparator<V>> comp) {
ObservableList<V> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(l1, l2.stream().sorted(comp.getValue()).toList());
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
comp.addListener((observable, oldValue, newValue) -> {
runnable.run();
});
BindingsHelper.preserve(l1, l2);
return l1;
}
public static <V> ObservableList<V> filteredContentBinding(ObservableList<V> l2, Predicate<V> predicate) {
return filteredContentBinding(l2, new SimpleObjectProperty<>(predicate));
}
public static <V> ObservableList<V> filteredContentBinding(
ObservableList<V> l2, Predicate<V> predicate, Observable... observables) {
return filteredContentBinding(
l2,
Bindings.createObjectBinding(
() -> {
return new Predicate<>() {
@Override
public boolean test(V v) {
return predicate.test(v);
}
};
},
Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new)));
}
public static <V> ObservableList<V> filteredContentBinding(
ObservableList<V> l2, ObservableValue<Predicate<V>> predicate) {
ObservableList<V> l1 = FXCollections.observableList(new ArrayList<>());
Runnable runnable = () -> {
setContent(
l1,
predicate.getValue() != null
? l2.stream().filter(predicate.getValue()).toList()
: l2);
};
runnable.run();
l2.addListener((ListChangeListener<? super V>) c -> {
runnable.run();
});
predicate.addListener((c, o, n) -> {
runnable.run();
});
BindingsHelper.preserve(l1, l2);
return l1;
}
public static <T> void setContent(ObservableList<T> target, List<? extends T> newList) {
if (target.equals(newList)) {
return;
}
if (target.size() == 0) {
target.setAll(newList);
return;
}
if (newList.size() == 0) {
target.clear();
return;
}
var targetSet = new HashSet<>(target);
var newSet = new HashSet<>(newList);
// Only add missing element
if (target.size() + 1 == newList.size() && newSet.containsAll(targetSet)) {
var l = new HashSet<>(newSet);
l.removeAll(targetSet);
if (l.size() > 0) {
var found = l.iterator().next();
var index = newList.indexOf(found);
target.add(index, found);
return;
}
}
// Only remove not needed element
if (target.size() - 1 == newList.size() && targetSet.containsAll(newSet)) {
var l = new HashSet<>(targetSet);
l.removeAll(newSet);
if (l.size() > 0) {
target.remove(l.iterator().next());
return;
}
}
// Other cases are more difficult
target.setAll(newList);
}
}

View file

@ -85,7 +85,6 @@ public class PlatformThread {
ov.removeListener(invListenerMap.getOrDefault(listener, listener)); ov.removeListener(invListenerMap.getOrDefault(listener, listener));
} }
}; };
BindingsHelper.linkPersistently(obs, ov);
return obs; return obs;
} }
@ -272,7 +271,6 @@ public class PlatformThread {
ol.removeListener(invListenerMap.getOrDefault(listener, listener)); ol.removeListener(invListenerMap.getOrDefault(listener, listener));
} }
}; };
BindingsHelper.linkPersistently(obs, ol);
return obs; return obs;
} }

View file

@ -41,7 +41,7 @@ public class Shortcuts {
DISPLAY_SHORTCUTS.put(region, comb); DISPLAY_SHORTCUTS.put(region, comb);
AtomicReference<Scene> scene = new AtomicReference<>(); AtomicReference<Scene> scene = new AtomicReference<>();
SimpleChangeListener.apply(region.sceneProperty(), s -> { region.sceneProperty().subscribe(s -> {
if (Objects.equals(s, scene.get())) { if (Objects.equals(s, scene.get())) {
return; return;
} }

View file

@ -1,19 +0,0 @@
package io.xpipe.app.fxcomps.util;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
@FunctionalInterface
public interface SimpleChangeListener<T> {
static <T> void apply(ObservableValue<T> obs, SimpleChangeListener<T> cl) {
obs.addListener(cl.wrapped());
cl.onChange(obs.getValue());
}
void onChange(T val);
default ChangeListener<T> wrapped() {
return (observable, oldValue, newValue) -> this.onChange(newValue);
}
}

View file

@ -49,11 +49,13 @@ public class AppPrefs {
map(new SimpleBooleanProperty(true), "saveWindowLocation", Boolean.class); map(new SimpleBooleanProperty(true), "saveWindowLocation", Boolean.class);
final ObjectProperty<ExternalTerminalType> terminalType = final ObjectProperty<ExternalTerminalType> terminalType =
map(new SimpleObjectProperty<>(), "terminalType", ExternalTerminalType.class); map(new SimpleObjectProperty<>(), "terminalType", ExternalTerminalType.class);
final ObjectProperty<ExternalRdpClientType> rdpClientType =
map(new SimpleObjectProperty<>(), "rdpClientType", ExternalRdpClientType.class);
final DoubleProperty windowOpacity = map(new SimpleDoubleProperty(1.0), "windowOpacity", Double.class); final DoubleProperty windowOpacity = map(new SimpleDoubleProperty(1.0), "windowOpacity", Double.class);
final StringProperty customRdpClientCommand =
map(new SimpleStringProperty(null), "customRdpClientCommand", String.class);
final StringProperty customTerminalCommand = final StringProperty customTerminalCommand =
map(new SimpleStringProperty(""), "customTerminalCommand", String.class); map(new SimpleStringProperty(null), "customTerminalCommand", String.class);
final BooleanProperty preferTerminalTabs =
map(new SimpleBooleanProperty(true), "preferTerminalTabs", Boolean.class);
final BooleanProperty clearTerminalOnInit = final BooleanProperty clearTerminalOnInit =
map(new SimpleBooleanProperty(true), "clearTerminalOnInit", Boolean.class); map(new SimpleBooleanProperty(true), "clearTerminalOnInit", Boolean.class);
public final BooleanProperty disableCertutilUse = public final BooleanProperty disableCertutilUse =
@ -104,7 +106,7 @@ public class AppPrefs {
map(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class); map(new SimpleBooleanProperty(false), "developerDisableGuiRestrictions", Boolean.class);
private final ObservableBooleanValue developerDisableGuiRestrictionsEffective = private final ObservableBooleanValue developerDisableGuiRestrictionsEffective =
bindDeveloperTrue(developerDisableGuiRestrictions); bindDeveloperTrue(developerDisableGuiRestrictions);
private final ObjectProperty<SupportedLocale> language = final ObjectProperty<SupportedLocale> language =
map(new SimpleObjectProperty<>(SupportedLocale.ENGLISH), "language", SupportedLocale.class); map(new SimpleObjectProperty<>(SupportedLocale.ENGLISH), "language", SupportedLocale.class);
@Getter @Getter
@ -139,6 +141,7 @@ public class AppPrefs {
new AppearanceCategory(), new AppearanceCategory(),
new TerminalCategory(), new TerminalCategory(),
new EditorCategory(), new EditorCategory(),
new RdpCategory(),
new SyncCategory(), new SyncCategory(),
new VaultCategory(), new VaultCategory(),
new LocalShellCategory(), new LocalShellCategory(),
@ -357,10 +360,18 @@ public class AppPrefs {
return terminalType; return terminalType;
} }
public ObservableValue<ExternalRdpClientType> rdpClientType() {
return rdpClientType;
}
public ObservableValue<String> customTerminalCommand() { public ObservableValue<String> customTerminalCommand() {
return customTerminalCommand; return customTerminalCommand;
} }
public ObservableValue<String> customRdpClientCommand() {
return customRdpClientCommand;
}
public ObservableValue<Path> storageDirectory() { public ObservableValue<Path> storageDirectory() {
return storageDirectory; return storageDirectory;
} }
@ -411,7 +422,12 @@ public class AppPrefs {
if (externalEditor.get() == null) { if (externalEditor.get() == null) {
ExternalEditorType.detectDefault(); ExternalEditorType.detectDefault();
} }
terminalType.set(ExternalTerminalType.determineDefault(terminalType.get())); terminalType.set(ExternalTerminalType.determineDefault(terminalType.get()));
if (rdpClientType.get() == null) {
rdpClientType.setValue(ExternalRdpClientType.determineDefault());
}
} }
public Comp<?> getCustomComp(String id) { public Comp<?> getCustomComp(String id) {

View file

@ -3,7 +3,6 @@ package io.xpipe.app.prefs;
import io.xpipe.app.core.AppFont; import io.xpipe.app.core.AppFont;
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.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
@ -28,7 +27,7 @@ public class AppPrefsComp extends SimpleComp {
.createRegion(); .createRegion();
})); }));
var pfxSp = new ScrollPane(); var pfxSp = new ScrollPane();
SimpleChangeListener.apply(AppPrefs.get().getSelectedCategory(), val -> { AppPrefs.get().getSelectedCategory().subscribe(val -> {
PlatformThread.runLaterIfNeeded(() -> { PlatformThread.runLaterIfNeeded(() -> {
pfxSp.setContent(map.get(val)); pfxSp.setContent(map.get(val));
}); });

View file

@ -6,7 +6,6 @@ import io.xpipe.app.fxcomps.Comp;
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.PlatformThread; import io.xpipe.app.fxcomps.util.PlatformThread;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
import javafx.css.PseudoClass; import javafx.css.PseudoClass;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.control.Button; import javafx.scene.control.Button;
@ -27,7 +26,7 @@ public class AppPrefsSidebarComp extends SimpleComp {
.apply(struc -> { .apply(struc -> {
struc.get().setTextAlignment(TextAlignment.LEFT); struc.get().setTextAlignment(TextAlignment.LEFT);
struc.get().setAlignment(Pos.CENTER_LEFT); struc.get().setAlignment(Pos.CENTER_LEFT);
SimpleChangeListener.apply(AppPrefs.get().getSelectedCategory(), val -> { AppPrefs.get().getSelectedCategory().subscribe(val -> {
struc.get().pseudoClassStateChanged(SELECTED, appPrefsCategory.equals(val)); struc.get().pseudoClassStateChanged(SELECTED, appPrefsCategory.equals(val));
}); });
}) })
@ -36,13 +35,15 @@ public class AppPrefsSidebarComp extends SimpleComp {
.toList(); .toList();
var vbox = new VerticalComp(buttons).styleClass("sidebar"); var vbox = new VerticalComp(buttons).styleClass("sidebar");
vbox.apply(struc -> { vbox.apply(struc -> {
SimpleChangeListener.apply(PlatformThread.sync(AppPrefs.get().getSelectedCategory()), val -> { AppPrefs.get().getSelectedCategory().subscribe(val -> {
var index = val != null ? AppPrefs.get().getCategories().indexOf(val) : 0; PlatformThread.runLaterIfNeeded(() -> {
if (index >= struc.get().getChildren().size()) { var index = val != null ? AppPrefs.get().getCategories().indexOf(val) : 0;
return; if (index >= struc.get().getChildren().size()) {
} return;
}
((Button) struc.get().getChildren().get(index)).fire(); ((Button) struc.get().getChildren().get(index)).fire();
});
}); });
}); });
return vbox.createRegion(); return vbox.createRegion();

View file

@ -2,12 +2,21 @@ package io.xpipe.app.prefs;
import atlantafx.base.controls.ProgressSliderSkin; import atlantafx.base.controls.ProgressSliderSkin;
import atlantafx.base.theme.Styles; import atlantafx.base.theme.Styles;
import io.xpipe.app.comp.base.ButtonComp;
import io.xpipe.app.core.AppI18n;
import io.xpipe.app.core.AppTheme; import io.xpipe.app.core.AppTheme;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.ChoiceComp; import io.xpipe.app.fxcomps.impl.ChoiceComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp;
import io.xpipe.app.fxcomps.impl.IntFieldComp; import io.xpipe.app.fxcomps.impl.IntFieldComp;
import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.OptionsBuilder;
import javafx.geometry.Pos;
import javafx.scene.control.Slider; import javafx.scene.control.Slider;
import org.kordamp.ikonli.javafx.FontIcon;
import java.util.Arrays;
import java.util.List;
public class AppearanceCategory extends AppPrefsCategory { public class AppearanceCategory extends AppPrefsCategory {
@ -16,12 +25,28 @@ public class AppearanceCategory extends AppPrefsCategory {
return "appearance"; return "appearance";
} }
private Comp<?> languageChoice() {
var prefs = AppPrefs.get();
var c = ChoiceComp.ofTranslatable(prefs.language, Arrays.asList(SupportedLocale.values()), false);
var visit = new ButtonComp(AppI18n.observable("translate"), new FontIcon("mdi2w-web"), () -> {
Hyperlinks.open(Hyperlinks.TRANSLATE);
});
return new HorizontalComp(List.of(c, visit)).apply(struc -> {
struc.get().setAlignment(Pos.CENTER_LEFT);
struc.get().setSpacing(10);
});
}
@Override @Override
protected Comp<?> create() { protected Comp<?> create() {
var prefs = AppPrefs.get(); var prefs = AppPrefs.get();
return new OptionsBuilder() return new OptionsBuilder()
.addTitle("uiOptions") .addTitle("uiOptions")
.sub(new OptionsBuilder() .sub(new OptionsBuilder()
.nameAndDescription("language")
.addComp(
languageChoice(),
prefs.language)
.nameAndDescription("theme") .nameAndDescription("theme")
.addComp( .addComp(
ChoiceComp.ofTranslatable(prefs.theme, AppTheme.Theme.ALL, false) ChoiceComp.ofTranslatable(prefs.theme, AppTheme.Theme.ALL, false)

View file

@ -0,0 +1,163 @@
package io.xpipe.app.prefs;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.util.*;
import io.xpipe.core.process.CommandBuilder;
import io.xpipe.core.process.OsType;
import lombok.Value;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Supplier;
public interface ExternalRdpClientType extends PrefsChoiceValue {
@Value
class LaunchConfiguration {
String title;
RdpConfig config;
UUID storeId;
SecretRetrievalStrategy password;
}
abstract class PathCheckType extends ExternalApplicationType.PathApplication implements ExternalRdpClientType {
public PathCheckType(String id, String executable, boolean explicityAsync) {
super(id, executable, explicityAsync);
}
}
abstract class MacOsType extends ExternalApplicationType.MacApplication implements ExternalRdpClientType {
public MacOsType(String id, String applicationName) {
super(id, applicationName);
}
}
void launch(LaunchConfiguration configuration) throws Exception;
default Path writeConfig(RdpConfig input) throws Exception {
var file = LocalShell.getShell().getSystemTemporaryDirectory().join("exec-" + ScriptHelper.getScriptId() + ".rdp");
var string = input.toString();
Files.writeString(file.toLocalPath(), string);
return file.toLocalPath();
}
ExternalRdpClientType MSTSC = new PathCheckType("app.mstsc", "mstsc.exe", true) {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
var adaptedRdpConfig = getAdaptedConfig(configuration);
var file = writeConfig(adaptedRdpConfig);
LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().add(executable).addFile(file.toString()));
}
private RdpConfig getAdaptedConfig(LaunchConfiguration configuration) throws Exception {
var input = configuration.getConfig();
if (input.get("password 51").isPresent()) {
return input;
}
var address = input.get("full address")
.map(typedValue -> typedValue.getValue())
.orElse("?");
var pass = SecretManager.retrieve(
configuration.getPassword(), "Password for " + address, configuration.getStoreId(), 0);
if (pass == null) {
return input;
}
var adapted = input.overlay(Map.of(
"password 51",
new RdpConfig.TypedValue("b", encrypt(pass.getSecretValue())),
"prompt for credentials",
new RdpConfig.TypedValue("i", "0")));
return adapted;
}
private String encrypt(String password) throws Exception {
var ps = LocalShell.getLocalPowershell();
var cmd = ps.command(
"(\"" + password + "\" | ConvertTo-SecureString -AsPlainText -Force) | ConvertFrom-SecureString;");
cmd.setSensitive();
return cmd.readStdoutOrThrow();
}
};
ExternalRdpClientType REMMINA = new PathCheckType("app.remmina", "remmina", true) {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
var file = writeConfig(configuration.getConfig());
LocalShell.getShell().executeSimpleCommand(CommandBuilder.of().add(executable).add("-c").addFile(file.toString()));
}
};
ExternalRdpClientType MICROSOFT_REMOTE_DESKTOP_MACOS_APP = new MacOsType("app.microsoftRemoteDesktopApp", "Microsoft Remote Desktop.app") {
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
var file = writeConfig(configuration.getConfig());
LocalShell.getShell()
.executeSimpleCommand(CommandBuilder.of()
.add("open", "-a")
.addQuoted("Microsoft Remote Desktop.app")
.addFile(file.toString()));
}
};
class CustomType extends ExternalApplicationType implements ExternalRdpClientType {
public CustomType() {
super("app.custom");
}
@Override
public void launch(LaunchConfiguration configuration) throws Exception {
var customCommand = AppPrefs.get().customRdpClientCommand().getValue();
if (customCommand == null || customCommand.isBlank()) {
throw ErrorEvent.expected(new IllegalStateException("No custom RDP command specified"));
}
var format = customCommand.toLowerCase(Locale.ROOT).contains("$file") ? customCommand : customCommand + " $FILE";
ExternalApplicationHelper.startAsync(CommandBuilder.of().add(ExternalApplicationHelper.replaceFileArgument(format, "FILE", writeConfig(configuration.getConfig()).toString())));
}
@Override
public boolean isAvailable() {
return true;
}
}
ExternalRdpClientType CUSTOM = new CustomType();
List<ExternalRdpClientType> WINDOWS_CLIENTS = List.of(MSTSC);
List<ExternalRdpClientType> LINUX_CLIENTS = List.of(REMMINA);
List<ExternalRdpClientType> MACOS_CLIENTS = List.of(MICROSOFT_REMOTE_DESKTOP_MACOS_APP);
@SuppressWarnings("TrivialFunctionalExpressionUsage")
List<ExternalRdpClientType> ALL = ((Supplier<List<ExternalRdpClientType>>) () -> {
var all = new ArrayList<ExternalRdpClientType>();
if (OsType.getLocal().equals(OsType.WINDOWS)) {
all.addAll(WINDOWS_CLIENTS);
}
if (OsType.getLocal().equals(OsType.LINUX)) {
all.addAll(LINUX_CLIENTS);
}
if (OsType.getLocal().equals(OsType.MACOS)) {
all.addAll(MACOS_CLIENTS);
}
all.add(CUSTOM);
return all;
}).get();
static ExternalRdpClientType determineDefault() {
return ALL.stream()
.filter(t -> !t.equals(CUSTOM))
.filter(t -> t.isAvailable())
.findFirst()
.orElse(null);
}
}

View file

@ -0,0 +1,32 @@
package io.xpipe.app.prefs;
import io.xpipe.app.ext.PrefsChoiceValue;
import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.ChoiceComp;
import io.xpipe.app.fxcomps.impl.TextFieldComp;
import io.xpipe.app.util.OptionsBuilder;
public class RdpCategory extends AppPrefsCategory {
@Override
protected String getId() {
return "rdp";
}
@Override
protected Comp<?> create() {
var prefs = AppPrefs.get();
return new OptionsBuilder()
.addTitle("rdpConfiguration")
.sub(new OptionsBuilder()
.nameAndDescription("rdpClient")
.addComp(ChoiceComp.ofTranslatable(
prefs.rdpClientType, PrefsChoiceValue.getSupported(ExternalRdpClientType.class), false))
.nameAndDescription("customRdpClientCommand")
.addComp(new TextFieldComp(prefs.customRdpClientCommand, true)
.apply(struc -> struc.get().setPromptText("myrdpclient -c $FILE"))
.hide(prefs.rdpClientType.isNotEqualTo(ExternalRdpClientType.CUSTOM)))
)
.buildComp();
}
}

View file

@ -11,8 +11,8 @@ import java.util.Locale;
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter
public enum SupportedLocale implements PrefsChoiceValue { public enum SupportedLocale implements PrefsChoiceValue {
ENGLISH(Locale.ENGLISH, "english"); ENGLISH(Locale.ENGLISH, "english"),
// GERMAN(Locale.GERMAN, "german"); GERMAN(Locale.GERMAN, "german");
private final Locale locale; private final Locale locale;
private final String id; private final String id;

View file

@ -8,7 +8,6 @@ import io.xpipe.app.fxcomps.impl.ChoiceComp;
import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.HorizontalComp;
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.BindingsHelper;
import io.xpipe.app.terminal.ExternalTerminalType; import io.xpipe.app.terminal.ExternalTerminalType;
import io.xpipe.app.util.Hyperlinks; import io.xpipe.app.util.Hyperlinks;
import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.OptionsBuilder;
@ -66,14 +65,14 @@ public class TerminalCategory extends AppPrefsCategory {
Hyperlinks.open(t.getWebsite()); Hyperlinks.open(t.getWebsite());
}); });
var visitVisible = BindingsHelper.persist(Bindings.createBooleanBinding(() -> { var visitVisible = Bindings.createBooleanBinding(() -> {
var t = prefs.terminalType().getValue(); var t = prefs.terminalType().getValue();
if (t == null || t.getWebsite() == null) { if (t == null || t.getWebsite() == null) {
return false; return false;
} }
return true; return true;
}, prefs.terminalType())); }, prefs.terminalType());
visit.visible(visitVisible); visit.visible(visitVisible);
return new HorizontalComp(List.of(c, visit)).apply(struc -> { return new HorizontalComp(List.of(c, visit)).apply(struc -> {
@ -108,22 +107,6 @@ public class TerminalCategory extends AppPrefsCategory {
.apply(struc -> struc.get().setPromptText("myterminal -e $CMD")) .apply(struc -> struc.get().setPromptText("myterminal -e $CMD"))
.hide(prefs.terminalType.isNotEqualTo(ExternalTerminalType.CUSTOM))) .hide(prefs.terminalType.isNotEqualTo(ExternalTerminalType.CUSTOM)))
.addComp(terminalTest) .addComp(terminalTest)
.name("preferTerminalTabs")
.description(Bindings.createStringBinding(
() -> {
var disabled = prefs.terminalType().getValue() != null
&& !prefs.terminalType.get().supportsTabs();
return !disabled
? AppI18n.get("preferTerminalTabs")
: AppI18n.get(
"preferTerminalTabsDisabled",
prefs.terminalType()
.getValue()
.toTranslatedString()
.getValue());
},
prefs.terminalType()))
.addToggle(prefs.preferTerminalTabs)
.disable(Bindings.createBooleanBinding( .disable(Bindings.createBooleanBinding(
() -> { () -> {
return prefs.terminalType().getValue() != null return prefs.terminalType().getValue() != null

View file

@ -27,7 +27,7 @@ public class UpdateCheckComp extends SimpleComp {
XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate())); XPipeDistributionType.get().getUpdateHandler().getPreparedUpdate()));
} }
private void restart() { private void performUpdateAndRestart() {
XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent(); XPipeDistributionType.get().getUpdateHandler().refreshUpdateCheckSilent();
UpdateAvailableAlert.showIfNeeded(); UpdateAvailableAlert.showIfNeeded();
} }
@ -82,7 +82,7 @@ public class UpdateCheckComp extends SimpleComp {
return new TileButtonComp(name, description, graphic, actionEvent -> { return new TileButtonComp(name, description, graphic, actionEvent -> {
actionEvent.consume(); actionEvent.consume();
if (updateReady.getValue()) { if (updateReady.getValue()) {
restart(); performUpdateAndRestart();
return; return;
} }

View file

@ -18,6 +18,12 @@ public class UpdateAvailableAlert {
return; return;
} }
// Check whether we still have the latest version prepared
uh.refreshUpdateCheckSilent();
if (uh.getPreparedUpdate().getValue() == null) {
return;
}
var u = uh.getPreparedUpdate().getValue(); var u = uh.getPreparedUpdate().getValue();
var update = AppWindowHelper.showBlockingAlert(alert -> { var update = AppWindowHelper.showBlockingAlert(alert -> {
alert.setTitle(AppI18n.get("updateReadyAlertTitle")); alert.setTitle(AppI18n.get("updateReadyAlertTitle"));

View file

@ -1,84 +0,0 @@
package io.xpipe.app.update;
import io.xpipe.app.core.AppProperties;
import io.xpipe.app.issue.ErrorEvent;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.beacon.XPipeInstance;
import io.xpipe.core.store.ShellStore;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
public class XPipeInstanceHelper {
public static UUID getInstanceId() {
var file = AppProperties.get().getDataDir().resolve("instance");
if (!Files.exists(file)) {
var id = UUID.randomUUID();
try {
Files.writeString(file, id.toString());
} catch (IOException e) {
ErrorEvent.fromThrowable(e).handle();
}
return id;
}
try {
return UUID.fromString(Files.readString(file));
} catch (Exception e) {
ErrorEvent.fromThrowable(e).handle();
return UUID.randomUUID();
}
}
public static boolean isSupported(ShellStore host) {
try (var pc = host.control().start();
var cmd = pc.command("xpipe")) {
cmd.discardOrThrow();
return true;
} catch (Exception e) {
return false;
}
}
public static Optional<XPipeInstance> getInstance(ShellStore store) {
if (!isSupported(store)) {
return Optional.empty();
}
// try (BeaconClient beaconClient = ProcessBeaconClient.create(store)) {
// beaconClient.sendRequest(InstanceExchange.Request.builder().build());
// InstanceExchange.Response response = beaconClient.receiveResponse();
// return Optional.of(response.getInstance());
// } catch (Exception e) {
// return Optional.empty();
// }
return Optional.empty();
}
public static XPipeInstance refresh() {
Map<ShellStore, Optional<XPipeInstance>> map = DataStorage.get().getStoreEntries().stream()
.filter(entry -> entry.getStore() instanceof ShellStore)
.collect(Collectors.toMap(
entry -> entry.getStore().asNeeded(),
entry -> getInstance(entry.getStore().asNeeded())));
var adjacent = map.entrySet().stream()
.filter(shellStoreOptionalEntry ->
shellStoreOptionalEntry.getValue().isPresent())
.collect(Collectors.toMap(
entry -> entry.getKey(), entry -> entry.getValue().get()));
var reachable = adjacent.values().stream()
.map(XPipeInstance::getReachable)
.flatMap(Collection::stream)
.toList();
var id = getInstanceId();
var name = "test";
return new XPipeInstance(id, name, adjacent, reachable);
}
}

View file

@ -4,7 +4,6 @@ import io.xpipe.app.comp.store.StoreCategoryWrapper;
import io.xpipe.app.comp.store.StoreViewState; import io.xpipe.app.comp.store.StoreViewState;
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.property.Property; import javafx.beans.property.Property;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.control.ComboBox; import javafx.scene.control.ComboBox;
@ -28,7 +27,7 @@ public class DataStoreCategoryChoiceComp extends SimpleComp {
@Override @Override
protected Region createSimple() { protected Region createSimple() {
SimpleChangeListener.apply(external, newValue -> { external.subscribe(newValue -> {
if (newValue == null) { if (newValue == null) {
value.setValue(root); value.setValue(root);
} else if (root == null) { } else if (root == null) {

View file

@ -9,6 +9,7 @@ public class Hyperlinks {
public static final String PRIVACY = "https://docs.xpipe.io/privacy-policy"; public static final String PRIVACY = "https://docs.xpipe.io/privacy-policy";
public static final String EULA = "https://docs.xpipe.io/end-user-license-agreement"; public static final String EULA = "https://docs.xpipe.io/end-user-license-agreement";
public static final String SECURITY = "https://docs.xpipe.io/security"; public static final String SECURITY = "https://docs.xpipe.io/security";
public static final String TRANSLATE = "https://github.com/xpipe-io/xpipe/lang";
public static final String DISCORD = "https://discord.gg/8y89vS8cRb"; public static final String DISCORD = "https://discord.gg/8y89vS8cRb";
public static final String SLACK = public static final String SLACK =
"https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg"; "https://join.slack.com/t/XPipe/shared_invite/zt-1awjq0t5j-5i4UjNJfNe1VN4b_auu6Cg";

View file

@ -297,7 +297,7 @@ public class OptionsBuilder {
public OptionsBuilder longDescription(String descriptionKey) { public OptionsBuilder longDescription(String descriptionKey) {
finishCurrent(); finishCurrent();
longDescription = AppI18n.getInstance().getMarkdownDocumentation(descriptionKey); longDescription = AppI18n.get().getMarkdownDocumentation(descriptionKey);
return this; return this;
} }

View file

@ -0,0 +1,64 @@
package io.xpipe.app.util;
import lombok.Value;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@Value
public class RdpConfig {
public static RdpConfig parseFile(String file) throws IOException {
var content = Files.readString(Path.of(file));
return parseContent(content);
}
public static RdpConfig parseContent(String content) {
var map = new LinkedHashMap<String, TypedValue>();
content.lines().forEach(s -> {
var split = s.split(":");
if (split.length < 2) {
return;
}
if (split.length == 2) {
map.put(split[0].trim(), new RdpConfig.TypedValue("s", split[1].trim()));
}
if (split.length == 3) {
map.put(split[0].trim(), new RdpConfig.TypedValue(split[1].trim(), split[2].trim()));
}
});
return new RdpConfig(map);
}
@Value
public static class TypedValue {
String type;
String value;
}
Map<String, TypedValue> content;
public RdpConfig overlay(Map<String, TypedValue> override) {
var newMap = new LinkedHashMap<>(content);
newMap.putAll(override);
return new RdpConfig(newMap);
}
public String toString() {
return content.entrySet().stream().map(e -> {
return e.getKey() + ":" + e.getValue().getType() + ":" + e.getValue().getValue();
}).collect(Collectors.joining("\n"));
}
public Optional<TypedValue> get(String key) {
return Optional.ofNullable(content.get(key));
}
}

View file

@ -7,7 +7,6 @@ import io.xpipe.app.core.AppI18n;
import io.xpipe.app.ext.ScanProvider; import io.xpipe.app.ext.ScanProvider;
import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.Comp;
import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp; import io.xpipe.app.fxcomps.impl.DataStoreChoiceComp;
import io.xpipe.app.fxcomps.util.SimpleChangeListener;
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.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntry;
@ -168,7 +167,7 @@ public class ScanAlert {
}) })
.padding(new Insets(20)); .padding(new Insets(20));
SimpleChangeListener.apply(entry, newValue -> { entry.subscribe(newValue -> {
selected.clear(); selected.clear();
stackPane.getChildren().clear(); stackPane.getChildren().clear();

View file

@ -77,6 +77,7 @@ open module io.xpipe.app {
requires jdk.management; requires jdk.management;
requires jdk.management.agent; requires jdk.management.agent;
requires net.steppschuh.markdowngenerator; requires net.steppschuh.markdowngenerator;
requires com.shinyhut.vernacular;
// Required by extensions // Required by extensions
requires java.security.jgss; requires java.security.jgss;
@ -150,6 +151,5 @@ open module io.xpipe.app {
TerminalWaitExchangeImpl, TerminalWaitExchangeImpl,
TerminalLaunchExchangeImpl, TerminalLaunchExchangeImpl,
QueryStoreExchangeImpl, QueryStoreExchangeImpl,
InstanceExchangeImpl,
VersionExchangeImpl; VersionExchangeImpl;
} }

View file

@ -78,6 +78,7 @@ browseInternalStorage=Browse internal storage
addTunnel=Tunnel ... addTunnel=Tunnel ...
addScript=Script ... addScript=Script ...
addHost=Remote Host ... addHost=Remote Host ...
addVisual=Visual ...
addShell=Shell Environment ... addShell=Shell Environment ...
addCommand=Custom Command ... addCommand=Custom Command ...
addAutomatically=Search Automatically ... addAutomatically=Search Automatically ...
@ -111,8 +112,6 @@ newLine=Newline
crlf=CRLF (Windows) crlf=CRLF (Windows)
lf=LF (Linux) lf=LF (Linux)
none=None none=None
expand=Expand
accessSubConnections=Access sub connections
common=Common common=Common
key=Key key=Key
color=Color color=Color
@ -140,7 +139,7 @@ test=Test
lockCreationAlertTitle=Set passphrase lockCreationAlertTitle=Set passphrase
lockCreationAlertHeader=Set your new master passphrase lockCreationAlertHeader=Set your new master passphrase
finish=Finish finish=Finish
error=An error occurred error=Error
downloadStageDescription=Downloads files to your local machine, so you can drag and drop them into your native desktop environment. downloadStageDescription=Downloads files to your local machine, so you can drag and drop them into your native desktop environment.
ok=Ok ok=Ok
search=Search search=Search

View file

@ -43,11 +43,6 @@
-fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(12, 11, 11) 40%, rgb(32, 32, 40) 50%, rgb(35, 29, 29) 100%); -fx-background-color: linear-gradient(from 100% 0% to 0% 100%, rgb(12, 11, 11) 40%, rgb(32, 32, 40) 50%, rgb(35, 29, 29) 100%);
} }
.store-header-bar .menu-button:hover, .root:key-navigation .store-header-bar .menu-button:focused {
-fx-background-color: -color-bg-default;
-fx-border-color: -color-fg-default;
}
.store-header-bar .menu-button > * { .store-header-bar .menu-button > * {
-fx-text-fill: -color-bg-default; -fx-text-fill: -color-bg-default;
} }
@ -61,15 +56,20 @@
-fx-border-width: 4; -fx-border-width: 4;
} }
.store-header-bar .menu-button:hover > *, .root:key-navigation .store-header-bar .menu-button:focused > * { .root .store-header-bar .menu-button:hover, .root:key-navigation .store-header-bar .menu-button:focused {
-fx-background-color: -color-bg-default;
-fx-border-color: -color-fg-default;
}
.root .store-header-bar .menu-button:hover > *, .root:key-navigation .store-header-bar .menu-button:focused > * {
-fx-text-fill: -color-fg-default; -fx-text-fill: -color-fg-default;
} }
.store-header-bar .menu-button:hover > * > .ikonli-font-icon, .root:key-navigation .store-header-bar .menu-button:focused > * > .ikonli-font-icon { .root .store-header-bar .menu-button:hover > * > .ikonli-font-icon, .root:key-navigation .store-header-bar .menu-button:focused > * > .ikonli-font-icon {
-fx-icon-color: -color-fg-default; -fx-icon-color: -color-fg-default;
} }
.store-header-bar .menu-button:hover .arrow, .root:key-navigation .store-header-bar .menu-button:focused .arrow { .root .store-header-bar .menu-button:hover .arrow, .root:key-navigation .store-header-bar .menu-button:focused .arrow {
-fx-border-color: -color-fg-default; -fx-border-color: -color-fg-default;
-fx-border-width: 4; -fx-border-width: 4;
} }

View file

@ -2,19 +2,11 @@
-fx-background-color: transparent; -fx-background-color: transparent;
} }
.root:pretty:dark.background { .root:dark.background {
-fx-background-color: linear-gradient(from 100% 0% to 0% 100%, derive(-color-bg-default, 5%) 40%, derive(-color-bg-default, 2%) 50%, derive(-color-bg-default, 5%) 100%);
}
.root:performance:dark.background {
-fx-background-color: derive(-color-bg-default, 5%); -fx-background-color: derive(-color-bg-default, 5%);
} }
.root:pretty:light.background { .root:light.background {
-fx-background-color: linear-gradient(from 100% 0% to 0% 100%, derive(-color-bg-default, -9%) 40%, derive(-color-bg-default, 1%) 50%, derive(-color-bg-default, -9%) 100%);
}
.root:performance:light.background {
-fx-background-color: derive(-color-bg-default, -9%); -fx-background-color: derive(-color-bg-default, -9%);
} }

View file

@ -11,10 +11,8 @@ import com.fasterxml.jackson.databind.node.TextNode;
import io.xpipe.beacon.exchange.MessageExchanges; import io.xpipe.beacon.exchange.MessageExchanges;
import io.xpipe.beacon.exchange.data.ClientErrorMessage; import io.xpipe.beacon.exchange.data.ClientErrorMessage;
import io.xpipe.beacon.exchange.data.ServerErrorMessage; import io.xpipe.beacon.exchange.data.ServerErrorMessage;
import io.xpipe.core.store.ShellStore;
import io.xpipe.core.util.Deobfuscator; import io.xpipe.core.util.Deobfuscator;
import io.xpipe.core.util.JacksonMapper; import io.xpipe.core.util.JacksonMapper;
import io.xpipe.core.util.ProxyManagerProvider;
import lombok.Builder; import lombok.Builder;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
@ -62,51 +60,6 @@ public class BeaconClient implements AutoCloseable {
return client; return client;
} }
public static BeaconClient connectProxy(ShellStore proxy) throws Exception {
var control = proxy.control().start();
if (!ProxyManagerProvider.get().setup(control)) {
throw new IOException("XPipe connector required to perform operation");
}
var command = control.command("xpipe beacon --raw").start();
command.discardErr();
return new BeaconClient(command, command.getStdout(), command.getStdin()) {
// {
// new Thread(() -> {
// while (true) {
// if (!control.isRunning()) {
// close();
// }
// }
// })
// }
@Override
public void close() throws ConnectorException {
try {
getRawInputStream().readAllBytes();
} catch (IOException ex) {
throw new ConnectorException(ex);
}
super.close();
}
@Override
public <T extends ResponseMessage> T receiveResponse()
throws ConnectorException, ClientException, ServerException {
try {
sendEOF();
getRawOutputStream().close();
} catch (IOException ex) {
throw new ConnectorException(ex);
}
return super.receiveResponse();
}
};
}
public static Optional<BeaconClient> tryEstablishConnection(ClientInformation information) { public static Optional<BeaconClient> tryEstablishConnection(ClientInformation information) {
try { try {
return Optional.of(establishConnection(information)); return Optional.of(establishConnection(information));

View file

@ -1,17 +0,0 @@
package io.xpipe.beacon;
import io.xpipe.core.store.ShellStore;
import lombok.Value;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@Value
public class XPipeInstance {
UUID uuid;
String name;
Map<ShellStore, XPipeInstance> adjacent;
List<XPipeInstance> reachable;
}

View file

@ -1,31 +0,0 @@
package io.xpipe.beacon.exchange.cli;
import io.xpipe.beacon.RequestMessage;
import io.xpipe.beacon.ResponseMessage;
import io.xpipe.beacon.XPipeInstance;
import io.xpipe.beacon.exchange.MessageExchange;
import lombok.Builder;
import lombok.NonNull;
import lombok.Value;
import lombok.extern.jackson.Jacksonized;
public class InstanceExchange implements MessageExchange {
@Override
public String getId() {
return "instance";
}
@Jacksonized
@Builder
@Value
public static class Request implements RequestMessage {}
@Jacksonized
@Builder
@Value
public static class Response implements ResponseMessage {
@NonNull
XPipeInstance instance;
}
}

View file

@ -27,7 +27,6 @@ open module io.xpipe.beacon {
SinkExchange, SinkExchange,
DrainExchange, DrainExchange,
LaunchExchange, LaunchExchange,
InstanceExchange,
EditStoreExchange, EditStoreExchange,
StoreProviderListExchange, StoreProviderListExchange,
ModeExchange, ModeExchange,

View file

@ -104,6 +104,7 @@ project.ext {
if (signingPassword == null) { if (signingPassword == null) {
signingPassword = '' signingPassword = ''
} }
deeplApiKey = findProperty('DEEPL_API_KEY')
} }
if (org.gradle.internal.os.OperatingSystem.current() == org.gradle.internal.os.OperatingSystem.LINUX) { if (org.gradle.internal.os.OperatingSystem.current() == org.gradle.internal.os.OperatingSystem.LINUX) {

View file

@ -3,6 +3,7 @@ package io.xpipe.core.store;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.NonNull; import lombok.NonNull;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -23,6 +24,10 @@ public final class FilePath {
} }
} }
public Path toLocalPath() {
return Path.of(value);
}
public String toString() { public String toString() {
return value; return value;
} }

View file

@ -1,12 +1,6 @@
package io.xpipe.core.store; package io.xpipe.core.store;
import io.xpipe.core.process.ProcessControl;
public interface LaunchableStore extends DataStore { public interface LaunchableStore extends DataStore {
default boolean canLaunch() { default void launch() throws Exception {}
return true;
}
ProcessControl prepareLaunchCommand() throws Exception;
} }

View file

@ -14,7 +14,6 @@ public interface ShellStore extends DataStore, LaunchableStore, FileSystemStore,
return new ConnectionFileSystem(control(), this); return new ConnectionFileSystem(control(), this);
} }
@Override
default ProcessControl prepareLaunchCommand() { default ProcessControl prepareLaunchCommand() {
return control(); return control();
} }

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