diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java index ac4b455f..f6d3bd3b 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserBreadcrumbBar.java @@ -3,7 +3,6 @@ package io.xpipe.app.browser; import atlantafx.base.controls.Breadcrumbs; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.core.store.FileNames; import javafx.scene.Node; import javafx.scene.control.Button; @@ -40,7 +39,7 @@ public class BrowserBreadcrumbBar extends SimpleComp { var breadcrumbs = new Breadcrumbs(); breadcrumbs.setMinWidth(0); - SimpleChangeListener.apply(PlatformThread.sync(model.getCurrentPath()), val -> { + PlatformThread.sync(model.getCurrentPath()).subscribe( val -> { if (val == null) { breadcrumbs.setSelectedCrumb(null); return; diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java index f9a35de3..429a8a6e 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserComp.java @@ -16,7 +16,6 @@ import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ThreadHelper; @@ -284,7 +283,7 @@ public class BrowserComp extends SimpleComp { var id = UUID.randomUUID().toString(); tab.setId(id); - SimpleChangeListener.apply(tabs.skinProperty(), newValue -> { + tabs.skinProperty().subscribe(newValue -> { if (newValue != null) { Platform.runLater(() -> { Label l = (Label) tabs.lookup("#" + id + " .tab-label"); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java index f1846e14..d702ae9e 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserFilterComp.java @@ -5,7 +5,6 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.TextFieldComp; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; import javafx.beans.property.SimpleBooleanProperty; import javafx.geometry.Pos; @@ -47,7 +46,7 @@ public class BrowserFilterComp extends Comp { text.setMinWidth(0); Styles.toggleStyleClass(text, Styles.LEFT_PILL); - SimpleChangeListener.apply(filterString, val -> { + filterString.subscribe(val -> { if (val == null) { text.getStyleClass().remove(Styles.SUCCESS); } else { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java index 3fffcc61..120de20b 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserNavBar.java @@ -10,7 +10,6 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.StackComp; import io.xpipe.app.fxcomps.impl.TextFieldComp; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.ThreadHelper; import javafx.application.Platform; @@ -42,7 +41,7 @@ public class BrowserNavBar extends SimpleComp { @Override protected Region createSimple() { var path = new SimpleStringProperty(model.getCurrentPath().get()); - SimpleChangeListener.apply(model.getCurrentPath(), (newValue) -> { + model.getCurrentPath().subscribe((newValue) -> { path.set(newValue); }); path.addListener((observable, oldValue, newValue) -> { @@ -58,7 +57,7 @@ public class BrowserNavBar extends SimpleComp { .styleClass(Styles.CENTER_PILL) .styleClass("path-text") .apply(struc -> { - SimpleChangeListener.apply(struc.get().focusedProperty(), val -> { + struc.get().focusedProperty().subscribe(val -> { struc.get() .pseudoClassStateChanged( INVISIBLE, @@ -71,7 +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 // If we start a new tab with a directory set, we have to set the pseudo class one pulse later Platform.runLater(() -> { diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java index 3700cbf3..e4418f07 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java @@ -5,6 +5,7 @@ import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ListBoxViewComp; import io.xpipe.app.comp.base.TileButtonComp; import io.xpipe.app.core.AppFont; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.HorizontalComp; @@ -54,7 +55,8 @@ public class BrowserWelcomeComp extends SimpleComp { hbox.setSpacing(15); if (state == null) { - var header = new Label("Here you will be able to see where you left off last time."); + var header = new Label(); + header.textProperty().bind(AppI18n.observable("browserWelcomeEmpty")); vbox.getChildren().add(header); hbox.setPadding(new Insets(40, 40, 40, 50)); return new VBox(hbox); @@ -74,13 +76,14 @@ public class BrowserWelcomeComp extends SimpleComp { }); var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list); - var header = new LabelComp(Bindings.createStringBinding( - () -> { - return !empty.get() - ? "You were recently connected to the following systems:" - : "Here you will be able to see where you left off last time."; - }, - empty)) + var headerBinding = BindingsHelper.mappedBinding(empty,b -> { + if (b) { + return AppI18n.observable("browserWelcomeEmpty"); + } else { + return AppI18n.observable("browserWelcomeSystems"); + } + }); + var header = new LabelComp(headerBinding) .createRegion(); AppFont.setSize(header, 1); vbox.getChildren().add(header); diff --git a/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java index cd6136a8..dbaa87a1 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ButtonComp.java @@ -3,7 +3,6 @@ package io.xpipe.app.comp.base; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; @@ -50,7 +49,7 @@ public class ButtonComp extends Comp> { var graphic = getGraphic(); if (graphic instanceof FontIcon f) { // f.iconColorProperty().bind(button.textFillProperty()); - SimpleChangeListener.apply(button.fontProperty(), c -> { + button.fontProperty().subscribe(c -> { f.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels()); }); } diff --git a/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java index 183088c1..f950f0c1 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/DropdownComp.java @@ -5,7 +5,6 @@ import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.css.Size; import javafx.css.SizeUnits; import javafx.scene.control.Button; @@ -43,7 +42,7 @@ public class DropdownComp extends Comp> { .toList())); var graphic = new FontIcon("mdi2c-chevron-double-down"); - SimpleChangeListener.apply(button.fontProperty(), c -> { + button.fontProperty().subscribe(c -> { graphic.setIconSize((int) new Size(c.getSize(), SizeUnits.PT).pixels()); }); diff --git a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java index 6cd7c417..9ca87049 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/LazyTextFieldComp.java @@ -3,7 +3,6 @@ package io.xpipe.app.comp.base; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; import javafx.beans.property.SimpleStringProperty; import javafx.scene.control.TextField; @@ -65,7 +64,7 @@ public class LazyTextFieldComp extends Comp { sp.prefHeightProperty().bind(r.prefHeightProperty()); r.setDisable(true); - SimpleChangeListener.apply(currentValue, n -> { + currentValue.subscribe(n -> { PlatformThread.runLaterIfNeeded(() -> { // Check if control value is the same. Then don't set it as that might cause bugs if (Objects.equals(r.getText(), n) || (n == null && r.getText().isEmpty())) { diff --git a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java index da75f656..41dbf783 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MarkdownComp.java @@ -6,7 +6,6 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.util.Hyperlinks; @@ -59,7 +58,7 @@ public class MarkdownComp extends Comp> { var url = AppResources.getResourceURL(AppResources.XPIPE_MODULE, theme).orElseThrow(); wv.getEngine().setUserStyleSheetLocation(url.toString()); - SimpleChangeListener.apply(PlatformThread.sync(markdown), val -> { + PlatformThread.sync(markdown).subscribe(val -> { // Workaround for https://bugs.openjdk.org/browse/JDK-8199014 try { var file = Files.createTempFile(null, ".html"); diff --git a/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java b/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java index 852b0843..f865f2c8 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/MultiContentComp.java @@ -3,7 +3,6 @@ package io.xpipe.app.comp.base; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.value.ObservableValue; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; @@ -25,7 +24,7 @@ public class MultiContentComp extends SimpleComp { for (Map.Entry, ObservableValue> entry : content.entrySet()) { var region = entry.getKey().createRegion(); stack.getChildren().add(region); - SimpleChangeListener.apply(PlatformThread.sync(entry.getValue()), val -> { + PlatformThread.sync(entry.getValue()).subscribe(val -> { region.setManaged(val); region.setVisible(val); }); diff --git a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java index 34669005..5b095568 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/SystemStateComp.java @@ -5,7 +5,6 @@ import io.xpipe.app.comp.store.StoreEntryWrapper; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.core.process.ShellStoreState; import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; @@ -35,7 +34,7 @@ public class SystemStateComp extends SimpleComp { state)); var fi = new FontIcon(); fi.getStyleClass().add("inner-icon"); - SimpleChangeListener.apply(icon, val -> fi.setIconLiteral(val)); + icon.subscribe(val -> fi.setIconLiteral(val)); var border = new FontIcon("mdi2c-circle-outline"); border.getStyleClass().add("outer-icon"); @@ -63,7 +62,7 @@ public class SystemStateComp extends SimpleComp { """; 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().add(val == State.SUCCESS ? success : val == State.FAILURE ? failure : other); }); diff --git a/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java b/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java index 836e2d45..78da5d79 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/TileButtonComp.java @@ -5,7 +5,6 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; @@ -57,7 +56,7 @@ public class TileButtonComp extends Comp { text.setSpacing(2); var fi = new FontIcon(); - SimpleChangeListener.apply(PlatformThread.sync(icon), val -> { + PlatformThread.sync(icon).subscribe(val -> { fi.setIconLiteral(val); }); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java index 76dc1dfb..9a32e93f 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCreationComp.java @@ -12,7 +12,6 @@ import io.xpipe.app.ext.DataStoreProviders; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.ExceptionConverter; import io.xpipe.app.issue.TrackEvent; @@ -381,7 +380,7 @@ public class StoreCreationComp extends DialogComp { providerChoice.apply(GrowAugment.create(true, false)); providerChoice.onSceneAssign(struc -> struc.get().requestFocus()); - SimpleChangeListener.apply(provider, n -> { + provider.subscribe(n -> { if (n != null) { var d = n.guiDialog(existingEntry, store); var propVal = new SimpleValidator(); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java index 9f66920d..f8b35400 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryComp.java @@ -15,7 +15,6 @@ import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.*; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreColor; @@ -138,7 +137,7 @@ public abstract class StoreEntryComp extends SimpleComp { } protected void applyState(Node node) { - SimpleChangeListener.apply(PlatformThread.sync(wrapper.getValidity()), val -> { + PlatformThread.sync(wrapper.getValidity()).subscribe(val -> { switch (val) { case LOAD_FAILED -> { node.pseudoClassStateChanged(FAILED, true); diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java index 5eb91dee..e7d4d555 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListStatusComp.java @@ -9,7 +9,6 @@ import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.impl.FilterComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.OsType; import javafx.beans.binding.Bindings; @@ -36,7 +35,7 @@ public class StoreEntryListStatusComp extends SimpleComp { public StoreEntryListStatusComp() { this.sortMode = new SimpleObjectProperty<>(); - SimpleChangeListener.apply(StoreViewState.get().getActiveCategory(), val -> { + StoreViewState.get().getActiveCategory().subscribe(val -> { sortMode.setValue(val.getSortMode().getValue()); }); sortMode.addListener((observable, oldValue, newValue) -> { @@ -51,18 +50,9 @@ public class StoreEntryListStatusComp extends SimpleComp { private Region createGroupListHeader() { var label = new Label(); - label.textProperty() - .bind(Bindings.createStringBinding( - () -> { - return StoreViewState.get() - .getActiveCategory() - .getValue() - .getRoot() - .equals(StoreViewState.get().getAllConnectionsCategory()) - ? "Connections" - : "Scripts"; - }, - StoreViewState.get().getActiveCategory())); + var name = BindingsHelper.flatMap(StoreViewState.get().getActiveCategory(), + categoryWrapper -> AppI18n.observable(categoryWrapper.getRoot().equals(StoreViewState.get().getAllConnectionsCategory()) ? "connections" : "scripts")); + label.textProperty().bind(name); label.getStyleClass().add("name"); var all = BindingsHelper.filteredContentBinding( diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java index 96d111ed..8ed3c7de 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionComp.java @@ -8,7 +8,6 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.util.ThreadHelper; import javafx.beans.binding.Bindings; @@ -162,7 +161,7 @@ public class StoreSectionComp extends Comp> { return full.styleClass("store-entry-section-comp") .apply(struc -> { struc.get().setFillWidth(true); - SimpleChangeListener.apply(expanded, val -> { + expanded.subscribe(val -> { struc.get().pseudoClassStateChanged(EXPANDED, val); }); struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0); @@ -170,7 +169,7 @@ public class StoreSectionComp extends Comp> { struc.get().pseudoClassStateChanged(ROOT, topLevel); struc.get().pseudoClassStateChanged(SUB, !topLevel); - SimpleChangeListener.apply(section.getWrapper().getColor(), val -> { + section.getWrapper().getColor().subscribe(val -> { if (!topLevel) { return; } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java index f8f756fa..a6556ea7 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSectionMiniComp.java @@ -9,7 +9,6 @@ import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.VerticalComp; import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.storage.DataStoreColor; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; @@ -160,7 +159,7 @@ public class StoreSectionMiniComp extends Comp> { return vert.styleClass("store-section-mini-comp") .apply(struc -> { struc.get().setFillWidth(true); - SimpleChangeListener.apply(expanded, val -> { + expanded.subscribe(val -> { struc.get().pseudoClassStateChanged(EXPANDED, val); }); struc.get().pseudoClassStateChanged(EVEN, section.getDepth() % 2 == 0); @@ -171,7 +170,7 @@ public class StoreSectionMiniComp extends Comp> { }) .apply(struc -> { if (section.getWrapper() != null) { - SimpleChangeListener.apply(section.getWrapper().getColor(), val -> { + section.getWrapper().getColor().subscribe(val -> { if (section.getDepth() != 1) { return; } diff --git a/app/src/main/java/io/xpipe/app/core/AppI18n.java b/app/src/main/java/io/xpipe/app/core/AppI18n.java index 327042b3..cffae71c 100644 --- a/app/src/main/java/io/xpipe/app/core/AppI18n.java +++ b/app/src/main/java/io/xpipe/app/core/AppI18n.java @@ -10,101 +10,70 @@ import io.xpipe.app.prefs.SupportedLocale; import io.xpipe.app.util.OptionsBuilder; import io.xpipe.app.util.Translatable; import io.xpipe.core.util.ModuleHelper; +import io.xpipe.core.util.XPipeInstallation; import javafx.beans.binding.Bindings; -import javafx.beans.binding.StringBinding; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import lombok.SneakyThrows; +import lombok.Value; import org.apache.commons.io.FilenameUtils; import org.ocpsoft.prettytime.PrettyTime; import java.io.IOException; +import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; -import java.time.Duration; -import java.time.Instant; -import java.util.HashMap; -import java.util.Map; -import java.util.Objects; -import java.util.Properties; +import java.util.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.UnaryOperator; import java.util.regex.Pattern; public class AppI18n { - private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$"); - private static final AppI18n INSTANCE = new AppI18n(); - private Map translations; - private Map markdownDocumentations; - private PrettyTime prettyTime; + @Value + static class LoadedTranslations { - public static void init() { - var i = INSTANCE; - if (i.translations != null) { - return; + Map translations; + Map markdownDocumentations; + PrettyTime prettyTime; + } + + private static final Pattern VAR_PATTERN = Pattern.compile("\\$\\w+?\\$"); + private static AppI18n INSTANCE; + private LoadedTranslations english; + private final Property 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) { - AppPrefs.get().language().addListener((c, o, n) -> { - i.clear(); - i.load(); + AppPrefs.get().language().subscribe(n -> { + try { + 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; } - public static StringBinding readableInstant(String s, ObservableValue instant) { - return readableInstant(instant, rs -> getValue(getInstance().getLocalised(s), rs)); - } - - public static StringBinding readableInstant(ObservableValue instant, UnaryOperator 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) { - 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) { - return Bindings.createStringBinding( - () -> { - if (duration.getValue() == null) { - return "null"; - } - - return getInstance() - .prettyTime - .formatDuration(getInstance() - .prettyTime - .approximateDuration(Instant.now().plus(duration.getValue()))); - }, - duration); + private LoadedTranslations getLoaded() { + return currentLanguage.getValue() != null ? currentLanguage.getValue() : english; } public static ObservableValue observable(String s, Object... vars) { @@ -115,7 +84,7 @@ public class AppI18n { var key = INSTANCE.getKey(s); return Bindings.createStringBinding(() -> { return get(key, vars); - }); + }, INSTANCE.currentLanguage); } public static String get(String s, Object... vars) { @@ -160,11 +129,6 @@ public class AppI18n { return ""; } - private void clear() { - translations.clear(); - prettyTime = null; - } - public String getKey(String s) { var key = s; if (!s.contains(".")) { @@ -173,62 +137,62 @@ public class AppI18n { return key; } - public boolean containsKey(String s) { - var key = getKey(s); - if (translations == null) { - return false; - } - - return translations.containsKey(key); - } - public String getLocalised(String s, Object... vars) { var key = getKey(s); - if (translations == null) { + if (english == null) { TrackEvent.warn("Translations not initialized for " + key); return s; } - if (!translations.containsKey(key)) { - TrackEvent.warn("Translation key not found for " + key); - return key; + if (currentLanguage.getValue() != null && currentLanguage.getValue().getTranslations().containsKey(key)) { + var localisedString = currentLanguage.getValue().getTranslations().get(key); + return getValue(localisedString, vars); } - var localisedString = translations.get(key); - return getValue(localisedString, vars); + if (english.getTranslations().containsKey(key)) { + var localisedString = english.getTranslations().get(key); + return getValue(localisedString, vars); + } + + TrackEvent.warn("Translation key not found for " + key); + return key; } - public boolean isLoaded() { - return translations != null; - } - - private boolean matchesLocale(Path f) { - var l = AppPrefs.get() != null - ? AppPrefs.get().language().getValue().getLocale() - : SupportedLocale.ENGLISH.getLocale(); + private boolean matchesLocale(Path f, Locale l) { var name = FilenameUtils.getBaseName(f.getFileName().toString()); var ending = "_" + l.toLanguageTag(); return name.endsWith(ending); } public String getMarkdownDocumentation(String name) { - if (!markdownDocumentations.containsKey(name)) { - TrackEvent.withWarn("Markdown documentation for key " + name + " not found") - .handle(); + if (currentLanguage.getValue() != null && currentLanguage.getValue().getMarkdownDocumentations().containsKey(name)) { + var localisedString = currentLanguage.getValue().getMarkdownDocumentations().get(name); + 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 ..."); - translations = new HashMap<>(); + var translations = new HashMap(); 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)) { - return; + continue; } AtomicInteger fileCounter = new AtomicInteger(); @@ -238,7 +202,7 @@ public class AppI18n { Files.walkFileTree(basePath, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (!matchesLocale(file)) { + if (!matchesLocale(file, l)) { return FileVisitResult.CONTINUE; } @@ -249,7 +213,7 @@ public class AppI18n { fileCounter.incrementAndGet(); try (var in = Files.newInputStream(file)) { var props = new Properties(); - props.load(in); + props.load(new InputStreamReader(in, StandardCharsets.UTF_8)); props.forEach((key, value) -> { var hasPrefix = key.toString().contains("."); var usedPrefix = hasPrefix ? "" : defaultPrefix; @@ -267,21 +231,20 @@ public class AppI18n { .tag("fileCount", fileCounter.get()) .tag("lineCount", lineCounter.get()) .handle(); - }); } - markdownDocumentations = new HashMap<>(); + var markdownDocumentations = new HashMap(); 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)) { - return; + continue; } var moduleName = FilenameUtils.getExtension(module.getName()); Files.walkFileTree(basePath, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { - if (!matchesLocale(file)) { + if (!matchesLocale(file, l)) { return FileVisitResult.CONTINUE; } @@ -302,13 +265,14 @@ public class AppI18n { return FileVisitResult.CONTINUE; } }); - }); } - this.prettyTime = new PrettyTime( + var prettyTime = new PrettyTime( AppPrefs.get() != null ? AppPrefs.get().language().getValue().getLocale() : SupportedLocale.ENGLISH.getLocale()); + + return new LoadedTranslations(translations,markdownDocumentations, prettyTime); } @SuppressWarnings("removal") diff --git a/app/src/main/java/io/xpipe/app/core/AppTheme.java b/app/src/main/java/io/xpipe/app/core/AppTheme.java index ff862560..a0f4570b 100644 --- a/app/src/main/java/io/xpipe/app/core/AppTheme.java +++ b/app/src/main/java/io/xpipe/app/core/AppTheme.java @@ -3,7 +3,6 @@ package io.xpipe.app.core; import atlantafx.base.theme.*; import io.xpipe.app.ext.PrefsChoiceValue; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.issue.TrackEvent; import io.xpipe.app.prefs.AppPrefs; @@ -44,7 +43,7 @@ public class AppTheme { return; } - SimpleChangeListener.apply(AppPrefs.get().theme, t -> { + AppPrefs.get().theme.subscribe(t -> { Theme.ALL.forEach( theme -> stage.getScene().getRoot().getStyleClass().remove(theme.getCssId())); if (t == null) { @@ -56,7 +55,7 @@ public class AppTheme { 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(PERFORMANCE, val); }); diff --git a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java index 450af36e..4fb42aa7 100644 --- a/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java +++ b/app/src/main/java/io/xpipe/app/core/mode/BaseMode.java @@ -47,6 +47,7 @@ public class BaseMode extends OperationMode { AppI18n.init(); LicenseProvider.get().init(); AppPrefs.initLocal(); + AppI18n.init(); AppCertutilCheck.check(); AppAvCheck.check(); AppSid.init(); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/Comp.java b/app/src/main/java/io/xpipe/app/fxcomps/Comp.java index 1b8d6483..988e56f9 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/Comp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/Comp.java @@ -6,7 +6,6 @@ import io.xpipe.app.fxcomps.augment.Augment; import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.FancyTooltipAugment; import io.xpipe.app.fxcomps.util.Shortcuts; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.application.Platform; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; @@ -144,7 +143,7 @@ public abstract class Comp> { public Comp hide(ObservableValue o) { return apply(struc -> { var region = struc.get(); - SimpleChangeListener.apply(o, n -> { + o.subscribe(n -> { if (!n) { region.setVisible(true); region.setManaged(true); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java index 779c5732..d7a9d41d 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoiceComp.java @@ -6,7 +6,6 @@ import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import io.xpipe.app.util.Translatable; import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; @@ -72,7 +71,7 @@ public class ChoiceComp extends Comp>> { throw new UnsupportedOperationException(); } }); - SimpleChangeListener.apply(range, c -> { + range.subscribe(c -> { var list = FXCollections.observableArrayList(c.keySet()); if (!list.contains(null) && includeNone) { list.add(null); @@ -84,7 +83,7 @@ public class ChoiceComp extends Comp>> { cb.valueProperty().addListener((observable, oldValue, newValue) -> { value.setValue(newValue); }); - SimpleChangeListener.apply(value, val -> { + value.subscribe(val -> { PlatformThread.runLaterIfNeeded(() -> cb.valueProperty().set(val)); }); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java index 2bc5f1d5..48ad4092 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ChoicePaneComp.java @@ -4,7 +4,6 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.property.Property; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; @@ -58,7 +57,7 @@ public class ChoicePaneComp extends Comp> { var vbox = new VBox(transformer.apply(cb)); vbox.setFillWidth(true); cb.prefWidthProperty().bind(vbox.widthProperty()); - SimpleChangeListener.apply(cb.valueProperty(), n -> { + cb.valueProperty().subscribe(n -> { if (n == null) { if (vbox.getChildren().size() > 1) { vbox.getChildren().remove(1); @@ -82,7 +81,7 @@ public class ChoicePaneComp extends Comp> { cb.valueProperty().addListener((observable, oldValue, newValue) -> { selected.setValue(newValue); }); - SimpleChangeListener.apply(selected, val -> { + selected.subscribe(val -> { PlatformThread.runLaterIfNeeded(() -> cb.valueProperty().set(val)); }); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java index 8652a666..bea99195 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/ContextualFileReferenceChoiceComp.java @@ -7,7 +7,6 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.core.AppWindowHelper; 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.prefs.AppPrefs; import io.xpipe.app.storage.ContextualFileReference; @@ -39,7 +38,7 @@ public class ContextualFileReferenceChoiceComp extends SimpleComp { public ContextualFileReferenceChoiceComp( ObservableValue> fileSystem, Property filePath) { this.fileSystem = new SimpleObjectProperty<>(); - SimpleChangeListener.apply(fileSystem, val -> { + fileSystem.subscribe(val -> { this.fileSystem.setValue(val); }); this.filePath = filePath; diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java index 7af8b6d0..466d3dd9 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/FancyTooltipAugment.java @@ -3,8 +3,10 @@ package io.xpipe.app.fxcomps.impl; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.augment.Augment; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.fxcomps.util.Shortcuts; +import javafx.beans.binding.Bindings; import javafx.beans.value.ObservableValue; import javafx.scene.control.Tooltip; @@ -24,11 +26,15 @@ public class FancyTooltipAugment> implements Augment< public void augment(S struc) { var region = struc.get(); var tt = new Tooltip(); - var toDisplay = text.getValue(); 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); + BindingsHelper.bindStrong(tt.textProperty(), binding); + } else { + BindingsHelper.bindStrong(tt.textProperty(),text); } - tt.textProperty().setValue(toDisplay); tt.setStyle("-fx-font-size: 11pt;"); tt.setWrapText(true); tt.setMaxWidth(400); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java index 20570897..f148fc6b 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/FilterComp.java @@ -1,10 +1,10 @@ package io.xpipe.app.fxcomps.impl; import io.xpipe.app.core.AppActionLinkDetector; +import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.util.PlatformThread; -import io.xpipe.app.fxcomps.util.SimpleChangeListener; import javafx.beans.binding.Bindings; import javafx.beans.property.Property; import javafx.scene.Node; @@ -28,12 +28,13 @@ public class FilterComp extends Comp { @Override public Structure createBase() { 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"); var filter = new TextField(); filter.setAccessibleText("Filter"); - SimpleChangeListener.apply(filterText, val -> { + filterText.subscribe(val -> { PlatformThread.runLaterIfNeeded(() -> { if (!Objects.equals(filter.getText(), val)) { filter.setText(val); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/LabelComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/LabelComp.java index da1a559a..45ed6e40 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/LabelComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/LabelComp.java @@ -3,6 +3,7 @@ package io.xpipe.app.fxcomps.impl; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.util.BindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import javafx.beans.property.SimpleStringProperty; import javafx.beans.value.ObservableValue; @@ -24,6 +25,7 @@ public class LabelComp extends Comp> { @Override public CompStructure