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 ae901830..7d83c935 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserOverviewComp.java @@ -6,18 +6,16 @@ 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.ListBindingsHelper; +import io.xpipe.app.fxcomps.util.DerivedObservableList; 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; @@ -70,9 +68,8 @@ public class BrowserOverviewComp extends SimpleComp { var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false); var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview); - var recent = ListBindingsHelper.mappedContentBinding( - model.getSavedState().getRecentDirectories(), - s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())); + var recent = new DerivedObservableList<>(model.getSavedState().getRecentDirectories(), true).mapped( + s -> FileSystem.FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory())).getList(); 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 182e5411..988b8e55 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.ListBindingsHelper; +import io.xpipe.app.fxcomps.util.DerivedObservableList; 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 = ListBindingsHelper.mappedContentBinding(syncItems, item -> item.getBrowserEntry()); + var binding = new DerivedObservableList<>(syncItems, true).mapped(item -> item.getBrowserEntry()).getList(); 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 6e4cbf72..e0f71d5c 100644 --- a/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java +++ b/app/src/main/java/io/xpipe/app/browser/BrowserWelcomeComp.java @@ -1,5 +1,7 @@ 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; @@ -13,10 +15,9 @@ import io.xpipe.app.fxcomps.impl.LabelComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.PrettySvgComp; import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.ListBindingsHelper; +import io.xpipe.app.fxcomps.util.DerivedObservableList; 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; @@ -30,9 +31,6 @@ 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 { @@ -67,7 +65,7 @@ public class BrowserWelcomeComp extends SimpleComp { return new VBox(hbox); } - var list = ListBindingsHelper.filteredContentBinding(state.getEntries(), e -> { + var list = new DerivedObservableList<>(state.getEntries(), true).filtered(e -> { var entry = DataStorage.get().getStoreEntryIfPresent(e.getUuid()); if (entry.isEmpty()) { return false; @@ -78,7 +76,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/session/BrowserFileChooserModel.java b/app/src/main/java/io/xpipe/app/browser/session/BrowserFileChooserModel.java index 9c681e59..689bf2cd 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.ListBindingsHelper; +import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.storage.DataStoreEntryRef; import io.xpipe.app.util.BooleanScope; import io.xpipe.app.util.FileReference; @@ -10,12 +10,10 @@ 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; @@ -40,7 +38,8 @@ public class BrowserFileChooserModel extends BrowserAbstractSessionModel(fileSelection, true); + l.bindContent(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 139202f5..20c7d6c5 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,8 +4,9 @@ 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; @@ -38,10 +39,13 @@ public class DropdownComp extends Comp> { })) .createRegion(); + List> l = cm.getItems().stream() + .map(menuItem -> menuItem.getGraphic().visibleProperty()) + .toList(); button.visibleProperty() - .bind(ListBindingsHelper.anyMatch(cm.getItems().stream() - .map(menuItem -> menuItem.getGraphic().visibleProperty()) - .toList())); + .bind(Bindings.createBooleanBinding(() -> { + return l.stream().anyMatch(booleanObservableValue -> booleanObservableValue.getValue()); + }, l.toArray(ObservableValue[]::new))); 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 da5a77dc..99786962 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,9 +3,8 @@ 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.ListBindingsHelper; +import io.xpipe.app.fxcomps.util.DerivedObservableList; import io.xpipe.app.fxcomps.util.PlatformThread; - import javafx.application.Platform; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -89,7 +88,8 @@ public class ListBoxViewComp extends Comp> { } if (!listView.getChildren().equals(newShown)) { - ListBindingsHelper.setContent(listView.getChildren(), newShown); + var d = new DerivedObservableList<>(listView.getChildren(), true); + d.setContent(newShown); } }; 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 4b2f47ea..c5ad100c 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().stream() + return StoreViewState.get().getCategories().getList().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().stream() + containedEntries.setAll(StoreViewState.get().getAllEntries().getList().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().stream() + children.setAll(StoreViewState.get().getCategories().getList().stream() .filter(storeCategoryWrapper -> getCategory() .getUuid() .equals(storeCategoryWrapper.getCategory().getParentCategory())) 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 53e1c65d..6e150d61 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 @@ -410,6 +410,7 @@ public abstract class StoreEntryComp extends SimpleComp { 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()); @@ -447,7 +448,7 @@ public abstract class StoreEntryComp extends SimpleComp { order.getItems().add(stick); order.getItems().add(new SeparatorMenuItem()); var section = StoreViewState.get().getParentSectionForWrapper(wrapper); - section.get().getAllChildren().forEach(other -> { + section.get().getAllChildren().getList().forEach(other -> { var ow = other.getWrapper(); var op = ow.getEntry().getProvider(); MenuItem m = new MenuItem(ow.getName().getValue(), 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 68df63ab..ad7a40d9 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(), - StoreViewState.get().getCurrentTopLevelSection().getAllChildren(), + StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList(), + StoreViewState.get().getCurrentTopLevelSection().getAllChildren().getList(), (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().stream() + var connections = StoreViewState.get().getAllEntries().getList().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(), + StoreViewState.get().getAllEntries().getList(), StoreViewState.get().getActiveCategory()); var map = new LinkedHashMap, ObservableValue>(); map.put( createList(), Bindings.not(Bindings.isEmpty( - StoreViewState.get().getCurrentTopLevelSection().getShownChildren()))); + StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList()))); map.put(new StoreIntroComp(), showIntro); map.put( new StoreNotFoundComp(), Bindings.and( - Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries())), + Bindings.not(Bindings.isEmpty(StoreViewState.get().getAllEntries().getList())), Bindings.isEmpty( - StoreViewState.get().getCurrentTopLevelSection().getShownChildren()))); + StoreViewState.get().getCurrentTopLevelSection().getShownChildren().getList()))); 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 9ae8eb8f..f657af52 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,7 +8,6 @@ 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; @@ -56,8 +55,7 @@ public class StoreEntryListStatusComp extends SimpleComp { label.textProperty().bind(name); label.getStyleClass().add("name"); - var all = ListBindingsHelper.filteredContentBinding( - StoreViewState.get().getAllEntries(), + var all = StoreViewState.get().getAllEntries().filtered( storeEntryWrapper -> { var rootCategory = storeEntryWrapper.getCategory().getValue().getRoot(); var inRootCategory = StoreViewState.get().getActiveCategory().getValue().getRoot().equals(rootCategory); @@ -68,14 +66,13 @@ public class StoreEntryListStatusComp extends SimpleComp { return inRootCategory && showProvider; }, StoreViewState.get().getActiveCategory()); - var shownList = ListBindingsHelper.filteredContentBinding( - all, + var shownList = all.filtered( storeEntryWrapper -> { return storeEntryWrapper.shouldShow( StoreViewState.get().getFilterString().getValue()); }, StoreViewState.get().getFilterString()); - var count = new CountComp<>(shownList, all); + var count = new CountComp<>(shownList.getList(), all.getList()); 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 3e374d75..5d96ed2d 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,17 +9,13 @@ 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.Comparator; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; @Getter public class StoreEntryWrapper { @@ -65,6 +61,10 @@ public class StoreEntryWrapper { setupListeners(); } + public List getUpdateObservables() { + return List.of(category); + } + public void moveTo(DataStoreCategory category) { ThreadHelper.runAsync(() -> { DataStorage.get().updateCategory(entry, category); 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 c97081b8..57d39e0e 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().isEmpty()) { + if (section.getShownChildren().getList().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.isEmpty()) { + if (c.getList().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) { + for (StoreSection sub : c.getList()) { 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 44d7d312..ac490c99 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,19 +2,16 @@ package io.xpipe.app.comp.store; import io.xpipe.app.fxcomps.Comp; import io.xpipe.app.fxcomps.util.BindingsHelper; -import io.xpipe.app.fxcomps.util.ListBindingsHelper; +import io.xpipe.app.fxcomps.util.DerivedObservableList; 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; @@ -24,15 +21,15 @@ import java.util.function.Predicate; public class StoreSection { StoreEntryWrapper wrapper; - ObservableList allChildren; - ObservableList shownChildren; + DerivedObservableList allChildren; + DerivedObservableList shownChildren; int depth; ObservableBooleanValue showDetails; public StoreSection( StoreEntryWrapper wrapper, - ObservableList allChildren, - ObservableList shownChildren, + DerivedObservableList allChildren, + DerivedObservableList shownChildren, int depth) { this.wrapper = wrapper; this.allChildren = allChildren; @@ -41,10 +38,10 @@ public class StoreSection { if (wrapper != null) { this.showDetails = Bindings.createBooleanBinding( () -> { - return wrapper.getExpanded().get() || allChildren.isEmpty(); + return wrapper.getExpanded().get() || allChildren.getList().isEmpty(); }, wrapper.getExpanded(), - allChildren); + allChildren.getList()); } else { this.showDetails = new SimpleBooleanProperty(true); } @@ -59,8 +56,8 @@ public class StoreSection { } } - private static ObservableList sorted( - ObservableList list, ObservableValue category) { + private static DerivedObservableList sorted( + DerivedObservableList list, ObservableValue category) { if (category == null) { return list; } @@ -94,9 +91,7 @@ public class StoreSection { var mappedSortMode = BindingsHelper.flatMap( category, storeCategoryWrapper -> storeCategoryWrapper != null ? storeCategoryWrapper.getSortMode() : null); - return ListBindingsHelper.orderedContentBinding( - list, - (o1, o2) -> { + return list.sorted((o1, o2) -> { var current = mappedSortMode.getValue(); if (current != null) { return comp.thenComparing(current.comparator()) @@ -109,23 +104,18 @@ public class StoreSection { } public static StoreSection createTopLevel( - ObservableList all, + DerivedObservableList all, Predicate entryFilter, ObservableStringValue filterString, ObservableValue category) { - var topLevel = ListBindingsHelper.filteredContentBinding( - all, - section -> { + var topLevel = all.filtered(section -> { return DataStorage.get().isRootEntry(section.getEntry()); }, category); - var cached = ListBindingsHelper.cachedMappedContentBinding( - topLevel, - topLevel, + var cached = topLevel.mapped( storeEntryWrapper -> create(storeEntryWrapper, 1, all, entryFilter, filterString, category)); var ordered = sorted(cached, category); - var shown = ListBindingsHelper.filteredContentBinding( - ordered, + var shown = ordered.filtered( section -> { var showFilter = filterString == null || section.matchesFilter(filterString.get()); var matchesSelector = section.anyMatches(entryFilter); @@ -142,15 +132,17 @@ public class StoreSection { private static StoreSection create( StoreEntryWrapper e, int depth, - ObservableList all, + DerivedObservableList all, Predicate entryFilter, ObservableStringValue filterString, ObservableValue category) { if (e.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) { - return new StoreSection(e, FXCollections.observableArrayList(), FXCollections.observableArrayList(), depth); + return new StoreSection(e, new DerivedObservableList<>( + FXCollections.observableArrayList(), true), new DerivedObservableList<>( + FXCollections.observableArrayList(), true), depth); } - var allChildren = ListBindingsHelper.filteredContentBinding(all, other -> { + var allChildren = all.filtered(other -> { // Legacy implementation that does not use children caches. Use for testing // if (true) return DataStorage.get() // .getDisplayParent(other.getEntry()) @@ -163,26 +155,25 @@ public class StoreSection { other.getEntry().getProvider().shouldShow(other); return isChildren && showProvider; }, e.getPersistentState(), e.getCache()); - var cached = ListBindingsHelper.cachedMappedContentBinding( - allChildren, - allChildren, + var cached = allChildren.mapped( entry1 -> create(entry1, depth + 1, all, entryFilter, filterString, category)); var ordered = sorted(cached, category); - var filtered = ListBindingsHelper.filteredContentBinding( - ordered, + var filtered = ordered.filtered( section -> { var showFilter = filterString == null || section.matchesFilter(filterString.get()); var matchesSelector = section.anyMatches(entryFilter); - var sameCategory = category == null + // Prevent updates for children on category switching by checking depth + var showCategory = category == null || category.getValue() == null - || showInCategory(category.getValue(), section.getWrapper()); + || showInCategory(category.getValue(), section.getWrapper()) + || depth > 0; // 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 && sameCategory && notRoot && showProvider; + return showFilter && matchesSelector && showCategory && notRoot && showProvider; }, category, filterString, @@ -217,6 +208,6 @@ public class StoreSection { public boolean anyMatches(Predicate c) { return c == null || c.test(wrapper) - || allChildren.stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c)); + || allChildren.getList().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 db79e515..a485d97f 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,10 +7,8 @@ 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; @@ -42,9 +40,9 @@ public class StoreSectionComp extends Comp> { private Comp> createQuickAccessButton() { var quickAccessDisabled = Bindings.createBooleanBinding( () -> { - return section.getShownChildren().isEmpty(); + return section.getShownChildren().getList().isEmpty(); }, - section.getShownChildren()); + section.getShownChildren().getList()); Consumer quickAccessAction = w -> { ThreadHelper.runFailableAsync(() -> { w.executeDefaultAction(); @@ -71,11 +69,11 @@ public class StoreSectionComp extends Comp> { var expandButton = new IconButtonComp( Bindings.createStringBinding( () -> section.getWrapper().getExpanded().get() - && section.getShownChildren().size() > 0 + && section.getShownChildren().getList().size() > 0 ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right", section.getWrapper().getExpanded(), - section.getShownChildren()), + section.getShownChildren().getList()), () -> { section.getWrapper().toggleExpanded(); }); @@ -89,7 +87,7 @@ public class StoreSectionComp extends Comp> { return "Expand " + section.getWrapper().getName().getValue(); }, section.getWrapper().getName())) - .disable(Bindings.size(section.getShownChildren()).isEqualTo(0)) + .disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0)) .styleClass("expand-button") .maxHeight(100) .vgrow(); @@ -128,13 +126,12 @@ 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 = ListBindingsHelper.filteredContentBinding( - section.getShownChildren(), - storeSection -> section.getAllChildren().size() <= 20 + var listSections = section.getShownChildren().filtered( + storeSection -> section.getAllChildren().getList().size() <= 20 || section.getWrapper().getExpanded().get(), section.getWrapper().getExpanded(), - section.getAllChildren()); - var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> { + section.getAllChildren().getList()); + var content = new ListBoxViewComp<>(listSections.getList(), section.getAllChildren().getList(), (StoreSection e) -> { return StoreSection.customSection(e, false).apply(GrowAugment.create(true, false)); }) .minHeight(0) @@ -143,10 +140,10 @@ public class StoreSectionComp extends Comp> { var expanded = Bindings.createBooleanBinding( () -> { return section.getWrapper().getExpanded().get() - && section.getShownChildren().size() > 0; + && section.getShownChildren().getList().size() > 0; }, section.getWrapper().getExpanded(), - section.getShownChildren()); + section.getShownChildren().getList()); var full = new VerticalComp(List.of( topEntryList, Comp.separator().hide(expanded.not()), @@ -155,7 +152,7 @@ public class StoreSectionComp extends Comp> { .apply(struc -> struc.get().setFillHeight(true)) .hide(Bindings.or( Bindings.not(section.getWrapper().getExpanded()), - Bindings.size(section.getShownChildren()).isEqualTo(0))))); + Bindings.size(section.getShownChildren().getList()).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 774606c3..511a5066 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,9 +8,7 @@ import io.xpipe.app.fxcomps.impl.HorizontalComp; import io.xpipe.app.fxcomps.impl.IconButtonComp; import io.xpipe.app.fxcomps.impl.PrettyImageHelper; import io.xpipe.app.fxcomps.impl.VerticalComp; -import io.xpipe.app.fxcomps.util.ListBindingsHelper; import io.xpipe.app.storage.DataStoreColor; - import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -84,7 +82,7 @@ public class StoreSectionMiniComp extends Comp> { expanded = new SimpleBooleanProperty(section.getWrapper().getExpanded().get() - && section.getShownChildren().size() > 0); + && section.getShownChildren().getList().size() > 0); var button = new IconButtonComp( Bindings.createStringBinding( () -> expanded.get() ? "mdal-keyboard_arrow_down" : "mdal-keyboard_arrow_right", @@ -101,15 +99,15 @@ public class StoreSectionMiniComp extends Comp> { + section.getWrapper().getName().getValue(); }, section.getWrapper().getName())) - .disable(Bindings.size(section.getShownChildren()).isEqualTo(0)) + .disable(Bindings.size(section.getShownChildren().getList()).isEqualTo(0)) .grow(false, true) .styleClass("expand-button"); var quickAccessDisabled = Bindings.createBooleanBinding( () -> { - return section.getShownChildren().isEmpty(); + return section.getShownChildren().getList().isEmpty(); }, - section.getShownChildren()); + section.getShownChildren().getList()); Consumer quickAccessAction = action; var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction) .vgrow() @@ -131,13 +129,12 @@ 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 - ? ListBindingsHelper.filteredContentBinding( - section.getShownChildren(), - storeSection -> section.getAllChildren().size() <= 20 || expanded.get(), + ? section.getShownChildren().filtered( + storeSection -> section.getAllChildren().getList().size() <= 20 || expanded.get(), expanded, - section.getAllChildren()) + section.getAllChildren().getList()) : section.getShownChildren(); - var content = new ListBoxViewComp<>(listSections, section.getAllChildren(), (StoreSection e) -> { + var content = new ListBoxViewComp<>(listSections.getList(), section.getAllChildren().getList(), (StoreSection e) -> { return new StoreSectionMiniComp(e, this.augment, this.action, this.condensedStyle); }) .minHeight(0) @@ -148,7 +145,7 @@ public class StoreSectionMiniComp extends Comp> { .apply(struc -> struc.get().setFillHeight(true)) .hide(Bindings.or( Bindings.not(expanded), - Bindings.size(section.getAllChildren()).isEqualTo(0)))); + Bindings.size(section.getAllChildren().getList()).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 0492edc8..69fc2b9c 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().stream() + s.getShownChildren().getList().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().stream() + s.getShownChildren().getList().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 1170c091..555990c8 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,22 +1,17 @@ package io.xpipe.app.comp.store; import io.xpipe.app.core.AppCache; -import io.xpipe.app.fxcomps.util.ListBindingsHelper; +import io.xpipe.app.fxcomps.util.DerivedObservableList; 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.Property; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; +import javafx.beans.property.*; +import javafx.beans.value.ObservableIntegerValue; import javafx.collections.FXCollections; -import javafx.collections.ObservableList; - import lombok.Getter; import java.util.*; @@ -29,12 +24,14 @@ public class StoreViewState { private final StringProperty filter = new SimpleStringProperty(); @Getter - private final ObservableList allEntries = - FXCollections.observableList(new CopyOnWriteArrayList<>()); + private final DerivedObservableList allEntries = + new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true); @Getter - private final ObservableList categories = - FXCollections.observableList(new CopyOnWriteArrayList<>()); + private final DerivedObservableList categories = + new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true); + + private final ObservableIntegerValue updateObservable = new SimpleIntegerProperty(); @Getter private final Property activeCategory = new SimpleObjectProperty<>(); @@ -76,8 +73,8 @@ public class StoreViewState { } private void updateContent() { - categories.forEach(c -> c.update()); - allEntries.forEach(e -> e.update()); + categories.getList().forEach(c -> c.update()); + allEntries.getList().forEach(e -> e.update()); } private void initSections() { @@ -86,16 +83,19 @@ public class StoreViewState { StoreSection.createTopLevel(allEntries, storeEntryWrapper -> true, filter, activeCategory); } catch (Exception exception) { currentTopLevelSection = - new StoreSection(null, FXCollections.emptyObservableList(), FXCollections.emptyObservableList(), 0); + new StoreSection(null, + new DerivedObservableList<>(FXCollections.observableArrayList(), true), + new DerivedObservableList<>(FXCollections.observableArrayList(), true), + 0); ErrorEvent.fromThrowable(exception).handle(); } } private void initContent() { - allEntries.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream() + allEntries.getList().setAll(FXCollections.observableArrayList(DataStorage.get().getStoreEntries().stream() .map(StoreEntryWrapper::new) .toList())); - categories.setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream() + categories.getList().setAll(FXCollections.observableArrayList(DataStorage.get().getStoreCategories().stream() .map(StoreCategoryWrapper::new) .toList())); @@ -103,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.stream() + activeCategory.setValue(categories.getList().stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().getUuid().equals(selected)) .findFirst() - .orElse(categories.stream() + .orElse(categories.getList().stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.DEFAULT_CATEGORY_UUID)) .findFirst() @@ -119,9 +119,9 @@ public class StoreViewState { AppPrefs.get().condenseConnectionDisplay().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> { synchronized (this) { - var l = new ArrayList<>(allEntries); - allEntries.clear(); - allEntries.setAll(l); + var l = new ArrayList<>(allEntries.getList()); + allEntries.getList().clear(); + allEntries.getList().setAll(l); } }); }); @@ -129,6 +129,7 @@ public class StoreViewState { // Watch out for synchronizing all calls to the entries and categories list! DataStorage.get().addListener(new StorageListener() { + @Override public void onStoreAdd(DataStoreEntry... entry) { var l = Arrays.stream(entry) @@ -142,11 +143,11 @@ public class StoreViewState { } synchronized (this) { - allEntries.addAll(l); + allEntries.getList().addAll(l); } synchronized (this) { - categories.stream() - .filter(storeCategoryWrapper -> allEntries.stream() + categories.getList().stream() + .filter(storeCategoryWrapper -> allEntries.getList().stream() .anyMatch(storeEntryWrapper -> storeEntryWrapper .getEntry() .getCategoryUuid() @@ -163,14 +164,14 @@ public class StoreViewState { var a = Arrays.stream(entry).collect(Collectors.toSet()); List l; synchronized (this) { - l = allEntries.stream() + l = allEntries.getList().stream() .filter(storeEntryWrapper -> a.contains(storeEntryWrapper.getEntry())) .toList(); } List cats; synchronized (this) { - cats = categories.stream() - .filter(storeCategoryWrapper -> allEntries.stream() + cats = categories.getList().stream() + .filter(storeCategoryWrapper -> allEntries.getList().stream() .anyMatch(storeEntryWrapper -> storeEntryWrapper .getEntry() .getCategoryUuid() @@ -186,7 +187,7 @@ public class StoreViewState { } synchronized (this) { - allEntries.removeAll(l); + allEntries.getList().removeAll(l); } cats.forEach(storeCategoryWrapper -> storeCategoryWrapper.update()); }); @@ -203,7 +204,7 @@ public class StoreViewState { } synchronized (this) { - categories.add(l); + categories.getList().add(l); } l.update(); }); @@ -213,7 +214,7 @@ public class StoreViewState { public void onCategoryRemove(DataStoreCategory category) { Optional found; synchronized (this) { - found = categories.stream() + found = categories.getList().stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().equals(category)) .findFirst(); @@ -229,7 +230,7 @@ public class StoreViewState { } synchronized (this) { - categories.remove(found.get()); + categories.getList().remove(found.get()); } var p = found.get().getParent(); if (p != null) { @@ -243,12 +244,12 @@ public class StoreViewState { public Optional getParentSectionForWrapper(StoreEntryWrapper wrapper) { StoreSection current = getCurrentTopLevelSection(); while (true) { - var child = current.getAllChildren().stream().filter(section -> section.getWrapper().equals(wrapper)).findFirst(); + var child = current.getAllChildren().getList().stream().filter(section -> section.getWrapper().equals(wrapper)).findFirst(); if (child.isPresent()) { return Optional.of(current); } - var traverse = current.getAllChildren().stream().filter(section -> section.anyMatches(w -> w.equals(wrapper))).findFirst(); + var traverse = current.getAllChildren().getList().stream().filter(section -> section.anyMatches(w -> w.equals(wrapper))).findFirst(); if (traverse.isPresent()) { current = traverse.get(); } else { @@ -257,7 +258,7 @@ public class StoreViewState { } } - public ObservableList getSortedCategories(StoreCategoryWrapper root) { + public DerivedObservableList getSortedCategories(StoreCategoryWrapper root) { Comparator comparator = new Comparator<>() { @Override public int compare(StoreCategoryWrapper o1, StoreCategoryWrapper o2) { @@ -294,13 +295,11 @@ public class StoreViewState { .compareToIgnoreCase(o2.nameProperty().getValue()); } }; - return ListBindingsHelper.filteredContentBinding( - categories, cat -> root == null || cat.getRoot().equals(root)) - .sorted(comparator); + return categories.filtered(cat -> root == null || cat.getRoot().equals(root)).sorted(comparator); } public StoreCategoryWrapper getAllConnectionsCategory() { - return categories.stream() + return categories.getList().stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)) .findFirst() @@ -308,7 +307,7 @@ public class StoreViewState { } public StoreCategoryWrapper getAllScriptsCategory() { - return categories.stream() + return categories.getList().stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().getUuid().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)) .findFirst() @@ -316,14 +315,14 @@ public class StoreViewState { } public StoreEntryWrapper getEntryWrapper(DataStoreEntry entry) { - return allEntries.stream() + return allEntries.getList().stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getEntry().equals(entry)) .findFirst() .orElseThrow(); } public StoreCategoryWrapper getCategoryWrapper(DataStoreCategory entry) { - return categories.stream() + return categories.getList().stream() .filter(storeCategoryWrapper -> storeCategoryWrapper.getCategory().equals(entry)) .findFirst() 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 a61fab6c..169f6966 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,17 +4,14 @@ 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; @@ -79,7 +76,7 @@ public class ChoiceComp extends Comp>> { list.add(null); } - ListBindingsHelper.setContent(cb.getItems(), list); + cb.getItems().setAll(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 b63cf5fc..c461917c 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,11 +11,10 @@ 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.ListBindingsHelper; +import io.xpipe.app.fxcomps.util.DerivedObservableList; 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; @@ -26,7 +25,6 @@ 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; @@ -79,13 +77,12 @@ public class StoreCategoryComp extends SimpleComp { showing.bind(cm.showingProperty()); return cm; })); - var shownList = ListBindingsHelper.filteredContentBinding( - category.getContainedEntries(), + var shownList = new DerivedObservableList<>(category.getContainedEntries(), true).filtered( storeEntryWrapper -> { return storeEntryWrapper.shouldShow( StoreViewState.get().getFilterString().getValue()); }, - StoreViewState.get().getFilterString()); + StoreViewState.get().getFilterString()).getList(); 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 new file mode 100644 index 00000000..166bf3ef --- /dev/null +++ b/app/src/main/java/io/xpipe/app/fxcomps/util/DerivedObservableList.java @@ -0,0 +1,228 @@ +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 deleted file mode 100644 index 9d0e95d9..00000000 --- a/app/src/main/java/io/xpipe/app/fxcomps/util/ListBindingsHelper.java +++ /dev/null @@ -1,190 +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.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/storage/DataStorage.java b/app/src/main/java/io/xpipe/app/storage/DataStorage.java index fb255fc8..893ec5a3 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStorage.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStorage.java @@ -329,13 +329,8 @@ public abstract class DataStorage { } var children = getDeepStoreChildren(entry); - var arr = Stream.concat(Stream.of(entry), children.stream()).toArray(DataStoreEntry[]::new); - listeners.forEach(storageListener -> storageListener.onStoreRemove(arr)); - entry.setCategoryUuid(newCategory.getUuid()); children.forEach(child -> child.setCategoryUuid(newCategory.getUuid())); - - listeners.forEach(storageListener -> storageListener.onStoreAdd(arr)); saveAsync(); } 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 27e696a9..0b705df8 100644 --- a/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java +++ b/app/src/main/java/io/xpipe/app/storage/DataStoreEntry.java @@ -408,7 +408,7 @@ public class DataStoreEntry extends StorageElement { stateObj.set("persistentState", storePersistentStateNode); obj.set("configuration", mapper.valueToTree(configuration)); stateObj.put("expanded", expanded); - stateObj.put("orderBefore", orderBefore.toString()); + 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/util/DataStoreCategoryChoiceComp.java b/app/src/main/java/io/xpipe/app/util/DataStoreCategoryChoiceComp.java index 27df53d5..1b2b42f2 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)); + var box = new ComboBox<>(StoreViewState.get().getSortedCategories(root).getList()); box.setValue(value.getValue()); box.valueProperty().addListener((observable, oldValue, newValue) -> { value.setValue(newValue);