From 3568fa65d4b0c8960e4cd74f46d46d19feedfca9 Mon Sep 17 00:00:00 2001 From: "maul.esel" Date: Wed, 6 Apr 2016 02:23:16 +0200 Subject: [PATCH 1/6] Add EasyBind.concat(List>) Creates a new list that combines the values of the given lists. Unlike FXCollections.concat(), updates to the source lists propagate to the combined list. --- .../java/org/fxmisc/easybind/ConcatList.java | 92 +++++++++++++++++++ .../java/org/fxmisc/easybind/EasyBind.java | 11 +++ 2 files changed, 103 insertions(+) create mode 100644 src/main/java/org/fxmisc/easybind/ConcatList.java diff --git a/src/main/java/org/fxmisc/easybind/ConcatList.java b/src/main/java/org/fxmisc/easybind/ConcatList.java new file mode 100644 index 0000000..1a52b16 --- /dev/null +++ b/src/main/java/org/fxmisc/easybind/ConcatList.java @@ -0,0 +1,92 @@ +package org.fxmisc.easybind; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.ObservableListBase; + +class ConcatList extends ObservableListBase { + private final List> sourceLists; + + public ObservableConcatList(List> sourceLists) { + assert sourceLists != null; + this.sourceLists = sourceLists; + for (ObservableList source : sourceLists) + source.addListener(this::onSourceListChanged); + } + + private void onSourceListChanged(ListChangeListener.Change change) { + ObservableList source = change.getList(); + int indexOffset = 0; + for (int i = 0; sourceLists.get(i) != source; ++i) + indexOffset += sourceLists.get(i).size(); + + beginChange(); + while (change.next()) { + if (change.wasPermutated()) { + int rangeSize = change.getTo() - change.getFrom(); + int[] permutation = new int[rangeSize]; + for (int i = 0; i < rangeSize; ++i) + permutation[i] = change.getPermutation(i + change.getFrom()) + indexOffset; + nextPermutation(change.getFrom() + indexOffset, change.getTo() + indexOffset, permutation); + } else if (change.wasUpdated()) { + for (int i = change.getFrom(); i < change.getTo(); ++i) + nextUpdate(i+indexOffset); + } else if (change.wasAdded()) { + nextAdd(change.getFrom()+indexOffset, change.getTo()+indexOffset); + } else + nextRemove(change.getFrom()+indexOffset, change.getRemoved()); + } + endChange(); + } + + @Override + public E get(int index) { + if (index < 0) + throw new IndexOutOfBoundsException("List index must be >= 0. Was " + index); + + for (ObservableList source : sourceLists) { + if (index < source.size()) + return source.get(index); + index -= source.size(); + } + throw new IndexOutOfBoundsException("Index too large."); + } + + @Override + public Iterator iterator() { + return new Iterator() { + Iterator> sourceIterator = sourceLists.iterator(); + Iterator currentIterator = null; + + @Override + public boolean hasNext() { + while (currentIterator == null || !currentIterator.hasNext()) + if (sourceIterator.hasNext()) + currentIterator = sourceIterator.next().iterator(); + else + return false; + return true; + } + + @Override + public E next() { + while (currentIterator == null || !currentIterator.hasNext()) + if (sourceIterator.hasNext()) + currentIterator = sourceIterator.next().iterator(); + else + throw new NoSuchElementException(); + return currentIterator.next(); + } + }; + } + + @Override + public int size() { + return sourceLists.stream().mapToInt(ObservableList::size).sum(); + } +} diff --git a/src/main/java/org/fxmisc/easybind/EasyBind.java b/src/main/java/org/fxmisc/easybind/EasyBind.java index 2fc189f..b92cddd 100644 --- a/src/main/java/org/fxmisc/easybind/EasyBind.java +++ b/src/main/java/org/fxmisc/easybind/EasyBind.java @@ -1,5 +1,6 @@ package org.fxmisc.easybind; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.function.BiFunction; @@ -116,6 +117,16 @@ public static ObservableList map( return new MappedList<>(sourceList, f); } + public static ObservableList concat( + List> sources) { + return new ConcatList<>(sources); + } + + public static ObservableList concat( + ObservableList... sources) { + return new ConcatList<>(Arrays.asList(sources)); + } + public static MonadicBinding combine( ObservableValue src1, ObservableValue src2, From ccf76e1a14e51f64555b37375c8360bc57f32703 Mon Sep 17 00:00:00 2001 From: "maul.esel" Date: Wed, 6 Apr 2016 12:44:45 +0200 Subject: [PATCH 2/6] Fix compiler errors --- src/main/java/org/fxmisc/easybind/ConcatList.java | 3 +-- src/main/java/org/fxmisc/easybind/EasyBind.java | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/fxmisc/easybind/ConcatList.java b/src/main/java/org/fxmisc/easybind/ConcatList.java index 1a52b16..ff4abef 100644 --- a/src/main/java/org/fxmisc/easybind/ConcatList.java +++ b/src/main/java/org/fxmisc/easybind/ConcatList.java @@ -1,6 +1,5 @@ package org.fxmisc.easybind; -import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; @@ -12,7 +11,7 @@ class ConcatList extends ObservableListBase { private final List> sourceLists; - public ObservableConcatList(List> sourceLists) { + public ConcatList(List> sourceLists) { assert sourceLists != null; this.sourceLists = sourceLists; for (ObservableList source : sourceLists) diff --git a/src/main/java/org/fxmisc/easybind/EasyBind.java b/src/main/java/org/fxmisc/easybind/EasyBind.java index b92cddd..5a02cd4 100644 --- a/src/main/java/org/fxmisc/easybind/EasyBind.java +++ b/src/main/java/org/fxmisc/easybind/EasyBind.java @@ -118,12 +118,12 @@ public static ObservableList map( } public static ObservableList concat( - List> sources) { + List> sources) { return new ConcatList<>(sources); } public static ObservableList concat( - ObservableList... sources) { + ObservableList... sources) { return new ConcatList<>(Arrays.asList(sources)); } From 5074448f0499c8541d2b728dff2537defb75a8f5 Mon Sep 17 00:00:00 2001 From: "maul.esel" Date: Wed, 6 Apr 2016 12:45:10 +0200 Subject: [PATCH 3/6] add some tests for concatenating lists --- .../org/fxmisc/easybind/ConcatListTest.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/test/java/org/fxmisc/easybind/ConcatListTest.java diff --git a/src/test/java/org/fxmisc/easybind/ConcatListTest.java b/src/test/java/org/fxmisc/easybind/ConcatListTest.java new file mode 100644 index 0000000..8a1cb1b --- /dev/null +++ b/src/test/java/org/fxmisc/easybind/ConcatListTest.java @@ -0,0 +1,45 @@ +package org.fxmisc.easybind; + +import static org.junit.Assert.*; + +import javafx.beans.binding.Bindings; +import javafx.beans.binding.StringBinding; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import org.junit.Test; + +public class ConcatListTest { + @Test + public void test() { + ObservableList a = FXCollections.observableArrayList("zero", "one", "two"); + ObservableList b = FXCollections.observableArrayList("three", "four", "five"); + ObservableList c = EasyBind.concat(a, b); + ObservableList d = EasyBind.concat(a, a); + + StringBinding bindOne = Bindings.stringValueAt(c, 1); + StringBinding bindFour = Bindings.stringValueAt(c, 4); + + assertEquals(6, c.size()); + assertEquals("one", bindOne.get()); + assertEquals("five", c.get(5)); + + a.remove(1); + assertEquals(5, c.size()); + assertEquals("three", c.get(2)); + assertEquals("two", bindOne.get()); + assertEquals("five", bindFour.get()); + + b.add(1, "x"); + assertEquals(6, c.size()); + assertEquals("x", c.get(3)); + assertEquals("four", bindFour.get()); + + a.set(0, "null"); + assertEquals("null", c.get(0)); + + assertEquals(4, d.size()); + assertEquals("two", d.get(1)); + assertEquals("null", d.get(2)); + } +} From aff84462e799db79c446d3f37275c87ca1912e42 Mon Sep 17 00:00:00 2001 From: Kevin Brightwell Date: Fri, 9 Dec 2016 11:45:28 -0500 Subject: [PATCH 4/6] Fix bugs in the ConcatList implementation * Duplicated List values will break the implementation's change events * Added better tests that test different combinations. This commit closes comments in #8. --- .../java/org/fxmisc/easybind/ConcatList.java | 73 ++++++-- .../org/fxmisc/easybind/ConcatListTest.java | 171 ++++++++++++++++-- 2 files changed, 206 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/fxmisc/easybind/ConcatList.java b/src/main/java/org/fxmisc/easybind/ConcatList.java index ff4abef..03c7335 100644 --- a/src/main/java/org/fxmisc/easybind/ConcatList.java +++ b/src/main/java/org/fxmisc/easybind/ConcatList.java @@ -1,8 +1,6 @@ package org.fxmisc.easybind; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; +import java.util.*; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -11,34 +9,71 @@ class ConcatList extends ObservableListBase { private final List> sourceLists; - public ConcatList(List> sourceLists) { + ConcatList(List> sourceLists) { assert sourceLists != null; this.sourceLists = sourceLists; - for (ObservableList source : sourceLists) - source.addListener(this::onSourceListChanged); + + // We make a Unique set of source lists, otherwise the event gets called multiple + // times if there are duplicate lists. + Set> sourcesSet = new HashSet<>(sourceLists); + sourcesSet.forEach(source -> source.addListener(this::onSourceListChanged)); } private void onSourceListChanged(ListChangeListener.Change change) { ObservableList source = change.getList(); - int indexOffset = 0; - for (int i = 0; sourceLists.get(i) != source; ++i) - indexOffset += sourceLists.get(i).size(); + + List offsets = new ArrayList<>(); + int calcOffset = 0; + for (ObservableList currList : sourceLists) { + if (currList == source) { + offsets.add(calcOffset); + } + + calcOffset += currList.size(); + } + + // Because a List could be duplicated, we have to do the change for EVERY offset. + // Annoying, but it's needed. beginChange(); while (change.next()) { if (change.wasPermutated()) { int rangeSize = change.getTo() - change.getFrom(); - int[] permutation = new int[rangeSize]; - for (int i = 0; i < rangeSize; ++i) - permutation[i] = change.getPermutation(i + change.getFrom()) + indexOffset; - nextPermutation(change.getFrom() + indexOffset, change.getTo() + indexOffset, permutation); + + // build up a set of permutations based on the offsets AND the actual permutation + int[] permutation = new int[rangeSize * offsets.size()]; + for (int offsetIdx = 0; offsetIdx < offsets.size(); ++offsetIdx) { + int indexOffset = offsets.get(offsetIdx); + for (int i = 0; i < rangeSize; ++i) { + permutation[i + offsetIdx * rangeSize] = + change.getPermutation(i + change.getFrom()) + indexOffset; + } + } + + for (int indexOffset: offsets) { + nextPermutation(change.getFrom() + indexOffset, change.getTo() + indexOffset, permutation); + } } else if (change.wasUpdated()) { - for (int i = change.getFrom(); i < change.getTo(); ++i) - nextUpdate(i+indexOffset); + + // For each update, it's just the index from getFrom()..getTo() + indexOffset + for (int indexOffset: offsets) { + for (int i = change.getFrom(); i < change.getTo(); ++i) { + nextUpdate(i + indexOffset); + } + } } else if (change.wasAdded()) { - nextAdd(change.getFrom()+indexOffset, change.getTo()+indexOffset); - } else - nextRemove(change.getFrom()+indexOffset, change.getRemoved()); + + // Each Add is just from() + the offset + for (int indexOffset: offsets) { + nextAdd(change.getFrom() + indexOffset, change.getTo() + indexOffset); + } + + } else { + // Each remove is indexed + for (int indexOffset: offsets) { + nextRemove(change.getFrom() + indexOffset, change.getRemoved()); + } + } } endChange(); } @@ -86,6 +121,6 @@ public E next() { @Override public int size() { - return sourceLists.stream().mapToInt(ObservableList::size).sum(); + return sourceLists.stream().mapToInt(ObservableList::size).sum(); } } diff --git a/src/test/java/org/fxmisc/easybind/ConcatListTest.java b/src/test/java/org/fxmisc/easybind/ConcatListTest.java index 8a1cb1b..16c8204 100644 --- a/src/test/java/org/fxmisc/easybind/ConcatListTest.java +++ b/src/test/java/org/fxmisc/easybind/ConcatListTest.java @@ -5,41 +5,174 @@ import javafx.beans.binding.Bindings; import javafx.beans.binding.StringBinding; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; +import org.junit.Before; import org.junit.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + public class ConcatListTest { - @Test - public void test() { - ObservableList a = FXCollections.observableArrayList("zero", "one", "two"); - ObservableList b = FXCollections.observableArrayList("three", "four", "five"); - ObservableList c = EasyBind.concat(a, b); - ObservableList d = EasyBind.concat(a, a); - StringBinding bindOne = Bindings.stringValueAt(c, 1); - StringBinding bindFour = Bindings.stringValueAt(c, 4); + abstract class CountedChangeListener implements ListChangeListener { + + private int callCount = 0; + + @Override + public void onChanged(Change c) { + callCount++; + actualChange(c); + } + + public int getCallCount() { + return callCount; + } + + public abstract void actualChange(Change c); + } + private ObservableList a; + private ObservableList b; + private ObservableList c; + private ObservableList d; + + private StringBinding bindCOne; + private StringBinding bindCFour; + + @Before + public void setup() { + a = FXCollections.observableArrayList("zero", "one", "two"); + b = FXCollections.observableArrayList("three", "four", "five"); + c = EasyBind.concat(a, b); + d = EasyBind.concat(a, a); + + bindCOne = Bindings.stringValueAt(c, 1); + bindCFour = Bindings.stringValueAt(c, 4); + } + + @Test + public void basicQuery() { assertEquals(6, c.size()); - assertEquals("one", bindOne.get()); + assertEquals("one", bindCOne.get()); + assertEquals("four", bindCFour.get()); assertEquals("five", c.get(5)); + } + + @Test + public void removeValueSub() { + + ListChangeListener checkChange = c -> { + assertTrue(c.wasRemoved()); + assertEquals(1, c.getRemovedSize()); + + assertEquals("one", c.getRemoved().get(0)); + }; + + CountedChangeListener c_index1Removed = + new VerifyCountedChangeListener<>(checkChange, 1); + + CountedChangeListener d_index1Removed = + new VerifyCountedChangeListener<>(checkChange, 2); + + c.addListener(c_index1Removed); + d.addListener(d_index1Removed); a.remove(1); assertEquals(5, c.size()); assertEquals("three", c.get(2)); - assertEquals("two", bindOne.get()); - assertEquals("five", bindFour.get()); + assertEquals("two", bindCOne.get()); + assertEquals("five", bindCFour.get()); + assertEquals(1, c_index1Removed.getCallCount()); - b.add(1, "x"); - assertEquals(6, c.size()); - assertEquals("x", c.get(3)); - assertEquals("four", bindFour.get()); + assertEquals(4, d.size()); + assertEquals("two", d.get(1)); + assertEquals("two", d.get(3)); + assertEquals(1, d_index1Removed.getCallCount()); + } + + @Test + public void addElement() { + + CountedChangeListener c_index1Added = + new VerifyCountedChangeListener<>(c -> { + assertTrue(c.wasAdded()); + assertEquals(1, c.getAddedSize()); + + assertEquals("x", c.getAddedSubList().get(0)); + }, 1); + + CountedChangeListener d_index1Added = new CountedChangeListener() { + @Override + public void actualChange(Change c) { + fail("Should not be called, d is not changed."); + } + }; + + c.addListener(c_index1Added); + d.addListener(d_index1Added); + + b.add(1, "x"); // "three", "x", "four", "five" + + List expectedC = Arrays.asList("zero", "one", "two", "three", "x", "four", "five"); + assertEquals(expectedC, c); + assertEquals("x", c.get(a.size() + 1)); + assertEquals("x", bindCFour.get()); + assertEquals(1, c_index1Added.getCallCount()); + } + + @Test + public void setItem() { + ListChangeListener checkChange = c -> { + assertTrue(c.wasAdded()); + assertEquals(1, c.getAddedSize()); + + assertEquals("null", c.getAddedSubList().get(0)); + }; + + CountedChangeListener c_index0Update = + new VerifyCountedChangeListener<>(checkChange, 1); + + CountedChangeListener d_index0Update = + new VerifyCountedChangeListener<>(checkChange, 2); + + c.addListener(c_index0Update); + d.addListener(d_index0Update); + + // Trigger an "Added" event for the list.. because odd.. not Updated a.set(0, "null"); - assertEquals("null", c.get(0)); - assertEquals(4, d.size()); - assertEquals("two", d.get(1)); - assertEquals("null", d.get(2)); + List expectedC = Arrays.asList("null", "one", "two", "three", "four", "five"); + List expectedD = Arrays.asList("null", "one", "two", "null", "one", "two"); + + assertEquals(expectedC, c); + assertEquals(expectedD, d); + assertEquals(1, c_index0Update.getCallCount()); + assertEquals(1, d_index0Update.getCallCount()); + } + + private class VerifyCountedChangeListener extends CountedChangeListener { + private final int iterationCount; + private final ListChangeListener checkChange; + + VerifyCountedChangeListener(ListChangeListener checkChange, int iterationCount) { + this.checkChange = checkChange; + this.iterationCount = iterationCount; + } + + @Override + public void actualChange(Change c) { + int iterationCount = 0; + while (c.next()) { + checkChange.onChanged(c); + iterationCount++; + } + + assertEquals(this.iterationCount, iterationCount); + } } } From 291d094e48f215700419fd8f3c75ba363e3f0b77 Mon Sep 17 00:00:00 2001 From: Kevin Brightwell Date: Fri, 9 Dec 2016 11:52:45 -0500 Subject: [PATCH 5/6] Add .idea to .gitignore, clean up test case --- .gitignore | 2 ++ src/test/java/org/fxmisc/easybind/ConcatListTest.java | 9 +++------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index f5a0859..e934f90 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ /.classpath /.project /bin + +.idea/ diff --git a/src/test/java/org/fxmisc/easybind/ConcatListTest.java b/src/test/java/org/fxmisc/easybind/ConcatListTest.java index 16c8204..9179b94 100644 --- a/src/test/java/org/fxmisc/easybind/ConcatListTest.java +++ b/src/test/java/org/fxmisc/easybind/ConcatListTest.java @@ -1,20 +1,17 @@ package org.fxmisc.easybind; -import static org.junit.Assert.*; - import javafx.beans.binding.Bindings; import javafx.beans.binding.StringBinding; import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; - import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.function.Function; + +import static org.junit.Assert.*; public class ConcatListTest { @@ -28,7 +25,7 @@ public void onChanged(Change c) { actualChange(c); } - public int getCallCount() { + int getCallCount() { return callCount; } From 9dc092ae97ceff402261d9c7850bced4ff2e58f3 Mon Sep 17 00:00:00 2001 From: Kevin Brightwell Date: Fri, 9 Dec 2016 12:54:35 -0500 Subject: [PATCH 6/6] Added support for an "ObservableList" input * Adds an ObservableList input * Tracks changes to the List, firing changes if the 2D list changes, this eases up the work when bigger changes happen * Change `concat` -> `flattenList`, it's a flatten operation after all --- .../java/org/fxmisc/easybind/EasyBind.java | 12 ++-- .../{ConcatList.java => FlattenedList.java} | 67 +++++++++++++++++-- ...atListTest.java => FlattenedListTest.java} | 51 +++++++++++--- 3 files changed, 108 insertions(+), 22 deletions(-) rename src/main/java/org/fxmisc/easybind/{ConcatList.java => FlattenedList.java} (64%) rename src/test/java/org/fxmisc/easybind/{ConcatListTest.java => FlattenedListTest.java} (79%) diff --git a/src/main/java/org/fxmisc/easybind/EasyBind.java b/src/main/java/org/fxmisc/easybind/EasyBind.java index 5a02cd4..482ebd0 100644 --- a/src/main/java/org/fxmisc/easybind/EasyBind.java +++ b/src/main/java/org/fxmisc/easybind/EasyBind.java @@ -1,6 +1,5 @@ package org.fxmisc.easybind; -import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.function.BiFunction; @@ -12,6 +11,7 @@ import javafx.beans.property.Property; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; @@ -117,14 +117,14 @@ public static ObservableList map( return new MappedList<>(sourceList, f); } - public static ObservableList concat( - List> sources) { - return new ConcatList<>(sources); + public static ObservableList flatten( + ObservableList> sources) { + return new FlattenedList<>(sources); } - public static ObservableList concat( + public static ObservableList flatten( ObservableList... sources) { - return new ConcatList<>(Arrays.asList(sources)); + return new FlattenedList<>(FXCollections.observableArrayList(sources)); } public static MonadicBinding combine( diff --git a/src/main/java/org/fxmisc/easybind/ConcatList.java b/src/main/java/org/fxmisc/easybind/FlattenedList.java similarity index 64% rename from src/main/java/org/fxmisc/easybind/ConcatList.java rename to src/main/java/org/fxmisc/easybind/FlattenedList.java index 03c7335..83bf164 100644 --- a/src/main/java/org/fxmisc/easybind/ConcatList.java +++ b/src/main/java/org/fxmisc/easybind/FlattenedList.java @@ -1,25 +1,80 @@ package org.fxmisc.easybind; import java.util.*; +import java.util.function.Consumer; import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.ObservableListBase; -class ConcatList extends ObservableListBase { - private final List> sourceLists; +class FlattenedList extends ObservableListBase { + private final ObservableList> sourceLists; + + FlattenedList(ObservableList> sourceLists) { + if (sourceLists == null) { + throw new NullPointerException("sourceLists = null"); + } - ConcatList(List> sourceLists) { - assert sourceLists != null; this.sourceLists = sourceLists; // We make a Unique set of source lists, otherwise the event gets called multiple // times if there are duplicate lists. Set> sourcesSet = new HashSet<>(sourceLists); - sourcesSet.forEach(source -> source.addListener(this::onSourceListChanged)); + sourcesSet.forEach(source -> source.addListener(this::onSourceChanged)); + + sourceLists.addListener(this::onSourcesListChanged); + } + + private void onSourcesListChanged(ListChangeListener.Change> change) { + + beginChange(); + + while (change.next()) { + int fromIdx = 0; // Flattened start idx + for (int i = 0; i < change.getFrom(); ++i) { + fromIdx += sourceLists.get(i).size(); + } + + int toIdx = fromIdx; // Flattened end idx + for (int i = change.getFrom(); i < change.getTo(); ++i) { + toIdx += sourceLists.get(i).size(); + } + + final int rangeSize = toIdx - fromIdx; + + if (change.wasPermutated()) { + + // build up a set of permutations based on the offsets AND the actual permutation + int[] permutation = new int[rangeSize]; + int fIdx = fromIdx; + for (int parentIdx = change.getFrom(); parentIdx < change.getTo(); ++parentIdx) { + for (int i = 0; i < sourceLists.get(i).size(); ++i, fIdx++) { + permutation[fIdx] = change.getPermutation(parentIdx) + i; + } + } + + nextPermutation(fromIdx, toIdx, permutation); + } else if (change.wasUpdated()) { + // Just iterate over the fromIdx..toIdx + for (int i = fromIdx; i < toIdx; ++i) { + nextUpdate(i); + } + } else if (change.wasAdded()) { + nextAdd(fromIdx, toIdx); + } else { + // Each remove is indexed + List itemsToRemove = new ArrayList<>(rangeSize); + + change.getRemoved().forEach(itemsToRemove::addAll); + + nextRemove(fromIdx, itemsToRemove); + } + } + + endChange(); } - private void onSourceListChanged(ListChangeListener.Change change) { + private void onSourceChanged(ListChangeListener.Change change) { ObservableList source = change.getList(); List offsets = new ArrayList<>(); diff --git a/src/test/java/org/fxmisc/easybind/ConcatListTest.java b/src/test/java/org/fxmisc/easybind/FlattenedListTest.java similarity index 79% rename from src/test/java/org/fxmisc/easybind/ConcatListTest.java rename to src/test/java/org/fxmisc/easybind/FlattenedListTest.java index 9179b94..148f75e 100644 --- a/src/test/java/org/fxmisc/easybind/ConcatListTest.java +++ b/src/test/java/org/fxmisc/easybind/FlattenedListTest.java @@ -13,7 +13,7 @@ import static org.junit.Assert.*; -public class ConcatListTest { +public class FlattenedListTest { abstract class CountedChangeListener implements ListChangeListener { @@ -37,6 +37,8 @@ int getCallCount() { private ObservableList c; private ObservableList d; + private ObservableList> aa; + private StringBinding bindCOne; private StringBinding bindCFour; @@ -45,7 +47,10 @@ public void setup() { a = FXCollections.observableArrayList("zero", "one", "two"); b = FXCollections.observableArrayList("three", "four", "five"); c = EasyBind.concat(a, b); - d = EasyBind.concat(a, a); + + aa = FXCollections.observableArrayList(a, a); + d = EasyBind.flatten(aa); + bindCOne = Bindings.stringValueAt(c, 1); bindCFour = Bindings.stringValueAt(c, 4); @@ -103,15 +108,8 @@ public void addElement() { assertEquals("x", c.getAddedSubList().get(0)); }, 1); - CountedChangeListener d_index1Added = new CountedChangeListener() { - @Override - public void actualChange(Change c) { - fail("Should not be called, d is not changed."); - } - }; - c.addListener(c_index1Added); - d.addListener(d_index1Added); + d.addListener(failOnRunListener); b.add(1, "x"); // "three", "x", "four", "five" @@ -152,6 +150,32 @@ public void setItem() { assertEquals(1, d_index0Update.getCallCount()); } + @Test + public void removeList() { + ListChangeListener checkChange = c -> { + assertTrue(c.wasRemoved()); + + assertEquals(Arrays.asList("zero", "one", "two"), c.getRemoved()); + }; + + CountedChangeListener d_index0Update = + new VerifyCountedChangeListener<>(checkChange, 1); + + c.addListener(failOnRunListener); + d.addListener(d_index0Update); + + // Trigger an "Added" event for the list.. because odd.. not Updated + + aa.remove(a); + + List expectedC = Arrays.asList("zero", "one", "two", "three", "four", "five"); + List expectedD = Arrays.asList("zero", "one", "two"); + + assertEquals(expectedC, c); + assertEquals(expectedD, d); + assertEquals(1, d_index0Update.getCallCount()); + } + private class VerifyCountedChangeListener extends CountedChangeListener { private final int iterationCount; private final ListChangeListener checkChange; @@ -172,4 +196,11 @@ public void actualChange(Change c) { assertEquals(this.iterationCount, iterationCount); } } + + private final CountedChangeListener failOnRunListener = new CountedChangeListener() { + @Override + public void actualChange(Change c) { + fail("Should not be called, d is not changed."); + } + }; }