1+ /*
2+ * Copyright (C) 2025 Parisi Alessandro - alessandro.parisi406@gmail.com
3+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
4+ *
5+ * MaterialFX is free software: you can redistribute it and/or
6+ * modify it under the terms of the GNU Lesser General Public License
7+ * as published by the Free Software Foundation; either version 3 of the License,
8+ * or (at your option) any later version.
9+ *
10+ * MaterialFX is distributed in the hope that it will be useful,
11+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
12+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13+ * See the GNU Lesser General Public License for more details.
14+ *
15+ * You should have received a copy of the GNU Lesser General Public License
16+ * along with MaterialFX. If not, see <http://www.gnu.org/licenses/>.
17+ */
18+
19+ package io .github .palexdev .mfxcore .base .bindings ;
20+
21+ import java .util .*;
22+ import java .util .function .Function ;
23+
24+ import io .github .palexdev .mfxcore .observables .When ;
25+ import javafx .beans .Observable ;
26+ import javafx .beans .value .ObservableValue ;
27+ import javafx .beans .value .WritableValue ;
28+
29+ /// This utility provides bidirectional binding between two [ObservableValues][ObservableValue] of different types,
30+ /// filling a gap in JavaFX's binding system by implementing type conversion through functions for this kind of bindings.
31+ ///
32+ /// **Usage Example:**
33+ /// ```java
34+ /// StringProperty first = new SimpleStringProperty();
35+ /// IntegerProperty second = new SimpleIntegerProperty();
36+ /// MappedBidirectionalBinding.bind(first, second)\
37+ /// .setFirstToSecondMapper(Integer::parseInt)
38+ /// .setSecondToFirstMapper(String::valueOf)
39+ /// .bind();
40+ ///```
41+ ///
42+ /// @param <A> the type of the first observable value
43+ /// @param <B> the type of the second observable value
44+ public class MappedBidirectionalBinding <A , B > {
45+ //================================================================================
46+ // Properties
47+ //================================================================================
48+ private ObservableValue <A > first ;
49+ private Mapper <A , B > firstToSecondMapper = Mapper .of (_ -> null );
50+ private ObservableValue <B > second ;
51+ private Mapper <B , A > secondToFirstMapper = Mapper .of (_ -> null );
52+
53+ private When <?> firstWhen ;
54+ private When <?> secondWhen ;
55+ private final Map <Target , Set <Observable >> dependencies = new HashMap <>();
56+ private boolean locked = false ;
57+
58+ //================================================================================
59+ // Constructors
60+ //================================================================================
61+
62+ public MappedBidirectionalBinding (ObservableValue <A > first , ObservableValue <B > second ) {
63+ this .first = Objects .requireNonNull (first );
64+ this .second = Objects .requireNonNull (second );
65+ }
66+
67+ public static <A , B > MappedBidirectionalBinding <A , B > bind (ObservableValue <A > first , ObservableValue <B > second ) {
68+ return new MappedBidirectionalBinding <>(first , second );
69+ }
70+
71+ //================================================================================
72+ // Methods
73+ //================================================================================
74+
75+ /// Delegates to [#bind(boolean)] with `false` as the parameter.
76+ public MappedBidirectionalBinding <A , B > bind () {
77+ return bind (false );
78+ }
79+
80+ /// Activates the bidirectional binding.
81+ ///
82+ /// This method sets up the listeners on both observable values and thus establishing the bidirectional relationship.<br >
83+ /// Once active, changes to either observable will trigger the appropriate mapper and update the other observable.
84+ ///
85+ /// @param lazyInit if `false`, immediately updates the second observable with the mapped value from the first;
86+ /// if `true`, waits for the first change to trigger initialization
87+ /// @throws IllegalStateException if the binding is already active
88+ /// @see Target
89+ public MappedBidirectionalBinding <A , B > bind (boolean lazyInit ) {
90+ if (isActive ()) throw new IllegalStateException ("The binding is already active" );
91+
92+ firstWhen = When .onInvalidated (first )
93+ .condition (_ -> !locked )
94+ .then (_ -> Target .SECOND .update (this ))
95+ .listen ();
96+ secondWhen = When .onInvalidated (second )
97+ .condition (_ -> !locked )
98+ .then (_ -> Target .FIRST .update (this ))
99+ .listen ();
100+ if (!lazyInit ) Target .SECOND .update (this );
101+
102+ // Register dependencies now that listeners have been created
103+ dependencies .forEach ((t , deps ) -> t .registerDependencies (this , deps ));
104+
105+ return this ;
106+ }
107+
108+ /// Adds additional dependencies that allow the binding to react to changes in other observables beyond
109+ /// the two primary bound values.<br >
110+ /// For example, if your mapper function depends on external configuration properties,
111+ /// you can register those as dependencies.
112+ ///
113+ /// Dependencies can be added before or after calling [#bind()].
114+ ///
115+ /// @param target determines which of the two observables depend on the given dependencies
116+ /// @see Target
117+ public MappedBidirectionalBinding <A , B > addDependenciesFor (Target target , Observable ... dependencies ) {
118+ Set <Observable > set = this .dependencies .computeIfAbsent (target , _ -> new LinkedHashSet <>());
119+ Collections .addAll (set , dependencies );
120+ if (isActive ()) target .registerDependencies (this , Arrays .asList (dependencies ));
121+ return this ;
122+ }
123+
124+ /// Checks whether this binding is currently active.
125+ ///
126+ /// A binding is considered active if it has been bound via [#bind()] and has not been subsequently unbound.
127+ public boolean isActive () {
128+ return firstWhen != null || secondWhen != null ;
129+ }
130+
131+ /// Deactivates the binding.
132+ ///
133+ /// This removes the change listeners and stops the automatic synchronization between the observable values.<br >
134+ /// The binding can be reactivated by calling [#bind()] again.
135+ ///
136+ /// @param clearDependencies if `true`, clears all registered dependencies;
137+ /// if `false`, preserves dependencies for potential rebinding
138+ public void unbind (boolean clearDependencies ) {
139+ if (firstWhen != null ) firstWhen .dispose ();
140+ if (secondWhen != null ) secondWhen .dispose ();
141+ if (clearDependencies ) dependencies .clear ();
142+ firstWhen = null ;
143+ secondWhen = null ;
144+ }
145+
146+ /// Completely disposes of this binding. After calling this method, the binding cannot be reused.<br >
147+ /// This should be called when the binding is no longer needed to prevent memory leaks.
148+ ///
149+ /// Partly delegates to [#unbind(boolean)] with `true` as the parameter.
150+ public void dispose () {
151+ unbind (true );
152+ first = null ;
153+ second = null ;
154+ }
155+
156+ //================================================================================
157+ // Getters/Setters
158+ //================================================================================
159+
160+ /// @return the mapper function for converting values from the first observable to the second.
161+ public Mapper <A , B > getFirstToSecondMapper () {
162+ return firstToSecondMapper ;
163+ }
164+
165+ /// Sets the mapper function for converting values from the first observable to the second.
166+ ///
167+ /// This mapper is used when the first observable changes, and its value needs to be
168+ /// converted and applied to the second observable.
169+ public MappedBidirectionalBinding <A , B > setFirstToSecondMapper (Function <A , B > firstToSecondMapper ) {
170+ this .firstToSecondMapper = Mapper .of (firstToSecondMapper );
171+ return this ;
172+ }
173+
174+ /// Same as [#setFirstToSecondMapper(Function)] but more flexible because a [Mapper] also allows to specify an 'orElse' value.
175+ public MappedBidirectionalBinding <A , B > setFirstToSecondMapper (Mapper <A , B > firstToSecondMapper ) {
176+ this .firstToSecondMapper = Objects .requireNonNull (firstToSecondMapper );
177+ return this ;
178+ }
179+
180+ /// @return the mapper function for converting values from the second observable to the first.
181+ public Mapper <B , A > getSecondToFirstMapper () {
182+ return secondToFirstMapper ;
183+ }
184+
185+ /// Sets the mapper function for converting values from the second observable to the first.
186+ ///
187+ /// This mapper is used when the second observable changes, and its value needs to be
188+ /// converted and applied to the first observable.
189+ public MappedBidirectionalBinding <A , B > setSecondToFirstMapper (Function <B , A > secondToFirstMapper ) {
190+ this .secondToFirstMapper = Mapper .of (secondToFirstMapper );
191+ return this ;
192+ }
193+
194+ /// Same as [#setSecondToFirstMapper(Function)] but more flexible because a [Mapper] also allows to specify an 'orElse' value.
195+ public MappedBidirectionalBinding <A , B > setSecondToFirstMapper (Mapper <B , A > secondToFirstMapper ) {
196+ this .secondToFirstMapper = Objects .requireNonNull (secondToFirstMapper );
197+ return this ;
198+ }
199+
200+ //================================================================================
201+ // Inner Classes
202+ //================================================================================
203+
204+ /// Enum representing the two directions of the bidirectional binding.<br >
205+ /// This is used internally to manage updates and dependencies while avoiding code duplication.
206+ ///
207+ /// While dependencies are added by [MappedBidirectionalBinding#addDependenciesFor(Target, Observable...)], they
208+ /// are effectively registered by the target constant, [#registerDependencies(MappedBidirectionalBinding, Collection)].
209+ public enum Target {
210+ /// Represents the direction from the second observable to the first.
211+ ///
212+ /// When the second observable changes, the `FIRST` target is updated using the second-to-first mapper.
213+ FIRST {
214+ @ SuppressWarnings ({"rawtypes" , "unchecked" })
215+ @ Override
216+ <A , B > void update (MappedBidirectionalBinding <A , B > binding ) {
217+ try {
218+ binding .locked = true ;
219+ A newVal = binding .secondToFirstMapper .apply (binding .second .getValue ());
220+ if (binding .first instanceof WritableValue wv ) wv .setValue (newVal );
221+ } finally {
222+ binding .locked = false ;
223+ }
224+ }
225+
226+ @ Override
227+ <A , B > void registerDependencies (MappedBidirectionalBinding <A , B > binding , Collection <Observable > dependencies ) {
228+ for (Observable dependency : dependencies ) {
229+ binding .secondWhen .invalidating (dependency );
230+ }
231+ }
232+ },
233+
234+ /// Represents the direction from the first observable to the second.
235+ ///
236+ /// When the first observable changes, the `SECOND` target is updated using the first-to-second mapper.
237+ SECOND {
238+ @ SuppressWarnings ({"rawtypes" , "unchecked" })
239+ @ Override
240+ <A , B > void update (MappedBidirectionalBinding <A , B > binding ) {
241+ try {
242+ binding .locked = true ;
243+ B newVal = binding .firstToSecondMapper .apply (binding .first .getValue ());
244+ if (binding .second instanceof WritableValue wv ) wv .setValue (newVal );
245+ } finally {
246+ binding .locked = false ;
247+ }
248+ }
249+
250+ @ Override
251+ <A , B > void registerDependencies (MappedBidirectionalBinding <A , B > binding , Collection <Observable > dependencies ) {
252+ for (Observable dependency : dependencies ) {
253+ binding .firstWhen .invalidating (dependency );
254+ }
255+ }
256+ };
257+
258+ abstract <A , B > void update (MappedBidirectionalBinding <A , B > binding );
259+
260+ abstract <A , B > void registerDependencies (MappedBidirectionalBinding <A , B > binding , Collection <Observable > dependencies );
261+ }
262+ }
0 commit comments