[Spec] Key Handling
See original GitHub issueThe Problem
We have a problem with our KeyEventArgs
. The problem is that on win32 the Key
is localized to the current keyboard layout whereas on macOS and Linux it is not.
The root of this problem is something that vscode and Chrome went through a while ago and is well described in this issue:
https://github.com/microsoft/vscode/issues/17521
Taking the relevant parts of that issue:
A keyboard layout on Windows consists of two mappings. The first one maps scan codes to virtual keys and the second one maps virtual keys and modifiers combinations to generated characters.
Keyboard layouts on Linux and Mac do not work through this double indirection (from scan code to key code, and from key code and modifiers to character), keyboard input on Linux and Mac goes straight from scan code and modifiers to characters.
The KeyEventArgs.Key
property which we inherited from WPF represents a virtual key, but that concept is a win32 concept.
This results in the problem that on win32, our Key
property reflects the key after mapping from a scan code to a virtual key code, but on other platforms it represents a scan code.
Scan codes are not localized so we end up with the situation where on win32 the Key
takes the keyboard layout into account but on macOS and Linux it doesn’t:
- https://github.com/AvaloniaUI/Avalonia/issues/11065
- https://github.com/AvaloniaUI/Avalonia/issues/10932
- https://github.com/AvaloniaUI/Avalonia/issues/9424
- https://github.com/AvaloniaUI/Avalonia/issues/2049
The Solution
The W3C spec for KeyboardEvent
is a one place to look for inspiration as they’ve already gone through this.
They have the following attributes on their KeyboardEvent
:
- The legacy
keyCode
is similar to what we currently have key
holds a key attribute value corresponding to the key pressed. The key attribute is not related to the legacy keyCode attribute and does not have the same set of values. The values are defined herecode
holds a string that identifies the physical key being pressed. The value is not affected by the current keyboard layout or modifier state, so a particular key will always return the same value (i.e. it is the scan code). The values are defined here
This looks like decent place to start. The only complication is that the key
attribute is kind of a combined enum (can be a string like Enter
, Backspace
etc) and unicode key (e.g. A
, %
, あ
) string. That doesn’t translate well to C# where enums are not typically strings.
The Proposal
- We deprecate
KeyEventArgs.Key
as its behavior differs between operating systems - We add a
ScanCode
enumKeyEventArgs.ScanCode
property to describes the physical key being pressed which is not affected by the current keyboard layout or modifier state - We add a
char? KeyCharacter
property which describes the unicode character that the key represents after mapping to the current keyboard layout, if it exists - We add a
Key? SpecialKey
property which describes special keys not represented by a unicode character, such as (taken from the W3C spec):- Modifier Keys
- Enter, Tab keys
- Navigation keys
- Editing keys (Backspace, Delete, Insert etc)
- UI keys (Play, Pause etc)
- Device keys (Eject, PrintScreen etc)
- IME and Composition keys
- Function keys
- Multimedia keys
- And so on… W3C list is really long
Questions
- What about keys which fall into two categories? For example W3C specifies that the string
Tab
be used instead of the\t
character - Is our existing
Keys
enum suitable forSpecialKey
? - Is the proliferation of key-related properties on
KeyEventArgs
going to be confusing? Can we tweak the API to make falling into the pit of success a little easier?
Issue Analytics
- State:
- Created 3 months ago
- Comments:12 (11 by maintainers)
Top GitHub Comments
I think that regardless of the considerations about shortcuts, etc., having access to the raw untranslated scan codes would be useful in many situations. (For example, I’m working on a PC emulator which needs to pass host scan codes through to the VM.)
The apps use the corresponding latin key and it always just works.
We probably need to have some layot-specific logic to handle that or copy what Chrome does