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 53e3e80d..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 @@ -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; @@ -79,6 +78,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. @@ -101,17 +110,13 @@ 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)

+ * 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 whose text to observe - * @return a ReadOnlyStringProperty with the composed text for the given node + * @param node the node to unregister * @since 4.0.25 */ - ReadOnlyStringProperty textPropertyForNode(Node node); + void removeKeyboardTypeForNode(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 98571a92..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 @@ -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) { @@ -77,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 0b4b493b..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 @@ -32,18 +32,19 @@ 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.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.HashMap; +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; @@ -60,22 +61,41 @@ 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 nodes to their visibility listeners. */ + private final Map> visibilityListeners = new WeakHashMap<>(); - /** Map of ids and nodes. */ - private static final Map idToNode = new HashMap<>(); + /** 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 + 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 @@ -83,52 +103,84 @@ 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 ReadOnlyStringProperty textPropertyForNode(Node node) { + public void removeKeyboardTypeForNode(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(); + nodeKeyboardTypes.remove(node); } - private void installEventFilter(Node node) { - node.addEventFilter(MouseEvent.MOUSE_CLICKED, e -> { - KeyboardType type = nodeKeyboardTypes.getOrDefault(node, KeyboardType.ASCII); - if (debug) { - LOG.info(String.format("Active keyboard type: %s for id %s", type, syntheticId(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; + } + 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(syntheticId(node)); - applyKeyboardType(type.getValue()); }); } - /** - * Uses the node's own {@link Node#getId() id} if set, otherwise falls back to an - * id based on its identity hash code. - */ - protected static String syntheticId(Node node) { - String id = node.getId(); - return id != null ? id : "attach-kb-" + System.identityHashCode(node); + private void trackScene(Scene scene) { + if (!trackedScenes.add(scene)) { + return; + } + scene.focusOwnerProperty().addListener((obs, ov, newNode) -> applyTypeFor(newNode)); } /** - * Called from the native callback to update the text property for the - * node identified by {@code id}. + * 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}. */ - protected static void updateTextForId(String id, String text) { - Node node = idToNode.get(id); - if (node == null) { + private void applyTypeFor(Node focused) { + if (focused == null) { + if (debug) { + LOG.info("Focus cleared, applying default ASCII keyboard"); + } + applyActiveNodeId(""); + applyKeyboardType(KeyboardType.ASCII.getValue()); return; } - ReadOnlyStringWrapper wrapper = nodeTextProperties.get(node); - if (wrapper != null && !Objects.equals(wrapper.get(), text)) { - Platform.runLater(() -> wrapper.set(text)); + 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()); + } + + /** + * Uses the node's own {@link Node#getId() id} if set, otherwise falls back to an + * id based on its identity hash code. + */ + protected static String syntheticId(Node node) { + String id = node.getId(); + return id != null ? id : "attach-kb-" + System.identityHashCode(node); } protected static void adjustPosition(Node node, Parent parent, double kh) { 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..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,11 +30,6 @@ 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,34 +49,12 @@ 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) { 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..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; @@ -53,7 +52,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 +113,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