Rework derived lists

This commit is contained in:
crschnick 2025-04-10 22:01:33 +00:00
parent c050890e6b
commit 43a7978f5d
17 changed files with 67 additions and 191 deletions

View file

@ -14,7 +14,13 @@ public class ConnectionAddExchangeImpl extends ConnectionAddExchange {
@Override
public Object handle(HttpExchange exchange, Request msg) throws Throwable {
var found = DataStorage.get().getStoreEntryIfPresent(msg.getData(), false);
if (found.isEmpty()) {
found = DataStorage.get().getStoreEntryIfPresent(msg.getName());
}
if (found.isPresent()) {
var data = msg.getData();
found.get().setStoreInternal(data, true);
return Response.builder().connection(found.get().getUuid()).build();
}

View file

@ -40,7 +40,7 @@ public class BrowserFileChooserSessionModel extends BrowserAbstractSessionModel<
return;
}
var l = new DerivedObservableList<>(fileSelection, true);
var l = DerivedObservableList.wrap(fileSelection, true);
l.bindContent(newValue.getFileList().getSelection());
});
}

View file

@ -36,7 +36,7 @@ public class BrowserHistoryTabComp extends SimpleComp {
@Override
protected Region createSimple() {
var state = BrowserHistorySavedStateImpl.get();
var list = new DerivedObservableList<>(state.getEntries(), true)
var list = DerivedObservableList.wrap(state.getEntries(), true)
.filtered(e -> {
if (DataStorage.get() == null) {
return false;

View file

@ -65,7 +65,7 @@ public class BrowserOverviewComp extends SimpleComp {
var rootsOverview = new BrowserFileOverviewComp(model, FXCollections.observableArrayList(roots), false);
var rootsPane = new SimpleTitledPaneComp(AppI18n.observable("roots"), rootsOverview, false);
var recent = new DerivedObservableList<>(model.getSavedState().getRecentDirectories(), true)
var recent = DerivedObservableList.wrap(model.getSavedState().getRecentDirectories(), true)
.mapped(s -> FileEntry.ofDirectory(model.getFileSystem(), s.getDirectory()))
.getList();
var recentOverview = new BrowserFileOverviewComp(model, recent, true);

View file

@ -51,7 +51,7 @@ public class BrowserTransferComp extends SimpleComp {
.styleClass("gray")
.styleClass("download-background");
var binding = new DerivedObservableList<>(model.getItems(), true)
var binding = DerivedObservableList.wrap(model.getItems(), true)
.mapped(item -> item.getBrowserEntry())
.getList();
var list = new BrowserFileSelectionListComp(binding, entry -> {

View file

@ -305,7 +305,7 @@ public class ListBoxViewComp<T> extends Comp<CompStructure<ScrollPane>> {
r.pseudoClassStateChanged(LAST, i == newShown.size() - 1);
}
var d = new DerivedObservableList<>(listView.getChildren(), true);
var d = DerivedObservableList.wrap(listView.getChildren(), true);
d.setContent(newShown);
if (refreshVisibilities) {
updateVisibilities(scroll, listView);

View file

@ -1,138 +0,0 @@
package io.xpipe.app.comp.base;
import io.xpipe.app.comp.Comp;
import io.xpipe.app.comp.CompStructure;
import io.xpipe.app.comp.SimpleCompStructure;
import io.xpipe.app.util.DerivedObservableList;
import io.xpipe.app.util.PlatformThread;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.skin.VirtualFlow;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import lombok.Setter;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
public class ListVirtualViewComp<T> extends Comp<CompStructure<ScrollPane>> {
private static final PseudoClass ODD = PseudoClass.getPseudoClass("odd");
private static final PseudoClass EVEN = PseudoClass.getPseudoClass("even");
private static final PseudoClass FIRST = PseudoClass.getPseudoClass("first");
private static final PseudoClass LAST = PseudoClass.getPseudoClass("last");
private final ObservableList<T> shown;
private final ObservableList<T> all;
private final Function<T, Comp<?>> compFunction;
private final int limit = Integer.MAX_VALUE;
private final boolean scrollBar;
@Setter
private int platformPauseInterval = -1;
public ListVirtualViewComp(
ObservableList<T> shown, ObservableList<T> all, Function<T, Comp<?>> compFunction, boolean scrollBar) {
this.shown = shown;
this.all = all;
this.compFunction = compFunction;
this.scrollBar = scrollBar;
}
@Override
public CompStructure<ScrollPane> createBase() {
Map<T, Region> cache = new IdentityHashMap<>();
var vbox = new VirtualFlow<>();
vbox.getStyleClass().add("list-box-content");
vbox.setFocusTraversable(false);
var scroll = new ScrollPane(vbox);
if (scrollBar) {
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
scroll.skinProperty().subscribe(newValue -> {
if (newValue != null) {
ScrollBar bar = (ScrollBar) scroll.lookup(".scroll-bar:vertical");
bar.opacityProperty()
.bind(Bindings.createDoubleBinding(
() -> {
var v = bar.getVisibleAmount();
// Check for rounding and accuracy issues
// It might not be exactly equal to 1.0
return v < 0.99 ? 1.0 : 0.0;
},
bar.visibleAmountProperty()));
}
});
} else {
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setFitToHeight(true);
}
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scroll.setFitToWidth(true);
scroll.getStyleClass().add("list-box-view-comp");
return new SimpleCompStructure<>(scroll);
}
private void refresh(
VBox listView, List<? extends T> shown, List<? extends T> all, Map<T, Region> cache, boolean asynchronous) {
Runnable update = () -> {
synchronized (cache) {
// Clear cache of unused values
cache.keySet().removeIf(t -> !all.contains(t));
}
final long[] lastPause = {System.currentTimeMillis()};
// Create copy to reduce chances of concurrent modification
var shownCopy = new ArrayList<>(shown);
var newShown = shownCopy.stream()
.map(v -> {
var elapsed = System.currentTimeMillis() - lastPause[0];
if (platformPauseInterval != -1 && elapsed > platformPauseInterval) {
PlatformThread.runNestedLoopIteration();
lastPause[0] = System.currentTimeMillis();
}
if (!cache.containsKey(v)) {
var comp = compFunction.apply(v);
cache.put(v, comp != null ? comp.createRegion() : null);
}
return cache.get(v);
})
.filter(region -> region != null)
.limit(limit)
.toList();
if (listView.getChildren().equals(newShown)) {
return;
}
for (int i = 0; i < newShown.size(); i++) {
var r = newShown.get(i);
r.pseudoClassStateChanged(ODD, i % 2 != 0);
r.pseudoClassStateChanged(EVEN, i % 2 == 0);
r.pseudoClassStateChanged(FIRST, i == 0);
r.pseudoClassStateChanged(LAST, i == newShown.size() - 1);
}
var d = new DerivedObservableList<>(listView.getChildren(), true);
d.setContent(newShown);
};
if (asynchronous) {
Platform.runLater(update);
} else {
PlatformThread.runLaterIfNeeded(update);
}
}
}

View file

@ -58,8 +58,8 @@ public class StoreCategoryWrapper {
this.lastAccess = new SimpleObjectProperty<>(category.getLastAccess());
this.sortMode = new SimpleObjectProperty<>(category.getSortMode());
this.sync = new SimpleObjectProperty<>(category.isSync());
this.children = new DerivedObservableList<>(FXCollections.observableArrayList(), true);
this.directContainedEntries = new DerivedObservableList<>(FXCollections.observableArrayList(), true);
this.children = DerivedObservableList.arrayList(true);
this.directContainedEntries = DerivedObservableList.arrayList(true);
this.color.setValue(category.getColor());
setupListeners();
}

View file

@ -9,6 +9,7 @@ import javafx.beans.property.*;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.control.*;
import javafx.scene.control.skin.ScrollPaneSkin;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
@ -76,10 +77,13 @@ public class StoreCreationComp extends ModalOverlayContentComp {
valSp.getChildren().add(propR);
var sp = new ScrollPane(valSp);
sp.setSkin(new ScrollPaneSkin(sp));
sp.setFitToWidth(true);
var vbar = (ScrollBar) sp.lookup(".scroll-bar:vertical");
var sep = new Separator();
sep.setPadding(new Insets(0, 0, 0, 0));
sep.visibleProperty().bind(vbar.visibleProperty());
var vbox = new VBox(sp, sep);
VBox.setVgrow(sp, Priority.ALWAYS);

View file

@ -43,7 +43,7 @@ public class StoreCreationDialog {
DataStorage.get().updateEntry(e, newE);
if (madeValid) {
StoreViewState.get().triggerStoreListUpdate();
if (e.getProvider().shouldShowScan()
if (validated && e.getProvider().shouldShowScan()
&& AppPrefs.get()
.openConnectionSearchWindowOnConnectionCreation()
.get()) {

View file

@ -200,7 +200,7 @@ public abstract class StoreEntryComp extends SimpleComp {
}
protected Region createButtonBar() {
var list = new DerivedObservableList<>(getWrapper().getActionProviders(), false);
var list = DerivedObservableList.wrap(getWrapper().getActionProviders(), false);
var buttons = list.mapped(actionProvider -> {
var button = buildButton(actionProvider);
return button != null ? button.createRegion() : null;

View file

@ -74,7 +74,7 @@ public class StoreEntryListStatusBarComp extends SimpleComp {
}
private ObservableList<Comp<?>> createActions(BooleanProperty busy) {
var l = new DerivedObservableList<ActionProvider>(FXCollections.observableArrayList(), true);
var l = DerivedObservableList.<ActionProvider>arrayList(true);
StoreViewState.get().getEffectiveBatchModeSelection().getList().addListener((ListChangeListener<
? super StoreEntryWrapper>)
c -> {

View file

@ -164,8 +164,8 @@ public class StoreSection {
if (e.getEntry().getValidity() == DataStoreEntry.Validity.LOAD_FAILED) {
return new StoreSection(
e,
new DerivedObservableList<>(FXCollections.observableArrayList(), true),
new DerivedObservableList<>(FXCollections.observableArrayList(), true),
DerivedObservableList.arrayList(true),
DerivedObservableList.arrayList(true),
depth);
}

View file

@ -27,12 +27,10 @@ public class StoreViewState {
private final StringProperty filter = new SimpleStringProperty();
@Getter
private final DerivedObservableList<StoreEntryWrapper> allEntries = new DerivedObservableList<>(
FXCollections.synchronizedObservableList(FXCollections.observableArrayList()), true);
private final DerivedObservableList<StoreEntryWrapper> allEntries = DerivedObservableList.synchronizedArrayList(true);
@Getter
private final DerivedObservableList<StoreCategoryWrapper> categories = new DerivedObservableList<>(
FXCollections.synchronizedObservableList(FXCollections.observableArrayList()), true);
private final DerivedObservableList<StoreCategoryWrapper> categories = DerivedObservableList.synchronizedArrayList(true);
@Getter
private final IntegerProperty entriesListVisibilityObservable = new SimpleIntegerProperty();
@ -50,8 +48,7 @@ public class StoreViewState {
private final BooleanProperty batchMode = new SimpleBooleanProperty(false);
@Getter
private final DerivedObservableList<StoreEntryWrapper> batchModeSelection = new DerivedObservableList<>(
FXCollections.synchronizedObservableList(FXCollections.observableArrayList()), true);
private final DerivedObservableList<StoreEntryWrapper> batchModeSelection = DerivedObservableList.synchronizedArrayList(true);
@Getter
private boolean initialized = false;
@ -111,6 +108,7 @@ public class StoreViewState {
}
public void selectBatchMode(StoreSection section) {
System.out.println("Select " + section.getWrapper());
var wrapper = section.getWrapper();
if (wrapper != null && !batchModeSelection.getList().contains(wrapper)) {
batchModeSelection.getList().add(wrapper);
@ -171,8 +169,8 @@ public class StoreViewState {
} catch (Exception exception) {
currentTopLevelSection = new StoreSection(
null,
new DerivedObservableList<>(FXCollections.observableArrayList(), true),
new DerivedObservableList<>(FXCollections.observableArrayList(), true),
DerivedObservableList.arrayList(true),
DerivedObservableList.arrayList(true),
0);
ErrorEvent.fromThrowable(exception).handle();
}

View file

@ -54,17 +54,11 @@ public class BitwardenPasswordManager implements PasswordManager {
return null;
}
var cmd = sc.command(CommandBuilder.of()
.add("bw", "unlock", "--passwordenv", "BW_PASSWORD")
.add("bw", "unlock", "--raw", "--passwordenv", "BW_PASSWORD")
.fixedEnvironment("BW_PASSWORD", pw.getSecret().getSecretValue()));
cmd.setSensitive();
var out = cmd.readStdoutOrThrow();
var matcher = Pattern.compile("export BW_SESSION=\"(.+)\"").matcher(out);
if (matcher.find()) {
var sessionKey = matcher.group(1);
sc.view().setSensitiveEnvironmentVariable("BW_SESSION", sessionKey);
} else {
return null;
}
sc.view().setSensitiveEnvironmentVariable("BW_SESSION", out);
}
var b = CommandBuilder.of()

View file

@ -525,6 +525,7 @@ public class DataStoreEntry extends StorageElement {
}
childrenCache = null;
dirty = true;
notifyUpdate(false, updateTime);
}
public void reassignStoreNode() {

View file

@ -14,25 +14,44 @@ import lombok.Getter;
import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
@Getter
public class DerivedObservableList<T> {
public static <T> DerivedObservableList<T> synchronizedArrayList(boolean unique) {
var list = new ArrayList<T>();
return new DerivedObservableList<>(list, FXCollections.synchronizedObservableList(FXCollections.observableList(list)), unique);
}
public static <T> DerivedObservableList<T> arrayList(boolean unique) {
var list = new ArrayList<T>();
return new DerivedObservableList<>(list, FXCollections.observableList(list), unique);
}
public static <T> DerivedObservableList<T> wrap(ObservableList<T> list, boolean unique) {
return new DerivedObservableList<>(null, list, unique);
}
private final List<T> backingList;
private final ObservableList<T> list;
private final boolean unique;
public DerivedObservableList(ObservableList<T> list, boolean unique) {
public DerivedObservableList(List<T> backingList, ObservableList<T> list, boolean unique) {
this.backingList = backingList;
this.list = list;
this.unique = unique;
}
private <V> DerivedObservableList<V> createNewDerived() {
var name = list.getClass().getSimpleName();
var backingList = new ArrayList<V>();
var l = name.toLowerCase().contains("synchronized")
? FXCollections.<V>synchronizedObservableList(FXCollections.observableArrayList())
: FXCollections.<V>observableArrayList();
BindingsHelper.preserve(l, list);
return new DerivedObservableList<>(l, unique);
? FXCollections.synchronizedObservableList(FXCollections.observableList(backingList))
: FXCollections.observableList(backingList);
var derived = new DerivedObservableList<>(backingList, l, unique);
BindingsHelper.preserve(l, this);
return derived;
}
public void setContent(List<? extends T> newList) {
@ -142,13 +161,21 @@ public class DerivedObservableList<T> {
list.setAll(newList);
}
private Stream<T> listStream() {
if (backingList != null) {
return backingList.stream();
}
return list.stream();
}
public <V> DerivedObservableList<V> mapped(Function<T, V> map) {
var cache = new HashMap<T, V>();
var l1 = this.<V>createNewDerived();
Runnable runnable = () -> {
var listSet = new HashSet<>(list);
cache.keySet().removeIf(t -> !listSet.contains(t));
l1.setContent(list.stream()
l1.setContent(listStream()
.map(v -> {
if (!cache.containsKey(v)) {
cache.put(v, map.apply(v));
@ -192,9 +219,8 @@ public class DerivedObservableList<T> {
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()
d.setContent(predicate.getValue() != null
? listStream().filter(predicate.getValue()).toList()
: list);
};
runnable.run();
@ -223,7 +249,7 @@ public class DerivedObservableList<T> {
public DerivedObservableList<T> sorted(ObservableValue<Comparator<T>> comp) {
var d = this.<T>createNewDerived();
Runnable runnable = () -> {
d.setContent(list.stream().sorted(comp.getValue()).toList());
d.setContent(listStream().sorted(comp.getValue()).toList());
};
runnable.run();
list.addListener((ListChangeListener<? super T>) c -> {
@ -234,19 +260,4 @@ public class DerivedObservableList<T> {
});
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;
}
}