1414 * limitations under the License.
1515 */
1616
17- package com .techsenger .toolkit .fx .collections ;
17+ package com .techsenger .toolkit .fx .binding ;
1818
1919import java .util .ArrayList ;
2020import java .util .List ;
2424import 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}
0 commit comments