From 880b17c7c190e584f9f243fc50a987e97e417ae5 Mon Sep 17 00:00:00 2001 From: crschnick Date: Fri, 7 Jun 2024 08:38:07 +0000 Subject: [PATCH] SSH config fixes --- app/build.gradle | 3 +- .../app/browser/BrowserOverviewComp.java | 9 +- .../app/browser/BrowserTransferComp.java | 4 +- .../xpipe/app/browser/BrowserWelcomeComp.java | 12 +- .../xpipe/app/browser/file/BrowserEntry.java | 8 - .../session/BrowserFileChooserModel.java | 7 +- .../io/xpipe/app/comp/base/DropdownComp.java | 12 +- .../xpipe/app/comp/base/ListBoxViewComp.java | 6 +- .../xpipe/app/comp/base/StoreToggleComp.java | 1 + .../app/comp/store/StoreCategoryWrapper.java | 6 +- .../app/comp/store/StoreCreationComp.java | 4 + .../xpipe/app/comp/store/StoreEntryComp.java | 97 ++------ .../app/comp/store/StoreEntryListComp.java | 14 +- .../comp/store/StoreEntryListStatusComp.java | 22 +- .../app/comp/store/StoreEntryWrapper.java | 18 +- .../store/StoreQuickAccessButtonComp.java | 6 +- .../io/xpipe/app/comp/store/StoreSection.java | 128 ++++------ .../app/comp/store/StoreSectionComp.java | 27 ++- .../app/comp/store/StoreSectionMiniComp.java | 21 +- .../xpipe/app/comp/store/StoreSortMode.java | 4 +- .../xpipe/app/comp/store/StoreViewState.java | 131 +++++----- .../io/xpipe/app/ext/DataStoreProvider.java | 4 - .../io/xpipe/app/fxcomps/impl/ChoiceComp.java | 5 +- .../app/fxcomps/impl/StoreCategoryComp.java | 9 +- .../fxcomps/util/DerivedObservableList.java | 228 ------------------ .../app/fxcomps/util/ListBindingsHelper.java | 190 +++++++++++++++ .../app/prefs/ExternalApplicationType.java | 7 +- .../xpipe/app/prefs/ExternalEditorType.java | 8 +- .../io/xpipe/app/storage/DataStorage.java | 24 +- .../io/xpipe/app/storage/DataStoreEntry.java | 44 +--- .../io/xpipe/app/storage/StorageListener.java | 4 - .../app/terminal/ExternalTerminalType.java | 41 +--- .../xpipe/app/terminal/WezTerminalType.java | 101 ++++++-- .../app/util/DataStoreCategoryChoiceComp.java | 2 +- .../java/io/xpipe/app/util/FileOpener.java | 11 +- build.gradle | 8 +- .../io/xpipe/core/process/ShellControl.java | 18 +- .../core/process/ShellNameStoreState.java | 24 -- .../xpipe/core/process/ShellStoreState.java | 25 +- .../io/xpipe/core/store/DataStoreState.java | 43 +++- .../xpipe/core/store/StatefulDataStore.java | 10 +- dist/changelogs/9.4_incremental.md | 20 +- dist/jpackage.gradle | 2 +- .../base/script/ScriptGroupStoreProvider.java | 6 +- .../io/xpipe/ext/base/script/ScriptStore.java | 17 +- .../script/SimpleScriptStoreProvider.java | 6 +- gradle/gradle_scripts/vernacular-1.16.jar | Bin 101276 -> 101280 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- lang/app/strings/translations_da.properties | 3 - lang/app/strings/translations_de.properties | 3 - lang/app/strings/translations_en.properties | 4 - lang/app/strings/translations_es.properties | 3 - lang/app/strings/translations_fr.properties | 3 - lang/app/strings/translations_it.properties | 3 - lang/app/strings/translations_ja.properties | 3 - lang/app/strings/translations_nl.properties | 3 - lang/app/strings/translations_pt.properties | 3 - lang/app/strings/translations_ru.properties | 3 - lang/app/strings/translations_tr.properties | 3 - lang/app/strings/translations_zh.properties | 3 - version | 2 +- 61 files changed, 650 insertions(+), 788 deletions(-) delete mode 100644 app/src/main/java/io/xpipe/app/fxcomps/util/DerivedObservableList.java create mode 100644 app/src/main/java/io/xpipe/app/fxcomps/util/ListBindingsHelper.java delete mode 100644 core/src/main/java/io/xpipe/core/process/ShellNameStoreState.java diff --git a/app/build.gradle b/app/build.gradle index d2a337a18..228576384 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -108,7 +108,6 @@ run { } workingDir = rootDir - jvmArgs += ['-XX:+EnableDynamicAgentLoading'] } task runAttachedDebugger(type: JavaExec) { @@ -122,7 +121,7 @@ task runAttachedDebugger(type: JavaExec) { "-javaagent:${System.getProperty("user.home")}/.attachme/attachme-agent-1.2.4.jar=port:7857,host:localhost".toString(), "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=127.0.0.1:0" ) - jvmArgs += ['-XX:+EnableDynamicAgentLoading'] + jvmArgs += '-XX:+EnableDynamicAgentLoading' systemProperties run.systemProperties } diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java index 7d83c935e..ae9018305 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java @@ -6,16 +6,18 @@ import io.xpipe.app.comp.base.SimpleTitledPaneComp; import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.ShellControl; import io.xpipe.core.store.FileSystem; + import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; + import lombok.SneakyThrows; import java.util.List; @@ -68,8 +70,9 @@ public class BrowserOverviewComp extends SimpleComp { var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false); var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview); - var recent = new DerivedObservableList<>(model.getSavedState().getRecentDirectories(), true).mapped( - s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())).getList(); + var recent = ListBindingsHelper.mappedContentBinding( + model.getSavedState().getRecentDirectories(), + s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())); var recentOverview = new BrowserFileOverviewComp(model, recent, true); var recentPane = new SimpleTitledPaneComp(AppI18n.observable("recent"), recentOverview); diff --git a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java index 988b8e557..182e54118 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserTransferComp.java @@ -8,7 +8,7 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.augment.DragOverPseudoClassAugment; import io.xpipe.app.fxcomps.impl.*; -import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.core.process.OsType; import javafx.beans.binding.Bindings; @@ -47,7 +47,7 @@ public class BrowserTransferComp extends SimpleComp { var backgroundStack = new StackComp(List.of(background)).grow(true, true).styleClass("download-background"); - var binding = new DerivedObservableList<>(syncItems, true).mapped(item -> item.getBrowserEntry()).getList(); + var binding = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getBrowserEntry()); var list = new BrowserSelectionListComp( binding, entry -> Bindings.createStringBinding( 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 e0f71d5c3..6e4cbf724 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java @@ -1,7 +1,5 @@ package io.xpipe.app.browser; -import atlantafx.base.controls.Spacer; -import atlantafx.base.theme.Styles; import io.xpipe.app.browser.session.BrowserSessionModel; import io.xpipe.app.comp.base.ButtonComp; import io.xpipe.app.comp.base.ListBoxViewComp; @@ -15,9 +13,10 @@ import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettySvgComp; import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.util.ThreadHelper; + import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -31,6 +30,9 @@ import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.VBox; +import atlantafx.base.controls.Spacer; +import atlantafx.base.theme.Styles; + import java.util.List; public class BrowserWelcomeComp extends SimpleComp { @@ -65,7 +67,7 @@ public class BrowserWelcomeComp extends SimpleComp { return new VBox(hbox); } - var list = new DerivedObservableList<>(state.getEntries(), true).filtered(e -> { + var list = ListBindingsHelper.filteredContentBinding(state.getEntries(), e -> { var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); if (entry.isEmpty()) { return false; @@ -76,7 +78,7 @@ public class BrowserWelcomeComp extends SimpleComp { } return true; - }).getList(); + }); var empty = Bindings.createBooleanBinding(() -> list.isEmpty(), list); var headerBinding = BindingsHelper.flatMap(empty, b -> { diff --git a/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java b/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java index bcc4e9404..de226a240 100644 --- a/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java +++ b/app/src/main/java/io/xpipe/app/browser/file/BrowserEntry.java @@ -24,10 +24,6 @@ public class BrowserEntry { } private static BrowserIconFileType fileType(FileSystem.FileEntry rawFileEntry) { - if (rawFileEntry == null) { - return null; - } - if (rawFileEntry.getKind() == FileKind.DIRECTORY) { return null; } @@ -42,10 +38,6 @@ public class BrowserEntry { } private static BrowserIconDirectoryType directoryType(FileSystem.FileEntry rawFileEntry) { - if (rawFileEntry == null) { - return null; - } - if (rawFileEntry.getKind() != FileKind.DIRECTORY) { return null; } diff --git a/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java index 689bf2cdd..9c681e599 100644 --- a/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java +++ b/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java @@ -2,7 +2,7 @@ package io.xpipe.app.browser.session; import io.xpipe.app.browser.file.BrowserEntry; import io.xpipe.app.browser.fs.OpenFileSystemModel; -import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.FileReference; @@ -10,10 +10,12 @@ import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.store.FileNames; import io.xpipe.core.store.FileSystemStore; import io.xpipe.core.util.FailableFunction; + import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; + import lombok.Getter; import lombok.Setter; @@ -38,8 +40,7 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel(fileSelection, true); - l.bindContent(newValue.getFileList().getSelection()); + ListBindingsHelper.bindContent(fileSelection, newValue.getFileList().getSelection()); }); } 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 20c7d6c59..139202f52 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 @@ -4,9 +4,8 @@ import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; -import javafx.beans.binding.Bindings; -import javafx.beans.value.ObservableValue; import javafx.css.Size; import javafx.css.SizeUnits; import javafx.scene.control.Button; @@ -39,13 +38,10 @@ public class DropdownComp extends Comp> { })) .createRegion(); - List> l = cm.getItems().stream() - .map(menuItem -> menuItem.getGraphic().visibleProperty()) - .toList(); button.visibleProperty() - .bind(Bindings.createBooleanBinding(() -> { - return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue()); - }, l.toArray(ObservableValue[]::new))); + .bind(ListBindingsHelper.anyMatch(cm.getItems().stream() + .map(menuItem -> menuItem.getGraphic().visibleProperty()) + .toList())); var graphic = new FontIcon("mdi2c-chevron-double-down"); button.fontProperty().subscribe(c -> { diff --git a/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java b/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java index 997869629..da5a77dc8 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/ListBoxViewComp.java @@ -3,8 +3,9 @@ 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.DerivedObservableList; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; + import javafx.application.Platform; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -88,8 +89,7 @@ public class ListBoxViewComp extends Comp> { } if (!listView.getChildren().equals(newShown)) { - var d = new DerivedObservableList<>(listView.getChildren(), true); - d.setContent(newShown); + ListBindingsHelper.setContent(listView.getChildren(), newShown); } }; diff --git a/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java b/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java index e765e784a..58e2430aa 100644 --- a/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java +++ b/app/src/main/java/io/xpipe/app/comp/base/StoreToggleComp.java @@ -52,6 +52,7 @@ public class StoreToggleComp extends SimpleComp { initial.apply(section.getWrapper().getEntry().getStore().asNeeded())), v -> { setter.accept(section.getWrapper().getEntry().getStore().asNeeded(), v); + section.getWrapper().refreshChildren(); }); } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java index c5ad100c6..4b2f47ea0 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreCategoryWrapper.java @@ -60,7 +60,7 @@ public class StoreCategoryWrapper { } public StoreCategoryWrapper getParent() { - return StoreViewState.get().getCategories().getList().stream() + return StoreViewState.get().getCategories().stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().getUuid().equals(category.getParentCategory())) .findAny() @@ -122,7 +122,7 @@ public class StoreCategoryWrapper { sortMode.setValue(category.getSortMode()); share.setValue(category.isShare()); - containedEntries.setAll(StoreViewState.get().getAllEntries().getList().stream() + containedEntries.setAll(StoreViewState.get().getAllEntries().stream() .filter(entry -> { return entry.getEntry().getCategoryUuid().equals(category.getUuid()) || (AppPrefs.get() @@ -132,7 +132,7 @@ public class StoreCategoryWrapper { .anyMatch(storeCategoryWrapper -> storeCategoryWrapper.contains(entry))); }) .toList()); - children.setAll(StoreViewState.get().getCategories().getList().stream() + children.setAll(StoreViewState.get().getCategories().stream() .filter(storeCategoryWrapper -> getCategory() .getUuid() .equals(storeCategoryWrapper.getCategory().getParentCategory())) 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 7220f5e82..a9588494a 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 @@ -286,6 +286,10 @@ public class StoreCreationComp extends DialogComp { if (ex instanceof ValidationException) { ErrorEvent.expected(ex); skippable.set(false); + } else if (ex instanceof StackOverflowError) { + // Cycles in connection graphs can fail hard but are expected + ErrorEvent.expected(ex); + skippable.set(false); } else { skippable.set(true); } 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 da3ab6bed..c8a7ef8a5 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 @@ -372,14 +372,6 @@ public abstract class StoreEntryComp extends SimpleComp { contextMenu.getItems().add(new SeparatorMenuItem()); } - var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text")); - notes.setOnAction(event -> { - wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes())); - event.consume(); - }); - notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null)); - contextMenu.getItems().add(notes); - if (AppPrefs.get().developerMode().getValue()) { var browse = new MenuItem(AppI18n.get("browseInternalStorage"), new FontIcon("mdi2f-folder-open-outline")); browse.setOnAction( @@ -387,6 +379,26 @@ public abstract class StoreEntryComp extends SimpleComp { contextMenu.getItems().add(browse); } + if (wrapper.getEntry().getProvider() != null) { + var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); + StoreViewState.get() + .getSortedCategories(wrapper.getCategory().getValue().getRoot()) + .forEach(storeCategoryWrapper -> { + MenuItem m = new MenuItem(); + m.textProperty().setValue(" ".repeat(storeCategoryWrapper.getDepth()) + storeCategoryWrapper.getName().getValue()); + m.setOnAction(event -> { + wrapper.moveTo(storeCategoryWrapper.getCategory()); + event.consume(); + }); + if (storeCategoryWrapper.getParent() == null || storeCategoryWrapper.equals(wrapper.getCategory().getValue())) { + m.setDisable(true); + } + + move.getItems().add(m); + }); + contextMenu.getItems().add(move); + } + if (DataStorage.get().isRootEntry(wrapper.getEntry())) { var color = new Menu(AppI18n.get("color"), new FontIcon("mdi2f-format-color-fill")); var none = new MenuItem("None"); @@ -406,72 +418,13 @@ public abstract class StoreEntryComp extends SimpleComp { contextMenu.getItems().add(color); } - if (wrapper.getEntry().getProvider() != null) { - var move = new Menu(AppI18n.get("moveTo"), new FontIcon("mdi2f-folder-move-outline")); - StoreViewState.get() - .getSortedCategories(wrapper.getCategory().getValue().getRoot()) - .getList() - .forEach(storeCategoryWrapper -> { - MenuItem m = new MenuItem(); - m.textProperty().setValue(" ".repeat(storeCategoryWrapper.getDepth()) + storeCategoryWrapper.getName().getValue()); - m.setOnAction(event -> { - wrapper.moveTo(storeCategoryWrapper.getCategory()); - event.consume(); - }); - if (storeCategoryWrapper.getParent() == null) { - m.setDisable(true); - } - - move.getItems().add(m); - }); - contextMenu.getItems().add(move); - } - - var order = new Menu(AppI18n.get("order"), new FontIcon("mdal-bookmarks")); - var noOrder = new MenuItem(AppI18n.get("none"), new FontIcon("mdi2r-reorder-horizontal")); - noOrder.setOnAction(event -> { - DataStorage.get().orderBefore(wrapper.getEntry(), null); + var notes = new MenuItem(AppI18n.get("addNotes"), new FontIcon("mdi2n-note-text")); + notes.setOnAction(event -> { + wrapper.getNotes().setValue(new StoreNotes(null, getDefaultNotes())); event.consume(); }); - if (wrapper.getEntry().getOrderBefore() == null) { - noOrder.setDisable(true); - } - order.getItems().add(noOrder); - order.getItems().add(new SeparatorMenuItem()); - var stick = new MenuItem(AppI18n.get("stickToTop"), new FontIcon("mdi2o-order-bool-descending")); - stick.setOnAction(event -> { - DataStorage.get().orderBefore(wrapper.getEntry(), wrapper.getEntry()); - event.consume(); - }); - if (wrapper.getEntry().getUuid().equals(wrapper.getEntry().getOrderBefore())) { - stick.setDisable(true); - } - order.getItems().add(stick); - order.getItems().add(new SeparatorMenuItem()); - var desc = new MenuItem(AppI18n.get("orderAheadOf"), new FontIcon("mdi2o-order-bool-descending-variant")); - desc.setDisable(true); - order.getItems().add(desc); - var section = StoreViewState.get().getParentSectionForWrapper(wrapper); - if (section.isPresent()) { - section.get().getAllChildren().getList().forEach(other -> { - var ow = other.getWrapper(); - var op = ow.getEntry().getProvider(); - MenuItem m = new MenuItem(ow.getName().getValue(), - op != null ? PrettyImageHelper.ofFixedSizeSquare(op.getDisplayIconFileName(ow.getEntry().getStore()), - 16).createRegion() : null); - if (other.getWrapper().equals(wrapper) || ow.getEntry().getUuid().equals(wrapper.getEntry().getOrderBefore())) { - m.setDisable(true); - } - m.setOnAction(event -> { - wrapper.orderBefore(ow); - event.consume(); - }); - order.getItems().add(m); - }); - } - contextMenu.getItems().add(order); - - contextMenu.getItems().add(new SeparatorMenuItem()); + notes.visibleProperty().bind(BindingsHelper.map(wrapper.getNotes(), s -> s.getCommited() == null)); + contextMenu.getItems().add(notes); var del = new MenuItem(AppI18n.get("remove"), new FontIcon("mdal-delete_outline")); del.disableProperty() diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java index ad7a40d9e..68df63ab9 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryListComp.java @@ -18,8 +18,8 @@ public class StoreEntryListComp extends SimpleComp { private Comp createList() { var content = new ListBoxViewComp<>( - StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList(), - StoreViewState.get().getCurrentTopLevelSection().getAllChildren().getList(), + StoreViewState.get().getCurrentTopLevelSection().getShownChildren(), + StoreViewState.get().getCurrentTopLevelSection().getAllChildren(), (StoreSection e) -> { var custom = StoreSection.customSection(e, true).hgrow(); return new HorizontalComp(List.of(Comp.hspacer(8), custom, Comp.hspacer(10))) @@ -35,7 +35,7 @@ public class StoreEntryListComp extends SimpleComp { var showIntro = Bindings.createBooleanBinding( () -> { var all = StoreViewState.get().getAllConnectionsCategory(); - var connections = StoreViewState.get().getAllEntries().getList().stream() + var connections = StoreViewState.get().getAllEntries().stream() .filter(wrapper -> all.contains(wrapper)) .toList(); return initialCount == connections.size() @@ -45,21 +45,21 @@ public class StoreEntryListComp extends SimpleComp { .getRoot() .equals(StoreViewState.get().getAllConnectionsCategory()); }, - StoreViewState.get().getAllEntries().getList(), + StoreViewState.get().getAllEntries(), StoreViewState.get().getActiveCategory()); var map = new LinkedHashMap, ObservableValue>(); map.put( createList(), Bindings.not(Bindings.isEmpty( - StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList()))); + StoreViewState.get().getCurrentTopLevelSection().getShownChildren()))); map.put(new StoreIntroComp(), showIntro); map.put( new StoreNotFoundComp(), Bindings.and( - Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries().getList())), + Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())), Bindings.isEmpty( - StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList()))); + StoreViewState.get().getCurrentTopLevelSection().getShownChildren()))); return new MultiContentComp(map).createRegion(); } } 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 f657af52c..ac750efc6 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 @@ -8,6 +8,7 @@ import io.xpipe.app.fxcomps.SimpleComp; 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.ListBindingsHelper; import io.xpipe.app.util.ThreadHelper; import io.xpipe.core.process.OsType; import javafx.beans.binding.Bindings; @@ -55,24 +56,25 @@ public class StoreEntryListStatusComp extends SimpleComp { label.textProperty().bind(name); label.getStyleClass().add("name"); - var all = StoreViewState.get().getAllEntries().filtered( + var all = ListBindingsHelper.filteredContentBinding( + StoreViewState.get().getAllEntries(), storeEntryWrapper -> { - var rootCategory = storeEntryWrapper.getCategory().getValue().getRoot(); - var inRootCategory = StoreViewState.get().getActiveCategory().getValue().getRoot().equals(rootCategory); - // Sadly the all binding does not update when the individual visibility of entries changes - // But it is good enough. - var showProvider = storeEntryWrapper.getEntry().getProvider() == null || - storeEntryWrapper.getEntry().getProvider().shouldShow(storeEntryWrapper); - return inRootCategory && showProvider; + var storeRoot = storeEntryWrapper.getCategory().getValue().getRoot(); + return StoreViewState.get() + .getActiveCategory() + .getValue() + .getRoot() + .equals(storeRoot); }, StoreViewState.get().getActiveCategory()); - var shownList = all.filtered( + var shownList = ListBindingsHelper.filteredContentBinding( + all, storeEntryWrapper -> { return storeEntryWrapper.shouldShow( StoreViewState.get().getFilterString().getValue()); }, StoreViewState.get().getFilterString()); - var count = new CountComp<>(shownList.getList(), all.getList()); + var count = new CountComp<>(shownList, all); var c = count.createRegion(); var topBar = new HBox( diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java index 5d96ed2dc..b9dab8f46 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreEntryWrapper.java @@ -9,13 +9,17 @@ import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.util.ThreadHelper; -import javafx.beans.Observable; + import javafx.beans.property.*; + import lombok.Getter; import java.time.Duration; import java.time.Instant; -import java.util.*; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; @Getter public class StoreEntryWrapper { @@ -61,22 +65,12 @@ public class StoreEntryWrapper { setupListeners(); } - public List getUpdateObservables() { - return List.of(category); - } - public void moveTo(DataStoreCategory category) { ThreadHelper.runAsync(() -> { DataStorage.get().updateCategory(entry, category); }); } - public void orderBefore(StoreEntryWrapper other) { - ThreadHelper.runAsync(() -> { - DataStorage.get().orderBefore(getEntry(),other.getEntry()); - }); - } - public boolean isInStorage() { return DataStorage.get().getStoreEntries().contains(entry); } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java b/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java index 57d39e0ef..c97081b85 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreQuickAccessButtonComp.java @@ -26,7 +26,7 @@ public class StoreQuickAccessButtonComp extends Comp> { } private ContextMenu createMenu() { - if (section.getShownChildren().getList().isEmpty()) { + if (section.getShownChildren().isEmpty()) { return null; } @@ -42,7 +42,7 @@ public class StoreQuickAccessButtonComp extends Comp> { var w = section.getWrapper(); var graphic = w.getEntry().getProvider().getDisplayIconFileName(w.getEntry().getStore()); - if (c.getList().isEmpty()) { + if (c.isEmpty()) { var item = ContextMenuHelper.item( PrettyImageHelper.ofFixedSizeSquare(graphic, 16), w.getName().getValue()); @@ -55,7 +55,7 @@ public class StoreQuickAccessButtonComp extends Comp> { } var items = new ArrayList(); - for (StoreSection sub : c.getList()) { + for (StoreSection sub : c) { if (!sub.getWrapper().getValidity().getValue().isUsable()) { continue; } diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java index 67bb0cda9..c57f324c9 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSection.java @@ -2,37 +2,37 @@ package io.xpipe.app.comp.store; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreEntry; + import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ObservableBooleanValue; import javafx.beans.value.ObservableStringValue; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + import lombok.Value; import java.util.Comparator; -import java.util.HashSet; -import java.util.Set; import java.util.function.Predicate; -import java.util.function.ToIntFunction; @Value public class StoreSection { StoreEntryWrapper wrapper; - DerivedObservableList allChildren; - DerivedObservableList shownChildren; + ObservableList allChildren; + ObservableList shownChildren; int depth; ObservableBooleanValue showDetails; public StoreSection( StoreEntryWrapper wrapper, - DerivedObservableList allChildren, - DerivedObservableList shownChildren, + ObservableList allChildren, + ObservableList shownChildren, int depth) { this.wrapper = wrapper; this.allChildren = allChildren; @@ -41,10 +41,10 @@ public class StoreSection { if (wrapper != null) { this.showDetails = Bindings.createBooleanBinding( () -> { - return wrapper.getExpanded().get() || allChildren.getList().isEmpty(); + return wrapper.getExpanded().get() || allChildren.isEmpty(); }, wrapper.getExpanded(), - allChildren.getList()); + allChildren); } else { this.showDetails = new SimpleBooleanProperty(true); } @@ -59,77 +59,51 @@ public class StoreSection { } } - private static DerivedObservableList sorted( - DerivedObservableList list, ObservableValue category) { + private static ObservableList sorted( + ObservableList list, ObservableValue category) { if (category == null) { return list; } - var explicitOrderComp = Comparator.comparingInt(new ToIntFunction<>() { - @Override - public int applyAsInt(StoreSection value) { - var explicit = value.getWrapper().getEntry().getOrderBefore(); - if (explicit == null) { - return 1; - } - - if (explicit.equals(value.getWrapper().getEntry().getUuid())) { - return Integer.MIN_VALUE; - } - - return -count(value.getWrapper(), new HashSet<>()); - } - - private int count(StoreEntryWrapper wrapper, Set seen) { - if (seen.contains(wrapper)) { - // Loop! - return 0; - } - seen.add(wrapper); - - var found = list.getList().stream().filter(section -> wrapper.getEntry().getOrderBefore().equals(section.getWrapper().getEntry().getUuid())).findFirst(); - if (found.isPresent()) { - return count(found.get().getWrapper(), seen); - } else { - return seen.size(); - } - } - }); - var usableComp = Comparator.comparingInt( + var c = Comparator.comparingInt( value -> value.getWrapper().getEntry().getValidity().isUsable() ? -1 : 1); - var comp = explicitOrderComp.thenComparing(usableComp); var mappedSortMode = BindingsHelper.flatMap( category, storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null); - return list.sorted((o1, o2) -> { + return ListBindingsHelper.orderedContentBinding( + list, + (o1, o2) -> { var current = mappedSortMode.getValue(); if (current != null) { - return comp.thenComparing(current.comparator()) + return c.thenComparing(current.comparator()) .compare(current.representative(o1), current.representative(o2)); } else { - return comp.compare(o1, o2); + return c.compare(o1, o2); } }, - mappedSortMode, - StoreViewState.get().getEntriesOrderChangeObservable()); + mappedSortMode); } public static StoreSection createTopLevel( - DerivedObservableList all, + ObservableList all, Predicate entryFilter, ObservableStringValue filterString, ObservableValue category) { - var topLevel = all.filtered(section -> { + var topLevel = ListBindingsHelper.filteredContentBinding( + all, + section -> { return DataStorage.get().isRootEntry(section.getEntry()); }, - category, - StoreViewState.get().getEntriesListChangeObservable()); - var cached = topLevel.mapped( + category); + var cached = ListBindingsHelper.cachedMappedContentBinding( + topLevel, + topLevel, storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category)); var ordered = sorted(cached, category); - var shown = ordered.filtered( + var shown = ListBindingsHelper.filteredContentBinding( + ordered, section -> { - var showFilter = filterString == null || section.matchesFilter(filterString.get()); + var showFilter = filterString == null || section.shouldShow(filterString.get()); var matchesSelector = section.anyMatches(entryFilter); var sameCategory = category == null || category.getValue() == null @@ -144,17 +118,15 @@ public class StoreSection { private static StoreSection create( StoreEntryWrapper e, int depth, - DerivedObservableList all, + ObservableList all, Predicate entryFilter, ObservableStringValue filterString, ObservableValue category) { if (e.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) { - return new StoreSection(e, new DerivedObservableList<>( - FXCollections.observableArrayList(), true), new DerivedObservableList<>( - FXCollections.observableArrayList(), true), depth); + return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth); } - var allChildren = all.filtered(other -> { + var allChildren = ListBindingsHelper.filteredContentBinding(all, other -> { // Legacy implementation that does not use children caches. Use for testing // if (true) return DataStorage.get() // .getDisplayParent(other.getEntry()) @@ -162,35 +134,29 @@ public class StoreSection { // .orElse(false); // This check is fast as the children are cached in the storage - var isChildren = DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()); - var showProvider = other.getEntry().getProvider() == null || - other.getEntry().getProvider().shouldShow(other); - return isChildren && showProvider; - }, e.getPersistentState(), e.getCache(), StoreViewState.get().getEntriesListChangeObservable()); - var cached = allChildren.mapped( + return DataStorage.get().getStoreChildren(e.getEntry()).contains(other.getEntry()); + }); + var cached = ListBindingsHelper.cachedMappedContentBinding( + allChildren, + allChildren, entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category)); var ordered = sorted(cached, category); - var filtered = ordered.filtered( + var filtered = ListBindingsHelper.filteredContentBinding( + ordered, section -> { - var showFilter = filterString == null || section.matchesFilter(filterString.get()); + var showFilter = filterString == null || section.shouldShow(filterString.get()); var matchesSelector = section.anyMatches(entryFilter); - // Prevent updates for children on category switching by checking depth - var showCategory = category == null + var sameCategory = category == null || category.getValue() == null - || showInCategory(category.getValue(), section.getWrapper()) - || depth > 0; + || showInCategory(category.getValue(), section.getWrapper()); // If this entry is already shown as root due to a different category than parent, don't show it // again here var notRoot = !DataStorage.get().isRootEntry(section.getWrapper().getEntry()); - var showProvider = section.getWrapper().getEntry().getProvider() == null || - section.getWrapper().getEntry().getProvider().shouldShow(section.getWrapper()); - return showFilter && matchesSelector && showCategory && notRoot && showProvider; + return showFilter && matchesSelector && sameCategory && notRoot; }, category, - filterString, - e.getPersistentState(), - e.getCache()); + filterString); return new StoreSection(e, cached, filtered, depth); } @@ -213,13 +179,13 @@ public class StoreSection { return false; } - public boolean matchesFilter(String filter) { + public boolean shouldShow(String filter) { return anyMatches(storeEntryWrapper -> storeEntryWrapper.shouldShow(filter)); } public boolean anyMatches(Predicate c) { return c == null || c.test(wrapper) - || allChildren.getList().stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c)); + || allChildren.stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c)); } } 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 a485d97f9..db79e5151 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 @@ -7,8 +7,10 @@ import io.xpipe.app.fxcomps.augment.GrowAugment; import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.VerticalComp; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStoreColor; import io.xpipe.app.util.ThreadHelper; + import javafx.beans.binding.Bindings; import javafx.css.PseudoClass; import javafx.scene.control.Button; @@ -40,9 +42,9 @@ public class StoreSectionComp extends Comp> { private Comp> createQuickAccessButton() { var quickAccessDisabled = Bindings.createBooleanBinding( () -> { - return section.getShownChildren().getList().isEmpty(); + return section.getShownChildren().isEmpty(); }, - section.getShownChildren().getList()); + section.getShownChildren()); Consumer quickAccessAction = w -> { ThreadHelper.runFailableAsync(() -> { w.executeDefaultAction(); @@ -69,11 +71,11 @@ public class StoreSectionComp extends Comp> { var expandButton = new IconButtonComp( Bindings.createStringBinding( () -> section.getWrapper().getExpanded().get() - && section.getShownChildren().getList().size() > 0 + && section.getShownChildren().size() > 0 ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right", section.getWrapper().getExpanded(), - section.getShownChildren().getList()), + section.getShownChildren()), () -> { section.getWrapper().toggleExpanded(); }); @@ -87,7 +89,7 @@ public class StoreSectionComp extends Comp> { return "Expand " + section.getWrapper().getName().getValue(); }, section.getWrapper().getName())) - .disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0)) + .disable(Bindings.size(section.getShownChildren()).isEqualTo(0)) .styleClass("expand-button") .maxHeight(100) .vgrow(); @@ -126,12 +128,13 @@ public class StoreSectionComp extends Comp> { // Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the // section is actually expanded - var listSections = section.getShownChildren().filtered( - storeSection -> section.getAllChildren().getList().size() <= 20 + var listSections = ListBindingsHelper.filteredContentBinding( + section.getShownChildren(), + storeSection -> section.getAllChildren().size() <= 20 || section.getWrapper().getExpanded().get(), section.getWrapper().getExpanded(), - section.getAllChildren().getList()); - var content = new ListBoxViewComp<>(listSections.getList(), section.getAllChildren().getList(), (StoreSection e) -> { + section.getAllChildren()); + var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> { return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false)); }) .minHeight(0) @@ -140,10 +143,10 @@ public class StoreSectionComp extends Comp> { var expanded = Bindings.createBooleanBinding( () -> { return section.getWrapper().getExpanded().get() - && section.getShownChildren().getList().size() > 0; + && section.getShownChildren().size() > 0; }, section.getWrapper().getExpanded(), - section.getShownChildren().getList()); + section.getShownChildren()); var full = new VerticalComp(List.of( topEntryList, Comp.separator().hide(expanded.not()), @@ -152,7 +155,7 @@ public class StoreSectionComp extends Comp> { .apply(struc -> struc.get().setFillHeight(true)) .hide(Bindings.or( Bindings.not(section.getWrapper().getExpanded()), - Bindings.size(section.getShownChildren().getList()).isEqualTo(0))))); + Bindings.size(section.getShownChildren()).isEqualTo(0))))); return full.styleClass("store-entry-section-comp") .apply(struc -> { struc.get().setFillWidth(true); 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 511a50668..774606c35 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 @@ -8,7 +8,9 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.VerticalComp; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStoreColor; + import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -82,7 +84,7 @@ public class StoreSectionMiniComp extends Comp> { expanded = new SimpleBooleanProperty(section.getWrapper().getExpanded().get() - && section.getShownChildren().getList().size() > 0); + && section.getShownChildren().size() > 0); var button = new IconButtonComp( Bindings.createStringBinding( () -> expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right", @@ -99,15 +101,15 @@ public class StoreSectionMiniComp extends Comp> { + section.getWrapper().getName().getValue(); }, section.getWrapper().getName())) - .disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0)) + .disable(Bindings.size(section.getShownChildren()).isEqualTo(0)) .grow(false, true) .styleClass("expand-button"); var quickAccessDisabled = Bindings.createBooleanBinding( () -> { - return section.getShownChildren().getList().isEmpty(); + return section.getShownChildren().isEmpty(); }, - section.getShownChildren().getList()); + section.getShownChildren()); Consumer quickAccessAction = action; var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction) .vgrow() @@ -129,12 +131,13 @@ public class StoreSectionMiniComp extends Comp> { // Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the // section is actually expanded var listSections = section.getWrapper() != null - ? section.getShownChildren().filtered( - storeSection -> section.getAllChildren().getList().size() <= 20 || expanded.get(), + ? ListBindingsHelper.filteredContentBinding( + section.getShownChildren(), + storeSection -> section.getAllChildren().size() <= 20 || expanded.get(), expanded, - section.getAllChildren().getList()) + section.getAllChildren()) : section.getShownChildren(); - var content = new ListBoxViewComp<>(listSections.getList(), section.getAllChildren().getList(), (StoreSection e) -> { + var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> { return new StoreSectionMiniComp(e, this.augment, this.action, this.condensedStyle); }) .minHeight(0) @@ -145,7 +148,7 @@ public class StoreSectionMiniComp extends Comp> { .apply(struc -> struc.get().setFillHeight(true)) .hide(Bindings.or( Bindings.not(expanded), - Bindings.size(section.getAllChildren().getList()).isEqualTo(0)))); + Bindings.size(section.getAllChildren()).isEqualTo(0)))); var vert = new VerticalComp(list); if (condensedStyle) { diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java b/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java index 69fc2b9c8..0492edc8a 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreSortMode.java @@ -48,7 +48,7 @@ public interface StoreSortMode { @Override public StoreSection representative(StoreSection s) { return Stream.concat( - s.getShownChildren().getList().stream() + s.getShownChildren().stream() .filter(section -> section.getWrapper() .getEntry() .getValidity() @@ -76,7 +76,7 @@ public interface StoreSortMode { @Override public StoreSection representative(StoreSection s) { return Stream.concat( - s.getShownChildren().getList().stream() + s.getShownChildren().stream() .filter(section -> section.getWrapper() .getEntry() .getValidity() diff --git a/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java b/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java index 238cfe0dc..199c185d0 100644 --- a/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java +++ b/app/src/main/java/io/xpipe/app/comp/store/StoreViewState.java @@ -1,16 +1,22 @@ package io.xpipe.app.comp.store; import io.xpipe.app.core.AppCache; -import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.AppPrefs; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.StorageListener; + import javafx.application.Platform; -import javafx.beans.property.*; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + import lombok.Getter; import java.util.*; @@ -23,18 +29,12 @@ public class StoreViewState { private final StringProperty filter = new SimpleStringProperty(); @Getter - private final DerivedObservableList allEntries = - new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true); + private final ObservableList allEntries = + FXCollections.observableList(new CopyOnWriteArrayList<>()); @Getter - private final DerivedObservableList categories = - new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true); - - @Getter - private final IntegerProperty entriesOrderChangeObservable = new SimpleIntegerProperty(); - - @Getter - private final IntegerProperty entriesListChangeObservable = new SimpleIntegerProperty(); + private final ObservableList categories = + FXCollections.observableList(new CopyOnWriteArrayList<>()); @Getter private final Property activeCategory = new SimpleObjectProperty<>(); @@ -76,8 +76,8 @@ public class StoreViewState { } private void updateContent() { - categories.getList().forEach(c -> c.update()); - allEntries.getList().forEach(e -> e.update()); + categories.forEach(c -> c.update()); + allEntries.forEach(e -> e.update()); } private void initSections() { @@ -86,19 +86,16 @@ public class StoreViewState { StoreSection.createTopLevel(allEntries, storeEntryWrapper -> true, filter, activeCategory); } catch (Exception exception) { currentTopLevelSection = - new StoreSection(null, - new DerivedObservableList<>(FXCollections.observableArrayList(), true), - new DerivedObservableList<>(FXCollections.observableArrayList(), true), - 0); + new StoreSection(null, FXCollections.emptyObservableList(), FXCollections.emptyObservableList(), 0); ErrorEvent.fromThrowable(exception).handle(); } } private void initContent() { - allEntries.getList().setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream() + allEntries.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream() .map(StoreEntryWrapper::new) .toList())); - categories.getList().setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream() + categories.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream() .map(StoreCategoryWrapper::new) .toList())); @@ -106,11 +103,11 @@ public class StoreViewState { DataStorage.get().setSelectedCategory(newValue.getCategory()); }); var selected = AppCache.get("selectedCategory", UUID.class, () -> DataStorage.DEFAULT_CATEGORY_UUID); - activeCategory.setValue(categories.getList().stream() + activeCategory.setValue(categories.stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().getUuid().equals(selected)) .findFirst() - .orElse(categories.getList().stream() + .orElse(categories.stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.DEFAULT_CATEGORY_UUID)) .findFirst() @@ -122,9 +119,9 @@ public class StoreViewState { AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { synchronized (this) { - var l = new ArrayList<>(allEntries.getList()); - allEntries.getList().clear(); - allEntries.getList().setAll(l); + var l = new ArrayList<>(allEntries); + allEntries.clear(); + allEntries.setAll(l); } }); }); @@ -132,21 +129,6 @@ public class StoreViewState { // Watch out for synchronizing all calls to the entries and categories list! DataStorage.get().addListener(new StorageListener() { - - @Override - public void onStoreOrderUpdate() { - Platform.runLater(() -> { - entriesOrderChangeObservable.set(entriesOrderChangeObservable.get() + 1); - }); - } - - @Override - public void onStoreListUpdate() { - Platform.runLater(() -> { - entriesListChangeObservable.set(entriesListChangeObservable.get() + 1); - }); - } - @Override public void onStoreAdd(DataStoreEntry... entry) { var l = Arrays.stream(entry) @@ -160,11 +142,11 @@ public class StoreViewState { } synchronized (this) { - allEntries.getList().addAll(l); + allEntries.addAll(l); } synchronized (this) { - categories.getList().stream() - .filter(storeCategoryWrapper -> allEntries.getList().stream() + categories.stream() + .filter(storeCategoryWrapper -> allEntries.stream() .anyMatch(storeEntryWrapper -> storeEntryWrapper .getEntry() .getCategoryUuid() @@ -181,14 +163,14 @@ public class StoreViewState { var a = Arrays.stream(entry).collect(Collectors.toSet()); List l; synchronized (this) { - l = allEntries.getList().stream() + l = allEntries.stream() .filter(storeEntryWrapper -> a.contains(storeEntryWrapper.getEntry())) .toList(); } List cats; synchronized (this) { - cats = categories.getList().stream() - .filter(storeCategoryWrapper -> allEntries.getList().stream() + cats = categories.stream() + .filter(storeCategoryWrapper -> allEntries.stream() .anyMatch(storeEntryWrapper -> storeEntryWrapper .getEntry() .getCategoryUuid() @@ -204,7 +186,7 @@ public class StoreViewState { } synchronized (this) { - allEntries.getList().removeAll(l); + allEntries.removeAll(l); } cats.forEach(storeCategoryWrapper -> storeCategoryWrapper.update()); }); @@ -221,7 +203,7 @@ public class StoreViewState { } synchronized (this) { - categories.getList().add(l); + categories.add(l); } l.update(); }); @@ -231,7 +213,7 @@ public class StoreViewState { public void onCategoryRemove(DataStoreCategory category) { Optional found; synchronized (this) { - found = categories.getList().stream() + found = categories.stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().equals(category)) .findFirst(); @@ -247,7 +229,7 @@ public class StoreViewState { } synchronized (this) { - categories.getList().remove(found.get()); + categories.remove(found.get()); } var p = found.get().getParent(); if (p != null) { @@ -258,34 +240,15 @@ public class StoreViewState { }); } - public Optional getParentSectionForWrapper(StoreEntryWrapper wrapper) { - StoreSection current = getCurrentTopLevelSection(); - while (true) { - var child = current.getAllChildren().getList().stream().filter(section -> section.getWrapper().equals(wrapper)).findFirst(); - if (child.isPresent()) { - return Optional.of(current); - } - - var traverse = current.getAllChildren().getList().stream().filter(section -> section.anyMatches(w -> w.equals(wrapper))).findFirst(); - if (traverse.isPresent()) { - current = traverse.get(); - } else { - return Optional.empty(); - } - } - } - - public DerivedObservableList getSortedCategories(StoreCategoryWrapper root) { + public ObservableList getSortedCategories(StoreCategoryWrapper root) { Comparator comparator = new Comparator<>() { @Override public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) { var o1Root = o1.getRoot(); var o2Root = o2.getRoot(); - if (o1Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) { return -1; } - if (o2Root.equals(getAllConnectionsCategory()) && !o1Root.equals(o2Root)) { return 1; } @@ -302,6 +265,22 @@ public class StoreViewState { return 1; } + if (o1.getDepth() > o2.getDepth()) { + if (o1.getParent() == o2) { + return 1; + } + + return compare(o1.getParent(), o2); + } + + if (o1.getDepth() < o2.getDepth()) { + if (o2.getParent() == o1) { + return -1; + } + + return compare(o1, o2.getParent()); + } + var parent = compare(o1.getParent(), o2.getParent()); if (parent != 0) { return parent; @@ -312,11 +291,13 @@ public class StoreViewState { .compareToIgnoreCase(o2.nameProperty().getValue()); } }; - return categories.filtered(cat -> root == null || cat.getRoot().equals(root)).sorted(comparator); + return ListBindingsHelper.filteredContentBinding( + categories, cat -> root == null || cat.getRoot().equals(root)) + .sorted(comparator); } public StoreCategoryWrapper getAllConnectionsCategory() { - return categories.getList().stream() + return categories.stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)) .findFirst() @@ -324,7 +305,7 @@ public class StoreViewState { } public StoreCategoryWrapper getAllScriptsCategory() { - return categories.getList().stream() + return categories.stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)) .findFirst() @@ -332,14 +313,14 @@ public class StoreViewState { } public StoreEntryWrapper getEntryWrapper(DataStoreEntry entry) { - return allEntries.getList().stream() + return allEntries.stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getEntry().equals(entry)) .findFirst() .orElseThrow(); } public StoreCategoryWrapper getCategoryWrapper(DataStoreCategory entry) { - return categories.getList().stream() + return categories.stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().equals(entry)) .findFirst() diff --git a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java index b05302b02..71c5d8f2d 100644 --- a/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java +++ b/app/src/main/java/io/xpipe/app/ext/DataStoreProvider.java @@ -28,10 +28,6 @@ import java.util.List; public interface DataStoreProvider { - default boolean shouldShow(StoreEntryWrapper w) { - return true; - } - default ObservableBooleanValue busy(StoreEntryWrapper wrapper) { return new SimpleBooleanProperty(false); } 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 169f69660..a61fab6cd 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 @@ -4,14 +4,17 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.CompStructure; import io.xpipe.app.fxcomps.SimpleCompStructure; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.fxcomps.util.PlatformThread; import io.xpipe.app.util.Translatable; + import javafx.beans.property.Property; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.scene.control.ComboBox; import javafx.util.StringConverter; + import lombok.AccessLevel; import lombok.experimental.FieldDefaults; @@ -76,7 +79,7 @@ public class ChoiceComp extends Comp>> { list.add(null); } - cb.getItems().setAll(list); + ListBindingsHelper.setContent(cb.getItems(), list); }); cb.valueProperty().addListener((observable, oldValue, newValue) -> { diff --git a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java index c461917c7..b63cf5fc9 100644 --- a/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java +++ b/app/src/main/java/io/xpipe/app/fxcomps/impl/StoreCategoryComp.java @@ -11,10 +11,11 @@ import io.xpipe.app.core.AppI18n; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.SimpleComp; import io.xpipe.app.fxcomps.augment.ContextMenuAugment; -import io.xpipe.app.fxcomps.util.DerivedObservableList; +import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStorage; import io.xpipe.app.storage.DataStoreCategory; import io.xpipe.app.util.ContextMenuHelper; + import javafx.beans.binding.Bindings; import javafx.beans.property.SimpleBooleanProperty; import javafx.css.PseudoClass; @@ -25,6 +26,7 @@ import javafx.scene.control.MenuItem; import javafx.scene.input.KeyCode; import javafx.scene.input.MouseButton; import javafx.scene.layout.Region; + import lombok.EqualsAndHashCode; import lombok.Value; import org.kordamp.ikonli.javafx.FontIcon; @@ -77,12 +79,13 @@ public class StoreCategoryComp extends SimpleComp { showing.bind(cm.showingProperty()); return cm; })); - var shownList = new DerivedObservableList<>(category.getContainedEntries(), true).filtered( + var shownList = ListBindingsHelper.filteredContentBinding( + category.getContainedEntries(), storeEntryWrapper -> { return storeEntryWrapper.shouldShow( StoreViewState.get().getFilterString().getValue()); }, - StoreViewState.get().getFilterString()).getList(); + StoreViewState.get().getFilterString()); var count = new CountComp<>(shownList, category.getContainedEntries(), string -> "(" + string + ")"); var hover = new SimpleBooleanProperty(); var focus = new SimpleBooleanProperty(); diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/DerivedObservableList.java b/app/src/main/java/io/xpipe/app/fxcomps/util/DerivedObservableList.java deleted file mode 100644 index 166bf3ef7..000000000 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/DerivedObservableList.java +++ /dev/null @@ -1,228 +0,0 @@ -package io.xpipe.app.fxcomps.util; - -import javafx.beans.Observable; -import javafx.beans.binding.Bindings; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import lombok.Getter; - -import java.util.*; -import java.util.function.Function; -import java.util.function.Predicate; - -@Getter -public class DerivedObservableList { - - private final ObservableList list; - private final boolean unique; - - public DerivedObservableList(ObservableList list, boolean unique) { - this.list = list; - this.unique = unique; - } - - private DerivedObservableList createNewDerived() { - var l = FXCollections.observableArrayList(); - BindingsHelper.preserve(l, list); - return new DerivedObservableList<>(l, unique); - } - - public void setContent(List newList) { - if (list.equals(newList)) { - return; - } - - if (list.size() == 0) { - list.addAll(newList); - return; - } - - if (newList.size() == 0) { - list.clear(); - return; - } - - if (unique) { - setContentUnique(newList); - } else { - setContentNonUnique(newList); - } - } - - public void setContentNonUnique(List newList) { - var target = list; - 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); - } - - private void setContentUnique(List newList) { - var listSet = new HashSet<>(list); - var newSet = new HashSet<>(newList); - // Addition - if (newSet.containsAll(list)) { - var l = new ArrayList<>(newList); - l.removeIf(t -> !listSet.contains(t)); - // Reordering occurred - if (!l.equals(list)) { - list.setAll(newList); - return; - } - - var start = 0; - for (int end = 0; end <= list.size(); end++) { - var index = end < list.size() ? newList.indexOf(list.get(end)) : newList.size(); - for (; start < index; start++) { - list.add(start, newList.get(start)); - } - start = index + 1; - } - return; - } - - // Removal - if (listSet.containsAll(newList)) { - var l = new ArrayList<>(list); - l.removeIf(t -> !newSet.contains(t)); - // Reordering occurred - if (!l.equals(newList)) { - list.setAll(newList); - return; - } - - var toRemove = new ArrayList<>(list); - toRemove.removeIf(t -> newSet.contains(t)); - list.removeAll(toRemove); - return; - } - - // Other cases are more difficult - list.setAll(newList); - } - - public DerivedObservableList mapped(Function map) { - var l1 = this.createNewDerived(); - Runnable runnable = () -> { - l1.setContent(list.stream().map(map).toList()); - }; - runnable.run(); - list.addListener((ListChangeListener) c -> { - runnable.run(); - }); - return l1; - } - - public void bindContent(ObservableList other) { - setContent(other); - other.addListener((ListChangeListener) c -> { - setContent(other); - }); - } - - public DerivedObservableList filtered(Predicate predicate) { - return filtered(new SimpleObjectProperty<>(predicate)); - } - - public DerivedObservableList filtered(Predicate predicate, Observable... observables) { - return filtered( - Bindings.createObjectBinding( - () -> { - return new Predicate<>() { - @Override - public boolean test(T v) { - return predicate.test(v); - } - }; - }, - Arrays.stream(observables).filter(Objects::nonNull).toArray(Observable[]::new))); - } - - public DerivedObservableList filtered(ObservableValue> predicate) { - var d = this.createNewDerived(); - Runnable runnable = () -> { - d.setContent( - predicate.getValue() != null - ? list.stream().filter(predicate.getValue()).toList() - : list); - }; - runnable.run(); - list.addListener((ListChangeListener) c -> { - runnable.run(); - }); - predicate.addListener(observable -> { - runnable.run(); - }); - return d; - } - - public DerivedObservableList sorted(Comparator comp, Observable... observables) { - return sorted(Bindings.createObjectBinding( - () -> { - return new Comparator<>() { - @Override - public int compare(T o1, T o2) { - return comp.compare(o1, o2); - } - }; - }, - observables)); - } - - public DerivedObservableList sorted(ObservableValue> comp) { - var d = this.createNewDerived(); - Runnable runnable = () -> { - d.setContent(list.stream().sorted(comp.getValue()).toList()); - }; - runnable.run(); - list.addListener((ListChangeListener) c -> { - runnable.run(); - }); - comp.addListener(observable -> { - d.list.sort(comp.getValue()); - }); - return d; - } - - public DerivedObservableList blockUpdatesIf(ObservableBooleanValue block) { - var d = this.createNewDerived(); - Runnable runnable = () -> { - d.setContent(list); - }; - runnable.run(); - list.addListener((ListChangeListener) c -> { - runnable.run(); - }); - block.addListener(observable -> { - runnable.run(); - }); - return d; - } -} diff --git a/app/src/main/java/io/xpipe/app/fxcomps/util/ListBindingsHelper.java b/app/src/main/java/io/xpipe/app/fxcomps/util/ListBindingsHelper.java new file mode 100644 index 000000000..9d0e95d94 --- /dev/null +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/ListBindingsHelper.java @@ -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 void bindContent(ObservableList l1, ObservableList l2) { + setContent(l1, l2); + l2.addListener((ListChangeListener) c -> { + setContent(l1, l2); + }); + } + + public static ObservableValue anyMatch(List> l) { + return Bindings.createBooleanBinding( + () -> { + return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue()); + }, + l.toArray(ObservableValue[]::new)); + } + + public static ObservableList mappedContentBinding(ObservableList l2, Function map) { + ObservableList l1 = FXCollections.observableList(new ArrayList<>()); + Runnable runnable = () -> { + setContent(l1, l2.stream().map(map).toList()); + }; + runnable.run(); + l2.addListener((ListChangeListener) c -> { + runnable.run(); + }); + BindingsHelper.preserve(l1, l2); + return l1; + } + + public static ObservableList cachedMappedContentBinding( + ObservableList all, ObservableList shown, Function map) { + var cache = new HashMap(); + + ObservableList 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) c -> { + runnable.run(); + }); + BindingsHelper.preserve(l1, all); + BindingsHelper.preserve(l1, shown); + return l1; + } + + public static ObservableList orderedContentBinding( + ObservableList l2, Comparator 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 ObservableList orderedContentBinding( + ObservableList l2, ObservableValue> comp) { + ObservableList l1 = FXCollections.observableList(new ArrayList<>()); + Runnable runnable = () -> { + setContent(l1, l2.stream().sorted(comp.getValue()).toList()); + }; + runnable.run(); + l2.addListener((ListChangeListener) c -> { + runnable.run(); + }); + comp.addListener((observable, oldValue, newValue) -> { + runnable.run(); + }); + BindingsHelper.preserve(l1, l2); + return l1; + } + + public static ObservableList filteredContentBinding(ObservableList l2, Predicate predicate) { + return filteredContentBinding(l2, new SimpleObjectProperty<>(predicate)); + } + + public static ObservableList filteredContentBinding( + ObservableList l2, Predicate 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 ObservableList filteredContentBinding( + ObservableList l2, ObservableValue> predicate) { + ObservableList l1 = FXCollections.observableList(new ArrayList<>()); + Runnable runnable = () -> { + setContent( + l1, + predicate.getValue() != null + ? l2.stream().filter(predicate.getValue()).toList() + : l2); + }; + runnable.run(); + l2.addListener((ListChangeListener) c -> { + runnable.run(); + }); + predicate.addListener((c, o, n) -> { + runnable.run(); + }); + BindingsHelper.preserve(l1, l2); + return l1; + } + + public static void setContent(ObservableList target, List 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); + } +} diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java index 58ebf9b10..e992eebb3 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalApplicationType.java @@ -45,16 +45,17 @@ public abstract class ExternalApplicationType implements PrefsChoiceValue { @Override public boolean isAvailable() { try (ShellControl pc = LocalShell.getShell().start()) { - return pc.command(String.format( + var out = pc.command(String.format( "mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications", applicationName)) - .executeAndCheck(); + .readStdoutIfPossible(); + return out.isPresent() && !out.get().isBlank(); } catch (Exception e) { ErrorEvent.fromThrowable(e).handle(); return false; } } - + @Override public boolean isSelectable() { return OsType.getLocal().equals(OsType.MACOS); diff --git a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java index dce1a5635..d044ea8c5 100644 --- a/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java +++ b/app/src/main/java/io/xpipe/app/prefs/ExternalEditorType.java @@ -194,10 +194,10 @@ public interface ExternalEditorType extends PrefsChoiceValue { @Override public void launch(Path file) throws Exception { - ExternalApplicationHelper.startAsync(CommandBuilder.of() - .add("open", "-a") - .addQuoted(applicationName) - .addFile(file.toString())); + try (var sc = LocalShell.getShell().start()) { + sc.executeSimpleCommand(CommandBuilder.of() + .add("open", "-a").addQuoted(applicationName).addFile(file.toString())); + } } } diff --git a/app/src/main/java/io/xpipe/app/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index dbdac50ce..83c722b97 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -328,16 +328,16 @@ public abstract class DataStorage { return; } - entry.setCategoryUuid(newCategory.getUuid()); var children = getDeepStoreChildren(entry); - children.forEach(child -> child.setCategoryUuid(newCategory.getUuid())); - listeners.forEach(storageListener -> storageListener.onStoreListUpdate()); - saveAsync(); - } + var toRemove = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new); + listeners.forEach(storageListener -> storageListener.onStoreRemove(toRemove)); - public void orderBefore(DataStoreEntry entry, DataStoreEntry reference) { - entry.setOrderBefore(reference != null ? reference.getUuid() : null); - listeners.forEach(storageListener -> storageListener.onStoreOrderUpdate()); + entry.setCategoryUuid(newCategory.getUuid()); + children.forEach(child -> child.setCategoryUuid(newCategory.getUuid())); + + var toAdd = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new); + listeners.forEach(storageListener -> storageListener.onStoreAdd(toAdd)); + saveAsync(); } public boolean refreshChildren(DataStoreEntry e) { @@ -439,8 +439,8 @@ public abstract class DataStorage { pair.getKey().setStoreInternal(merged, false); } - var s = pair.getKey().getStorePersistentState(); - var mergedState = s.mergeCopy(pair.getValue().get().getStorePersistentState()); + var mergedState = pair.getKey().getStorePersistentState().deepCopy(); + mergedState.merge(pair.getValue().get().getStorePersistentState()); pair.getKey().setStorePersistentState(mergedState); } } @@ -788,7 +788,9 @@ public abstract class DataStorage { public Optional getStoreEntryIfPresent(@NonNull DataStore store, boolean identityOnly) { return storeEntriesSet.stream() - .filter(n -> n.getStore() == store || (!identityOnly && (n.getStore() != null + .filter(n -> n.getStore() == store + || (!identityOnly + && (n.getStore() != null && Objects.equals( store.getClass(), n.getStore().getClass()) && store.equals(n.getStore())))) diff --git a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java index 0b705df86..5062c15b0 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -72,9 +72,6 @@ public class DataStoreEntry extends StorageElement { @NonFinal String notes; - @NonFinal - UUID orderBefore; - private DataStoreEntry( Path directory, UUID uuid, @@ -89,8 +86,7 @@ public class DataStoreEntry extends StorageElement { JsonNode storePersistentState, boolean expanded, DataStoreColor color, - String notes, UUID orderBefore - ) { + String notes) { super(directory, uuid, name, lastUsed, lastModified, dirty); this.categoryUuid = categoryUuid; this.store = DataStorageParser.storeFromNode(storeNode); @@ -99,7 +95,6 @@ public class DataStoreEntry extends StorageElement { this.configuration = configuration; this.expanded = expanded; this.color = color; - this.orderBefore = orderBefore; this.provider = store != null ? DataStoreProviders.byStoreClass(store.getClass()).orElse(null) : null; @@ -114,12 +109,10 @@ public class DataStoreEntry extends StorageElement { String name, Instant lastUsed, Instant lastModified, - DataStore store, UUID orderBefore - ) { + DataStore store) { super(directory, uuid, name, lastUsed, lastModified, false); this.categoryUuid = categoryUuid; this.store = store; - this.orderBefore = orderBefore; this.storeNode = null; this.validity = Validity.INCOMPLETE; this.configuration = Configuration.defaultConfiguration(); @@ -137,8 +130,7 @@ public class DataStoreEntry extends StorageElement { UUID.randomUUID().toString(), Instant.now(), Instant.now(), - store, - null); + store); } public static DataStoreEntry createNew(@NonNull String name, @NonNull DataStore store) { @@ -167,7 +159,6 @@ public class DataStoreEntry extends StorageElement { null, false, null, - null, null); return entry; } @@ -185,8 +176,7 @@ public class DataStoreEntry extends StorageElement { JsonNode storePersistentState, boolean expanded, DataStoreColor color, - String notes, - UUID orderBeforeEntry) { + String notes) { return new DataStoreEntry( directory, uuid, @@ -201,8 +191,7 @@ public class DataStoreEntry extends StorageElement { storePersistentState, expanded, color, - notes, - orderBeforeEntry); + notes); } public static Optional fromDirectory(Path dir) throws Exception { @@ -237,15 +226,6 @@ public class DataStoreEntry extends StorageElement { .map(jsonNode -> jsonNode.textValue()) .map(Instant::parse) .orElse(Instant.EPOCH); - var order = Optional.ofNullable(stateJson.get("orderBefore")) - .map(node -> { - try { - return mapper.treeToValue(node, UUID.class); - } catch (JsonProcessingException e) { - return null; - } - }) - .orElse(null); var configuration = Optional.ofNullable(json.get("configuration")) .map(node -> { try { @@ -295,19 +275,10 @@ public class DataStoreEntry extends StorageElement { persistentState, expanded, color, - notes, - order + notes )); } - public void setOrderBefore(UUID uuid) { - var changed = !Objects.equals(orderBefore, uuid); - this.orderBefore = uuid; - if (changed) { - notifyUpdate(false, true); - } - } - @Override public int hashCode() { return getUuid().hashCode(); @@ -359,7 +330,7 @@ public class DataStoreEntry extends StorageElement { storePersistentStateNode = JacksonMapper.getDefault().valueToTree(storePersistentState); } } - return (T) storePersistentState; + return (T) sds.getStateClass().cast(storePersistentState); } public void setStorePersistentState(DataStoreState value) { @@ -408,7 +379,6 @@ public class DataStoreEntry extends StorageElement { stateObj.set("persistentState", storePersistentStateNode); obj.set("configuration", mapper.valueToTree(configuration)); stateObj.put("expanded", expanded); - stateObj.put("orderBefore", orderBefore != null ? orderBefore.toString() : null); var entryString = mapper.writeValueAsString(obj); var stateString = mapper.writeValueAsString(stateObj); diff --git a/app/src/main/java/io/xpipe/app/storage/StorageListener.java b/app/src/main/java/io/xpipe/app/storage/StorageListener.java index 92b0ea47d..730a26884 100644 --- a/app/src/main/java/io/xpipe/app/storage/StorageListener.java +++ b/app/src/main/java/io/xpipe/app/storage/StorageListener.java @@ -2,10 +2,6 @@ package io.xpipe.app.storage; public interface StorageListener { - void onStoreOrderUpdate(); - - void onStoreListUpdate(); - void onStoreAdd(DataStoreEntry... entry); void onStoreRemove(DataStoreEntry... entry); diff --git a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java index 5fa676473..580cfdac7 100644 --- a/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/ExternalTerminalType.java @@ -514,17 +514,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override public void launch(LaunchConfiguration configuration) throws Exception { - try (ShellControl pc = LocalShell.getShell()) { - var suffix = "\"" + configuration.getScriptFile().toString().replaceAll("\"", "\\\\\"") + "\""; - pc.osascriptCommand(String.format( - """ - activate application "Terminal" - delay 1 - tell app "Terminal" to do script %s - """, - suffix)) - .execute(); - } + LocalShell.getShell() + .executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("Terminal.app") + .addFile(configuration.getScriptFile())); } }; ExternalTerminalType ITERM2 = new MacOsType("app.iterm2", "iTerm") { @@ -550,26 +544,11 @@ public interface ExternalTerminalType extends PrefsChoiceValue { @Override public void launch(LaunchConfiguration configuration) throws Exception { - try (ShellControl pc = LocalShell.getShell()) { - pc.osascriptCommand(String.format( - """ - if application "iTerm" is not running then - launch application "iTerm" - delay 1 - tell application "iTerm" - tell current tab of current window - close - end tell - end tell - end if - tell application "iTerm" - activate - create window with default profile command "%s" - end tell - """, - configuration.getScriptFile().toString().replaceAll("\"", "\\\\\""))) - .execute(); - } + LocalShell.getShell() + .executeSimpleCommand(CommandBuilder.of() + .add("open", "-a") + .addQuoted("iTerm.app") + .addFile(configuration.getScriptFile())); } }; ExternalTerminalType WARP = new MacOsType("app.warp", "Warp") { diff --git a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java index 8bf48d1a4..f63d33be6 100644 --- a/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java +++ b/app/src/main/java/io/xpipe/app/terminal/WezTerminalType.java @@ -1,9 +1,14 @@ package io.xpipe.app.terminal; +import io.xpipe.app.issue.ErrorEvent; import io.xpipe.app.prefs.ExternalApplicationHelper; +import io.xpipe.app.prefs.ExternalApplicationType; import io.xpipe.app.util.LocalShell; +import io.xpipe.app.util.ThreadHelper; import io.xpipe.app.util.WindowsRegistry; import io.xpipe.core.process.CommandBuilder; +import io.xpipe.core.process.OsType; +import io.xpipe.core.process.ShellControl; import java.nio.file.Path; import java.util.Optional; @@ -26,7 +31,7 @@ public interface WezTerminalType extends ExternalTerminalType { @Override default boolean isRecommended() { - return false; + return OsType.getLocal() != OsType.WINDOWS; } @Override @@ -51,25 +56,62 @@ public interface WezTerminalType extends ExternalTerminalType { @Override protected Optional determineInstallation() { - Optional launcherDir; - launcherDir = WindowsRegistry.local().readValue( - WindowsRegistry.HKEY_LOCAL_MACHINE, - "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BCF6F0DA-5B9A-408D-8562-F680AE6E1EAF}_is1", - "InstallLocation") - .map(p -> p + "\\wezterm-gui.exe"); - return launcherDir.map(Path::of); + try { + var foundKey = WindowsRegistry.local().findKeyForEqualValueMatchRecursive(WindowsRegistry.HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall", "http://wezfurlong.org/wezterm"); + if (foundKey.isPresent()) { + var installKey = WindowsRegistry.local().readValue( + foundKey.get().getHkey(), + foundKey.get().getKey(), + "InstallLocation"); + if (installKey.isPresent()) { + return installKey.map(p -> p + "\\wezterm-gui.exe").map(Path::of); + } + } + } catch (Exception ex) { + ErrorEvent.fromThrowable(ex).omit().handle(); + } + + try (ShellControl pc = LocalShell.getShell()) { + if (pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm-gui"))) { + return Optional.of(Path.of("wezterm-gui")); + } + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + } + + return Optional.empty(); } } - class Linux extends SimplePathType implements WezTerminalType { + class Linux extends ExternalApplicationType implements WezTerminalType { public Linux() { - super("app.wezterm", "wezterm-gui", true); + super("app.wezterm"); + } + + public boolean isAvailable() { + try (ShellControl pc = LocalShell.getShell()) { + return pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm")) && + pc.executeSimpleBooleanCommand(pc.getShellDialect().getWhichCommand("wezterm-gui")); + } catch (Exception e) { + ErrorEvent.fromThrowable(e).omit().handle(); + return false; + } } @Override - protected CommandBuilder toCommand(LaunchConfiguration configuration) { - return CommandBuilder.of().add("start").addFile(configuration.getScriptFile()); + public void launch(LaunchConfiguration configuration) throws Exception { + var spawn = LocalShell.getShell().command(CommandBuilder.of().addFile("wezterm") + .add("cli", "spawn") + .addFile(configuration.getScriptFile())) + .executeAndCheck(); + if (!spawn) { + ExternalApplicationHelper.startAsync(CommandBuilder.of() + .addFile("wezterm-gui") + .add("start") + .addFile(configuration.getScriptFile())); + } } } @@ -81,20 +123,27 @@ public interface WezTerminalType extends ExternalTerminalType { @Override public void launch(LaunchConfiguration configuration) throws Exception { - var path = LocalShell.getShell() - .command(String.format( - "mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null", - applicationName)) - .readStdoutOrThrow(); - var c = CommandBuilder.of() - .addFile(Path.of(path) - .resolve("Contents") - .resolve("MacOS") - .resolve("wezterm-gui") - .toString()) - .add("start") - .add(configuration.getDialectLaunchCommand()); - ExternalApplicationHelper.startAsync(c); + try (var sc = LocalShell.getShell()) { + var path = sc.command( + String.format("mdfind -name '%s' -onlyin /Applications -onlyin ~/Applications -onlyin /System/Applications 2>/dev/null", + applicationName)).readStdoutOrThrow(); + var spawn = sc.command(CommandBuilder.of().addFile(Path.of(path) + .resolve("Contents") + .resolve("MacOS") + .resolve("wezterm").toString()) + .add("cli", "spawn", "--pane-id", "0") + .addFile(configuration.getScriptFile())) + .executeAndCheck(); + if (!spawn) { + ExternalApplicationHelper.startAsync(CommandBuilder.of() + .addFile(Path.of(path) + .resolve("Contents") + .resolve("MacOS") + .resolve("wezterm-gui").toString()) + .add("start") + .addFile(configuration.getScriptFile())); + } + } } } } diff --git a/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java b/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java index 1b2b42f2b..27df53d5a 100644 --- a/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java +++ b/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java @@ -40,7 +40,7 @@ public class DataStoreCategoryChoiceComp extends SimpleComp { value.setValue(newValue); } }); - var box = new ComboBox<>(StoreViewState.get().getSortedCategories(root).getList()); + var box = new ComboBox<>(StoreViewState.get().getSortedCategories(root)); box.setValue(value.getValue()); box.valueProperty().addListener((observable, oldValue, newValue) -> { value.setValue(newValue); diff --git a/app/src/main/java/io/xpipe/app/util/FileOpener.java b/app/src/main/java/io/xpipe/app/util/FileOpener.java index 62390236d..bd742a624 100644 --- a/app/src/main/java/io/xpipe/app/util/FileOpener.java +++ b/app/src/main/java/io/xpipe/app/util/FileOpener.java @@ -28,10 +28,9 @@ public class FileOpener { try { editor.launch(Path.of(localFile).toRealPath()); } catch (Exception e) { - ErrorEvent.fromThrowable(e) - .description("Unable to launch editor " + ErrorEvent.fromThrowable("Unable to launch editor " + editor.toTranslatedString().getValue() - + ".\nMaybe try to use a different editor in the settings.") + + ".\nMaybe try to use a different editor in the settings.", e) .expected() .handle(); } @@ -52,8 +51,7 @@ public class FileOpener { } } } catch (Exception e) { - ErrorEvent.fromThrowable(e) - .description("Unable to open file " + localFile) + ErrorEvent.fromThrowable("Unable to open file " + localFile, e) .handle(); } } @@ -68,8 +66,7 @@ public class FileOpener { pc.executeSimpleCommand("open \"" + localFile + "\""); } } catch (Exception e) { - ErrorEvent.fromThrowable(e) - .description("Unable to open file " + localFile) + ErrorEvent.fromThrowable("Unable to open file " + localFile, e) .handle(); } } diff --git a/build.gradle b/build.gradle index f94acf1c8..8baa20c67 100644 --- a/build.gradle +++ b/build.gradle @@ -88,7 +88,6 @@ project.ext { arch = getArchName() privateExtensions = file("$rootDir/private_extensions.txt").exists() ? file("$rootDir/private_extensions.txt").readLines() : [] isFullRelease = System.getenv('RELEASE') != null && Boolean.parseBoolean(System.getenv('RELEASE')) - isPreRelease = System.getenv('PRERELEASE') != null && Boolean.parseBoolean(System.getenv('PRERELEASE')) isStage = System.getenv('STAGE') != null && Boolean.parseBoolean(System.getenv('STAGE')) rawVersion = file('version').text.trim() versionString = rawVersion + (isFullRelease || isStage ? '' : '-SNAPSHOT') @@ -106,7 +105,7 @@ project.ext { website = 'https://xpipe.io' sourceWebsite = isStage ? 'https://github.com/xpipe-io/xpipe-ptb' : 'https://github.com/xpipe-io/xpipe' authors = 'Christopher Schnick' - javafxVersion = '22.0.1' + javafxVersion = '23-ea+18' platformName = getPlatformName() languages = ["en", "nl", "es", "fr", "de", "it", "pt", "ru", "ja", "zh", "tr", "da"] jvmRunArgs = [ @@ -159,6 +158,11 @@ if (isFullRelease && rawVersion.contains("-")) { throw new IllegalArgumentException("Releases must have canonical versions") } + +if (isStage && !rawVersion.contains("-")) { + throw new IllegalArgumentException("Stage releases must have release numbers") +} + def replaceVariablesInFileAsString(String f, Map replacements) { def fileName = file(f).getName() def text = file(f).text diff --git a/core/src/main/java/io/xpipe/core/process/ShellControl.java b/core/src/main/java/io/xpipe/core/process/ShellControl.java index fdcd066ce..a50e1266e 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellControl.java +++ b/core/src/main/java/io/xpipe/core/process/ShellControl.java @@ -58,13 +58,12 @@ public interface ShellControl extends ProcessControl { default ShellControl withShellStateInit(StatefulDataStore store) { return onInit(shellControl -> { - var s = store.getState().toBuilder() - .osType(shellControl.getOsType()) - .shellDialect(shellControl.getOriginalShellDialect()) - .running(true) - .osName(shellControl.getOsName()) - .build(); - store.setState(s.asNeeded()); + var s = store.getState(); + s.setOsType(shellControl.getOsType()); + s.setShellDialect(shellControl.getOriginalShellDialect()); + s.setRunning(true); + s.setOsName(shellControl.getOsName()); + store.setState(s); }); } @@ -75,8 +74,9 @@ public interface ShellControl extends ProcessControl { return; } - var s = store.getState().toBuilder().running(false).build(); - store.setState(s.asNeeded()); + var s = store.getState(); + s.setRunning(false); + store.setState(s); }); } diff --git a/core/src/main/java/io/xpipe/core/process/ShellNameStoreState.java b/core/src/main/java/io/xpipe/core/process/ShellNameStoreState.java deleted file mode 100644 index 55a06aea4..000000000 --- a/core/src/main/java/io/xpipe/core/process/ShellNameStoreState.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.xpipe.core.process; - -import io.xpipe.core.store.DataStoreState; -import lombok.EqualsAndHashCode; -import lombok.Value; -import lombok.experimental.SuperBuilder; -import lombok.extern.jackson.Jacksonized; - -@Value -@EqualsAndHashCode(callSuper=true) -@SuperBuilder(toBuilder = true) -@Jacksonized -public class ShellNameStoreState extends ShellStoreState { - - String shellName; - - @Override - public DataStoreState mergeCopy(DataStoreState newer) { - var n = (ShellNameStoreState) newer; - var b = toBuilder(); - mergeBuilder(n,b); - return b.shellName(useNewer(shellName, n.shellName)).build(); - } -} diff --git a/core/src/main/java/io/xpipe/core/process/ShellStoreState.java b/core/src/main/java/io/xpipe/core/process/ShellStoreState.java index 26d8208f3..faf4d688e 100644 --- a/core/src/main/java/io/xpipe/core/process/ShellStoreState.java +++ b/core/src/main/java/io/xpipe/core/process/ShellStoreState.java @@ -1,18 +1,19 @@ package io.xpipe.core.process; import io.xpipe.core.store.DataStoreState; + import lombok.AccessLevel; -import lombok.EqualsAndHashCode; import lombok.Getter; +import lombok.Setter; import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; -@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) +@FieldDefaults(level = AccessLevel.PRIVATE) +@Setter @Getter -@EqualsAndHashCode(callSuper=true) -@SuperBuilder(toBuilder = true) @Jacksonized +@SuperBuilder public class ShellStoreState extends DataStoreState implements OsNameState { OsType.Any osType; @@ -25,17 +26,11 @@ public class ShellStoreState extends DataStoreState implements OsNameState { } @Override - public DataStoreState mergeCopy(DataStoreState newer) { + public void merge(DataStoreState newer) { var shellStoreState = (ShellStoreState) newer; - var b = toBuilder(); - mergeBuilder(shellStoreState, b); - return b.build(); - } - - protected void mergeBuilder(ShellStoreState shellStoreState, ShellStoreStateBuilder b) { - b.osType(useNewer(osType, shellStoreState.getOsType())) - .osName(useNewer(osName, shellStoreState.getOsName())) - .shellDialect(useNewer(shellDialect, shellStoreState.getShellDialect())) - .running(useNewer(running, shellStoreState.getRunning())); + osType = useNewer(osType, shellStoreState.getOsType()); + osName = useNewer(osName, shellStoreState.getOsName()); + shellDialect = useNewer(shellDialect, shellStoreState.getShellDialect()); + running = useNewer(running, shellStoreState.getRunning()); } } diff --git a/core/src/main/java/io/xpipe/core/store/DataStoreState.java b/core/src/main/java/io/xpipe/core/store/DataStoreState.java index 03b7f9ec8..7b40db544 100644 --- a/core/src/main/java/io/xpipe/core/store/DataStoreState.java +++ b/core/src/main/java/io/xpipe/core/store/DataStoreState.java @@ -1,22 +1,49 @@ package io.xpipe.core.store; +import io.xpipe.core.util.JacksonMapper; + +import lombok.SneakyThrows; import lombok.experimental.SuperBuilder; -@SuperBuilder(toBuilder = true) +@SuperBuilder public abstract class DataStoreState { public DataStoreState() {} - @SuppressWarnings("unchecked") - public DS asNeeded() { - return (DS) this; - } - protected static T useNewer(T older, T newer) { return newer != null ? newer : older; } - public DataStoreState mergeCopy(DataStoreState newer) { - return this; + public abstract void merge(DataStoreState newer); + + @SneakyThrows + public DataStoreState deepCopy() { + return JacksonMapper.getDefault().treeToValue(JacksonMapper.getDefault().valueToTree(this), getClass()); + } + + @Override + public final int hashCode() { + var tree = JacksonMapper.getDefault().valueToTree(this); + return tree.hashCode(); + } + + @Override + public final boolean equals(Object o) { + if (this == o) { + return true; + } + if (o != null && getClass() != o.getClass()) { + return false; + } + + var tree = JacksonMapper.getDefault().valueToTree(this); + var otherTree = JacksonMapper.getDefault().valueToTree(o); + return tree.equals(otherTree); + } + + @SneakyThrows + public String toString() { + var tree = JacksonMapper.getDefault().valueToTree(this); + return tree.toPrettyString(); } } diff --git a/core/src/main/java/io/xpipe/core/store/StatefulDataStore.java b/core/src/main/java/io/xpipe/core/store/StatefulDataStore.java index 5bda9d93e..ff571c084 100644 --- a/core/src/main/java/io/xpipe/core/store/StatefulDataStore.java +++ b/core/src/main/java/io/xpipe/core/store/StatefulDataStore.java @@ -1,9 +1,11 @@ package io.xpipe.core.store; import io.xpipe.core.util.DataStateProvider; + import lombok.SneakyThrows; import java.util.Arrays; +import java.util.function.Supplier; public interface StatefulDataStore extends DataStore { @@ -17,14 +19,20 @@ public interface StatefulDataStore extends DataStore { return getStateClass().cast(m.invoke(b)); } + @SuppressWarnings("unchecked") default T getState() { - return DataStateProvider.get().getState(this, this::createDefaultState); + return (T) + DataStateProvider.get().getState(this, this::createDefaultState).deepCopy(); } default void setState(T val) { DataStateProvider.get().setState(this, val); } + default T getState(Supplier def) { + return DataStateProvider.get().getState(this, def); + } + @SneakyThrows @SuppressWarnings("unchecked") default Class getStateClass() { diff --git a/dist/changelogs/9.4_incremental.md b/dist/changelogs/9.4_incremental.md index 09f53bde0..cc1a9e9df 100644 --- a/dist/changelogs/9.4_incremental.md +++ b/dist/changelogs/9.4_incremental.md @@ -8,6 +8,8 @@ The file transfer mechanism when editing files had some flaws, which under rare The entire transfer implementation has been rewritten to iron out these issues and increase reliability. Other file browser actions have also been made more reliable. +There seems to be another separate issue with a PowerShell bug when connecting to a Windows system, causing file uploads to be slow. For now, xpipe can fall back to pwsh if it is installed to work around this issue. + ## Git vault improvements The conflict resolution has been improved @@ -15,11 +17,27 @@ The conflict resolution has been improved - In case of a merge conflict, overwriting local changes will now preserve all connections that are not added to the git vault, including local connections - You now have the option to force push changes when a conflict occurs while XPipe is saving while running, not requiring a restart anymore +## Terminal improvements + +The terminal integration got reworked for some terminals: +- iTerm can now launch tabs instead of individual windows. There were also a few issues fixed that prevented it from launching sometimes +- WezTerm now supports tabs on Linux and macOS. The Windows installation detection has been improved to detect all installed versions +- Terminal.app will now launch faster + ## Other - You can now add simple RDP connections without a file - Fix VMware Player/Workstation and MSYS2 not being detected on Windows. Now simply searching for connections should add them automatically if they are installed - The file browser sidebar now only contains connections that can be opened in it, reducing the amount of connection shown +- Clarify error message for RealVNC servers, highlighting that RealVNC uses a proprietary protocol spec that can't be supported by third-party VNC clients like xpipe +- Fix Linux builds containing unnecessary debug symbols +- Fix AUR package also installing a debug package +- Fix application restart not working properly on macOS +- Fix possibility of selecting own children connections as hosts, causing a stack overflow. Please don't try to create cycles in your connection graphs +- Fix vault secrets not correctly updating unless restarted when changing vault passphrase +- Fix connection launcher desktop shortcuts and URLs not properly executing if xpipe is not running +- Fix move to ... menu sometimes not ordering categories correctly - Fix SSH command failing on macOS with homebrew openssh package installed -- Fix SSH connections not opening the correct shell environment on Windows when username contained spaces due to an OpenSSH bug +- Fix SSH connections not opening the correct shell environment on Windows systems when username contained spaces due to an OpenSSH bug - Fix newly added connections not having the correct order +- Fix error messages of external editor programs not being shown when they failed to start diff --git a/dist/jpackage.gradle b/dist/jpackage.gradle index b4dd6a47e..62ae673d3 100644 --- a/dist/jpackage.gradle +++ b/dist/jpackage.gradle @@ -58,7 +58,7 @@ jlink { ] if (org.gradle.internal.os.OperatingSystem.current().isLinux()) { - options += ['--strip-native-debug-symbols'] + options.addAll('--strip-native-debug-symbols', 'exclude-debuginfo-files') } if (useBundledJavaFx) { diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java index c72da8880..185dd0370 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptGroupStoreProvider.java @@ -31,13 +31,15 @@ public class ScriptGroupStoreProvider implements DataStoreProvider { var def = StoreToggleComp.simpleToggle( "base.isDefaultGroup", sec, s -> s.getState().isDefault(), (s, aBoolean) -> { - var state = s.getState().toBuilder().isDefault(aBoolean).build(); + var state = s.getState(); + state.setDefault(aBoolean); s.setState(state); }); var bring = StoreToggleComp.simpleToggle( "base.bringToShells", sec, s -> s.getState().isBringToShell(), (s, aBoolean) -> { - var state = s.getState().toBuilder().bringToShell(aBoolean).build(); + var state = s.getState(); + state.setBringToShell(aBoolean); s.setState(state); }); diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java index bb7531199..b74cde281 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/ScriptStore.java @@ -6,14 +6,15 @@ import io.xpipe.app.storage.DataStoreEntry; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.ShellTemp; import io.xpipe.app.util.Validators; -import io.xpipe.core.process.ShellControl; import io.xpipe.core.process.ShellInitCommand; +import io.xpipe.core.process.ShellControl; import io.xpipe.core.store.DataStore; import io.xpipe.core.store.DataStoreState; import io.xpipe.core.store.FileNames; import io.xpipe.core.store.StatefulDataStore; import io.xpipe.core.util.JacksonizedValue; import lombok.*; +import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; import lombok.extern.jackson.Jacksonized; @@ -221,12 +222,20 @@ public abstract class ScriptStore extends JacksonizedValue implements DataStore, public abstract List> getEffectiveScripts(); - @Value - @EqualsAndHashCode(callSuper=true) - @SuperBuilder(toBuilder = true) + @FieldDefaults(level = AccessLevel.PRIVATE) + @Setter + @Getter + @SuperBuilder @Jacksonized public static class State extends DataStoreState { boolean isDefault; boolean bringToShell; + + @Override + public void merge(DataStoreState newer) { + var s = (State) newer; + isDefault = s.isDefault; + bringToShell = s.bringToShell; + } } } diff --git a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java index 808f3b187..0d6cbb9fb 100644 --- a/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java +++ b/ext/base/src/main/java/io/xpipe/ext/base/script/SimpleScriptStoreProvider.java @@ -56,13 +56,15 @@ public class SimpleScriptStoreProvider implements DataStoreProvider { var def = StoreToggleComp.simpleToggle( "base.isDefaultGroup", sec, s -> s.getState().isDefault(), (s, aBoolean) -> { - var state = s.getState().toBuilder().isDefault(aBoolean).build(); + var state = s.getState(); + state.setDefault(aBoolean); s.setState(state); }); var bring = StoreToggleComp.simpleToggle( "base.bringToShells", sec, s -> s.getState().isBringToShell(), (s, aBoolean) -> { - var state = s.getState().toBuilder().bringToShell(aBoolean).build(); + var state = s.getState(); + state.setBringToShell(aBoolean); s.setState(state); }); diff --git a/gradle/gradle_scripts/vernacular-1.16.jar b/gradle/gradle_scripts/vernacular-1.16.jar index 062d3b270b4f227b4998849ea7d8c2abf2cf031f..261aaa8fb4cdb4e45a33ea9258abe3ddd944b739 100644 GIT binary patch delta 4470 zcmZWsd0fotAD?Gtx-XMdI;N9qnwl<_O6gL%9VMbe+pSZ&Pe)Ree&6NM66J@D95Ixv z9HA7Y#qO3P<*GIcKXxU@?|Gi@c-h&Vf4vGh}(8$0r^9pmcyS%NspCfi-zA1h(Y81-OxS38pJ~_h$N%XEoEFJRdNf z$Wxc)LFD3DUgW(6m_UnCpe*vukY+HjdBC2Fom2DJJo1cVe@C7N*v?p8IKoa4O(RT(D*SD9Aq;XW>k@bCRO;Sq);zNjztuHy;8~KV1xT!o{Wh?a(^K?A0%f6 zrz8Ud;VZjB+Vc40&-9@)&PxKTHf05ZMZ`!`>~wIOrrE6o2Whd|{AfwyL49rp9n$!! zJU}Q(p=ap?*E-Cng`e)yAu>%<13(1LnX=zxWLyTz_Dqc5g2C+%gTC4Ji?I52HL}c0T24{No$x*qL@GALz`pW{6xFt|4cCz zF&0mDL?%bD7>scV>hgRpxlG}wmI@j%xlOI~!LM(CwiMf@N!#9hJ&Q%qfAvgcThQ=9 zmVni@9K_Q?e19cc?Td{neOUG32waq6de=ihaS2HjQzl{1gQp9W#BJG z+^A<36O5X?^#O}8g(1I=(a{NlTbQK2toqLplbN3!=-_zauW3NJQHSY+T0;q=T zA8&F|;=80!%~G73hLncBJk>p{S1_7-XMxJ++3AMu0b@@+n-3Lg8*zRJp6YUmE#;cG zaLtsBI^WOk^gSBp#f|OiagTkpzd)<_=A|zVp8IO=40_CNV_V5saAWh#bIc#*=e)i3 zCFv2kK9Hp~ucWRdZg^9x#pxTNDJ`AV+AU|#nDRSn9-Pt+Hn(ERLA$A5lex*G)8lSMu)b z3qSIzCdUS1rx~dSRy+tD>~0Jl6eO&FJHMc0eQ(Oau7jfzW)<4@ z(a6!_wc6B6_A{UGnPJxjOS#kBBCB}jR_g77dc({e!^xQo)hwpED+_sxHJ)4eZJls! zvoCwOPfS(CdQUhCT0#o+MtoSUWf~KSJFwrc|Nt z+Vc+c*%`skwt`rXC#tU(%`tQEopE7okInQp&pz*N{?Rqhtt(0o`dA0^G_NsP*6JUY zNqco@$G(YkaUE84UpP?}QM1)TerQ|k%y|zv#RdPXirzQM98z~ln{hPGBHC_$PhEn{ zxaFfG|A{gjT9KCcK*(x7wzaeLrM11iMps0oV^`16=^3RLlHZ%;Sq30vW&X1`pi~~dsXh&%H6IlDb{Sixa6*H zv>?*>XN7`W%Ju2n$KKW^3)ovm)>qtqYV;vYqv@o4v7O<&0rQS3^9gQ;snpZSRjE6< zs=0QoCfqTg1=LH=C&(WwOGVc^# zXZtStQGHS_Vsh8LUGa|%=S8bTDh)llb(mxIY0K-pmG29_*C>5CczZHWevNUFy-Lo8 zt6Wvt!xJ-)-(1o=>cf|ebZb4eb2yP1k#^E(IKp~MibmRy$^hww%S`cD1uwjaK)0O%FX^2`ZRpRIV&hIk_O=*plLPAJm+-yvQ4#P_pJp`tH}- ze|tl_%J#wKC3oAnQvP02(tmHyZ;K5tkEK4p($i&@pq-w?PeMQ)881!c5nz%j_jL{8 z$(`p7>gbsqqP&O9r!&pq7imSjFP<7%gl3@jpQS-@c3TLy*X<#*NvNLui9WnHv86pY z=m60Y+5YA#J#;EAyU=0uln9v$fT_Y#`MP`I&M4rB2NLX!0ycPjL8WLQAP1*tFbB^l zm>UgT2&FX|coK?I3WS47>gDWR&R|3K}1+P4g?blQrCn# zmn>AaU=FkBhmeg{xpP_=e?lESz#fa7Bj3Ao}BZ`44aUEWY zh`T)q5JyUZ2620!D)8o`dpYN=GMgvh8qDZsGk9*6aCmEqB`N>M0gTL+6`XV4CEmz6L6U|X_QmH8T8Kt zO5~Q3o~dfsp=!@Z@p+0FOwPvG!TLy^=Lrv8=#%%wcE0gI)&=XRm?-fMH~mxyoBhGfr#?ua4-i5U@3YbRNVqZ{iPBGL$?@=DF9OmryMum>769g z-*bTp!K9)_$3_BGN4DfY@9Xw1Q9>%zjq9p(!{FJv0w?9WN5s@83}VVm*k)dgaDl4- zG_|)HL$HiQF@XzhcO7afE2y{v$ifv#qIL_zBSxm4q<=US7!q-S+ePyP-9-LLq9%N~ z0a0y)XvXM5-bPVpb>S)qIQKR4DNvTds9rEJAsrE=#z^Rm>7t=v9h3NB z52}^@2ov$)y8SKA#~jYvESlvF`AC1~1bvks^MM`#^cE}q{AyIMR>+8jSg~#X^fgNk zytfwJFWxx$C`*<*15kU*_*7!Qtl2-NZ%}>6@VTqVUK466KQVi30H~ABy7WG(*a$31 zR!7kgV!5#~iwEDP0WCeer8xY?$?s4^&(Xh6cQGq-WdO8Xhn6XZrDL!0zI#}G4s_>cmuVkkk zQ8WujiMFBXVJ8+QQy;+ae87i~6H!Vyptx!iz8$ju5Z#osnCwj>?7AM4Z#!o{PA`)%qJq1Izv zT7_9=RM#72*pEIY2q4CDXNTTH%jWREmd)mHA`X>u0hfr@&(Y2aL)hK=S7moah_D;a RK(nEw3@A#LMIob%{{!Tp9F+h7 delta 4573 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 510f28ae4..6f7a6eb33 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-rc-1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/lang/app/strings/translations_da.properties b/lang/app/strings/translations_da.properties index 6ef13280d..ebdcf59ed 100644 --- a/lang/app/strings/translations_da.properties +++ b/lang/app/strings/translations_da.properties @@ -457,6 +457,3 @@ history=Browsing-historik skipAll=Spring alle over notes=Bemærkninger addNotes=Tilføj noter -order=Bestille ... -stickToTop=Hold dig på toppen -orderAheadOf=Bestil på forhånd ... diff --git a/lang/app/strings/translations_de.properties b/lang/app/strings/translations_de.properties index 50229daea..6007c593e 100644 --- a/lang/app/strings/translations_de.properties +++ b/lang/app/strings/translations_de.properties @@ -451,6 +451,3 @@ history=Browsing-Verlauf skipAll=Alles überspringen notes=Anmerkungen addNotes=Notizen hinzufügen -order=Bestellen ... -stickToTop=Oben bleiben -orderAheadOf=Vorbestellen ... diff --git a/lang/app/strings/translations_en.properties b/lang/app/strings/translations_en.properties index 6d5fbbda9..1a7a51780 100644 --- a/lang/app/strings/translations_en.properties +++ b/lang/app/strings/translations_en.properties @@ -454,7 +454,3 @@ history=Browsing history skipAll=Skip all notes=Notes addNotes=Add notes -#context: verb -order=Order ... -stickToTop=Keep on top -orderAheadOf=Order ahead of ... diff --git a/lang/app/strings/translations_es.properties b/lang/app/strings/translations_es.properties index b93169c4b..f76d3d62c 100644 --- a/lang/app/strings/translations_es.properties +++ b/lang/app/strings/translations_es.properties @@ -438,6 +438,3 @@ history=Historial de navegación skipAll=Saltar todo notes=Notas addNotes=Añadir notas -order=Ordenar ... -stickToTop=Mantener arriba -orderAheadOf=Haz tu pedido antes de ... diff --git a/lang/app/strings/translations_fr.properties b/lang/app/strings/translations_fr.properties index 1c88ecabb..1694af0fe 100644 --- a/lang/app/strings/translations_fr.properties +++ b/lang/app/strings/translations_fr.properties @@ -438,6 +438,3 @@ history=Historique de navigation skipAll=Sauter tout notes=Notes addNotes=Ajouter des notes -order=Commander... -stickToTop=Garde le dessus -orderAheadOf=Commande en avance... diff --git a/lang/app/strings/translations_it.properties b/lang/app/strings/translations_it.properties index 3f4d5ddbb..395a0cccf 100644 --- a/lang/app/strings/translations_it.properties +++ b/lang/app/strings/translations_it.properties @@ -438,6 +438,3 @@ history=Cronologia di navigazione skipAll=Salta tutto notes=Note addNotes=Aggiungi note -order=Ordinare ... -stickToTop=Continua a essere in cima -orderAheadOf=Ordina prima di ... diff --git a/lang/app/strings/translations_ja.properties b/lang/app/strings/translations_ja.properties index 79f1ae8f4..3291c001c 100644 --- a/lang/app/strings/translations_ja.properties +++ b/lang/app/strings/translations_ja.properties @@ -438,6 +438,3 @@ history=閲覧履歴 skipAll=すべてスキップする notes=備考 addNotes=メモを追加する -order=注文する -stickToTop=トップをキープする -orderAheadOf=先に注文する diff --git a/lang/app/strings/translations_nl.properties b/lang/app/strings/translations_nl.properties index 9236fd5e2..d4eacd941 100644 --- a/lang/app/strings/translations_nl.properties +++ b/lang/app/strings/translations_nl.properties @@ -438,6 +438,3 @@ history=Browsegeschiedenis skipAll=Alles overslaan notes=Opmerkingen addNotes=Opmerkingen toevoegen -order=Bestellen ... -stickToTop=Bovenaan houden -orderAheadOf=Vooruitbestellen ... diff --git a/lang/app/strings/translations_pt.properties b/lang/app/strings/translations_pt.properties index ae0171a76..8a33fd4d7 100644 --- a/lang/app/strings/translations_pt.properties +++ b/lang/app/strings/translations_pt.properties @@ -438,6 +438,3 @@ history=Histórico de navegação skipAll=Salta tudo notes=Nota addNotes=Adiciona notas -order=Encomenda ... -stickToTop=Mantém-te no topo -orderAheadOf=Encomenda antes de ... diff --git a/lang/app/strings/translations_ru.properties b/lang/app/strings/translations_ru.properties index 06c9f19ec..fcc988d47 100644 --- a/lang/app/strings/translations_ru.properties +++ b/lang/app/strings/translations_ru.properties @@ -438,6 +438,3 @@ history=История просмотров skipAll=Пропустить все notes=Заметки addNotes=Добавляй заметки -order=Заказать ... -stickToTop=Держись на высоте -orderAheadOf=Заказать заранее ... diff --git a/lang/app/strings/translations_tr.properties b/lang/app/strings/translations_tr.properties index 24e7ed2bc..d967eefa0 100644 --- a/lang/app/strings/translations_tr.properties +++ b/lang/app/strings/translations_tr.properties @@ -439,6 +439,3 @@ history=Tarama geçmişi skipAll=Tümünü atla notes=Notlar addNotes=Notlar ekleyin -order=Sipariş ... -stickToTop=Zirvede kal -orderAheadOf=Önceden sipariş verin ... diff --git a/lang/app/strings/translations_zh.properties b/lang/app/strings/translations_zh.properties index 56d1dcc51..798616c51 100644 --- a/lang/app/strings/translations_zh.properties +++ b/lang/app/strings/translations_zh.properties @@ -438,6 +438,3 @@ history=浏览历史 skipAll=全部跳过 notes=说明 addNotes=添加注释 -order=订购 ... -stickToTop=保持在顶部 -orderAheadOf=提前订购... diff --git a/version b/version index cdebdcf6b..0359f2432 100644 --- a/version +++ b/version @@ -1 +1 @@ -9.4-3 +9.4