diff --git a/src/main/java/com/tobiasdiez/easybind/EasyBind.java b/src/main/java/com/tobiasdiez/easybind/EasyBind.java index cf46cad..dbd4e5b 100644 --- a/src/main/java/com/tobiasdiez/easybind/EasyBind.java +++ b/src/main/java/com/tobiasdiez/easybind/EasyBind.java @@ -295,12 +295,12 @@ public static EasyObservableList map(ObservableList sourc return new MappedList<>(sourceList, f); } - public static EasyObservableList flatten(ObservableList> sources) { + public static EasyObservableList flatten(ObservableList> sources) { return new FlattenedList<>(sources); } @SafeVarargs - public static EasyObservableList concat(ObservableList... sources) { + public static EasyObservableList concat(ObservableList... sources) { return new FlattenedList<>(FXCollections.observableArrayList(sources)); } @@ -308,12 +308,27 @@ public static EasyObservableList concat(ObservableList... so * Creates a new list in which each element is converted using the provided mapping. * All changes to the underlying list are propagated to the converted list. *

+ * If the change event indicates that an item was updated, as determined by {@link ListChangeListener.Change#wasUpdated()}, + * the mapping function is called to create a new object reflecting the updated value. + *

* In contrast to {@link #map(ObservableList, Function)}, - * the items are converted when the are inserted instead of when they are accessed. - * Thus the initial CPU overhead and memory consumption is higher but the access to list items is quicker. + * the items are converted when they are inserted instead of when they are accessed. + * Thus, the initial CPU overhead and memory consumption is higher but the access to list items is quicker. */ public static EasyObservableList mapBacked(ObservableList source, Function mapper) { - return new MappedBackedList<>(source, mapper); + return new MappedBackedList<>(source, mapper, true); + } + + /** + * Similar to {@link #mapBacked(ObservableList, Function)}, but allows specifying if new objects should be created on update. + *

+ * If {@code mapOnUpdate} is {@code true}, new objects are created when items in the source list are updated. + *

+ * If {@code mapOnUpdate} is {@code false}, updates do not create new objects. This can be useful in scenarios where + * the mapped objects already have bindings or listeners that reflect changes from the source objects. + */ + public static EasyObservableList mapBacked(ObservableList source, Function mapper, boolean mapOnUpdate) { + return new MappedBackedList<>(source, mapper, mapOnUpdate); } public static EasyBinding combine(ObservableValue src1, ObservableValue src2, BiFunction f) { diff --git a/src/main/java/com/tobiasdiez/easybind/MappedBackedList.java b/src/main/java/com/tobiasdiez/easybind/MappedBackedList.java index 8f483ee..693ad0f 100644 --- a/src/main/java/com/tobiasdiez/easybind/MappedBackedList.java +++ b/src/main/java/com/tobiasdiez/easybind/MappedBackedList.java @@ -11,10 +11,12 @@ class MappedBackedList extends TransformationList implements EasyObs private final Function mapper; private final List backingList; + private final boolean mapOnUpdate; - public MappedBackedList(ObservableList sourceList, Function mapper) { + public MappedBackedList(ObservableList sourceList, Function mapper, boolean mapOnUpdate) { super(sourceList); this.mapper = mapper; + this.mapOnUpdate = mapOnUpdate; this.backingList = new ArrayList<>(sourceList.size()); sourceList.stream().map(mapper).forEach(backingList::add); } @@ -45,8 +47,16 @@ protected void sourceChanged(ListChangeListener.Change change) { } nextPermutation(from, to, permutation); } else if (change.wasUpdated()) { - backingList.set(change.getFrom(), mapper.apply(getSource().get(change.getFrom()))); - nextUpdate(change.getFrom()); + if (mapOnUpdate) { + for (int i = change.getFrom(); i < change.getTo(); i++) { + E old = backingList.set(i, mapper.apply(getSource().get(i))); + nextSet(i, old); + } + } else { + for (int i = change.getFrom(); i < change.getTo(); i++) { + nextUpdate(i); + } + } } else { if (change.wasRemoved()) { int removePosition = change.getFrom(); diff --git a/src/test/java/com/tobiasdiez/easybind/MappedBackedListTest.java b/src/test/java/com/tobiasdiez/easybind/MappedBackedListTest.java new file mode 100644 index 0000000..9e72852 --- /dev/null +++ b/src/test/java/com/tobiasdiez/easybind/MappedBackedListTest.java @@ -0,0 +1,90 @@ +package com.tobiasdiez.easybind; + +import java.util.List; + +import javafx.beans.Observable; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class MappedBackedListTest { + + @Test + public void testMappedBackedListWithMappingOnUpdate() { + ObservableList list = FXCollections.observableArrayList(number -> new Observable[]{number}); + ObservableList mappedList = EasyBind.mapBacked(list, IntegerProperty::get, true); + + IntegerProperty number = new SimpleIntegerProperty(1); + list.add(number); + + assertEquals(1, mappedList.get(0)); + + number.set(2); + + assertEquals(2, mappedList.get(0)); + } + + @Test + public void testMappedBackedListWithoutMappingOnUpdate() { + ObservableList list = FXCollections.observableArrayList(number -> new Observable[]{number}); + ObservableList mappedList = EasyBind.mapBacked(list, IntegerProperty::get, false); + + IntegerProperty number = new SimpleIntegerProperty(1); + list.add(number); + + assertEquals(1, mappedList.get(0)); + + number.set(2); + + assertEquals(1, mappedList.get(0)); + } + + @Test + public void testUnSortedListUpdatesWithMappedBackedList() { + ObservableList list = FXCollections.observableArrayList(number -> new Observable[]{number}); + ObservableList mappedList = EasyBind.mapBacked(list, IntegerProperty::get); + SortedList sortedList = new SortedList<>(mappedList); + + IntegerProperty num1 = new SimpleIntegerProperty(1); + IntegerProperty num2 = new SimpleIntegerProperty(3); + IntegerProperty num3 = new SimpleIntegerProperty(2); + + list.addAll(num1, num2, num3); + + // list= [1, 3, 2], sortedList= [1, 3, 2] + assertEquals(List.of(1, 3, 2), sortedList); + + num2.set(4); + + // list= [1, 4, 2], sortedList= [1, 4, 2] + assertEquals(List.of(1, 4, 2), sortedList); + } + + + @Test + public void testSortedListUpdatesWithMappedBackedList() { + ObservableList list = FXCollections.observableArrayList(number -> new Observable[]{number}); + ObservableList mappedList = EasyBind.mapBacked(list, IntegerProperty::get); + SortedList sortedList = new SortedList<>(mappedList, Integer::compareTo); + + IntegerProperty num1 = new SimpleIntegerProperty(1); + IntegerProperty num2 = new SimpleIntegerProperty(3); + IntegerProperty num3 = new SimpleIntegerProperty(2); + + list.addAll(num1, num2, num3); + + // list= [1, 3, 2], sortedList= [1, 2, 3] + assertEquals(List.of(1, 2, 3), sortedList); + + num2.set(4); + + // list= [1, 4, 2], sortedList= [1, 2, 4] + assertEquals(List.of(1, 2, 4), sortedList); + } +}