Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions gradle/include/android/grandroid_ext.h
Original file line number Diff line number Diff line change
Expand Up @@ -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() \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -79,6 +78,16 @@ static Optional<KeyboardService> 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.
Expand All @@ -101,17 +110,13 @@ static Optional<KeyboardService> 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.
*
* <p>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)</p>
* 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.
Comment on lines +113 to +115
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Javadoc says this removes an “event filter”, but the implementation now uses a scene-level focusOwnerProperty() listener rather than an event filter on the node. Please update the documentation to describe what is actually installed/removed (and what the lifecycle expectations are).

Suggested change
* 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.
* Removes the keyboard type assignment previously registered via
* {@link #setKeyboardTypeForNode(Node, KeyboardType)} and stops tracking scene
* focus-owner changes for that node. The registration remains active until this
* method is called. After this call the node will simply use the default keyboard
* type.

Copilot uses AI. Check for mistakes.
*
* @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);

}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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) {
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -60,75 +61,126 @@ public abstract class BaseKeyboardService implements KeyboardService {
/** Map of nodes and keyboard types. */
private final Map<Node, KeyboardType> nodeKeyboardTypes = new WeakHashMap<>();

/** Map of nodes and text properties. */
private static final Map<Node, ReadOnlyStringWrapper> nodeTextProperties = new WeakHashMap<>();
/** Map of nodes to their visibility listeners. */
private final Map<Node, ChangeListener<Number>> visibilityListeners = new WeakHashMap<>();

Comment on lines +64 to 66
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

visibilityListeners is a WeakHashMap, but each stored listener lambda closes over the node key, which defeats the weak-key behavior and can keep nodes alive until releaseVisibilityForNode is called. Consider using a regular HashMap (and documenting the need to release), or refactor the listener to hold only weak references to the node/parent if you want automatic cleanup.

Copilot uses AI. Check for mistakes.
/** Map of ids and nodes. */
private static final Map<String, Node> idToNode = new HashMap<>();
/** Scenes for which a focusOwner listener has already been installed. */
private final Set<Scene> 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());
}
});
}
Comment on lines 70 to +71
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The previous implementation reset the active node id and keyboard type back to ASCII when VISIBLE_HEIGHT dropped to 0 (keyboard hidden). With the constructor now empty, that reset no longer happens, which changes behavior and may conflict with the setKeyboardTypeForNode contract/Javadoc. If the reset is still intended, reintroduce it (possibly with the updated focus-based approach); otherwise consider updating the public Javadoc to match the new behavior.

Copilot uses AI. Check for mistakes.

@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<Number> 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<Number> 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");
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);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removeKeyboardTypeForNode only removes the stored mapping. If the node is currently the focus owner, the native keyboard type won’t revert to the default until focus changes again. Consider detecting this case (e.g., node.getScene()!=null && node.getScene().getFocusOwner()==node) and calling the same apply logic so the default ASCII type/id is pushed immediately.

Suggested change
nodeKeyboardTypes.remove(node);
nodeKeyboardTypes.remove(node);
Scene scene = node.getScene();
if (scene != null && scene.getFocusOwner() == node) {
applyTypeFor(node);
}

Copilot uses AI. Check for mistakes.
}

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;
Comment on lines +123 to +131
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When tracking a Scene for the first time, the current focus owner’s keyboard type/id isn’t applied unless it happens to be the node being registered. Since focusOwnerProperty() listeners don’t fire on registration, the native layer can remain out of sync until the next focus change. Consider applying applyTypeFor(scene.getFocusOwner()) right after installing the listener (and similarly after trackScene(nv) in the deferred branch).

Copilot uses AI. Check for mistakes.
}
node.sceneProperty().addListener(new ChangeListener<>() {
@Override
public void changed(ObservableValue<? extends Scene> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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
Expand Down
19 changes: 0 additions & 19 deletions modules/keyboard/src/main/native/android/c/keyboard.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Comment on lines 52 to 55
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notifyComposingText support was removed, but jAttach_notifyComposingTextMethod is still declared at the top of the file. With no initialization or use, this can trigger unused-variable warnings in stricter builds. Please remove the leftover jAttach_notifyComposingTextMethod declaration (and any related includes/defs if present).

Copilot uses AI. Check for mistakes.
ATTACH_LOG_FINE("Initializing native Keyboard done");
return JNI_VERSION_1_8;
Expand Down Expand Up @@ -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 //
///////////////////////////
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"]}
]
}
]
Loading