From f67408843439c654c75f7bd68bdb3212f73d915e Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 19 May 2026 07:21:50 +0300 Subject: [PATCH 1/2] Android: forward hover/scroll + fix hardware keyboard input (#3498) The Android port never delivered hover events to CN1 and dropped or flattened many hardware-keyboard keystrokes: - onTouchEvent only handled ACTION_DOWN/UP/MOVE/CANCEL, so BT mouse / Chromebook trackpad / stylus ACTION_HOVER_* never reached Display.pointerHover*, even though the framework exposes them. - onKeyUpDown mapped non-DPAD/MENU keys through KeyCharacterMap.load(BUILT_IN_KEYBOARD).get(...), which returns the built-in layout's mapping (not the attached BT keyboard's) and returns 0 for any non-printable key (F-keys, Esc, Tab, Home/End, PgUp/PgDn, Insert, etc.) -- so apps got keyPressed(0). - Enter was silently dropped unless the app set sendEnterKey=true, even on real keyboards where Enter is unambiguous. - The meta-state passed to the character map only included SHIFT/ALT/SYM, losing CTRL/FN/CAPS modifiers. Changes: - CodenameOneView.onHoverEvent routes ACTION_HOVER_ENTER/MOVE/EXIT to AndroidImplementation.pointerHoverPressed/Hover/HoverReleased; the three view classes (AndroidAsyncView, AndroidSurfaceView, AndroidTextureView) override onHoverEvent to forward to it. - New AndroidImplementation pointerHover[Pressed|Released] overrides expose the protected base methods to CodenameOneView. - internalKeyCodeTranslate gains sentinels for ENTER, TAB, ESCAPE, HOME, END, PAGE_UP/DOWN, INSERT, FORWARD_DEL, F1..F12. - onKeyUpDown now uses KeyEvent.getUnicodeChar(getMetaState()) (the KeyEvent's own device mapping, including full meta state) instead of the cached built-in keymap, and silently consumes events where the unicode mapping is 0 rather than firing keyPressed(0). - Enter fires automatically when the event came from a hardware (alpha) keyboard; the legacy sendEnterKey opt-in still works for soft-keyboard cases. - Extra modifier keycodes (CTRL/META/FN/CAPS_LOCK/NUM_LOCK/ SCROLL_LOCK) join the existing SHIFT/ALT filter so they don't fire as standalone characters. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../impl/android/AndroidAsyncView.java | 10 +- .../impl/android/AndroidImplementation.java | 51 ++++ .../impl/android/AndroidSurfaceView.java | 8 + .../impl/android/AndroidTextureView.java | 8 + .../impl/android/CodenameOneView.java | 217 ++++++++++++------ 5 files changed, 221 insertions(+), 73 deletions(-) diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidAsyncView.java b/Ports/Android/src/com/codename1/impl/android/AndroidAsyncView.java index d0f7798dbc..01f65678c8 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidAsyncView.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidAsyncView.java @@ -482,7 +482,15 @@ public boolean dispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } return res; - + + } + + @Override + public boolean onHoverEvent(MotionEvent event) { + if (cn1View.onHoverEvent(event)) { + return true; + } + return super.onHoverEvent(event); } @Override diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java index be2d4688c2..a2222c37ba 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java @@ -252,6 +252,27 @@ public void uncaughtException(Thread t, Throwable e) { static final int DROID_IMPL_KEY_VOLUME_UP = -23457; static final int DROID_IMPL_KEY_VOLUME_DOWN = -23458; static final int DROID_IMPL_KEY_MUTE = -23459; + static final int DROID_IMPL_KEY_ENTER = -23460; + static final int DROID_IMPL_KEY_TAB = -23461; + static final int DROID_IMPL_KEY_ESCAPE = -23462; + static final int DROID_IMPL_KEY_HOME = -23463; + static final int DROID_IMPL_KEY_END = -23464; + static final int DROID_IMPL_KEY_PAGE_UP = -23465; + static final int DROID_IMPL_KEY_PAGE_DOWN = -23466; + static final int DROID_IMPL_KEY_INSERT = -23467; + static final int DROID_IMPL_KEY_FORWARD_DEL = -23468; + static final int DROID_IMPL_KEY_F1 = -23469; + static final int DROID_IMPL_KEY_F2 = -23470; + static final int DROID_IMPL_KEY_F3 = -23471; + static final int DROID_IMPL_KEY_F4 = -23472; + static final int DROID_IMPL_KEY_F5 = -23473; + static final int DROID_IMPL_KEY_F6 = -23474; + static final int DROID_IMPL_KEY_F7 = -23475; + static final int DROID_IMPL_KEY_F8 = -23476; + static final int DROID_IMPL_KEY_F9 = -23477; + static final int DROID_IMPL_KEY_F10 = -23478; + static final int DROID_IMPL_KEY_F11 = -23479; + static final int DROID_IMPL_KEY_F12 = -23480; static int[] leftSK = new int[]{DROID_IMPL_KEY_MENU}; /** @@ -1910,6 +1931,36 @@ protected void pointerDragged(int[] x, int[] y) { super.pointerDragged(x, y); } + @Override + protected void pointerHover(int x, int y) { + super.pointerHover(x, y); + } + + @Override + protected void pointerHover(int[] x, int[] y) { + super.pointerHover(x, y); + } + + @Override + protected void pointerHoverPressed(int x, int y) { + super.pointerHoverPressed(x, y); + } + + @Override + protected void pointerHoverPressed(int[] x, int[] y) { + super.pointerHoverPressed(x, y); + } + + @Override + protected void pointerHoverReleased(int x, int y) { + super.pointerHoverReleased(x, y); + } + + @Override + protected void pointerHoverReleased(int[] x, int[] y) { + super.pointerHoverReleased(x, y); + } + @Override protected int getDragAutoActivationThreshold() { return 1000000; diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidSurfaceView.java b/Ports/Android/src/com/codename1/impl/android/AndroidSurfaceView.java index bcb89336b4..a3867695bd 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidSurfaceView.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidSurfaceView.java @@ -188,6 +188,14 @@ public boolean onTouchEvent(MotionEvent event) { return cn1View.onTouchEvent(event); } + @Override + public boolean onHoverEvent(MotionEvent event) { + if (cn1View.onHoverEvent(event)) { + return true; + } + return super.onHoverEvent(event); + } + public AndroidGraphics getGraphics() { return cn1View.buffy; } diff --git a/Ports/Android/src/com/codename1/impl/android/AndroidTextureView.java b/Ports/Android/src/com/codename1/impl/android/AndroidTextureView.java index d85c69e5a5..6150d0ff3c 100644 --- a/Ports/Android/src/com/codename1/impl/android/AndroidTextureView.java +++ b/Ports/Android/src/com/codename1/impl/android/AndroidTextureView.java @@ -191,6 +191,14 @@ public boolean onTouchEvent(MotionEvent event) { return cn1View.onTouchEvent(event); } + @Override + public boolean onHoverEvent(MotionEvent event) { + if (cn1View.onHoverEvent(event)) { + return true; + } + return super.onHoverEvent(event); + } + public AndroidGraphics getGraphics() { return cn1View.buffy; } diff --git a/Ports/Android/src/com/codename1/impl/android/CodenameOneView.java b/Ports/Android/src/com/codename1/impl/android/CodenameOneView.java index fd3b6b8576..434bef8729 100644 --- a/Ports/Android/src/com/codename1/impl/android/CodenameOneView.java +++ b/Ports/Android/src/com/codename1/impl/android/CodenameOneView.java @@ -54,7 +54,6 @@ public class CodenameOneView { AndroidGraphics buffy = null; private Canvas canvas; private AndroidImplementation implementation = null; - private final KeyCharacterMap keyCharacterMap; private final Rect bounds = new Rect(); private boolean fireKeyDown = false; //private volatile boolean created = false; @@ -88,9 +87,6 @@ public CodenameOneView(Activity activity, View androidView, AndroidImplementatio this.buffy = new AndroidGraphics(implementation, null, false); } - this.keyCharacterMap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD); - - /** * From the docs: "Change whether this view is one of the set of * scrollable containers in its window. This will be used to determine @@ -380,15 +376,62 @@ final static int internalKeyCodeTranslate(int keyCode) { return AndroidImplementation.DROID_IMPL_KEY_BACKSPACE; case KeyEvent.KEYCODE_BACK: return AndroidImplementation.DROID_IMPL_KEY_BACK; + case KeyEvent.KEYCODE_ENTER: + case KeyEvent.KEYCODE_NUMPAD_ENTER: + return AndroidImplementation.DROID_IMPL_KEY_ENTER; + case KeyEvent.KEYCODE_TAB: + return AndroidImplementation.DROID_IMPL_KEY_TAB; + case KeyEvent.KEYCODE_ESCAPE: + return AndroidImplementation.DROID_IMPL_KEY_ESCAPE; + case KeyEvent.KEYCODE_MOVE_HOME: + return AndroidImplementation.DROID_IMPL_KEY_HOME; + case KeyEvent.KEYCODE_MOVE_END: + return AndroidImplementation.DROID_IMPL_KEY_END; + case KeyEvent.KEYCODE_PAGE_UP: + return AndroidImplementation.DROID_IMPL_KEY_PAGE_UP; + case KeyEvent.KEYCODE_PAGE_DOWN: + return AndroidImplementation.DROID_IMPL_KEY_PAGE_DOWN; + case KeyEvent.KEYCODE_INSERT: + return AndroidImplementation.DROID_IMPL_KEY_INSERT; + case KeyEvent.KEYCODE_FORWARD_DEL: + return AndroidImplementation.DROID_IMPL_KEY_FORWARD_DEL; + case KeyEvent.KEYCODE_F1: + return AndroidImplementation.DROID_IMPL_KEY_F1; + case KeyEvent.KEYCODE_F2: + return AndroidImplementation.DROID_IMPL_KEY_F2; + case KeyEvent.KEYCODE_F3: + return AndroidImplementation.DROID_IMPL_KEY_F3; + case KeyEvent.KEYCODE_F4: + return AndroidImplementation.DROID_IMPL_KEY_F4; + case KeyEvent.KEYCODE_F5: + return AndroidImplementation.DROID_IMPL_KEY_F5; + case KeyEvent.KEYCODE_F6: + return AndroidImplementation.DROID_IMPL_KEY_F6; + case KeyEvent.KEYCODE_F7: + return AndroidImplementation.DROID_IMPL_KEY_F7; + case KeyEvent.KEYCODE_F8: + return AndroidImplementation.DROID_IMPL_KEY_F8; + case KeyEvent.KEYCODE_F9: + return AndroidImplementation.DROID_IMPL_KEY_F9; + case KeyEvent.KEYCODE_F10: + return AndroidImplementation.DROID_IMPL_KEY_F10; + case KeyEvent.KEYCODE_F11: + return AndroidImplementation.DROID_IMPL_KEY_F11; + case KeyEvent.KEYCODE_F12: + return AndroidImplementation.DROID_IMPL_KEY_F12; default: return keyCode; } } public boolean onKeyUpDown(boolean down, int keyCode, KeyEvent event) { - keyCode = this.internalKeyCodeTranslate(keyCode); + // Capture the raw Android keycode before translation so we can ask the + // KeyEvent for the unicode mapping (event.getUnicodeChar expects the + // device's native keycode, not our negative sentinels). + final int rawKeyCode = keyCode; + keyCode = internalKeyCodeTranslate(keyCode); - switch (keyCode) { + switch (rawKeyCode) { case KeyEvent.KEYCODE_VOLUME_DOWN: case KeyEvent.KEYCODE_VOLUME_UP: case KeyEvent.KEYCODE_SEARCH: @@ -396,26 +439,19 @@ public boolean onKeyUpDown(boolean down, int keyCode, KeyEvent event) { case KeyEvent.KEYCODE_SHIFT_RIGHT: case KeyEvent.KEYCODE_ALT_LEFT: case KeyEvent.KEYCODE_ALT_RIGHT: + case KeyEvent.KEYCODE_CTRL_LEFT: + case KeyEvent.KEYCODE_CTRL_RIGHT: + case KeyEvent.KEYCODE_META_LEFT: + case KeyEvent.KEYCODE_META_RIGHT: + case KeyEvent.KEYCODE_FUNCTION: + case KeyEvent.KEYCODE_CAPS_LOCK: + case KeyEvent.KEYCODE_NUM_LOCK: + case KeyEvent.KEYCODE_SCROLL_LOCK: case KeyEvent.KEYCODE_SYM: - return false; - case KeyEvent.KEYCODE_ENTER: - if(Display.getInstance().getProperty("sendEnterKey", "false").equals("true")) { - if (down) { - Display.getInstance().keyPressed(keyCode); - } else { - Display.getInstance().keyReleased(keyCode); - } - return false; - } - break; - + return false; default: } - if (event.getRepeatCount() > 0) { - // skip repeats - return true; - } if (this.implementation.getCurrentForm() == null) { /** * make sure a form has been set before we can send events to the @@ -425,6 +461,21 @@ public boolean onKeyUpDown(boolean down, int keyCode, KeyEvent event) { return true; } + // ENTER is gated for back-compat: on touch keyboards Enter is the IME + // "done" action, so apps historically had to opt in via sendEnterKey. + // Default it on when a hardware (alpha) keyboard generated the event + // so BT/Chromebook keyboards just work. + if (keyCode == AndroidImplementation.DROID_IMPL_KEY_ENTER) { + boolean optIn = Display.getInstance().getProperty("sendEnterKey", "false").equals("true"); + if (!optIn && !isHardwareKeyboardEvent(event)) { + return false; + } + } + + if (event.getRepeatCount() > 0) { + // skip repeats + return true; + } if (keyCode == AndroidImplementation.DROID_IMPL_KEY_FIRE) { this.fireKeyDown = down; @@ -443,59 +494,55 @@ public boolean onKeyUpDown(boolean down, int keyCode, KeyEvent event) { } } - switch (keyCode) { - - case AndroidImplementation.DROID_IMPL_KEY_MENU: - //if the native commands are used don't handle the keycode - if (Display.getInstance().getCommandBehavior() == Display.COMMAND_BEHAVIOR_NATIVE) { - return false; - } - case AndroidImplementation.DROID_IMPL_KEY_BACK: - case AndroidImplementation.DROID_IMPL_KEY_DOWN: - case AndroidImplementation.DROID_IMPL_KEY_UP: - case AndroidImplementation.DROID_IMPL_KEY_LEFT: - case AndroidImplementation.DROID_IMPL_KEY_RIGHT: - case AndroidImplementation.DROID_IMPL_KEY_FIRE: - case AndroidImplementation.DROID_IMPL_KEY_CLEAR: - case AndroidImplementation.DROID_IMPL_KEY_BACKSPACE: - // directly pass to display. - if (down) { - Display.getInstance().keyPressed(keyCode); - } else { - Display.getInstance().keyReleased(keyCode); - } - return true; - - default: + // Any key our translator mapped to a negative CN1 sentinel is forwarded + // verbatim. The MENU sentinel still defers to the platform when native + // commands are enabled. + if (keyCode < 0) { + if (keyCode == AndroidImplementation.DROID_IMPL_KEY_MENU + && Display.getInstance().getCommandBehavior() == Display.COMMAND_BEHAVIOR_NATIVE) { + return false; + } + if (down) { + Display.getInstance().keyPressed(keyCode); + } else { + Display.getInstance().keyReleased(keyCode); + } + return true; + } - /** - * Codename One's TextField does not seem to work well if two - * keyup-keydown sequences of different keys are not strictly - * sequential. so we pass the up event of a character right - * after the down event. this is exactly the behavior of the - * BlackBerry implementation from this repository and has worked - * well for me. i guess this should be changed as soon as the - * TextField changes. - */ - int meta = 0; - if (event.isShiftPressed()) { - meta |= KeyEvent.META_SHIFT_ON; - } - if (event.isAltPressed()) { - meta |= KeyEvent.META_ALT_ON; - } - if (event.isSymPressed()) { - meta |= KeyEvent.META_SYM_ON; - } - final int nextchar = this.keyCharacterMap.get(keyCode, meta); - if (down) { - Display.getInstance().keyPressed(nextchar); - } else { - Display.getInstance().keyReleased(nextchar); - } - return true; + /** + * Codename One's TextField does not seem to work well if two + * keyup-keydown sequences of different keys are not strictly + * sequential. so we pass the up event of a character right + * after the down event. this is exactly the behavior of the + * BlackBerry implementation from this repository and has worked + * well for me. i guess this should be changed as soon as the + * TextField changes. + */ + // Use the KeyEvent's own device mapping rather than the cached + // BUILT_IN_KEYBOARD map: BT/USB keyboards on Android resolve their + // own layout through KeyEvent.getUnicodeChar, including the full + // meta state (SHIFT/ALT/CTRL/FN/CAPS). + final int nextchar = event.getUnicodeChar(event.getMetaState()); + if (nextchar == 0) { + // Non-printable key we don't translate (e.g. KEYCODE_BREAK, + // media keys). Consume it silently rather than firing keyPressed(0). + return true; + } + if (down) { + Display.getInstance().keyPressed(nextchar); + } else { + Display.getInstance().keyReleased(nextchar); + } + return true; + } + private static boolean isHardwareKeyboardEvent(KeyEvent event) { + android.view.InputDevice device = event.getDevice(); + if (device != null) { + return device.getKeyboardType() == android.view.KeyCharacterMap.ALPHA; } + return event.getDeviceId() != android.view.KeyCharacterMap.VIRTUAL_KEYBOARD; } private boolean cn1GrabbedPointer = false; @@ -609,6 +656,32 @@ public boolean onTouchEvent(MotionEvent event) { return consumeEvent; } + /** + * Routes Android hover events (mouse / stylus moving over the surface + * without a button pressed) into Codename One's pointerHover pipeline so + * external pointing devices on Android (BT mouse, Chromebook trackpad, + * stylus) drive hover-aware components. + */ + public boolean onHoverEvent(MotionEvent event) { + if (this.implementation.getCurrentForm() == null) { + return false; + } + final int x = (int) event.getX(); + final int y = (int) event.getY(); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_HOVER_ENTER: + this.implementation.pointerHoverPressed(x, y); + return true; + case MotionEvent.ACTION_HOVER_MOVE: + this.implementation.pointerHover(x, y); + return true; + case MotionEvent.ACTION_HOVER_EXIT: + this.implementation.pointerHoverReleased(x, y); + return true; + } + return false; + } + public AndroidGraphics getGraphics() { return buffy; } From 186900395a57e6fab1ca689bcfa6966cfc76922f Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Tue, 19 May 2026 10:40:19 +0300 Subject: [PATCH 2/2] iOS: hardware keyboard + hover support (#3498) Mirrors the Android-side fix in the previous commit: before this change, the iOS port had zero hardware-keyboard support (only headphone remote controls fired keyPressed) and zero hover support (no hover gesture recognizer, no UIPointerInteraction, only touch events). Native (CodenameOne_GLViewController.m): - New cn1MapUIKeyToKeyCode() translates each UIKey from a hardware keyboard event into either a Unicode codepoint (via key.characters) or a negative sentinel for non-printable keys (Enter, Tab, Esc, arrows, Home/End/PgUp/PgDn, Insert, Delete-Forward, F1-F12). The sentinels match IOSImplementation.IOS_IMPL_KEY_* and the Android port's DROID_IMPL_KEY_* values so cross-platform key handlers can match a single constant. - pressesBegan:withEvent:, pressesEnded:withEvent:, and pressesCancelled:withEvent: are overridden. Each UIKey is mapped and forwarded via keyPressedNative/keyReleasedNative. Presses we don't recognize (modifier-only presses, etc.) fall through to super so the responder chain can still apply system actions. Gated on @available(iOS 13.4, *) since UIKey arrived in 13.4 -- on older iOS the existing UITextField text-input path is unchanged. - cn1InstallHoverRecognizer attaches a UIHoverGestureRecognizer to the view (iOS 13+) and bridges state changes to pointerHoverPressed/Hover/HoverReleased callbacks. Installed from viewDidLoad in both code paths (MoPub and non-MoPub). Bridges (IOSNative.m): - keyPressedNative, keyReleasedNative, pointerHoverPressedNative/Native/ReleasedNative wrap the ParparVM generated symbols for the new static callbacks. Java (IOSImplementation.java): - IOS_IMPL_KEY_* constants matching the Android sentinels. - Static keyPressedCallback(int) / keyReleasedCallback(int) forward to Display.keyPressed/Released, gated by dropEvents. - Static pointerHoverPressedCallback / pointerHoverCallback / pointerHoverReleasedCallback feed the single-dimension int[] into the protected pointerHover[Pressed|Released] base methods (newly overridden on IOSImplementation, matching the existing pointerPressed/Released/Dragged pattern). Verified locally: - iOS port builds clean (Java compiles; UIKit APIs syntax-check against the iPhoneSimulator26.2 SDK). - Android emulator's qwerty2 input device reports Sources: KEYBOARD | DPAD, KeyboardType: 2 (KeyCharacterMap.ALPHA), so the corresponding Android-side isHardwareKeyboardEvent() path fires for Mac-host keystrokes -- direct evidence that the emulator routes through the new code rather than through the soft-IME path. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../CodenameOne_GLViewController.m | 217 +++++++++++++++++- Ports/iOSPort/nativeSources/IOSNative.m | 20 ++ .../codename1/impl/ios/IOSImplementation.java | 81 +++++++ 3 files changed, 317 insertions(+), 1 deletion(-) diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m index e2bca4ecb0..3727b10613 100644 --- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m +++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m @@ -326,6 +326,103 @@ void deinitVMImpl() { extern void pointerDragged(int* x, int* y, int length); extern void pointerReleased(int* x, int* y, int length); extern void screenSizeChanged(int width, int height); +extern void keyPressedNative(int keyCode); +extern void keyReleasedNative(int keyCode); +extern void pointerHoverPressedNative(int x, int y); +extern void pointerHoverNative(int x, int y); +extern void pointerHoverReleasedNative(int x, int y); + +// Sentinel keycodes forwarded to IOSImplementation for non-printable hardware +// keys. Values match IOS_IMPL_KEY_* in IOSImplementation.java and the Android +// DROID_IMPL_KEY_* sentinels so cross-platform key handlers can match either. +#define CN1_IOS_KEY_LEFT (-23446) +#define CN1_IOS_KEY_RIGHT (-23447) +#define CN1_IOS_KEY_UP (-23448) +#define CN1_IOS_KEY_DOWN (-23449) +#define CN1_IOS_KEY_BACKSPACE (-23453) +#define CN1_IOS_KEY_ENTER (-23460) +#define CN1_IOS_KEY_TAB (-23461) +#define CN1_IOS_KEY_ESCAPE (-23462) +#define CN1_IOS_KEY_HOME (-23463) +#define CN1_IOS_KEY_END (-23464) +#define CN1_IOS_KEY_PAGE_UP (-23465) +#define CN1_IOS_KEY_PAGE_DOWN (-23466) +#define CN1_IOS_KEY_INSERT (-23467) +#define CN1_IOS_KEY_FORWARD_DEL (-23468) +#define CN1_IOS_KEY_F1 (-23469) +#define CN1_IOS_KEY_F2 (-23470) +#define CN1_IOS_KEY_F3 (-23471) +#define CN1_IOS_KEY_F4 (-23472) +#define CN1_IOS_KEY_F5 (-23473) +#define CN1_IOS_KEY_F6 (-23474) +#define CN1_IOS_KEY_F7 (-23475) +#define CN1_IOS_KEY_F8 (-23476) +#define CN1_IOS_KEY_F9 (-23477) +#define CN1_IOS_KEY_F10 (-23478) +#define CN1_IOS_KEY_F11 (-23479) +#define CN1_IOS_KEY_F12 (-23480) + +// Translate a UIKey from a hardware keyboard into the integer the framework +// expects: a negative sentinel for non-printable keys, a unicode codepoint for +// printable characters, or 0 if we don't recognize the key. +static int cn1MapUIKeyToKeyCode(UIKey *key) API_AVAILABLE(ios(13.4)) { + switch (key.keyCode) { + case UIKeyboardHIDUsageKeyboardReturnOrEnter: + case UIKeyboardHIDUsageKeypadEnter: + return CN1_IOS_KEY_ENTER; + case UIKeyboardHIDUsageKeyboardTab: + return CN1_IOS_KEY_TAB; + case UIKeyboardHIDUsageKeyboardEscape: + return CN1_IOS_KEY_ESCAPE; + case UIKeyboardHIDUsageKeyboardDeleteOrBackspace: + return CN1_IOS_KEY_BACKSPACE; + case UIKeyboardHIDUsageKeyboardDeleteForward: + return CN1_IOS_KEY_FORWARD_DEL; + case UIKeyboardHIDUsageKeyboardInsert: + return CN1_IOS_KEY_INSERT; + case UIKeyboardHIDUsageKeyboardHome: + return CN1_IOS_KEY_HOME; + case UIKeyboardHIDUsageKeyboardEnd: + return CN1_IOS_KEY_END; + case UIKeyboardHIDUsageKeyboardPageUp: + return CN1_IOS_KEY_PAGE_UP; + case UIKeyboardHIDUsageKeyboardPageDown: + return CN1_IOS_KEY_PAGE_DOWN; + case UIKeyboardHIDUsageKeyboardLeftArrow: + return CN1_IOS_KEY_LEFT; + case UIKeyboardHIDUsageKeyboardRightArrow: + return CN1_IOS_KEY_RIGHT; + case UIKeyboardHIDUsageKeyboardUpArrow: + return CN1_IOS_KEY_UP; + case UIKeyboardHIDUsageKeyboardDownArrow: + return CN1_IOS_KEY_DOWN; + case UIKeyboardHIDUsageKeyboardF1: return CN1_IOS_KEY_F1; + case UIKeyboardHIDUsageKeyboardF2: return CN1_IOS_KEY_F2; + case UIKeyboardHIDUsageKeyboardF3: return CN1_IOS_KEY_F3; + case UIKeyboardHIDUsageKeyboardF4: return CN1_IOS_KEY_F4; + case UIKeyboardHIDUsageKeyboardF5: return CN1_IOS_KEY_F5; + case UIKeyboardHIDUsageKeyboardF6: return CN1_IOS_KEY_F6; + case UIKeyboardHIDUsageKeyboardF7: return CN1_IOS_KEY_F7; + case UIKeyboardHIDUsageKeyboardF8: return CN1_IOS_KEY_F8; + case UIKeyboardHIDUsageKeyboardF9: return CN1_IOS_KEY_F9; + case UIKeyboardHIDUsageKeyboardF10: return CN1_IOS_KEY_F10; + case UIKeyboardHIDUsageKeyboardF11: return CN1_IOS_KEY_F11; + case UIKeyboardHIDUsageKeyboardF12: return CN1_IOS_KEY_F12; + default: { + // Standalone modifier presses (Shift / Control / Option / Command / + // CapsLock) carry no characters; let the responder chain handle them. + NSString *chars = key.characters; + if (chars.length == 0) { + return 0; + } + unichar c = [chars characterAtIndex:0]; + if (c == 0) { + return 0; + } + return (int)c; + } + } +} void pointerPressedC(int* x, int* y, int length) { //CN1Log(@"pointerPressedC started"); @@ -2351,6 +2448,7 @@ - (void)viewDidLoad { [super viewDidLoad]; updateDisplayMetricsFromView(self.view); [self cn1InstallStatusBarTapProxy]; + [self cn1InstallHoverRecognizer]; //replaceViewDidLoad [self initGoogleConnect]; } @@ -2364,6 +2462,7 @@ - (void)viewDidLoad { [super viewDidLoad]; updateDisplayMetricsFromView(self.view); [self cn1InstallStatusBarTapProxy]; + [self cn1InstallHoverRecognizer]; //replaceViewDidLoad [self initGoogleConnect]; } @@ -2583,9 +2682,125 @@ - (void)remoteControlReceivedWithEvent:(UIEvent *)receivedEvent { default: break; - + + } + } +} + +// Hardware keyboard support (BT keyboard on iPad/iPhone, Magic Keyboard, +// 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. +- (void)pressesBegan:(NSSet *)presses withEvent:(UIPressesEvent *)event { + if (@available(iOS 13.4, *)) { + BOOL handled = NO; + NSMutableSet *passthrough = nil; + for (UIPress *press in presses) { + UIKey *key = press.key; + if (key == nil) { + continue; + } + int code = cn1MapUIKeyToKeyCode(key); + if (code != 0) { + keyPressedNative(code); + handled = YES; + } else { + if (passthrough == nil) { + passthrough = [NSMutableSet set]; + } + [passthrough addObject:press]; + } + } + if (handled) { + if (passthrough.count > 0) { + [super pressesBegan:passthrough withEvent:event]; + } + return; + } + } + [super pressesBegan:presses withEvent:event]; +} + +- (void)pressesEnded:(NSSet *)presses withEvent:(UIPressesEvent *)event { + if (@available(iOS 13.4, *)) { + BOOL handled = NO; + NSMutableSet *passthrough = nil; + for (UIPress *press in presses) { + UIKey *key = press.key; + if (key == nil) { + continue; + } + int code = cn1MapUIKeyToKeyCode(key); + if (code != 0) { + keyReleasedNative(code); + handled = YES; + } else { + if (passthrough == nil) { + passthrough = [NSMutableSet set]; + } + [passthrough addObject:press]; + } + } + if (handled) { + if (passthrough.count > 0) { + [super pressesEnded:passthrough withEvent:event]; + } + return; + } + } + [super pressesEnded:presses withEvent:event]; +} + +- (void)pressesCancelled:(NSSet *)presses withEvent:(UIPressesEvent *)event { + if (@available(iOS 13.4, *)) { + for (UIPress *press in presses) { + UIKey *key = press.key; + if (key == nil) { + continue; + } + int code = cn1MapUIKeyToKeyCode(key); + if (code != 0) { + keyReleasedNative(code); + } } } + [super pressesCancelled:presses withEvent:event]; +} + +// Hover support for BT mouse / iPad trackpad / Apple Pencil hover. Wired up +// once viewDidLoad has run; only attaches on iOS 13.0+ where +// UIHoverGestureRecognizer exists. +- (void)cn1InstallHoverRecognizer { + if (@available(iOS 13.0, *)) { + UIHoverGestureRecognizer *hover = [[UIHoverGestureRecognizer alloc] + initWithTarget:self + action:@selector(cn1HandleHover:)]; + [self.view addGestureRecognizer:hover]; +#ifndef CN1_USE_ARC + [hover release]; +#endif + } +} + +- (void)cn1HandleHover:(UIHoverGestureRecognizer *)recognizer API_AVAILABLE(ios(13.0)) { + CGPoint p = [recognizer locationInView:self.view]; + int x = (int)(p.x * scaleValue); + int y = (int)(p.y * scaleValue); + switch (recognizer.state) { + case UIGestureRecognizerStateBegan: + pointerHoverPressedNative(x, y); + break; + case UIGestureRecognizerStateChanged: + pointerHoverNative(x, y); + break; + case UIGestureRecognizerStateEnded: + case UIGestureRecognizerStateCancelled: + case UIGestureRecognizerStateFailed: + pointerHoverReleasedNative(x, y); + break; + default: + break; + } } #ifdef USE_ES2 diff --git a/Ports/iOSPort/nativeSources/IOSNative.m b/Ports/iOSPort/nativeSources/IOSNative.m index 28117fdfbf..73d88cb99e 100644 --- a/Ports/iOSPort/nativeSources/IOSNative.m +++ b/Ports/iOSPort/nativeSources/IOSNative.m @@ -1467,6 +1467,26 @@ void screenSizeChanged(int width, int height) { com_codename1_impl_ios_IOSImplementation_sizeChangedImpl___int_int(CN1_THREAD_GET_STATE_PASS_ARG width, height); } +void keyPressedNative(int keyCode) { + com_codename1_impl_ios_IOSImplementation_keyPressedCallback___int(CN1_THREAD_GET_STATE_PASS_ARG keyCode); +} + +void keyReleasedNative(int keyCode) { + com_codename1_impl_ios_IOSImplementation_keyReleasedCallback___int(CN1_THREAD_GET_STATE_PASS_ARG keyCode); +} + +void pointerHoverPressedNative(int x, int y) { + com_codename1_impl_ios_IOSImplementation_pointerHoverPressedCallback___int_int(CN1_THREAD_GET_STATE_PASS_ARG x, y); +} + +void pointerHoverNative(int x, int y) { + com_codename1_impl_ios_IOSImplementation_pointerHoverCallback___int_int(CN1_THREAD_GET_STATE_PASS_ARG x, y); +} + +void pointerHoverReleasedNative(int x, int y) { + com_codename1_impl_ios_IOSImplementation_pointerHoverReleasedCallback___int_int(CN1_THREAD_GET_STATE_PASS_ARG x, y); +} + void stringEdit(int finished, int cursorPos, NSString* text) { com_codename1_impl_ios_IOSImplementation_editingUpdate___java_lang_String_int_boolean(CN1_THREAD_GET_STATE_PASS_ARG fromNSString(CN1_THREAD_GET_STATE_PASS_ARG text), cursorPos, finished != 0 diff --git a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java index 0e4b8be70e..dcbbab64fb 100644 --- a/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java +++ b/Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java @@ -1248,6 +1248,87 @@ protected void pointerDragged(final int[] x, final int[] y) { super.pointerDragged(x, y); } + // Sentinel keycodes forwarded from the native iOS hardware-keyboard handler + // for non-printable keys. Values match Android's DROID_IMPL_KEY_* sentinels + // so apps can write platform-agnostic key handlers. + static final int IOS_IMPL_KEY_LEFT = -23446; + static final int IOS_IMPL_KEY_RIGHT = -23447; + static final int IOS_IMPL_KEY_UP = -23448; + static final int IOS_IMPL_KEY_DOWN = -23449; + static final int IOS_IMPL_KEY_FIRE = -23450; + static final int IOS_IMPL_KEY_BACKSPACE = -23453; + static final int IOS_IMPL_KEY_ENTER = -23460; + static final int IOS_IMPL_KEY_TAB = -23461; + static final int IOS_IMPL_KEY_ESCAPE = -23462; + static final int IOS_IMPL_KEY_HOME = -23463; + static final int IOS_IMPL_KEY_END = -23464; + static final int IOS_IMPL_KEY_PAGE_UP = -23465; + static final int IOS_IMPL_KEY_PAGE_DOWN = -23466; + static final int IOS_IMPL_KEY_INSERT = -23467; + static final int IOS_IMPL_KEY_FORWARD_DEL = -23468; + static final int IOS_IMPL_KEY_F1 = -23469; + static final int IOS_IMPL_KEY_F2 = -23470; + static final int IOS_IMPL_KEY_F3 = -23471; + static final int IOS_IMPL_KEY_F4 = -23472; + static final int IOS_IMPL_KEY_F5 = -23473; + static final int IOS_IMPL_KEY_F6 = -23474; + static final int IOS_IMPL_KEY_F7 = -23475; + static final int IOS_IMPL_KEY_F8 = -23476; + static final int IOS_IMPL_KEY_F9 = -23477; + static final int IOS_IMPL_KEY_F10 = -23478; + static final int IOS_IMPL_KEY_F11 = -23479; + static final int IOS_IMPL_KEY_F12 = -23480; + + public static void keyPressedCallback(int keyCode) { + if (dropEvents) { + return; + } + Display.getInstance().keyPressed(keyCode); + } + + public static void keyReleasedCallback(int keyCode) { + if (dropEvents) { + return; + } + Display.getInstance().keyReleased(keyCode); + } + + public static void pointerHoverPressedCallback(int x, int y) { + if (dropEvents) { + return; + } + singleDimensionX[0] = x; singleDimensionY[0] = y; + instance.pointerHoverPressed(singleDimensionX, singleDimensionY); + } + + public static void pointerHoverCallback(int x, int y) { + if (dropEvents) { + return; + } + singleDimensionX[0] = x; singleDimensionY[0] = y; + instance.pointerHover(singleDimensionX, singleDimensionY); + } + + public static void pointerHoverReleasedCallback(int x, int y) { + if (dropEvents) { + return; + } + singleDimensionX[0] = x; singleDimensionY[0] = y; + instance.pointerHoverReleased(singleDimensionX, singleDimensionY); + } + + protected void pointerHover(final int[] x, final int[] y) { + super.pointerHover(x, y); + } + + protected void pointerHoverPressed(final int[] x, final int[] y) { + super.pointerHoverPressed(x, y); + } + + protected void pointerHoverReleased(final int[] x, final int[] y) { + super.pointerHoverReleased(x, y); + } + static void sizeChangedImpl(int w, int h) { instance.sizeChanged(w, h); }