Rework list bindings

This commit is contained in:
crschnick 2024-05-29 14:25:45 +00:00
parent e14b38b31f
commit ba7c83a1e8
24 changed files with 373 additions and 366 deletions

View file

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

View file

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

View file

@ -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 -> {

View file

@ -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<OpenFil
return;
}
ListBindingsHelper.bindContent(fileSelection, newValue.getFileList().getSelection());
var l = new DerivedObservableList<>(fileSelection, true);
l.bindContent(newValue.getFileList().getSelection());
});
}

View file

@ -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<CompStructure<Button>> {
}))
.createRegion();
List<? extends ObservableValue<Boolean>> 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 -> {

View file

@ -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<T> extends Comp<CompStructure<ScrollPane>> {
}
if (!listView.getChildren().equals(newShown)) {
ListBindingsHelper.setContent(listView.getChildren(), newShown);
var d = new DerivedObservableList<>(listView.getChildren(), true);
d.setContent(newShown);
}
};

View file

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

View file

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

View file

@ -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<Comp<?>, ObservableValue<Boolean>>();
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();
}
}

View file

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

View file

@ -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<Observable> getUpdateObservables() {
return List.of(category);
}
public void moveTo(DataStoreCategory category) {
ThreadHelper.runAsync(() -> {
DataStorage.get().updateCategory(entry, category);

View file

@ -26,7 +26,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
}
private ContextMenu createMenu() {
if (section.getShownChildren().isEmpty()) {
if (section.getShownChildren().getList().isEmpty()) {
return null;
}
@ -42,7 +42,7 @@ public class StoreQuickAccessButtonComp extends Comp<CompStructure<Button>> {
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<CompStructure<Button>> {
}
var items = new ArrayList<MenuItem>();
for (StoreSection sub : c) {
for (StoreSection sub : c.getList()) {
if (!sub.getWrapper().getValidity().getValue().isUsable()) {
continue;
}

View file

@ -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<StoreSection> allChildren;
ObservableList<StoreSection> shownChildren;
DerivedObservableList<StoreSection> allChildren;
DerivedObservableList<StoreSection> shownChildren;
int depth;
ObservableBooleanValue showDetails;
public StoreSection(
StoreEntryWrapper wrapper,
ObservableList<StoreSection> allChildren,
ObservableList<StoreSection> shownChildren,
DerivedObservableList<StoreSection> allChildren,
DerivedObservableList<StoreSection> 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<StoreSection> sorted(
ObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> category) {
private static DerivedObservableList<StoreSection> sorted(
DerivedObservableList<StoreSection> list, ObservableValue<StoreCategoryWrapper> 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<StoreEntryWrapper> all,
DerivedObservableList<StoreEntryWrapper> all,
Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> 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<StoreEntryWrapper> all,
DerivedObservableList<StoreEntryWrapper> all,
Predicate<StoreEntryWrapper> entryFilter,
ObservableStringValue filterString,
ObservableValue<StoreCategoryWrapper> 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<StoreEntryWrapper> c) {
return c == null
|| c.test(wrapper)
|| allChildren.stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c));
|| allChildren.getList().stream().anyMatch(storeEntrySection -> storeEntrySection.anyMatches(c));
}
}

View file

@ -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<CompStructure<VBox>> {
private Comp<CompStructure<Button>> createQuickAccessButton() {
var quickAccessDisabled = Bindings.createBooleanBinding(
() -> {
return section.getShownChildren().isEmpty();
return section.getShownChildren().getList().isEmpty();
},
section.getShownChildren());
section.getShownChildren().getList());
Consumer<StoreEntryWrapper> quickAccessAction = w -> {
ThreadHelper.runFailableAsync(() -> {
w.executeDefaultAction();
@ -71,11 +69,11 @@ public class StoreSectionComp extends Comp<CompStructure<VBox>> {
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<CompStructure<VBox>> {
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<CompStructure<VBox>> {
// 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<CompStructure<VBox>> {
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<CompStructure<VBox>> {
.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);

View file

@ -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<CompStructure<VBox>> {
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<CompStructure<VBox>> {
+ 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<StoreEntryWrapper> quickAccessAction = action;
var quickAccessButton = new StoreQuickAccessButtonComp(section, quickAccessAction)
.vgrow()
@ -131,13 +129,12 @@ public class StoreSectionMiniComp extends Comp<CompStructure<VBox>> {
// Optimization for large sections. If there are more than 20 children, only add the nodes to the scene if the
// 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<CompStructure<VBox>> {
.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) {

View file

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

View file

@ -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<StoreEntryWrapper> allEntries =
FXCollections.observableList(new CopyOnWriteArrayList<>());
private final DerivedObservableList<StoreEntryWrapper> allEntries =
new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true);
@Getter
private final ObservableList<StoreCategoryWrapper> categories =
FXCollections.observableList(new CopyOnWriteArrayList<>());
private final DerivedObservableList<StoreCategoryWrapper> categories =
new DerivedObservableList<>(FXCollections.observableList(new CopyOnWriteArrayList<>()), true);
private final ObservableIntegerValue updateObservable = new SimpleIntegerProperty();
@Getter
private final Property<StoreCategoryWrapper> 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<StoreEntryWrapper> l;
synchronized (this) {
l = allEntries.stream()
l = allEntries.getList().stream()
.filter(storeEntryWrapper -> a.contains(storeEntryWrapper.getEntry()))
.toList();
}
List<StoreCategoryWrapper> 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<StoreCategoryWrapper> 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<StoreSection> 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<StoreCategoryWrapper> getSortedCategories(StoreCategoryWrapper root) {
public DerivedObservableList<StoreCategoryWrapper> getSortedCategories(StoreCategoryWrapper root) {
Comparator<StoreCategoryWrapper> 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()

View file

@ -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<T> extends Comp<CompStructure<ComboBox<T>>> {
list.add(null);
}
ListBindingsHelper.setContent(cb.getItems(), list);
cb.getItems().setAll(list);
});
cb.valueProperty().addListener((observable, oldValue, newValue) -> {

View file

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

View file

@ -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<T> {
private final ObservableList<T> list;
private final boolean unique;
public DerivedObservableList(ObservableList<T> list, boolean unique) {
this.list = list;
this.unique = unique;
}
private <V> DerivedObservableList<V> createNewDerived() {
var l = FXCollections.<V>observableArrayList();
BindingsHelper.preserve(l, list);
return new DerivedObservableList<>(l, unique);
}
public void setContent(List<? extends T> 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<? extends T> 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<? extends T> 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 <V> DerivedObservableList<V> mapped(Function<T, V> map) {
var l1 = this.<V>createNewDerived();
Runnable runnable = () -> {
l1.setContent(list.stream().map(map).toList());
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
return l1;
}
public void bindContent(ObservableList<T> other) {
setContent(other);
other.addListener((ListChangeListener<? super T>) c -> {
setContent(other);
});
}
public DerivedObservableList<T> filtered(Predicate<T> predicate) {
return filtered(new SimpleObjectProperty<>(predicate));
}
public DerivedObservableList<T> filtered(Predicate<T> 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<T> filtered(ObservableValue<Predicate<T>> predicate) {
var d = this.<T>createNewDerived();
Runnable runnable = () -> {
d.setContent(
predicate.getValue() != null
? list.stream().filter(predicate.getValue()).toList()
: list);
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
predicate.addListener(observable -> {
runnable.run();
});
return d;
}
public DerivedObservableList<T> sorted(Comparator<T> 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<T> sorted(ObservableValue<Comparator<T>> comp) {
var d = this.<T>createNewDerived();
Runnable runnable = () -> {
d.setContent(list.stream().sorted(comp.getValue()).toList());
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
comp.addListener(observable -> {
d.list.sort(comp.getValue());
});
return d;
}
public DerivedObservableList<T> blockUpdatesIf(ObservableBooleanValue block) {
var d = this.<T>createNewDerived();
Runnable runnable = () -> {
d.setContent(list);
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
runnable.run();
});
block.addListener(observable -> {
runnable.run();
});
return d;
}
}

View file

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

View file

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

View file

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

View file

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