Skip to content

Commit 6ca739e

Browse files
committed
Add onAdded and onRemoved callbacks to ListSynchronizer
1 parent 49c58a0 commit 6ca739e

File tree

2 files changed

+179
-50
lines changed

2 files changed

+179
-50
lines changed

toolkit-fx/src/main/java/com/techsenger/toolkit/fx/collections/ListSynchronizer.java

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.List;
21+
import java.util.function.Consumer;
2122
import java.util.function.Function;
2223
import javafx.collections.ListChangeListener;
2324
import javafx.collections.ObservableList;
@@ -33,48 +34,67 @@
3334
* the {@code targetList} is automatically updated to reflect these changes.
3435
* Synchronization is one-way only — modifications to the {@code targetList} do not affect the {@code sourceList}.
3536
* <p>
37+
* It’s important to note that the JavaFX API does not define when permutation changes should occur. For example,
38+
* {@code FXCollections.reverse(list)} performs a reordering using two types of changes — added and removed. On the
39+
* other hand, {@code List.sort(Comparator.reverseOrder())} produces permutation changes.
40+
* <p>
3641
* To stop synchronization, call {@link #dispose()}.
3742
*
38-
* @param <S> the type of elements in the source list
39-
* @param <T> the type of elements in the target list
43+
* @param <T> the type of elements in the source list
44+
* @param <S> the type of elements in the target list
4045
*
4146
* @author Pavel Castornii
4247
*/
43-
public class ListSynchronizer<S, T> {
48+
public class ListSynchronizer<T, S> {
49+
50+
private final ObservableList<T> sourceList;
51+
52+
private final ObservableList<S> targetList;
4453

45-
private final ObservableList<S> sourceList;
54+
private final Function<T, S> converter;
4655

47-
private final ObservableList<T> targetList;
56+
private final ListChangeListener<T> listener;
4857

49-
private final Function<S, T> converter;
58+
private final Consumer<T> onAdded;
5059

51-
private final ListChangeListener<S> listener;
60+
private final Consumer<T> onRemoved;
61+
62+
public ListSynchronizer(ObservableList<T> sourceList, ObservableList<S> targetList, Function<T, S> converter) {
63+
this(sourceList, targetList, converter, null, null);
64+
}
5265

53-
public ListSynchronizer(ObservableList<S> sourceList,
54-
ObservableList<T> targetList,
55-
Function<S, T> converter) {
66+
public ListSynchronizer(ObservableList<T> sourceList, ObservableList<S> targetList, Function<T, S> converter,
67+
Consumer<T> onAdded, Consumer<T> onRemoved) {
5668
this.sourceList = sourceList;
5769
this.targetList = targetList;
5870
this.converter = converter;
59-
71+
this.onAdded = onAdded;
72+
this.onRemoved = onRemoved;
6073
synchronizeAll();
61-
6274
this.listener = this::handleChanges;
6375
sourceList.addListener(listener);
6476
}
6577

66-
public ObservableList<S> getSourceList() {
78+
public ObservableList<T> getSourceList() {
6779
return sourceList;
6880
}
6981

70-
public ObservableList<T> getTargetList() {
82+
public ObservableList<S> getTargetList() {
7183
return targetList;
7284
}
7385

74-
public Function<S, T> getConverter() {
86+
public Function<T, S> getConverter() {
7587
return converter;
7688
}
7789

90+
public Consumer<T> getOnAdded() {
91+
return onAdded;
92+
}
93+
94+
public Consumer<T> getOnRemoved() {
95+
return onRemoved;
96+
}
97+
7898
/**
7999
* Stops list synchronization. After calling this method, changes in sourceList will no longer be reflected
80100
* in targetList.
@@ -88,7 +108,14 @@ private void synchronizeAll() {
88108
sourceList.forEach(item -> targetList.add(converter.apply(item)));
89109
}
90110

91-
private void handleChanges(ListChangeListener.Change<? extends S> change) {
111+
private void handleChanges(ListChangeListener.Change<? extends T> change) {
112+
// | Operation | wasAdded | wasRemoved | wasReplaced | wasPermutated | wasUpdated |
113+
// | ---------- | -------- | ---------- | ----------- | ------------- | ---------- |
114+
// | Replaced | + | + | + | - | - |
115+
// | Permutated | - | - | - | + | - |
116+
// | Updated | - | - | - | - | + |
117+
// | Added | + | - | - | - | - |
118+
// | Removed | - | + | - | - | - |
92119
while (change.next()) {
93120
if (change.wasPermutated()) {
94121
handlePermutations(change);
@@ -107,34 +134,52 @@ private void handleChanges(ListChangeListener.Change<? extends S> change) {
107134
}
108135
}
109136

110-
private void handleAdditions(ListChangeListener.Change<? extends S> change) {
137+
private void handleAdditions(ListChangeListener.Change<? extends T> change) {
111138
int startIndex = change.getFrom();
112139
for (int i = 0; i < change.getAddedSize(); i++) {
113-
S addedItem = change.getList().get(startIndex + i);
140+
T addedItem = change.getList().get(startIndex + i);
141+
if (this.onAdded != null) {
142+
this.onAdded.accept(addedItem);
143+
}
114144
targetList.add(startIndex + i, converter.apply(addedItem));
115145
}
116146
}
117147

118-
private void handleRemovals(ListChangeListener.Change<? extends S> change) {
148+
private void handleRemovals(ListChangeListener.Change<? extends T> change) {
119149
targetList.subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
150+
if (this.onRemoved != null) {
151+
for (var item : change.getRemoved()) {
152+
this.onRemoved.accept(item);
153+
}
154+
}
120155
}
121156

122-
private void handleReplacements(ListChangeListener.Change<? extends S> change) {
157+
private void handleReplacements(ListChangeListener.Change<? extends T> change) {
158+
if (this.onRemoved != null && change.wasRemoved()) {
159+
for (var item : change.getRemoved()) {
160+
this.onRemoved.accept(item);
161+
}
162+
}
163+
if (this.onAdded != null && change.wasAdded()) {
164+
for (var item : change.getAddedSubList()) {
165+
this.onAdded.accept(item);
166+
}
167+
}
123168
for (int i = change.getFrom(); i < change.getTo(); i++) {
124-
S newItem = change.getList().get(i);
169+
T newItem = change.getList().get(i);
125170
targetList.set(i, converter.apply(newItem));
126171
}
127172
}
128173

129-
private void handlePermutations(ListChangeListener.Change<? extends S> change) {
130-
List<T> tempCopy = new ArrayList<>(targetList);
174+
private void handlePermutations(ListChangeListener.Change<? extends T> change) {
175+
List<S> tempCopy = new ArrayList<>(targetList);
131176
for (int oldIndex = change.getFrom(); oldIndex < change.getTo(); oldIndex++) {
132177
int newIndex = change.getPermutation(oldIndex);
133178
targetList.set(newIndex, tempCopy.get(oldIndex));
134179
}
135180
}
136181

137-
private void handleUpdates(ListChangeListener.Change<? extends S> change) {
182+
private void handleUpdates(ListChangeListener.Change<? extends T> change) {
138183
for (int i = change.getFrom(); i < change.getTo(); i++) {
139184
targetList.set(i, converter.apply(change.getList().get(i)));
140185
}

toolkit-fx/src/test/java/com/techsenger/toolkit/fx/collections/ListSynchronizerTest.java

Lines changed: 110 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616

1717
package com.techsenger.toolkit.fx.collections;
1818

19+
import java.util.ArrayList;
20+
import java.util.Comparator;
21+
import java.util.List;
22+
import javafx.beans.property.IntegerProperty;
23+
import javafx.beans.property.Property;
24+
import javafx.beans.property.SimpleIntegerProperty;
1925
import javafx.collections.FXCollections;
2026
import javafx.collections.ObservableList;
2127
import static org.assertj.core.api.Assertions.assertThat;
@@ -29,15 +35,40 @@
2935
*/
3036
public class ListSynchronizerTest {
3137

38+
private static class TestItem {
39+
40+
private final IntegerProperty value = new SimpleIntegerProperty();
41+
42+
TestItem(int value) {
43+
this.value.set(value);
44+
}
45+
46+
public int getValue() {
47+
return value.get();
48+
}
49+
50+
public void setValue(int value) {
51+
this.value.set(value);
52+
}
53+
54+
public IntegerProperty valueProperty() {
55+
return value;
56+
}
57+
}
58+
3259
private ObservableList<Integer> source;
3360
private ObservableList<String> target;
3461
private ListSynchronizer<Integer, String> synchronizer;
62+
private List<Integer> addedItems;
63+
private List<Integer> removedItems;
3564

3665
@BeforeEach
3766
void setUp() {
3867
source = FXCollections.observableArrayList();
3968
target = FXCollections.observableArrayList();
40-
synchronizer = new ListSynchronizer<>(source, target, Object::toString);
69+
addedItems = new ArrayList<>();
70+
removedItems = new ArrayList<>();
71+
synchronizer = new ListSynchronizer<>(source, target, Object::toString, addedItems::add, removedItems::add);
4172
}
4273

4374
@AfterEach
@@ -49,57 +80,110 @@ void tearDown() {
4980
void initialSync_withAddedElements_shouldMirrorToSecondary() {
5081
source.addAll(1, 2, 3);
5182

52-
assertThat(target)
53-
.hasSize(3)
54-
.containsExactly("1", "2", "3");
83+
assertThat(target).containsExactly("1", "2", "3");
84+
assertThat(addedItems).containsExactly(1, 2, 3);
5585
}
5686

5787
@Test
5888
void addAtIndex_whenInsertingMiddleElement_shouldMaintainOrder() {
5989
source.addAll(1, 3);
6090
source.add(1, 2);
6191

62-
assertThat(target)
63-
.hasSize(3)
64-
.containsExactly("1", "2", "3");
92+
assertThat(target).containsExactly("1", "2", "3");
93+
assertThat(addedItems).containsExactly(1, 3, 2);
6594
}
6695

6796
@Test
6897
void remove_whenMiddleElementRemoved_shouldUpdateSecondary() {
6998
source.addAll(1, 2, 3);
7099
source.remove(1);
71100

72-
assertThat(target)
73-
.hasSize(2)
74-
.containsExactly("1", "3");
101+
assertThat(target).containsExactly("1", "3");
102+
assertThat(removedItems).containsExactly(2);
75103
}
76104

77105
@Test
78-
void replace_whenFirstElementReplaced_shouldUpdateSecondary() {
79-
source.addAll(1, 2);
80-
source.set(0, 3);
106+
void replace_whenElementReplaced_shouldUpdateSecondary() {
107+
source.addAll(1, 2, 3);
108+
source.set(1, 4);
81109

82-
assertThat(target)
83-
.hasSize(2)
84-
.containsExactly("3", "2");
110+
assertThat(target).containsExactly("1", "4", "3");
111+
assertThat(addedItems).containsExactly(1, 2, 3, 4);
112+
assertThat(removedItems).containsExactly(2);
85113
}
86114

87115
@Test
88-
void dispose_whenCalled_shouldStopSynchronization() {
89-
synchronizer.dispose();
90-
source.add(1);
116+
void permutation_whenListSorted_shouldMirrorOrder() {
117+
source.addAll(3, 1, 2);
118+
FXCollections.sort(source);
91119

92-
assertThat(target).isEmpty();
120+
assertThat(target).containsExactly("1", "2", "3");
121+
assertThat(addedItems).containsExactly(3, 1, 2);
122+
assertThat(removedItems).isEmpty();
93123
}
94124

95125
@Test
96-
void permutation_whenListSorted_shouldMirrorOrder() {
97-
source.addAll(3, 1, 2);
98-
source.sort(Integer::compareTo);
126+
void permutation_whenListReversed_shouldMirrorOrder() {
127+
source.addAll(1, 2, 3);
128+
source.sort(Comparator.reverseOrder());
129+
130+
assertThat(target).containsExactly("3", "2", "1");
131+
assertThat(addedItems).containsExactly(1, 2, 3);
132+
assertThat(removedItems).isEmpty();
133+
}
99134

100-
assertThat(target)
101-
.hasSize(3)
102-
.containsExactly("1", "2", "3");
135+
@Test
136+
void update_whenElementUpdated_shouldUpdateSecondary() {
137+
ObservableList<TestItem> sourceItems = FXCollections.observableArrayList(
138+
item -> new Property[] {item.valueProperty()}
139+
);
140+
ObservableList<String> targetItems = FXCollections.observableArrayList();
141+
List<TestItem> added = new ArrayList<>();
142+
List<TestItem> removed = new ArrayList<>();
143+
144+
ListSynchronizer<TestItem, String> itemSynchronizer = new ListSynchronizer<>(
145+
sourceItems, targetItems,
146+
item -> Integer.toString(item.getValue()),
147+
added::add,
148+
removed::add
149+
);
150+
151+
TestItem item1 = new TestItem(1);
152+
TestItem item2 = new TestItem(2);
153+
sourceItems.addAll(item1, item2);
154+
155+
item1.setValue(10);
156+
157+
assertThat(targetItems).containsExactly("10", "2");
158+
assertThat(added).containsExactly(item1, item2);
159+
assertThat(removed).isEmpty();
160+
161+
itemSynchronizer.dispose();
162+
}
163+
164+
@Test
165+
void multipleReplacements_shouldWorkCorrectly() {
166+
source.addAll(1, 2, 3, 4);
167+
source.set(0, 5);
168+
source.set(2, 6);
169+
source.set(3, 7);
170+
171+
assertThat(target).containsExactly("5", "2", "6", "7");
172+
assertThat(addedItems).containsExactly(1, 2, 3, 4, 5, 6, 7);
173+
assertThat(removedItems).containsExactly(1, 3, 4);
103174
}
104175

176+
@Test
177+
void complexOperations_combinationOfAllTypes() {
178+
source.addAll(1, 2, 3);
179+
source.remove(1);
180+
source.add(4);
181+
source.set(0, 5);
182+
FXCollections.sort(source);
183+
184+
assertThat(target).containsExactly("3", "4", "5");
185+
assertThat(addedItems).containsExactly(1, 2, 3, 4, 5);
186+
assertThat(removedItems).containsExactly(2, 1);
187+
}
105188
}
189+

0 commit comments

Comments
 (0)