From 6ec14755d5b2f3deda8dd114ba27189d80305fa2 Mon Sep 17 00:00:00 2001 From: "jose.pereda" Date: Fri, 17 Apr 2026 17:55:00 +0200 Subject: [PATCH 1/5] Add API to unregister nodes from keyboard visibility changes --- .../attach/keyboard/KeyboardService.java | 10 ++++++ .../keyboard/impl/AndroidKeyboardService.java | 17 ---------- .../keyboard/impl/BaseKeyboardService.java | 33 +++++++++++++++++++ .../keyboard/impl/IOSKeyboardService.java | 16 --------- 4 files changed, 43 insertions(+), 33 deletions(-) diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java index 53e3e80d..3a976795 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java @@ -79,6 +79,16 @@ static Optional create() { */ void keepVisibilityForNode(Node node, Parent parent); + /** + * Stops adjusting the node when the software keyboard shows up, + * removing the listener previously registered via + * {@link #keepVisibilityForNode(Node)} or {@link #keepVisibilityForNode(Node, Parent)}. + * + * @param node the Node that was previously registered + * @since 4.0.25 + */ + void releaseVisibilityForNode(Node node); + /** * Gets the visible height of the Keyboard, so scene or views can adjusted * to prevent some of their content from being covered by the keyboard. diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/AndroidKeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/AndroidKeyboardService.java index 98571a92..bad10a04 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/AndroidKeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/AndroidKeyboardService.java @@ -28,9 +28,6 @@ package com.gluonhq.attach.keyboard.impl; import javafx.application.Platform; -import javafx.beans.property.ReadOnlyFloatProperty; -import javafx.scene.Node; -import javafx.scene.Parent; public class AndroidKeyboardService extends BaseKeyboardService { @@ -41,20 +38,6 @@ public class AndroidKeyboardService extends BaseKeyboardService { public AndroidKeyboardService() { } - @Override - public void keepVisibilityForNode(Node node) { - keepVisibilityForNode(node, null); - } - - @Override - public void keepVisibilityForNode(Node node, Parent parent) { - VISIBLE_HEIGHT.addListener((obs, ov, nv) -> adjustPosition(node, parent, nv.doubleValue())); - } - - @Override - public ReadOnlyFloatProperty visibleHeightProperty() { - return VISIBLE_HEIGHT.getReadOnlyProperty(); - } @Override protected void applyKeyboardType(int nativeValue) { diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java index 0b4b493b..56f5cbc6 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java @@ -33,9 +33,11 @@ import javafx.animation.Interpolator; import javafx.animation.TranslateTransition; import javafx.application.Platform; +import javafx.beans.property.ReadOnlyFloatProperty; import javafx.beans.property.ReadOnlyFloatWrapper; import javafx.beans.property.ReadOnlyStringProperty; import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.value.ChangeListener; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.input.MouseEvent; @@ -66,6 +68,9 @@ public abstract class BaseKeyboardService implements KeyboardService { /** Map of ids and nodes. */ private static final Map idToNode = new HashMap<>(); + /** Map of nodes to their visibility listeners. */ + private final Map> visibilityListeners = new WeakHashMap<>(); + BaseKeyboardService() { VISIBLE_HEIGHT.addListener((obs, ov, nv) -> { if (nv != null && nv.doubleValue() <= 0) { @@ -78,6 +83,34 @@ public abstract class BaseKeyboardService implements KeyboardService { }); } + @Override + public void keepVisibilityForNode(Node node) { + keepVisibilityForNode(node, null); + } + + @Override + public void keepVisibilityForNode(Node node, Parent parent) { + Objects.requireNonNull(node, "node must not be null"); + releaseVisibilityForNode(node); + ChangeListener listener = (obs, ov, nv) -> adjustPosition(node, parent, nv.doubleValue()); + visibilityListeners.put(node, listener); + VISIBLE_HEIGHT.addListener(listener); + } + + @Override + public void releaseVisibilityForNode(Node node) { + Objects.requireNonNull(node, "node must not be null"); + ChangeListener listener = visibilityListeners.remove(node); + if (listener != null) { + VISIBLE_HEIGHT.removeListener(listener); + } + } + + @Override + public ReadOnlyFloatProperty visibleHeightProperty() { + return VISIBLE_HEIGHT.getReadOnlyProperty(); + } + @Override public void setKeyboardTypeForNode(Node node, KeyboardType type) { Objects.requireNonNull(node, "node must not be null"); diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/IOSKeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/IOSKeyboardService.java index a5302c03..56ef57ac 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/IOSKeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/IOSKeyboardService.java @@ -30,10 +30,8 @@ import com.gluonhq.attach.lifecycle.LifecycleEvent; import com.gluonhq.attach.lifecycle.LifecycleService; import javafx.application.Platform; -import javafx.beans.property.ReadOnlyFloatProperty; import javafx.beans.property.ReadOnlyStringProperty; import javafx.scene.Node; -import javafx.scene.Parent; import javafx.scene.control.TextInputControl; public class IOSKeyboardService extends BaseKeyboardService { @@ -54,20 +52,6 @@ public IOSKeyboardService() { startObserver(); } - @Override - public void keepVisibilityForNode(Node node) { - keepVisibilityForNode(node, null); - } - - @Override - public void keepVisibilityForNode(Node node, Parent parent) { - VISIBLE_HEIGHT.addListener((obs, ov, nv) -> adjustPosition(node, parent, nv.doubleValue())); - } - - @Override - public ReadOnlyFloatProperty visibleHeightProperty() { - return VISIBLE_HEIGHT.getReadOnlyProperty(); - } @Override protected void applyKeyboardType(int nativeValue) { From ce8078aaa8fc312a9265d6273d9cd257451bb1e1 Mon Sep 17 00:00:00 2001 From: "jose.pereda" Date: Fri, 17 Apr 2026 18:22:34 +0200 Subject: [PATCH 2/5] withdraw textPropertyForNode API --- gradle/include/android/grandroid_ext.h | 2 -- .../attach/keyboard/KeyboardService.java | 15 --------- .../keyboard/impl/AndroidKeyboardService.java | 8 ----- .../keyboard/impl/BaseKeyboardService.java | 33 ------------------- .../keyboard/impl/IOSKeyboardService.java | 11 ------- .../src/main/native/android/c/keyboard.c | 18 ---------- .../config/jniconfig-aarch64-android.json | 3 +- 7 files changed, 1 insertion(+), 89 deletions(-) diff --git a/gradle/include/android/grandroid_ext.h b/gradle/include/android/grandroid_ext.h index 4fa956c5..1638cdd0 100644 --- a/gradle/include/android/grandroid_ext.h +++ b/gradle/include/android/grandroid_ext.h @@ -58,11 +58,9 @@ jobject substrateGetActivity(); #ifdef SUBSTRATE void __attribute__((weak)) attach_setActivityResult(jint requestCode, jint resultCode, jobject intent) {} void __attribute__((weak)) attach_setLifecycleEvent(const char *event) {} -void __attribute__((weak)) attach_setComposingText(const char *id, const char *text) {} #else void attach_setActivityResult(jint requestCode, jint resultCode, jobject intent); void attach_setLifecycleEvent(const char *event); -void attach_setComposingText(const char *id, const char *text); #endif #define ATTACH_GRAAL() \ diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java index 3a976795..e6a34aa6 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java @@ -29,7 +29,6 @@ import com.gluonhq.attach.util.Services; import javafx.beans.property.ReadOnlyFloatProperty; -import javafx.beans.property.ReadOnlyStringProperty; import javafx.scene.Node; import javafx.scene.Parent; @@ -110,18 +109,4 @@ static Optional create() { */ void setKeyboardTypeForNode(Node node, KeyboardType type); - /** - * Returns a read-only property that reflects the current composing text for the given node - * (typically a {@link javafx.scene.control.TextInputControl}), as reported by the native IME. - * - *

Note that the JavaFX text input control default {@code textProperty()} will still - * catch all the internals of the text composition when predictive text is enabled (that could show - * partial text being removed and added back again while the user is typing)

- * - * @param node the node whose text to observe - * @return a ReadOnlyStringProperty with the composed text for the given node - * @since 4.0.25 - */ - ReadOnlyStringProperty textPropertyForNode(Node node); - } diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/AndroidKeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/AndroidKeyboardService.java index bad10a04..f3904404 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/AndroidKeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/AndroidKeyboardService.java @@ -60,12 +60,4 @@ private static void notifyVisibleHeight(float height) { } } - /** - * Called from keyboard.c when the native layer receives composing text - * tagged with a node id. - */ - private static void notifyComposingText(String id, String text) { - updateTextForId(id, text); - } - } \ No newline at end of file diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java index 56f5cbc6..2daf0786 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java @@ -32,18 +32,14 @@ import com.gluonhq.attach.util.Util; import javafx.animation.Interpolator; import javafx.animation.TranslateTransition; -import javafx.application.Platform; import javafx.beans.property.ReadOnlyFloatProperty; import javafx.beans.property.ReadOnlyFloatWrapper; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.beans.property.ReadOnlyStringWrapper; import javafx.beans.value.ChangeListener; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.input.MouseEvent; import javafx.util.Duration; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.WeakHashMap; @@ -62,11 +58,6 @@ public abstract class BaseKeyboardService implements KeyboardService { /** Map of nodes and keyboard types. */ private final Map nodeKeyboardTypes = new WeakHashMap<>(); - /** Map of nodes and text properties. */ - private static final Map nodeTextProperties = new WeakHashMap<>(); - - /** Map of ids and nodes. */ - private static final Map idToNode = new HashMap<>(); /** Map of nodes to their visibility listeners. */ private final Map> visibilityListeners = new WeakHashMap<>(); @@ -119,16 +110,6 @@ public void setKeyboardTypeForNode(Node node, KeyboardType type) { installEventFilter(node); } - @Override - public ReadOnlyStringProperty textPropertyForNode(Node node) { - Objects.requireNonNull(node, "node must not be null"); - installEventFilter(node); - return nodeTextProperties.computeIfAbsent(node, n -> { - idToNode.put(syntheticId(n), n); - return new ReadOnlyStringWrapper(""); - }).getReadOnlyProperty(); - } - private void installEventFilter(Node node) { node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> { KeyboardType type = nodeKeyboardTypes.getOrDefault(node, KeyboardType.ASCII); @@ -149,20 +130,6 @@ protected static String syntheticId(Node node) { return id != null ? id : "attach-kb-" + System.identityHashCode(node); } - /** - * Called from the native callback to update the text property for the - * node identified by {@code id}. - */ - protected static void updateTextForId(String id, String text) { - Node node = idToNode.get(id); - if (node == null) { - return; - } - ReadOnlyStringWrapper wrapper = nodeTextProperties.get(node); - if (wrapper != null && !Objects.equals(wrapper.get(), text)) { - Platform.runLater(() -> wrapper.set(text)); - } - } protected static void adjustPosition(Node node, Parent parent, double kh) { if (node == null || node.getScene() == null || node.getScene().getWindow() == null) { diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/IOSKeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/IOSKeyboardService.java index 56ef57ac..2d7f36b5 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/IOSKeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/IOSKeyboardService.java @@ -30,9 +30,6 @@ import com.gluonhq.attach.lifecycle.LifecycleEvent; import com.gluonhq.attach.lifecycle.LifecycleService; import javafx.application.Platform; -import javafx.beans.property.ReadOnlyStringProperty; -import javafx.scene.Node; -import javafx.scene.control.TextInputControl; public class IOSKeyboardService extends BaseKeyboardService { @@ -58,14 +55,6 @@ protected void applyKeyboardType(int nativeValue) { nativeSetKeyboardType(nativeValue); } - @Override - public ReadOnlyStringProperty textPropertyForNode(Node node) { - if (node instanceof TextInputControl) { - return ((TextInputControl) node).textProperty(); - } - return super.textPropertyForNode(node); - } - @Override protected void applyActiveNodeId(String id) { // no-op: iOS does not track active node, so no need to inform native layer diff --git a/modules/keyboard/src/main/native/android/c/keyboard.c b/modules/keyboard/src/main/native/android/c/keyboard.c index e95270d6..b388e230 100644 --- a/modules/keyboard/src/main/native/android/c/keyboard.c +++ b/modules/keyboard/src/main/native/android/c/keyboard.c @@ -53,7 +53,6 @@ JNI_OnLoad_keyboard(JavaVM *vm, void *reserved) ATTACH_LOG_FINE("Initializing native Keyboard from OnLoad"); jAttachKeyboardClass = (*env)->NewGlobalRef(env, (*env)->FindClass(env, "com/gluonhq/attach/keyboard/impl/AndroidKeyboardService")); jAttach_notifyHeightMethod = (*env)->GetStaticMethodID(env, jAttachKeyboardClass, "notifyVisibleHeight", "(F)V"); - jAttach_notifyComposingTextMethod = (*env)->GetStaticMethodID(env, jAttachKeyboardClass, "notifyComposingText", "(Ljava/lang/String;Ljava/lang/String;)V"); initKeyboard(); ATTACH_LOG_FINE("Initializing native Keyboard done"); return JNI_VERSION_1_8; @@ -115,23 +114,6 @@ JNIEXPORT void JNICALL Java_com_gluonhq_attach_keyboard_impl_AndroidKeyboardServ ATTACH_LOG_FINE("nativeSetActiveNodeId done"); } -////////////////////////////////// -// native (Substrate) to Java // -////////////////////////////////// - -void attach_setComposingText(const char *id, const char *text) -{ - ATTACH_LOG_FINE("attach_setComposingText: forwarding to Graal: id=%s, text=%s", id, text); - ATTACH_GRAAL(); - jstring graalId = (*graalEnv)->NewStringUTF(graalEnv, id); - jstring graalText = (*graalEnv)->NewStringUTF(graalEnv, text); - (*graalEnv)->CallStaticVoidMethod(graalEnv, jAttachKeyboardClass, jAttach_notifyComposingTextMethod, graalId, graalText); - (*graalEnv)->DeleteLocalRef(graalEnv, graalText); - (*graalEnv)->DeleteLocalRef(graalEnv, graalId); - DETACH_GRAAL(); - ATTACH_LOG_FINE("attach_setComposingText done"); -} - /////////////////////////// // From Dalvik to native // /////////////////////////// diff --git a/modules/keyboard/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json b/modules/keyboard/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json index fd604614..308c815b 100644 --- a/modules/keyboard/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json +++ b/modules/keyboard/src/main/resources/META-INF/substrate/config/jniconfig-aarch64-android.json @@ -2,8 +2,7 @@ { "name" : "com.gluonhq.attach.keyboard.impl.AndroidKeyboardService", "methods":[ - {"name":"notifyVisibleHeight","parameterTypes":["float"]}, - {"name":"notifyComposingText","parameterTypes":["java.lang.String","java.lang.String"]} + {"name":"notifyVisibleHeight","parameterTypes":["float"]} ] } ] \ No newline at end of file From fb6d6375d4b432a11fc17445a9c7ca65b547064c Mon Sep 17 00:00:00 2001 From: "jose.pereda" Date: Fri, 17 Apr 2026 18:36:02 +0200 Subject: [PATCH 3/5] Add removeKeyboardTypeForNode API --- .../attach/keyboard/KeyboardService.java | 10 ++++++ .../keyboard/impl/BaseKeyboardService.java | 32 ++++++++++++++++--- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java index e6a34aa6..c971936a 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/KeyboardService.java @@ -109,4 +109,14 @@ static Optional create() { */ void setKeyboardTypeForNode(Node node, KeyboardType type); + /** + * Removes the keyboard type assignment and event filter previously installed via + * {@link #setKeyboardTypeForNode(Node, KeyboardType)}. After this call the node + * will simply use the default keyboard type. + * + * @param node the node to unregister + * @since 4.0.25 + */ + void removeKeyboardTypeForNode(Node node); + } diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java index 2daf0786..2e7ae0ba 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java @@ -35,6 +35,7 @@ import javafx.beans.property.ReadOnlyFloatProperty; import javafx.beans.property.ReadOnlyFloatWrapper; import javafx.beans.value.ChangeListener; +import javafx.event.EventHandler; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.input.MouseEvent; @@ -58,6 +59,8 @@ public abstract class BaseKeyboardService implements KeyboardService { /** Map of nodes and keyboard types. */ private final Map nodeKeyboardTypes = new WeakHashMap<>(); + /** Map of nodes to their installed event filters. */ + private final Map> nodeEventFilters = new WeakHashMap<>(); /** Map of nodes to their visibility listeners. */ private final Map> visibilityListeners = new WeakHashMap<>(); @@ -110,15 +113,35 @@ public void setKeyboardTypeForNode(Node node, KeyboardType type) { installEventFilter(node); } + @Override + public void removeKeyboardTypeForNode(Node node) { + Objects.requireNonNull(node, "node must not be null"); + nodeKeyboardTypes.remove(node); + uninstallEventFilter(node); + } + private void installEventFilter(Node node) { - node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> { + if (nodeEventFilters.containsKey(node)) { + return; + } + EventHandler handler = e -> { KeyboardType type = nodeKeyboardTypes.getOrDefault(node, KeyboardType.ASCII); + String id = syntheticId(node); if (debug) { - LOG.info(String.format("Active keyboard type: %s for id %s", type, syntheticId(node))); + LOG.info(String.format("Active keyboard type: %s for id %s", type, id)); } - applyActiveNodeId(syntheticId(node)); + applyActiveNodeId(id); applyKeyboardType(type.getValue()); - }); + }; + nodeEventFilters.put(node, handler); + node.addEventFilter(MouseEvent.MOUSE_CLICKED, handler); + } + + private void uninstallEventFilter(Node node) { + EventHandler handler = nodeEventFilters.remove(node); + if (handler != null) { + node.removeEventFilter(MouseEvent.MOUSE_CLICKED, handler); + } } /** @@ -130,7 +153,6 @@ protected static String syntheticId(Node node) { return id != null ? id : "attach-kb-" + System.identityHashCode(node); } - protected static void adjustPosition(Node node, Parent parent, double kh) { if (node == null || node.getScene() == null || node.getScene().getWindow() == null) { return; From c5440bbe8fb883aa70ec781b68aa1cd935cbd8fc Mon Sep 17 00:00:00 2001 From: "jose.pereda" Date: Fri, 17 Apr 2026 19:25:36 +0200 Subject: [PATCH 4/5] Rework keyboardtype deactivation --- .../keyboard/impl/BaseKeyboardService.java | 94 ++++++++++++------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java index 2e7ae0ba..d44a9c79 100644 --- a/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java +++ b/modules/keyboard/src/main/java/com/gluonhq/attach/keyboard/impl/BaseKeyboardService.java @@ -35,14 +35,16 @@ import javafx.beans.property.ReadOnlyFloatProperty; import javafx.beans.property.ReadOnlyFloatWrapper; import javafx.beans.value.ChangeListener; -import javafx.event.EventHandler; +import javafx.beans.value.ObservableValue; import javafx.scene.Node; import javafx.scene.Parent; -import javafx.scene.input.MouseEvent; +import javafx.scene.Scene; import javafx.util.Duration; +import java.util.Collections; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; @@ -59,22 +61,13 @@ public abstract class BaseKeyboardService implements KeyboardService { /** Map of nodes and keyboard types. */ private final Map nodeKeyboardTypes = new WeakHashMap<>(); - /** Map of nodes to their installed event filters. */ - private final Map> nodeEventFilters = new WeakHashMap<>(); - /** Map of nodes to their visibility listeners. */ private final Map> visibilityListeners = new WeakHashMap<>(); + /** Scenes for which a focusOwner listener has already been installed. */ + private final Set trackedScenes = Collections.newSetFromMap(new WeakHashMap<>()); + BaseKeyboardService() { - VISIBLE_HEIGHT.addListener((obs, ov, nv) -> { - if (nv != null && nv.doubleValue() <= 0) { - if (debug) { - LOG.info("Keyboard hidden, reset default type"); - } - applyActiveNodeId(""); // reset active node - applyKeyboardType(KeyboardType.ASCII.getValue()); - } - }); } @Override @@ -110,38 +103,75 @@ public void setKeyboardTypeForNode(Node node, KeyboardType type) { Objects.requireNonNull(node, "node must not be null"); Objects.requireNonNull(type, "type must not be null"); nodeKeyboardTypes.put(node, type); - installEventFilter(node); + attachFocusTracker(node); } @Override public void removeKeyboardTypeForNode(Node node) { Objects.requireNonNull(node, "node must not be null"); nodeKeyboardTypes.remove(node); - uninstallEventFilter(node); } - private void installEventFilter(Node node) { - if (nodeEventFilters.containsKey(node)) { + /** + * Ensures a single focusOwner listener is installed on the scene that + * contains {@code node}. The listener drives the native keyboard type for + * every focus change in that scene, whether the newly focused node was + * explicitly registered via {@link #setKeyboardTypeForNode} or not. + * If {@code node} is not yet in a scene, the installation is deferred + * until it is. + */ + private void attachFocusTracker(Node node) { + Scene scene = node.getScene(); + if (scene != null) { + trackScene(scene); + // If this node is already the focus owner, apply its type now + if (scene.getFocusOwner() == node) { + applyTypeFor(node); + } return; } - EventHandler handler = e -> { - KeyboardType type = nodeKeyboardTypes.getOrDefault(node, KeyboardType.ASCII); - String id = syntheticId(node); - if (debug) { - LOG.info(String.format("Active keyboard type: %s for id %s", type, id)); + node.sceneProperty().addListener(new ChangeListener<>() { + @Override + public void changed(ObservableValue obs, Scene ov, Scene nv) { + if (nv != null) { + trackScene(nv); + if (nv.getFocusOwner() == node) { + applyTypeFor(node); + } + obs.removeListener(this); + } } - applyActiveNodeId(id); - applyKeyboardType(type.getValue()); - }; - nodeEventFilters.put(node, handler); - node.addEventFilter(MouseEvent.MOUSE_CLICKED, handler); + }); } - private void uninstallEventFilter(Node node) { - EventHandler handler = nodeEventFilters.remove(node); - if (handler != null) { - node.removeEventFilter(MouseEvent.MOUSE_CLICKED, handler); + private void trackScene(Scene scene) { + if (!trackedScenes.add(scene)) { + return; + } + scene.focusOwnerProperty().addListener((obs, ov, newNode) -> applyTypeFor(newNode)); + } + + /** + * Pushes the id and keyboard type for {@code focused} down to the native + * layer. Registered nodes use their stored {@link KeyboardType}; any other + * focus owner (including {@code null}) falls back to {@link KeyboardType#ASCII}. + */ + private void applyTypeFor(Node focused) { + if (focused == null) { + if (debug) { + LOG.info("Focus cleared, applying default ASCII keyboard"); + } + applyActiveNodeId(""); + applyKeyboardType(KeyboardType.ASCII.getValue()); + return; + } + KeyboardType type = nodeKeyboardTypes.getOrDefault(focused, KeyboardType.ASCII); + String id = syntheticId(focused); + if (debug) { + LOG.info(String.format("Active keyboard type: %s for id %s", type, id)); } + applyActiveNodeId(id); + applyKeyboardType(type.getValue()); } /** From e7e3f2a2cc89f62d8c4ac2d30efdd743f0e024b6 Mon Sep 17 00:00:00 2001 From: "jose.pereda" Date: Sat, 18 Apr 2026 00:38:49 +0200 Subject: [PATCH 5/5] cleanup --- modules/keyboard/src/main/native/android/c/keyboard.c | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/keyboard/src/main/native/android/c/keyboard.c b/modules/keyboard/src/main/native/android/c/keyboard.c index b388e230..1a8883ab 100644 --- a/modules/keyboard/src/main/native/android/c/keyboard.c +++ b/modules/keyboard/src/main/native/android/c/keyboard.c @@ -31,7 +31,6 @@ static jclass jKeyboardServiceClass; static jclass jAttachKeyboardClass; static jclass jActivityClass; static jmethodID jAttach_notifyHeightMethod; -static jmethodID jAttach_notifyComposingTextMethod; static jmethodID jActivity_setKeyboardTypeMethod; static jmethodID jActivity_setActiveNodeIdMethod;