Fix keyDown event handling for macOS input methods - #21906
Conversation
AvnView passed every keyDown to the NSTextInputContext first and only raised a KeyDown when the context did not consume the event. The context consumes every printable key by routing it to insertText:, so while a text input client was active no KeyDown was raised for space, letters or digits and a KeyGesture on those keys could never match. Only modified keys were special-cased. keyDown: now always raises exactly one KeyDown per NSEvent before the input context is consulted. Composition state only decides which key is reported: while hasMarkedText is set the key is masked as AvnKeyImeProcessed, the same way Win32 reports VK_PROCESSKEY, so user code still observes the event but no gesture matches it. The physical key and the key symbol keep their real values in both cases. If user code handles the event the input context is skipped entirely, so no text and no preedit is produced. This mirrors the Win32 behaviour of swallowing WM_CHAR after a handled WM_KEYDOWN. The modifier special case, the handleKeyDown: helper and _lastKeyDownEvent are gone. doCommandBySelector: is kept but empty, since falling back to NSResponder would perform the default action and beep. Adds a Keyboard page to IntegrationTestApp that reports the last key down, a key down count and the last text input, plus Appium tests covering gestures with and without a modifier, text suppression for a handled gesture, and a single key down per key press.
…hods hasMarkedText only becomes true once a composition is in progress, so the keystroke that starts one was still reported with its real key and could match a KeyGesture. Win32 reports VK_PROCESSKEY for that keystroke too. keyDown: now also masks the key as AvnKeyImeProcessed when a printable character key is typed into a text input client while the selected keyboard input source is a composing input method. Whether an input source composes is resolved through TISGetInputSourceProperty: anything that is not a plain keyboard layout composes, except an input method in the alphanumeric input mode, which passes keys straight through and therefore keeps the real key. Unknown input sources keep the real key as well, so gestures are never lost to a failed lookup. The lookup includes installed but not enabled input sources, since an input method mode can be selected without being listed as enabled, and the result is cached per input source id because this runs for every key down. Only printable character keys are masked. KeySymbolFromScanCode reports the control character for Backspace, Enter, Tab and Escape, so the presence of a key symbol is not enough to decide: masking those would stop TextBox from reacting to them, which broke Backspace while an input method was selected. Command and Control combinations are shortcuts and are not masked either. Plain keyboard layouts are unaffected: dead key compositions are not driven by the input source and stay covered by hasMarkedText. The Keyboard page now subscribes to KeyDown and TextInput with handledEventsToo, so keys TextBox handles in its class handler, such as Backspace and the arrows, stay visible in the readout.
|
You can test this PR using the following package version. |
|
Integration tests ran just fine on my Mac. It looks like CI is using a different driver. |
|
You can test this PR using the following package version. |
MrJul
left a comment
There was a problem hiding this comment.
Tested with various keyboard layouts and IMEs, it works as expected.
This is a nice improvement, key handling has been a pain point in our macOS backend forever.
|
You can test this PR using the following package version. |
|
macOS integration test failures aren't a fluke.
Probably something not compatible with Appium v1. |
The tests synthesize key input through W3C actions, which the mac2 driver bundled with Appium 1 does not implement, so on the macOS CI leg they could only fail. They keep running on Windows, where WinAppDriver handles W3C actions, and on macOS builds with IsRunningAppium2, which target a current mac2 driver. The infrastructure change that would run them on macOS CI is split out for a separate pull request.
|
You can test this PR using the following package version. |
* fix(macOS): always raise keyDown before the input context sees the event AvnView passed every keyDown to the NSTextInputContext first and only raised a KeyDown when the context did not consume the event. The context consumes every printable key by routing it to insertText:, so while a text input client was active no KeyDown was raised for space, letters or digits and a KeyGesture on those keys could never match. Only modified keys were special-cased. keyDown: now always raises exactly one KeyDown per NSEvent before the input context is consulted. Composition state only decides which key is reported: while hasMarkedText is set the key is masked as AvnKeyImeProcessed, the same way Win32 reports VK_PROCESSKEY, so user code still observes the event but no gesture matches it. The physical key and the key symbol keep their real values in both cases. If user code handles the event the input context is skipped entirely, so no text and no preedit is produced. This mirrors the Win32 behaviour of swallowing WM_CHAR after a handled WM_KEYDOWN. The modifier special case, the handleKeyDown: helper and _lastKeyDownEvent are gone. doCommandBySelector: is kept but empty, since falling back to NSResponder would perform the default action and beep. Adds a Keyboard page to IntegrationTestApp that reports the last key down, a key down count and the last text input, plus Appium tests covering gestures with and without a modifier, text suppression for a handled gesture, and a single key down per key press. * fix(macOS): mask the composition starting key for composing input methods hasMarkedText only becomes true once a composition is in progress, so the keystroke that starts one was still reported with its real key and could match a KeyGesture. Win32 reports VK_PROCESSKEY for that keystroke too. keyDown: now also masks the key as AvnKeyImeProcessed when a printable character key is typed into a text input client while the selected keyboard input source is a composing input method. Whether an input source composes is resolved through TISGetInputSourceProperty: anything that is not a plain keyboard layout composes, except an input method in the alphanumeric input mode, which passes keys straight through and therefore keeps the real key. Unknown input sources keep the real key as well, so gestures are never lost to a failed lookup. The lookup includes installed but not enabled input sources, since an input method mode can be selected without being listed as enabled, and the result is cached per input source id because this runs for every key down. Only printable character keys are masked. KeySymbolFromScanCode reports the control character for Backspace, Enter, Tab and Escape, so the presence of a key symbol is not enough to decide: masking those would stop TextBox from reacting to them, which broke Backspace while an input method was selected. Command and Control combinations are shortcuts and are not masked either. Plain keyboard layouts are unaffected: dead key compositions are not driven by the input source and stay covered by hasMarkedText. The Keyboard page now subscribes to KeyDown and TextInput with handledEventsToo, so keys TextBox handles in its class handler, such as Backspace and the arrows, stay visible in the readout. * test(macOS): skip the Keyboard tests on the Appium 1 leg The tests synthesize key input through W3C actions, which the mac2 driver bundled with Appium 1 does not implement, so on the macOS CI leg they could only fail. They keep running on Windows, where WinAppDriver handles W3C actions, and on macOS builds with IsRunningAppium2, which target a current mac2 driver. The infrastructure change that would run them on macOS CI is split out for a separate pull request. --------- Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
* fix(macOS): always raise keyDown before the input context sees the event AvnView passed every keyDown to the NSTextInputContext first and only raised a KeyDown when the context did not consume the event. The context consumes every printable key by routing it to insertText:, so while a text input client was active no KeyDown was raised for space, letters or digits and a KeyGesture on those keys could never match. Only modified keys were special-cased. keyDown: now always raises exactly one KeyDown per NSEvent before the input context is consulted. Composition state only decides which key is reported: while hasMarkedText is set the key is masked as AvnKeyImeProcessed, the same way Win32 reports VK_PROCESSKEY, so user code still observes the event but no gesture matches it. The physical key and the key symbol keep their real values in both cases. If user code handles the event the input context is skipped entirely, so no text and no preedit is produced. This mirrors the Win32 behaviour of swallowing WM_CHAR after a handled WM_KEYDOWN. The modifier special case, the handleKeyDown: helper and _lastKeyDownEvent are gone. doCommandBySelector: is kept but empty, since falling back to NSResponder would perform the default action and beep. Adds a Keyboard page to IntegrationTestApp that reports the last key down, a key down count and the last text input, plus Appium tests covering gestures with and without a modifier, text suppression for a handled gesture, and a single key down per key press. * fix(macOS): mask the composition starting key for composing input methods hasMarkedText only becomes true once a composition is in progress, so the keystroke that starts one was still reported with its real key and could match a KeyGesture. Win32 reports VK_PROCESSKEY for that keystroke too. keyDown: now also masks the key as AvnKeyImeProcessed when a printable character key is typed into a text input client while the selected keyboard input source is a composing input method. Whether an input source composes is resolved through TISGetInputSourceProperty: anything that is not a plain keyboard layout composes, except an input method in the alphanumeric input mode, which passes keys straight through and therefore keeps the real key. Unknown input sources keep the real key as well, so gestures are never lost to a failed lookup. The lookup includes installed but not enabled input sources, since an input method mode can be selected without being listed as enabled, and the result is cached per input source id because this runs for every key down. Only printable character keys are masked. KeySymbolFromScanCode reports the control character for Backspace, Enter, Tab and Escape, so the presence of a key symbol is not enough to decide: masking those would stop TextBox from reacting to them, which broke Backspace while an input method was selected. Command and Control combinations are shortcuts and are not masked either. Plain keyboard layouts are unaffected: dead key compositions are not driven by the input source and stay covered by hasMarkedText. The Keyboard page now subscribes to KeyDown and TextInput with handledEventsToo, so keys TextBox handles in its class handler, such as Backspace and the arrows, stay visible in the readout. * test(macOS): skip the Keyboard tests on the Appium 1 leg The tests synthesize key input through W3C actions, which the mac2 driver bundled with Appium 1 does not implement, so on the macOS CI leg they could only fail. They keep running on Windows, where WinAppDriver handles W3C actions, and on macOS builds with IsRunningAppium2, which target a current mac2 driver. The infrastructure change that would run them on macOS CI is split out for a separate pull request. --------- Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
* fix(macOS): always raise keyDown before the input context sees the event AvnView passed every keyDown to the NSTextInputContext first and only raised a KeyDown when the context did not consume the event. The context consumes every printable key by routing it to insertText:, so while a text input client was active no KeyDown was raised for space, letters or digits and a KeyGesture on those keys could never match. Only modified keys were special-cased. keyDown: now always raises exactly one KeyDown per NSEvent before the input context is consulted. Composition state only decides which key is reported: while hasMarkedText is set the key is masked as AvnKeyImeProcessed, the same way Win32 reports VK_PROCESSKEY, so user code still observes the event but no gesture matches it. The physical key and the key symbol keep their real values in both cases. If user code handles the event the input context is skipped entirely, so no text and no preedit is produced. This mirrors the Win32 behaviour of swallowing WM_CHAR after a handled WM_KEYDOWN. The modifier special case, the handleKeyDown: helper and _lastKeyDownEvent are gone. doCommandBySelector: is kept but empty, since falling back to NSResponder would perform the default action and beep. Adds a Keyboard page to IntegrationTestApp that reports the last key down, a key down count and the last text input, plus Appium tests covering gestures with and without a modifier, text suppression for a handled gesture, and a single key down per key press. * fix(macOS): mask the composition starting key for composing input methods hasMarkedText only becomes true once a composition is in progress, so the keystroke that starts one was still reported with its real key and could match a KeyGesture. Win32 reports VK_PROCESSKEY for that keystroke too. keyDown: now also masks the key as AvnKeyImeProcessed when a printable character key is typed into a text input client while the selected keyboard input source is a composing input method. Whether an input source composes is resolved through TISGetInputSourceProperty: anything that is not a plain keyboard layout composes, except an input method in the alphanumeric input mode, which passes keys straight through and therefore keeps the real key. Unknown input sources keep the real key as well, so gestures are never lost to a failed lookup. The lookup includes installed but not enabled input sources, since an input method mode can be selected without being listed as enabled, and the result is cached per input source id because this runs for every key down. Only printable character keys are masked. KeySymbolFromScanCode reports the control character for Backspace, Enter, Tab and Escape, so the presence of a key symbol is not enough to decide: masking those would stop TextBox from reacting to them, which broke Backspace while an input method was selected. Command and Control combinations are shortcuts and are not masked either. Plain keyboard layouts are unaffected: dead key compositions are not driven by the input source and stay covered by hasMarkedText. The Keyboard page now subscribes to KeyDown and TextInput with handledEventsToo, so keys TextBox handles in its class handler, such as Backspace and the arrows, stay visible in the readout. * test(macOS): skip the Keyboard tests on the Appium 1 leg The tests synthesize key input through W3C actions, which the mac2 driver bundled with Appium 1 does not implement, so on the macOS CI leg they could only fail. They keep running on Windows, where WinAppDriver handles W3C actions, and on macOS builds with IsRunningAppium2, which target a current mac2 driver. The infrastructure change that would run them on macOS CI is split out for a separate pull request. --------- Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
* fix(macOS): always raise keyDown before the input context sees the event AvnView passed every keyDown to the NSTextInputContext first and only raised a KeyDown when the context did not consume the event. The context consumes every printable key by routing it to insertText:, so while a text input client was active no KeyDown was raised for space, letters or digits and a KeyGesture on those keys could never match. Only modified keys were special-cased. keyDown: now always raises exactly one KeyDown per NSEvent before the input context is consulted. Composition state only decides which key is reported: while hasMarkedText is set the key is masked as AvnKeyImeProcessed, the same way Win32 reports VK_PROCESSKEY, so user code still observes the event but no gesture matches it. The physical key and the key symbol keep their real values in both cases. If user code handles the event the input context is skipped entirely, so no text and no preedit is produced. This mirrors the Win32 behaviour of swallowing WM_CHAR after a handled WM_KEYDOWN. The modifier special case, the handleKeyDown: helper and _lastKeyDownEvent are gone. doCommandBySelector: is kept but empty, since falling back to NSResponder would perform the default action and beep. Adds a Keyboard page to IntegrationTestApp that reports the last key down, a key down count and the last text input, plus Appium tests covering gestures with and without a modifier, text suppression for a handled gesture, and a single key down per key press. * fix(macOS): mask the composition starting key for composing input methods hasMarkedText only becomes true once a composition is in progress, so the keystroke that starts one was still reported with its real key and could match a KeyGesture. Win32 reports VK_PROCESSKEY for that keystroke too. keyDown: now also masks the key as AvnKeyImeProcessed when a printable character key is typed into a text input client while the selected keyboard input source is a composing input method. Whether an input source composes is resolved through TISGetInputSourceProperty: anything that is not a plain keyboard layout composes, except an input method in the alphanumeric input mode, which passes keys straight through and therefore keeps the real key. Unknown input sources keep the real key as well, so gestures are never lost to a failed lookup. The lookup includes installed but not enabled input sources, since an input method mode can be selected without being listed as enabled, and the result is cached per input source id because this runs for every key down. Only printable character keys are masked. KeySymbolFromScanCode reports the control character for Backspace, Enter, Tab and Escape, so the presence of a key symbol is not enough to decide: masking those would stop TextBox from reacting to them, which broke Backspace while an input method was selected. Command and Control combinations are shortcuts and are not masked either. Plain keyboard layouts are unaffected: dead key compositions are not driven by the input source and stay covered by hasMarkedText. The Keyboard page now subscribes to KeyDown and TextInput with handledEventsToo, so keys TextBox handles in its class handler, such as Backspace and the arrows, stay visible in the readout. * test(macOS): skip the Keyboard tests on the Appium 1 leg The tests synthesize key input through W3C actions, which the mac2 driver bundled with Appium 1 does not implement, so on the macOS CI leg they could only fail. They keep running on Windows, where WinAppDriver handles W3C actions, and on macOS builds with IsRunningAppium2, which target a current mac2 driver. The infrastructure change that would run them on macOS CI is split out for a separate pull request. --------- Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
What does the pull request do?
Makes every keystroke observable to user code on macOS, matching Win32. AvnView used to give each keyDown to the NSTextInputContext first and only raised a KeyDown when the context did not consume it, but the context consumes every printable key while a text input client is active. Now exactly one KeyDown is raised per NSEvent before the context sees it, and keystrokes that belong to the input method are masked as
Key.ImeProcessed, the way Win32 reportsVK_PROCESSKEY. Also adds a Keyboard page to IntegrationTestApp and Appium tests for both the Windows and macOS legs.What is the current behavior?
What is the updated/expected behavior with this PR?
Key.ImeProcessed;PhysicalKeyandKeySymbolkeep their real values.To test: run the Appium KeyboardTests, or use the IntegrationTestApp Keyboard page: gestures fire on a plain layout without inserting text, typing under a Japanese/Chinese input method shows
ImeProcessed|<physical>|[<symbol>]while composition works normally. Validated on macOS arm64 with Kana input, including forward delete and focus loss mid-composition.How was the solution implemented (if it's not obvious)?
CanStartComposition) while the selected input source composes.CanStartCompositionrejects keys whose symbol is a control character or DEL (0x7F), so editing keys stay real.com.apple.inputmethod.Roman); cached per input source id; unknown sources keep the real key.handleKeyDown:re-raise path throughdoCommandBySelector:is removed; the empty override stays to avoid the NSResponder beep.Checklist
Breaking changes
No API changes. Behavioral, macOS only:
Key.ImeProcessed.Obsoletions / Deprecations
None.
Fixed issues
Fixes #20255