From 8e5f98d4f8d2c77db38014b45ab060e2bf9da1f9 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 23 May 2026 14:15:14 +0300 Subject: [PATCH 1/3] Fix #5010: don't swallow UIPress events while editing a native text component The hardware-keyboard handler in CodenameOne_GLViewController consumed every UIPress whose UIKey mapped to a non-zero CN1 keycode -- and for printable characters cn1MapUIKeyToKeyCode returns the unicode codepoint, which is always non-zero. With handled=YES the press was never forwarded to [super pressesBegan:], so UIKit's text-input pipeline never converted it into insertText: on the focused CN1UITextField / CN1UITextView. That broke hardware-keyboard typing outright on iOS 13.4+ (BT keyboards, Magic Keyboard, simulator HW keyboard) the moment a text field was being edited. On iOS 26.x devices, where some on-screen keyboard interactions also reach the responder chain as UIPress events, the same path also swallowed virtual-keyboard typing -- the reporter's "cursor blinks but typing does nothing" symptom in #5010. Bypass the intercept entirely while editingComponent != nil. CN1 KeyEvent listeners legitimately stop firing during text editing, which matches the intuition that arrow keys etc. should move the caret rather than trigger app-level navigation while a field is focused. Verified on iPhone 17 Pro / iOS 26.3 simulator: with a single TextField focused, typing on the Mac host keyboard now lands characters in the field. Prior to the fix the same keystrokes were silently dropped. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../CodenameOne_GLViewController.m | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index 4942a3cd56..d789c399b7 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -2701,7 +2701,21 @@ - (void)remoteControlReceivedWithEvent:(UIEvent *)receivedEvent { // Mac Catalyst host keyboard, hardware keyboard in the iOS simulator via // Cmd-Shift-K). UIKey arrived in iOS 13.4 -- on older versions the // responder chain falls back to the existing UITextField editing path. +// +// While a native text editor is up (editingComponent != nil) we must not +// consume any UIPress -- UIKit's text-input pipeline needs every press +// (including printable characters, which cn1MapUIKeyToKeyCode returns as +// their unicode codepoint) to reach the focused CN1UITextField / +// CN1UITextView for insertion. The original implementation swallowed +// every press that mapped to a non-zero CN1 keycode, which on iOS 13.4+ +// broke hardware-keyboard typing outright and on iOS 26.x devices, where +// some on-screen keyboard interactions also surface as UIPress events, +// broke virtual-keyboard typing too -- see #5010. - (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event { + if (editingComponent != nil) { + [super pressesBegan:presses withEvent:event]; + return; + } if (@available(iOS 13.4, *)) { BOOL handled = NO; NSMutableSet *passthrough = nil; @@ -2732,6 +2746,10 @@ - (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)eve } - (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event { + if (editingComponent != nil) { + [super pressesEnded:presses withEvent:event]; + return; + } if (@available(iOS 13.4, *)) { BOOL handled = NO; NSMutableSet *passthrough = nil; @@ -2762,6 +2780,10 @@ - (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)eve } - (void)pressesCancelled:(NSSet *)presses withEvent:(UIPressesEvent *)event { + if (editingComponent != nil) { + [super pressesCancelled:presses withEvent:event]; + return; + } if (@available(iOS 13.4, *)) { for (UIPress *press in presses) { UIKey *key = press.key; From 600d7051cff0bebb0a631091f1f94f09dc6639ec Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 23 May 2026 14:20:29 +0300 Subject: [PATCH 2/3] Input-validation: add keytype step (XCUITest coverage for #5010) Extends the input-validation pipeline with a fourth gesture step that proves keyboard input lands in a CN1 TextField. The driver taps the field to bring up native iOS editing, then synthesises keystrokes via XCUIApplication.typeText -- which routes through the simulator's HW-keyboard pathway, raising UIPress events that walk through CodenameOne_GLViewController on iOS 13.4+. Without the companion fix in CodenameOne_GLViewController.m, every printable keystroke is consumed by pressesBegan: before UIKit converts it into insertText: on the focused CN1UITextField, the DataChangedListener never fires, and this step times out. With the fix, "cn1" propagates end-to-end through CN1UITextField -> UITextFieldTextDidChangeNotification -> EAGLView.textFieldDidChange -> stringEdit -> TextField.setText -> DataChangedListener and the step asserts CN1IV:EVENT:keytype. The pipeline already covers #5003's tap/drag/longpress regressions along the same touch chain; #5010 sits one layer deeper on the responder chain (UIPress instead of UITouch) and needs its own probe. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/input-validation-app/README.adoc | 13 +++- .../gestures/GestureSuite.java | 3 +- .../inputvalidation/gestures/KeyTypeStep.java | 65 +++++++++++++++++++ .../input-validation-app/drivers/run-ios.sh | 2 + .../Sources/InputValidationUITests.swift | 23 +++++++ 5 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/KeyTypeStep.java diff --git a/scripts/input-validation-app/README.adoc b/scripts/input-validation-app/README.adoc index c27929fd81..6fc71c1be4 100644 --- a/scripts/input-validation-app/README.adoc +++ b/scripts/input-validation-app/README.adoc @@ -1,8 +1,8 @@ = CN1 Input Validation App A minimal Codename One app whose only purpose is to assert that physical -input events (tap, drag, long-press) reach Component listeners end-to-end -on iOS. Driven by XCUITest on the iOS simulator. +input events (tap, drag, long-press, keyboard) reach Component listeners +end-to-end on iOS. Driven by XCUITest on the iOS simulator. == Why a separate pipeline? @@ -44,6 +44,15 @@ the expected time. | `addLongPressListener` fires for a press-and-hold. Routes through the same touch chain as tap, so a recognizer that cancels touches mid-press causes this step to time out. + +| `keytype` +| Typed characters reach a CN1 `TextField.DataChangedListener`. The + driver taps the field to bring up native iOS editing, then synthesises + keystrokes via `XCUIApplication.typeText`. typeText drives the + simulator's HW-keyboard pathway, which on iOS 13.4+ surfaces as + `UIPress` events through `GLViewController` -- the path #5010 broke + by treating every printable key as "handled" and never forwarding it + to UIKit's text-input pipeline. |=== == Log markers diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java index 1b58d95cef..f86da84827 100644 --- a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java +++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/GestureSuite.java @@ -38,7 +38,8 @@ public GestureSuite() { this.steps = new GestureStep[] { new TapStep(), new DragStep(), - new LongPressStep() + new LongPressStep(), + new KeyTypeStep() }; this.form = new Form("Input Validation", new BorderLayout()); this.statusLabel = new Label("Initializing"); diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/KeyTypeStep.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/KeyTypeStep.java new file mode 100644 index 0000000000..692125bbb7 --- /dev/null +++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/KeyTypeStep.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + */ +package com.codenameone.inputvalidation.gestures; + +import com.codename1.ui.Container; +import com.codename1.ui.Label; +import com.codename1.ui.TextArea; +import com.codename1.ui.TextField; +import com.codename1.ui.layouts.BorderLayout; +import com.codename1.ui.layouts.BoxLayout; + +/// Validates that keyboard input lands in a CN1 TextField end-to-end. +/// The XCUITest driver taps the field to bring up native iOS editing +/// (CN1UITextField becomes first responder), then synthesises keystrokes +/// via `XCUIApplication.typeText`. typeText drives the simulator's +/// hardware-keyboard pathway, which on iOS 13.4+ surfaces as UIPress +/// events that walk the responder chain through `GLViewController`. +/// +/// Regression coverage for #5010: a pressesBegan: handler in +/// CodenameOne_GLViewController.m treated every UIPress whose UIKey +/// mapped to a non-zero CN1 keycode as consumed -- and printable +/// characters map to their unicode codepoint, which is always non-zero. +/// That swallowed every HW keystroke before UIKit could convert it into +/// insertText: on the focused CN1UITextField, and on iOS 26.x devices +/// the same path also swallowed virtual-keyboard input. The fix bypasses +/// the intercept while editingComponent != nil. This step fails to +/// receive EXPECTED_TEXT and times out if either bug ever returns. +public final class KeyTypeStep implements GestureStep { + /// XCUITest types this exact string. Kept short, lowercase, and not a + /// dictionary word so iOS auto-capitalisation / auto-correct cannot + /// silently rewrite the characters before the CN1 TextField sees them. + public static final String EXPECTED_TEXT = "cn1"; + + @Override + public String name() { + return "keytype"; + } + + @Override + public void install(Container target, Callback callback) { + // Disable predictive text in the constraint so simulated keystrokes + // land verbatim. Without this iOS auto-capitalisation can rewrite + // the first character (`Cn1` instead of `cn1`) and make the + // assertion brittle across keyboard configurations. + final TextField field = new TextField("", "Type here", + EXPECTED_TEXT.length() + 8, + TextArea.ANY | TextArea.NON_PREDICTIVE); + field.setName("cn1iv-keytype-target"); + final boolean[] fired = {false}; + field.addDataChangedListener((type, index) -> { + String text = field.getText(); + if (!fired[0] && text != null && text.contains(EXPECTED_TEXT)) { + fired[0] = true; + callback.onDetected("text=" + text); + } + }); + Container col = new Container(BoxLayout.y()); + Label hint = new Label("Type " + EXPECTED_TEXT + " here"); + col.add(hint); + col.add(field); + target.add(BorderLayout.NORTH, col); + } +} diff --git a/scripts/input-validation-app/drivers/run-ios.sh b/scripts/input-validation-app/drivers/run-ios.sh index 22a0ec14af..6ebc1c83e5 100755 --- a/scripts/input-validation-app/drivers/run-ios.sh +++ b/scripts/input-validation-app/drivers/run-ios.sh @@ -187,6 +187,8 @@ REQUIRED_EVENTS=( "CN1IV:EVENT:drag" "CN1IV:READY:longpress" "CN1IV:EVENT:longpress" + "CN1IV:READY:keytype" + "CN1IV:EVENT:keytype" "CN1IV:SUITE:FINISHED" ) FAILED=0 diff --git a/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift b/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift index 725871a2a4..f7de544a3d 100644 --- a/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift +++ b/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift @@ -52,6 +52,9 @@ final class InputValidationUITests: XCTestCase { try driveLongPress(app: app) Thread.sleep(forTimeInterval: stepDelaySeconds) + + try driveKeyType(app: app) + Thread.sleep(forTimeInterval: stepDelaySeconds) } private func driveTap(app: XCUIApplication) throws { @@ -71,4 +74,24 @@ final class InputValidationUITests: XCTestCase { let center = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) center.press(forDuration: 1.5) } + + private func driveKeyType(app: XCUIApplication) throws { + // KeyTypeStep places its TextField in BorderLayout.NORTH of the + // target area; the suite's status label is also in NORTH but + // small, so the field sits a little below the top of the + // visible CN1 form. Tap a band that lands inside the field on + // every iPhone size class we test (iPhone 16 ... iPhone 17 Pro + // Max), wait briefly for CN1's editStringAtImpl to install the + // native CN1UITextField and bring up the keyboard, then type + // through the simulator's HW-keyboard pathway. typeText raises + // UIPress events that walk the responder chain through + // GLViewController -- exactly the path #5010 broke. + let fieldArea = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.18)) + fieldArea.tap() + // Give iOS time to attach the native text editor and animate + // the keyboard in -- typeText fails silently if no first + // responder is yet accepting input. + Thread.sleep(forTimeInterval: 1.5) + app.typeText("cn1") + } } From 2dd8278c09a6d2c0ca308b6ff8d01c380e6a63e5 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sat, 23 May 2026 15:24:30 +0300 Subject: [PATCH 3/3] Input-validation: fix keytype tap-target placement so XCUITest hits it The first CI run of the new keytype step timed out: XCUITest threw "Neither element nor any descendant has keyboard focus" from `app.typeText("cn1")` because the (0.5, 0.18) tap landed in empty space below the TextField. The field had been added to BorderLayout.NORTH of the target area, which on the CI iPhone SE runner put it well above the y=0.18 band the driver tapped, so the field never entered editing mode and no first responder was available for typeText to write into. Switch the layout to match TapStep / LongPressStep: TextField in BorderLayout.CENTER with the same padding+margin sizing, tapped at the centered (0.5, 0.5) coordinate the other steps already use successfully. Also bumps the post-tap sleep to 2.0s for the slower boot of the CI simulator's keyboard animation. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../inputvalidation/gestures/KeyTypeStep.java | 19 +++++++----- .../Sources/InputValidationUITests.swift | 29 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/KeyTypeStep.java b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/KeyTypeStep.java index 692125bbb7..28bbed5a53 100644 --- a/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/KeyTypeStep.java +++ b/scripts/input-validation-app/common/src/main/java/com/codenameone/inputvalidation/gestures/KeyTypeStep.java @@ -5,11 +5,10 @@ package com.codenameone.inputvalidation.gestures; import com.codename1.ui.Container; -import com.codename1.ui.Label; +import com.codename1.ui.Font; import com.codename1.ui.TextArea; import com.codename1.ui.TextField; import com.codename1.ui.layouts.BorderLayout; -import com.codename1.ui.layouts.BoxLayout; /// Validates that keyboard input lands in a CN1 TextField end-to-end. /// The XCUITest driver taps the field to bring up native iOS editing @@ -44,10 +43,18 @@ public void install(Container target, Callback callback) { // land verbatim. Without this iOS auto-capitalisation can rewrite // the first character (`Cn1` instead of `cn1`) and make the // assertion brittle across keyboard configurations. - final TextField field = new TextField("", "Type here", + final TextField field = new TextField("", "Type " + EXPECTED_TEXT + " here", EXPECTED_TEXT.length() + 8, TextArea.ANY | TextArea.NON_PREDICTIVE); field.setName("cn1iv-keytype-target"); + // Match TapStep / LongPressStep tap-target sizing so the XCUITest + // driver can use the same (0.5, 0.5) coordinate to focus the + // field on every iPhone size class on the CI runner. A NORTH + // placement put the field above where the existing steps tap + // and the driver missed it -- see #5010 CI failure. + field.getAllStyles().setFont(Font.createSystemFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE)); + field.getAllStyles().setPadding(48, 48, 48, 48); + field.getAllStyles().setMargin(48, 48, 48, 48); final boolean[] fired = {false}; field.addDataChangedListener((type, index) -> { String text = field.getText(); @@ -56,10 +63,6 @@ public void install(Container target, Callback callback) { callback.onDetected("text=" + text); } }); - Container col = new Container(BoxLayout.y()); - Label hint = new Label("Type " + EXPECTED_TEXT + " here"); - col.add(hint); - col.add(field); - target.add(BorderLayout.NORTH, col); + target.add(BorderLayout.CENTER, field); } } diff --git a/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift b/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift index f7de544a3d..96f8484dd2 100644 --- a/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift +++ b/scripts/input-validation-app/ios-tests/Sources/InputValidationUITests.swift @@ -76,22 +76,19 @@ final class InputValidationUITests: XCTestCase { } private func driveKeyType(app: XCUIApplication) throws { - // KeyTypeStep places its TextField in BorderLayout.NORTH of the - // target area; the suite's status label is also in NORTH but - // small, so the field sits a little below the top of the - // visible CN1 form. Tap a band that lands inside the field on - // every iPhone size class we test (iPhone 16 ... iPhone 17 Pro - // Max), wait briefly for CN1's editStringAtImpl to install the - // native CN1UITextField and bring up the keyboard, then type - // through the simulator's HW-keyboard pathway. typeText raises - // UIPress events that walk the responder chain through - // GLViewController -- exactly the path #5010 broke. - let fieldArea = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.18)) - fieldArea.tap() - // Give iOS time to attach the native text editor and animate - // the keyboard in -- typeText fails silently if no first - // responder is yet accepting input. - Thread.sleep(forTimeInterval: 1.5) + // KeyTypeStep places its TextField in BorderLayout.CENTER with + // generous padding/margin, matching the layout TapStep and + // LongPressStep use so a single screen-center tap focuses it on + // every iPhone size class on the CI runner. Wait briefly after + // the tap for CN1's editStringAtImpl to install the native + // CN1UITextField and animate the keyboard in -- typeText raises + // "Neither element nor any descendant has keyboard focus" if no + // first responder accepts input yet -- then type. typeText + // synthesises HW-keyboard UIPress events that walk through + // GLViewController, exactly the path #5010 broke. + let center = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)) + center.tap() + Thread.sleep(forTimeInterval: 2.0) app.typeText("cn1") } }