3232import com .gluonhq .attach .util .Util ;
3333import javafx .animation .Interpolator ;
3434import javafx .animation .TranslateTransition ;
35- import javafx .application . Platform ;
35+ import javafx .beans . property . ReadOnlyFloatProperty ;
3636import javafx .beans .property .ReadOnlyFloatWrapper ;
37- import javafx .beans .property . ReadOnlyStringProperty ;
38- import javafx .beans .property . ReadOnlyStringWrapper ;
37+ import javafx .beans .value . ChangeListener ;
38+ import javafx .beans .value . ObservableValue ;
3939import javafx .scene .Node ;
4040import javafx .scene .Parent ;
41- import javafx .scene .input . MouseEvent ;
41+ import javafx .scene .Scene ;
4242import javafx .util .Duration ;
4343
44- import java .util .HashMap ;
44+ import java .util .Collections ;
4545import java .util .Map ;
4646import java .util .Objects ;
47+ import java .util .Set ;
4748import java .util .WeakHashMap ;
4849import java .util .logging .Level ;
4950import java .util .logging .Logger ;
@@ -60,75 +61,126 @@ public abstract class BaseKeyboardService implements KeyboardService {
6061 /** Map of nodes and keyboard types. */
6162 private final Map <Node , KeyboardType > nodeKeyboardTypes = new WeakHashMap <>();
6263
63- /** Map of nodes and text properties . */
64- private static final Map <Node , ReadOnlyStringWrapper > nodeTextProperties = new WeakHashMap <>();
64+ /** Map of nodes to their visibility listeners . */
65+ private final Map <Node , ChangeListener < Number >> visibilityListeners = new WeakHashMap <>();
6566
66- /** Map of ids and nodes . */
67- private static final Map < String , Node > idToNode = new HashMap <>();
67+ /** Scenes for which a focusOwner listener has already been installed . */
68+ private final Set < Scene > trackedScenes = Collections . newSetFromMap ( new WeakHashMap <>() );
6869
6970 BaseKeyboardService () {
70- VISIBLE_HEIGHT .addListener ((obs , ov , nv ) -> {
71- if (nv != null && nv .doubleValue () <= 0 ) {
72- if (debug ) {
73- LOG .info ("Keyboard hidden, reset default type" );
74- }
75- applyActiveNodeId ("" ); // reset active node
76- applyKeyboardType (KeyboardType .ASCII .getValue ());
77- }
78- });
71+ }
72+
73+ @ Override
74+ public void keepVisibilityForNode (Node node ) {
75+ keepVisibilityForNode (node , null );
76+ }
77+
78+ @ Override
79+ public void keepVisibilityForNode (Node node , Parent parent ) {
80+ Objects .requireNonNull (node , "node must not be null" );
81+ releaseVisibilityForNode (node );
82+ ChangeListener <Number > listener = (obs , ov , nv ) -> adjustPosition (node , parent , nv .doubleValue ());
83+ visibilityListeners .put (node , listener );
84+ VISIBLE_HEIGHT .addListener (listener );
85+ }
86+
87+ @ Override
88+ public void releaseVisibilityForNode (Node node ) {
89+ Objects .requireNonNull (node , "node must not be null" );
90+ ChangeListener <Number > listener = visibilityListeners .remove (node );
91+ if (listener != null ) {
92+ VISIBLE_HEIGHT .removeListener (listener );
93+ }
94+ }
95+
96+ @ Override
97+ public ReadOnlyFloatProperty visibleHeightProperty () {
98+ return VISIBLE_HEIGHT .getReadOnlyProperty ();
7999 }
80100
81101 @ Override
82102 public void setKeyboardTypeForNode (Node node , KeyboardType type ) {
83103 Objects .requireNonNull (node , "node must not be null" );
84104 Objects .requireNonNull (type , "type must not be null" );
85105 nodeKeyboardTypes .put (node , type );
86- installEventFilter (node );
106+ attachFocusTracker (node );
87107 }
88108
89109 @ Override
90- public ReadOnlyStringProperty textPropertyForNode (Node node ) {
110+ public void removeKeyboardTypeForNode (Node node ) {
91111 Objects .requireNonNull (node , "node must not be null" );
92- installEventFilter (node );
93- return nodeTextProperties .computeIfAbsent (node , n -> {
94- idToNode .put (syntheticId (n ), n );
95- return new ReadOnlyStringWrapper ("" );
96- }).getReadOnlyProperty ();
112+ nodeKeyboardTypes .remove (node );
97113 }
98114
99- private void installEventFilter (Node node ) {
100- node .addEventFilter (MouseEvent .MOUSE_CLICKED , e -> {
101- KeyboardType type = nodeKeyboardTypes .getOrDefault (node , KeyboardType .ASCII );
102- if (debug ) {
103- LOG .info (String .format ("Active keyboard type: %s for id %s" , type , syntheticId (node )));
115+ /**
116+ * Ensures a single focusOwner listener is installed on the scene that
117+ * contains {@code node}. The listener drives the native keyboard type for
118+ * every focus change in that scene, whether the newly focused node was
119+ * explicitly registered via {@link #setKeyboardTypeForNode} or not.
120+ * If {@code node} is not yet in a scene, the installation is deferred
121+ * until it is.
122+ */
123+ private void attachFocusTracker (Node node ) {
124+ Scene scene = node .getScene ();
125+ if (scene != null ) {
126+ trackScene (scene );
127+ // If this node is already the focus owner, apply its type now
128+ if (scene .getFocusOwner () == node ) {
129+ applyTypeFor (node );
130+ }
131+ return ;
132+ }
133+ node .sceneProperty ().addListener (new ChangeListener <>() {
134+ @ Override
135+ public void changed (ObservableValue <? extends Scene > obs , Scene ov , Scene nv ) {
136+ if (nv != null ) {
137+ trackScene (nv );
138+ if (nv .getFocusOwner () == node ) {
139+ applyTypeFor (node );
140+ }
141+ obs .removeListener (this );
142+ }
104143 }
105- applyActiveNodeId (syntheticId (node ));
106- applyKeyboardType (type .getValue ());
107144 });
108145 }
109146
110- /**
111- * Uses the node's own {@link Node#getId() id} if set, otherwise falls back to an
112- * id based on its identity hash code.
113- */
114- protected static String syntheticId (Node node ) {
115- String id = node .getId ();
116- return id != null ? id : "attach-kb-" + System .identityHashCode (node );
147+ private void trackScene (Scene scene ) {
148+ if (!trackedScenes .add (scene )) {
149+ return ;
150+ }
151+ scene .focusOwnerProperty ().addListener ((obs , ov , newNode ) -> applyTypeFor (newNode ));
117152 }
118153
119154 /**
120- * Called from the native callback to update the text property for the
121- * node identified by {@code id}.
155+ * Pushes the id and keyboard type for {@code focused} down to the native
156+ * layer. Registered nodes use their stored {@link KeyboardType}; any other
157+ * focus owner (including {@code null}) falls back to {@link KeyboardType#ASCII}.
122158 */
123- protected static void updateTextForId (String id , String text ) {
124- Node node = idToNode .get (id );
125- if (node == null ) {
159+ private void applyTypeFor (Node focused ) {
160+ if (focused == null ) {
161+ if (debug ) {
162+ LOG .info ("Focus cleared, applying default ASCII keyboard" );
163+ }
164+ applyActiveNodeId ("" );
165+ applyKeyboardType (KeyboardType .ASCII .getValue ());
126166 return ;
127167 }
128- ReadOnlyStringWrapper wrapper = nodeTextProperties .get (node );
129- if (wrapper != null && !Objects .equals (wrapper .get (), text )) {
130- Platform .runLater (() -> wrapper .set (text ));
168+ KeyboardType type = nodeKeyboardTypes .getOrDefault (focused , KeyboardType .ASCII );
169+ String id = syntheticId (focused );
170+ if (debug ) {
171+ LOG .info (String .format ("Active keyboard type: %s for id %s" , type , id ));
131172 }
173+ applyActiveNodeId (id );
174+ applyKeyboardType (type .getValue ());
175+ }
176+
177+ /**
178+ * Uses the node's own {@link Node#getId() id} if set, otherwise falls back to an
179+ * id based on its identity hash code.
180+ */
181+ protected static String syntheticId (Node node ) {
182+ String id = node .getId ();
183+ return id != null ? id : "attach-kb-" + System .identityHashCode (node );
132184 }
133185
134186 protected static void adjustPosition (Node node , Parent parent , double kh ) {
0 commit comments