Skip to content

Commit cad6c7e

Browse files
committed
Refactor list content binding
1 parent 6ca739e commit cad6c7e

File tree

3 files changed

+102
-111
lines changed

3 files changed

+102
-111
lines changed

toolkit-fx/src/main/java/com/techsenger/toolkit/fx/collections/ListSynchronizer.java renamed to toolkit-fx/src/main/java/com/techsenger/toolkit/fx/binding/ListBinder.java

Lines changed: 61 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.techsenger.toolkit.fx.collections;
17+
package com.techsenger.toolkit.fx.binding;
1818

1919
import java.util.ArrayList;
2020
import java.util.List;
@@ -24,91 +24,82 @@
2424
import javafx.collections.ObservableList;
2525

2626
/**
27-
* A utility class that synchronizes two {@link ObservableList} instances in one direction.
28-
* <p>
29-
* The {@code sourceList} contains elements of type {@code S}.
30-
* The {@code targetList} contains corresponding elements of type {@code T},
31-
* which are derived from the source elements using the provided {@link Function} converter.
32-
* <p>
33-
* Whenever the {@code sourceList} changes (additions, removals, replacements, permutations, or updates),
34-
* the {@code targetList} is automatically updated to reflect these changes.
35-
* Synchronization is one-way only — modifications to the {@code targetList} do not affect the {@code sourceList}.
36-
* <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>
41-
* To stop synchronization, call {@link #dispose()}.
27+
* Binds the content of two {@link ObservableList}s by keeping one list synchronized with another using a mapping
28+
* function.
4229
*
43-
* @param <T> the type of elements in the source list
44-
* @param <S> the type of elements in the target list
30+
* @param <T> the element type of the target list
31+
* @param <S> the element type of the source list
4532
*
4633
* @author Pavel Castornii
4734
*/
48-
public class ListSynchronizer<T, S> {
35+
public final class ListBinder<T, S> {
4936

50-
private final ObservableList<T> sourceList;
37+
/**
38+
* Creates a binder that keeps {@code targetList} synchronized with {@code sourceList} by mapping elements of
39+
* {@code sourceList} to {@code targetList}.
40+
*
41+
* @param targetList the target list to be synchronized
42+
* @param sourceList the source list to synchronize from
43+
* @param mapper maps elements of {@code sourceList} to elements of {@code targetList}
44+
*
45+
* @return a {@code ListBinder} that keeps the lists synchronized
46+
*/
47+
public static <T, S> ListBinder<T, S> bindContent(ObservableList<T> targetList, ObservableList<S> sourceList,
48+
Function<S, T> mapper) {
49+
return new ListBinder<>(targetList, sourceList, mapper, null, null);
50+
}
5151

52-
private final ObservableList<S> targetList;
52+
/**
53+
* Creates a binder that keeps {@code targetList} synchronized with {@code sourceList} and invokes callbacks when
54+
* elements are added or removed.
55+
*
56+
* @param targetList the target list to be synchronized
57+
* @param sourceList the source list to synchronize from
58+
* @param mapper maps elements of {@code sourceList} to elements of {@code targetList}
59+
* @param onAdded callback invoked when an element is added to {@code sourceList}
60+
* @param onRemoved callback invoked when an element is removed from {@code sourceList}
61+
*
62+
* @return a {@code ListBinder} that keeps the lists synchronized
63+
*/
64+
public static <T, S> ListBinder<T, S> bindContent(ObservableList<T> targetList, ObservableList<S> sourceList,
65+
Function<S, T> mapper, Consumer<S> onAdded, Consumer<S> onRemoved) {
66+
return new ListBinder<>(targetList, sourceList, mapper, onAdded, onRemoved);
67+
}
5368

54-
private final Function<T, S> converter;
69+
private final ObservableList<T> targetList;
5570

56-
private final ListChangeListener<T> listener;
71+
private final ObservableList<S> sourceList;
5772

58-
private final Consumer<T> onAdded;
73+
private final Function<S, T> mapper;
5974

60-
private final Consumer<T> onRemoved;
75+
private final ListChangeListener<S> listener;
6176

62-
public ListSynchronizer(ObservableList<T> sourceList, ObservableList<S> targetList, Function<T, S> converter) {
63-
this(sourceList, targetList, converter, null, null);
64-
}
77+
private final Consumer<S> onAdded;
6578

66-
public ListSynchronizer(ObservableList<T> sourceList, ObservableList<S> targetList, Function<T, S> converter,
67-
Consumer<T> onAdded, Consumer<T> onRemoved) {
68-
this.sourceList = sourceList;
79+
private final Consumer<S> onRemoved;
80+
81+
private ListBinder(ObservableList<T> targetList, ObservableList<S> sourceList, Function<S, T> mapper,
82+
Consumer<S> onAdded, Consumer<S> onRemoved) {
6983
this.targetList = targetList;
70-
this.converter = converter;
84+
this.sourceList = sourceList;
85+
this.mapper = mapper;
7186
this.onAdded = onAdded;
7287
this.onRemoved = onRemoved;
7388
synchronizeAll();
7489
this.listener = this::handleChanges;
7590
sourceList.addListener(listener);
7691
}
7792

78-
public ObservableList<T> getSourceList() {
79-
return sourceList;
80-
}
81-
82-
public ObservableList<S> getTargetList() {
83-
return targetList;
84-
}
85-
86-
public Function<T, S> getConverter() {
87-
return converter;
88-
}
89-
90-
public Consumer<T> getOnAdded() {
91-
return onAdded;
92-
}
93-
94-
public Consumer<T> getOnRemoved() {
95-
return onRemoved;
96-
}
97-
98-
/**
99-
* Stops list synchronization. After calling this method, changes in sourceList will no longer be reflected
100-
* in targetList.
101-
*/
102-
public void dispose() {
93+
public void unbind() {
10394
sourceList.removeListener(listener);
10495
}
10596

10697
private void synchronizeAll() {
10798
targetList.clear();
108-
sourceList.forEach(item -> targetList.add(converter.apply(item)));
99+
sourceList.forEach(item -> targetList.add(mapper.apply(item)));
109100
}
110101

111-
private void handleChanges(ListChangeListener.Change<? extends T> change) {
102+
private void handleChanges(ListChangeListener.Change<? extends S> change) {
112103
// | Operation | wasAdded | wasRemoved | wasReplaced | wasPermutated | wasUpdated |
113104
// | ---------- | -------- | ---------- | ----------- | ------------- | ---------- |
114105
// | Replaced | + | + | + | - | - |
@@ -134,18 +125,18 @@ private void handleChanges(ListChangeListener.Change<? extends T> change) {
134125
}
135126
}
136127

137-
private void handleAdditions(ListChangeListener.Change<? extends T> change) {
128+
private void handleAdditions(ListChangeListener.Change<? extends S> change) {
138129
int startIndex = change.getFrom();
139130
for (int i = 0; i < change.getAddedSize(); i++) {
140-
T addedItem = change.getList().get(startIndex + i);
131+
S addedItem = change.getList().get(startIndex + i);
141132
if (this.onAdded != null) {
142133
this.onAdded.accept(addedItem);
143134
}
144-
targetList.add(startIndex + i, converter.apply(addedItem));
135+
targetList.add(startIndex + i, mapper.apply(addedItem));
145136
}
146137
}
147138

148-
private void handleRemovals(ListChangeListener.Change<? extends T> change) {
139+
private void handleRemovals(ListChangeListener.Change<? extends S> change) {
149140
targetList.subList(change.getFrom(), change.getFrom() + change.getRemovedSize()).clear();
150141
if (this.onRemoved != null) {
151142
for (var item : change.getRemoved()) {
@@ -154,7 +145,7 @@ private void handleRemovals(ListChangeListener.Change<? extends T> change) {
154145
}
155146
}
156147

157-
private void handleReplacements(ListChangeListener.Change<? extends T> change) {
148+
private void handleReplacements(ListChangeListener.Change<? extends S> change) {
158149
if (this.onRemoved != null && change.wasRemoved()) {
159150
for (var item : change.getRemoved()) {
160151
this.onRemoved.accept(item);
@@ -166,22 +157,22 @@ private void handleReplacements(ListChangeListener.Change<? extends T> change) {
166157
}
167158
}
168159
for (int i = change.getFrom(); i < change.getTo(); i++) {
169-
T newItem = change.getList().get(i);
170-
targetList.set(i, converter.apply(newItem));
160+
S newItem = change.getList().get(i);
161+
targetList.set(i, mapper.apply(newItem));
171162
}
172163
}
173164

174-
private void handlePermutations(ListChangeListener.Change<? extends T> change) {
175-
List<S> tempCopy = new ArrayList<>(targetList);
165+
private void handlePermutations(ListChangeListener.Change<? extends S> change) {
166+
List<T> tempCopy = new ArrayList<>(targetList);
176167
for (int oldIndex = change.getFrom(); oldIndex < change.getTo(); oldIndex++) {
177168
int newIndex = change.getPermutation(oldIndex);
178169
targetList.set(newIndex, tempCopy.get(oldIndex));
179170
}
180171
}
181172

182-
private void handleUpdates(ListChangeListener.Change<? extends T> change) {
173+
private void handleUpdates(ListChangeListener.Change<? extends S> change) {
183174
for (int i = change.getFrom(); i < change.getTo(); i++) {
184-
targetList.set(i, converter.apply(change.getList().get(i)));
175+
targetList.set(i, mapper.apply(change.getList().get(i)));
185176
}
186177
}
187178
}

toolkit-fx/src/main/java/module-info.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
requires javafx.controls;
2020

2121
exports com.techsenger.toolkit.fx;
22-
exports com.techsenger.toolkit.fx.collections;
22+
exports com.techsenger.toolkit.fx.binding;
2323
exports com.techsenger.toolkit.fx.color;
2424
exports com.techsenger.toolkit.fx.input;
2525
exports com.techsenger.toolkit.fx.pulse;

toolkit-fx/src/test/java/com/techsenger/toolkit/fx/collections/ListSynchronizerTest.java renamed to toolkit-fx/src/test/java/com/techsenger/toolkit/fx/binding/ListBinderTest.java

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
package com.techsenger.toolkit.fx.collections;
17+
package com.techsenger.toolkit.fx.binding;
1818

1919
import java.util.ArrayList;
2020
import java.util.Comparator;
@@ -33,7 +33,7 @@
3333
*
3434
* @author Pavel Castornii
3535
*/
36-
public class ListSynchronizerTest {
36+
public class ListBinderTest {
3737

3838
private static class TestItem {
3939

@@ -56,78 +56,78 @@ public IntegerProperty valueProperty() {
5656
}
5757
}
5858

59-
private ObservableList<Integer> source;
60-
private ObservableList<String> target;
61-
private ListSynchronizer<Integer, String> synchronizer;
59+
private ListBinder<String, Integer> binder;
60+
private ObservableList<String> targetList;
61+
private ObservableList<Integer> sourceList;
6262
private List<Integer> addedItems;
6363
private List<Integer> removedItems;
6464

6565
@BeforeEach
6666
void setUp() {
67-
source = FXCollections.observableArrayList();
68-
target = FXCollections.observableArrayList();
67+
targetList = FXCollections.observableArrayList();
68+
sourceList = FXCollections.observableArrayList();
6969
addedItems = new ArrayList<>();
7070
removedItems = new ArrayList<>();
71-
synchronizer = new ListSynchronizer<>(source, target, Object::toString, addedItems::add, removedItems::add);
71+
binder = ListBinder.bindContent(targetList, sourceList, Object::toString, addedItems::add, removedItems::add);
7272
}
7373

7474
@AfterEach
7575
void tearDown() {
76-
synchronizer.dispose();
76+
binder.unbind();
7777
}
7878

7979
@Test
8080
void initialSync_withAddedElements_shouldMirrorToSecondary() {
81-
source.addAll(1, 2, 3);
81+
sourceList.addAll(1, 2, 3);
8282

83-
assertThat(target).containsExactly("1", "2", "3");
83+
assertThat(targetList).containsExactly("1", "2", "3");
8484
assertThat(addedItems).containsExactly(1, 2, 3);
8585
}
8686

8787
@Test
8888
void addAtIndex_whenInsertingMiddleElement_shouldMaintainOrder() {
89-
source.addAll(1, 3);
90-
source.add(1, 2);
89+
sourceList.addAll(1, 3);
90+
sourceList.add(1, 2);
9191

92-
assertThat(target).containsExactly("1", "2", "3");
92+
assertThat(targetList).containsExactly("1", "2", "3");
9393
assertThat(addedItems).containsExactly(1, 3, 2);
9494
}
9595

9696
@Test
9797
void remove_whenMiddleElementRemoved_shouldUpdateSecondary() {
98-
source.addAll(1, 2, 3);
99-
source.remove(1);
98+
sourceList.addAll(1, 2, 3);
99+
sourceList.remove(1);
100100

101-
assertThat(target).containsExactly("1", "3");
101+
assertThat(targetList).containsExactly("1", "3");
102102
assertThat(removedItems).containsExactly(2);
103103
}
104104

105105
@Test
106106
void replace_whenElementReplaced_shouldUpdateSecondary() {
107-
source.addAll(1, 2, 3);
108-
source.set(1, 4);
107+
sourceList.addAll(1, 2, 3);
108+
sourceList.set(1, 4);
109109

110-
assertThat(target).containsExactly("1", "4", "3");
110+
assertThat(targetList).containsExactly("1", "4", "3");
111111
assertThat(addedItems).containsExactly(1, 2, 3, 4);
112112
assertThat(removedItems).containsExactly(2);
113113
}
114114

115115
@Test
116116
void permutation_whenListSorted_shouldMirrorOrder() {
117-
source.addAll(3, 1, 2);
118-
FXCollections.sort(source);
117+
sourceList.addAll(3, 1, 2);
118+
FXCollections.sort(sourceList);
119119

120-
assertThat(target).containsExactly("1", "2", "3");
120+
assertThat(targetList).containsExactly("1", "2", "3");
121121
assertThat(addedItems).containsExactly(3, 1, 2);
122122
assertThat(removedItems).isEmpty();
123123
}
124124

125125
@Test
126126
void permutation_whenListReversed_shouldMirrorOrder() {
127-
source.addAll(1, 2, 3);
128-
source.sort(Comparator.reverseOrder());
127+
sourceList.addAll(1, 2, 3);
128+
sourceList.sort(Comparator.reverseOrder());
129129

130-
assertThat(target).containsExactly("3", "2", "1");
130+
assertThat(targetList).containsExactly("3", "2", "1");
131131
assertThat(addedItems).containsExactly(1, 2, 3);
132132
assertThat(removedItems).isEmpty();
133133
}
@@ -141,8 +141,8 @@ void update_whenElementUpdated_shouldUpdateSecondary() {
141141
List<TestItem> added = new ArrayList<>();
142142
List<TestItem> removed = new ArrayList<>();
143143

144-
ListSynchronizer<TestItem, String> itemSynchronizer = new ListSynchronizer<>(
145-
sourceItems, targetItems,
144+
ListBinder<String, TestItem> itemBinder = ListBinder.bindContent(
145+
targetItems, sourceItems,
146146
item -> Integer.toString(item.getValue()),
147147
added::add,
148148
removed::add
@@ -158,30 +158,30 @@ void update_whenElementUpdated_shouldUpdateSecondary() {
158158
assertThat(added).containsExactly(item1, item2);
159159
assertThat(removed).isEmpty();
160160

161-
itemSynchronizer.dispose();
161+
itemBinder.unbind();
162162
}
163163

164164
@Test
165165
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);
166+
sourceList.addAll(1, 2, 3, 4);
167+
sourceList.set(0, 5);
168+
sourceList.set(2, 6);
169+
sourceList.set(3, 7);
170170

171-
assertThat(target).containsExactly("5", "2", "6", "7");
171+
assertThat(targetList).containsExactly("5", "2", "6", "7");
172172
assertThat(addedItems).containsExactly(1, 2, 3, 4, 5, 6, 7);
173173
assertThat(removedItems).containsExactly(1, 3, 4);
174174
}
175175

176176
@Test
177177
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);
178+
sourceList.addAll(1, 2, 3);
179+
sourceList.remove(1);
180+
sourceList.add(4);
181+
sourceList.set(0, 5);
182+
FXCollections.sort(sourceList);
183183

184-
assertThat(target).containsExactly("3", "4", "5");
184+
assertThat(targetList).containsExactly("3", "4", "5");
185185
assertThat(addedItems).containsExactly(1, 2, 3, 4, 5);
186186
assertThat(removedItems).containsExactly(2, 1);
187187
}

0 commit comments

Comments
 (0)