Browse Source

Key handling improvements (#12549)

* Physical key handling for Windows

* Physical key handling for macOS

* Physical key handling for X11

* Physical keys: cleanup unused keys

* Key symbols: ensure consistent behavior between platforms

* Fix dead key symbol for Windows

* Physical key handling for browser

* Physical keys: use new overloads where possible

* Key symbol for VNC

* Physical key handling in previewer

* Key symbol for forwarded X11 IME key

* Key symbol for Android

* Obsolete old RawKeyEventArgs ctor

* Fix key symbols for macOS with modifiers

* Adjust PhysicalKey members naming

* Use explicit std::hash for AvnKey/AvnPhysicalKey

Should hopefully satisfy the older compiler on the CI server

* Headless: added KeyPressQwerty

---------

Co-authored-by: Dan Walmsley <dan@walms.co.uk>
Co-authored-by: Steven Kirk <grokys@users.noreply.github.com>
pull/12996/head
Julien Lebosquain 2 years ago
committed by GitHub
parent
commit
ac00fe2bf4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 23
      native/Avalonia.Native/src/OSX/AvnView.mm
  2. 11
      native/Avalonia.Native/src/OSX/KeyTransform.h
  3. 866
      native/Avalonia.Native/src/OSX/KeyTransform.mm
  4. 23
      native/Avalonia.Native/src/OSX/menu.mm
  5. 5
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs
  6. 76
      src/Avalonia.Base/Input/KeyEventArgs.cs
  7. 26
      src/Avalonia.Base/Input/KeySymbolHelper.cs
  8. 6
      src/Avalonia.Base/Input/KeyboardDevice.cs
  9. 944
      src/Avalonia.Base/Input/PhysicalKey.cs
  10. 394
      src/Avalonia.Base/Input/PhysicalKeyExtensions.cs
  11. 29
      src/Avalonia.Base/Input/Raw/RawKeyEventArgs.cs
  12. 5
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  13. 4
      src/Avalonia.Controls/TopLevel.cs
  14. 22
      src/Avalonia.Native/WindowImplBase.cs
  15. 549
      src/Avalonia.Native/avn.idl
  16. 2
      src/Avalonia.Native/regen.sh
  17. 1
      src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
  18. 11
      src/Avalonia.Remote.Protocol/InputMessages.cs
  19. 25
      src/Avalonia.X11/X11Info.cs
  20. 209
      src/Avalonia.X11/X11KeyTransform.cs
  21. 256
      src/Avalonia.X11/X11Window.Ime.cs
  22. 3
      src/Avalonia.X11/X11Window.cs
  23. 32
      src/Avalonia.X11/XLib.cs
  24. 48
      src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs
  25. 435
      src/Browser/Avalonia.Browser/KeyInterop.cs
  26. 129
      src/Browser/Avalonia.Browser/Keycodes.cs
  27. 408
      src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs
  28. 32
      src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs
  29. 27
      src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs
  30. 6
      src/Headless/Avalonia.Headless/IHeadlessWindow.cs
  31. 8
      src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs
  32. 816
      src/Windows/Avalonia.Win32/Input/KeyInterop.cs
  33. 98
      src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs
  34. 25
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  35. 49
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  36. 13
      src/iOS/Avalonia.iOS/TextInputResponder.cs
  37. 12
      tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs
  38. 4
      tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs
  39. 6
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
  40. 24
      tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs

23
native/Avalonia.Native/src/OSX/AvnView.mm

@ -447,24 +447,25 @@
- (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type - (void) keyboardEvent: (NSEvent *) event withType: (AvnRawKeyEventType)type
{ {
if([self ignoreUserInput: false]) if([self ignoreUserInput: false] || _parent == nullptr)
{ {
return; return;
} }
auto key = s_KeyMap[[event keyCode]]; auto scanCode = [event keyCode];
auto key = VirtualKeyFromScanCode(scanCode, [event modifierFlags]);
auto physicalKey = PhysicalKeyFromScanCode(scanCode);
auto keySymbol = KeySymbolFromScanCode(scanCode, [event modifierFlags]);
auto keySymbolUtf8 = keySymbol == nullptr ? nullptr : [keySymbol UTF8String];
uint64_t timestamp = static_cast<uint64_t>([event timestamp] * 1000); auto timestamp = static_cast<uint64_t>([event timestamp] * 1000);
auto modifiers = [self getModifiers:[event modifierFlags]]; auto modifiers = [self getModifiers:[event modifierFlags]];
if(_parent != nullptr) auto handled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key, physicalKey, keySymbolUtf8);
{ if (key != AvnKeyLeftCtrl && key != AvnKeyRightCtrl) {
auto handled = _parent->BaseEvents->RawKeyEvent(type, timestamp, modifiers, key); _lastKeyHandled = handled;
if (key != LeftCtrl && key != RightCtrl) { } else {
_lastKeyHandled = handled; _lastKeyHandled = false;
} else {
_lastKeyHandled = false;
}
} }
} }

11
native/Avalonia.Native/src/OSX/KeyTransform.h

@ -1,14 +1,15 @@
#ifndef keytransform_h #ifndef keytransform_h
#define keytransform_h #define keytransform_h
#import <cstdint>
#include "common.h" #include "common.h"
#include <map>
extern std::map<int, AvnKey> s_KeyMap; AvnPhysicalKey PhysicalKeyFromScanCode(uint16_t scanCode);
extern std::map<AvnKey, int> s_AvnKeyMap; AvnKey VirtualKeyFromScanCode(uint16_t scanCode, NSEventModifierFlags modifierFlags);
extern std::map<int, const char*> s_QwertyKeyMap; NSString* KeySymbolFromScanCode(uint16_t scanCode, NSEventModifierFlags modifierFlags);
extern std::map<AvnKey, int> s_UnicodeKeyMap; uint16_t MenuCharFromVirtualKey(AvnKey key);
#endif #endif

866
native/Avalonia.Native/src/OSX/KeyTransform.mm

@ -1,391 +1,511 @@
#include "KeyTransform.h" #include "KeyTransform.h"
const int kVK_ANSI_A = 0x00; #import <Carbon/Carbon.h>
const int kVK_ANSI_S = 0x01; #include <array>
const int kVK_ANSI_D = 0x02; #include <unordered_map>
const int kVK_ANSI_F = 0x03;
const int kVK_ANSI_H = 0x04; struct KeyInfo
const int kVK_ANSI_G = 0x05;
const int kVK_ANSI_Z = 0x06;
const int kVK_ANSI_X = 0x07;
const int kVK_ANSI_C = 0x08;
const int kVK_ANSI_V = 0x09;
const int kVK_ANSI_B = 0x0B;
const int kVK_ANSI_Q = 0x0C;
const int kVK_ANSI_W = 0x0D;
const int kVK_ANSI_E = 0x0E;
const int kVK_ANSI_R = 0x0F;
const int kVK_ANSI_Y = 0x10;
const int kVK_ANSI_T = 0x11;
const int kVK_ANSI_1 = 0x12;
const int kVK_ANSI_2 = 0x13;
const int kVK_ANSI_3 = 0x14;
const int kVK_ANSI_4 = 0x15;
const int kVK_ANSI_6 = 0x16;
const int kVK_ANSI_5 = 0x17;
const int kVK_ANSI_Equal = 0x18;
const int kVK_ANSI_9 = 0x19;
const int kVK_ANSI_7 = 0x1A;
const int kVK_ANSI_Minus = 0x1B;
const int kVK_ANSI_8 = 0x1C;
const int kVK_ANSI_0 = 0x1D;
const int kVK_ANSI_RightBracket = 0x1E;
const int kVK_ANSI_O = 0x1F;
const int kVK_ANSI_U = 0x20;
const int kVK_ANSI_LeftBracket = 0x21;
const int kVK_ANSI_I = 0x22;
const int kVK_ANSI_P = 0x23;
const int kVK_ANSI_L = 0x25;
const int kVK_ANSI_J = 0x26;
const int kVK_ANSI_Quote = 0x27;
const int kVK_ANSI_K = 0x28;
const int kVK_ANSI_Semicolon = 0x29;
const int kVK_ANSI_Backslash = 0x2A;
const int kVK_ANSI_Comma = 0x2B;
const int kVK_ANSI_Slash = 0x2C;
const int kVK_ANSI_N = 0x2D;
const int kVK_ANSI_M = 0x2E;
const int kVK_ANSI_Period = 0x2F;
const int kVK_ANSI_Grave = 0x32;
const int kVK_ANSI_KeypadDecimal = 0x41;
const int kVK_ANSI_KeypadMultiply = 0x43;
const int kVK_ANSI_KeypadPlus = 0x45;
const int kVK_ANSI_KeypadClear = 0x47;
const int kVK_ANSI_KeypadDivide = 0x4B;
const int kVK_ANSI_KeypadEnter = 0x4C;
const int kVK_ANSI_KeypadMinus = 0x4E;
const int kVK_ANSI_KeypadEquals = 0x51;
const int kVK_ANSI_Keypad0 = 0x52;
const int kVK_ANSI_Keypad1 = 0x53;
const int kVK_ANSI_Keypad2 = 0x54;
const int kVK_ANSI_Keypad3 = 0x55;
const int kVK_ANSI_Keypad4 = 0x56;
const int kVK_ANSI_Keypad5 = 0x57;
const int kVK_ANSI_Keypad6 = 0x58;
const int kVK_ANSI_Keypad7 = 0x59;
const int kVK_ANSI_Keypad8 = 0x5B;
const int kVK_ANSI_Keypad9 = 0x5C;
const int kVK_Return = 0x24;
const int kVK_Tab = 0x30;
const int kVK_Space = 0x31;
const int kVK_Delete = 0x33;
const int kVK_Escape = 0x35;
const int kVK_Command = 0x37;
const int kVK_Shift = 0x38;
const int kVK_CapsLock = 0x39;
const int kVK_Option = 0x3A;
const int kVK_Control = 0x3B;
const int kVK_RightCommand = 0x36;
const int kVK_RightShift = 0x3C;
const int kVK_RightOption = 0x3D;
const int kVK_RightControl = 0x3E;
//const int kVK_Function = 0x3F;
const int kVK_F17 = 0x40;
const int kVK_VolumeUp = 0x48;
const int kVK_VolumeDown = 0x49;
const int kVK_Mute = 0x4A;
const int kVK_F18 = 0x4F;
const int kVK_F19 = 0x50;
const int kVK_F20 = 0x5A;
const int kVK_F5 = 0x60;
const int kVK_F6 = 0x61;
const int kVK_F7 = 0x62;
const int kVK_F3 = 0x63;
const int kVK_F8 = 0x64;
const int kVK_F9 = 0x65;
const int kVK_F11 = 0x67;
const int kVK_F13 = 0x69;
const int kVK_F16 = 0x6A;
const int kVK_F14 = 0x6B;
const int kVK_F10 = 0x6D;
const int kVK_F12 = 0x6F;
const int kVK_F15 = 0x71;
const int kVK_Help = 0x72;
const int kVK_Home = 0x73;
const int kVK_PageUp = 0x74;
const int kVK_ForwardDelete = 0x75;
const int kVK_F4 = 0x76;
const int kVK_End = 0x77;
const int kVK_F2 = 0x78;
const int kVK_PageDown = 0x79;
const int kVK_F1 = 0x7A;
const int kVK_LeftArrow = 0x7B;
const int kVK_RightArrow = 0x7C;
const int kVK_DownArrow = 0x7D;
const int kVK_UpArrow = 0x7E;
//const int kVK_ISO_Section = 0x0A;
//const int kVK_JIS_Yen = 0x5D;
//const int kVK_JIS_Underscore = 0x5E;
//const int kVK_JIS_KeypadComma = 0x5F;
//const int kVK_JIS_Eisu = 0x66;
const int kVK_JIS_Kana = 0x68;
// converts from AvaloniaKeys to UnicodeSpecial keys.
std::map<AvnKey, int> s_UnicodeKeyMap =
{ {
{ Up, NSUpArrowFunctionKey }, uint16_t scanCode;
{ Down, NSDownArrowFunctionKey }, AvnPhysicalKey physicalKey;
{ Left, NSLeftArrowFunctionKey }, AvnKey qwertyKey;
{ Right, NSRightArrowFunctionKey }, uint16_t menuChar;
{ F1, NSF1FunctionKey },
{ F2, NSF2FunctionKey },
{ F3, NSF3FunctionKey },
{ F4, NSF4FunctionKey },
{ F5, NSF5FunctionKey },
{ F6, NSF6FunctionKey },
{ F7, NSF7FunctionKey },
{ F8, NSF8FunctionKey },
{ F9, NSF9FunctionKey },
{ F10, NSF10FunctionKey },
{ F11, NSF11FunctionKey },
{ F12, NSF12FunctionKey },
{ F13, NSF13FunctionKey },
{ F14, NSF14FunctionKey },
{ F15, NSF15FunctionKey },
{ F16, NSF16FunctionKey },
{ F17, NSF17FunctionKey },
{ F18, NSF18FunctionKey },
{ F19, NSF19FunctionKey },
{ F20, NSF20FunctionKey },
{ F21, NSF21FunctionKey },
{ F22, NSF22FunctionKey },
{ F23, NSF23FunctionKey },
{ F24, NSF24FunctionKey },
{ Insert, NSInsertFunctionKey },
{ Delete, NSDeleteFunctionKey },
{ Home, NSHomeFunctionKey },
//{ Begin, NSBeginFunctionKey },
{ End, NSEndFunctionKey },
{ PageUp, NSPageUpFunctionKey },
{ PageDown, NSPageDownFunctionKey },
{ PrintScreen, NSPrintScreenFunctionKey },
{ Scroll, NSScrollLockFunctionKey },
//{ SysReq, NSSysReqFunctionKey },
//{ Break, NSBreakFunctionKey },
//{ Reset, NSResetFunctionKey },
//{ Stop, NSStopFunctionKey },
//{ Menu, NSMenuFunctionKey },
//{ UserFunction, NSUserFunctionKey },
//{ SystemFunction, NSSystemFunctionKey },
{ Print, NSPrintFunctionKey },
//{ ClearLine, NSClearLineFunctionKey },
//{ ClearDisplay, NSClearDisplayFunctionKey },
}; };
// Converts from Ansi virtual keys to Qwerty Keyboard map. // ScanCode - PhysicalKey - Key mapping (the virtual key is mapped as in a standard QWERTY keyboard)
std::map<int, const char*> s_QwertyKeyMap = // https://github.com/chromium/chromium/blob/main/ui/events/keycodes/dom/dom_code_data.inc
// This list has the same order as the PhysicalKey enum.
const KeyInfo keyInfos[] =
{ {
{ 0, "a" }, // Writing System Keys
{ 1, "s" }, { 0x32, AvnPhysicalKeyBackquote, AvnKeyOem3, '`' },
{ 2, "d" }, { 0x2A, AvnPhysicalKeyBackslash, AvnKeyOem5, '\\' },
{ 3, "f" }, { 0x21, AvnPhysicalKeyBracketLeft,AvnKeyOem4, '[' },
{ 4, "h" }, { 0x1E, AvnPhysicalKeyBracketRight, AvnKeyOem6, ']' },
{ 5, "g" }, { 0x2B, AvnPhysicalKeyComma, AvnKeyOemComma, ',' },
{ 6, "z" }, { 0x1D, AvnPhysicalKeyDigit0, AvnKeyD0, '0' },
{ 7, "x" }, { 0x12, AvnPhysicalKeyDigit1, AvnKeyD1, '1' },
{ 8, "c" }, { 0x13, AvnPhysicalKeyDigit2, AvnKeyD2, '2' },
{ 9, "v" }, { 0x14, AvnPhysicalKeyDigit3, AvnKeyD3, '3' },
{ 10, "§" }, { 0x15, AvnPhysicalKeyDigit4, AvnKeyD4, '4' },
{ 11, "b" }, { 0x17, AvnPhysicalKeyDigit5, AvnKeyD5, '5' },
{ 12, "q" }, { 0x16, AvnPhysicalKeyDigit6, AvnKeyD6, '6' },
{ 13, "w" }, { 0x1A, AvnPhysicalKeyDigit7, AvnKeyD7, '7' },
{ 14, "e" }, { 0x1C, AvnPhysicalKeyDigit8, AvnKeyD8, '8' },
{ 15, "r" }, { 0x19, AvnPhysicalKeyDigit9, AvnKeyD9, '9' },
{ 16, "y" }, { 0x18, AvnPhysicalKeyEqual, AvnKeyOemMinus, '-' },
{ 17, "t" }, { 0x0A, AvnPhysicalKeyIntlBackslash, AvnKeyOem102, 0 },
{ 18, "1" }, { 0x5E, AvnPhysicalKeyIntlRo, AvnKeyOem102, 0 },
{ 19, "2" }, { 0x5D, AvnPhysicalKeyIntlYen, AvnKeyOem5, 0 },
{ 20, "3" }, { 0x00, AvnPhysicalKeyA, AvnKeyA, 'a' },
{ 21, "4" }, { 0x0B, AvnPhysicalKeyB, AvnKeyB, 'b' },
{ 22, "6" }, { 0x08, AvnPhysicalKeyC, AvnKeyC, 'c' },
{ 23, "5" }, { 0x02, AvnPhysicalKeyD, AvnKeyD, 'd' },
{ 24, "=" }, { 0x0E, AvnPhysicalKeyE, AvnKeyE, 'e' },
{ 25, "9" }, { 0x03, AvnPhysicalKeyF, AvnKeyF, 'f' },
{ 26, "7" }, { 0x05, AvnPhysicalKeyG, AvnKeyG, 'g' },
{ 27, "-" }, { 0x04, AvnPhysicalKeyH, AvnKeyH, 'h' },
{ 28, "8" }, { 0x22, AvnPhysicalKeyI, AvnKeyI, 'i' },
{ 29, "0" }, { 0x26, AvnPhysicalKeyJ, AvnKeyJ, 'j' },
{ 30, "]" }, { 0x28, AvnPhysicalKeyK, AvnKeyK, 'k' },
{ 31, "o" }, { 0x25, AvnPhysicalKeyL, AvnKeyL, 'l' },
{ 32, "u" }, { 0x2E, AvnPhysicalKeyM, AvnKeyM, 'm' },
{ 33, "[" }, { 0x2D, AvnPhysicalKeyN, AvnKeyN, 'n' },
{ 34, "i" }, { 0x1F, AvnPhysicalKeyO, AvnKeyO, 'o' },
{ 35, "p" }, { 0x23, AvnPhysicalKeyP, AvnKeyP, 'p' },
{ 37, "l" }, { 0x0C, AvnPhysicalKeyQ, AvnKeyQ, 'q' },
{ 38, "j" }, { 0x0F, AvnPhysicalKeyR, AvnKeyR, 'r' },
{ 39, "'" }, { 0x01, AvnPhysicalKeyS, AvnKeyS, 's' },
{ 40, "k" }, { 0x11, AvnPhysicalKeyT, AvnKeyT, 't' },
{ 41, ";" }, { 0x20, AvnPhysicalKeyU, AvnKeyU, 'u' },
{ 42, "\\" }, { 0x09, AvnPhysicalKeyV, AvnKeyV, 'v' },
{ 43, "," }, { 0x0D, AvnPhysicalKeyW, AvnKeyW, 'w' },
{ 44, "/" }, { 0x07, AvnPhysicalKeyX, AvnKeyX, 'x' },
{ 45, "n" }, { 0x10, AvnPhysicalKeyY, AvnKeyY, 'y' },
{ 46, "m" }, { 0x06, AvnPhysicalKeyZ, AvnKeyZ, 'z' },
{ 47, "." }, { 0x1B, AvnPhysicalKeyMinus, AvnKeyOemMinus, '-' },
{ 48, "\t" }, { 0x2F, AvnPhysicalKeyPeriod, AvnKeyOemPeriod, '.' },
{ 49, " " }, { 0x27, AvnPhysicalKeyQuote, AvnKeyOem7, '\'' },
{ 50, "`" }, { 0x29, AvnPhysicalKeySemicolon, AvnKeyOem1, ';' },
{ 51, "" }, { 0x2C, AvnPhysicalKeySlash, AvnKeyOem2, '/' },
{ 52, "" },
{ 53, "" }, // Functional Keys
{ 65, "." }, { 0x3A, AvnPhysicalKeyAltLeft, AvnKeyLeftAlt, 0 },
{ 66, "" }, { 0x3D, AvnPhysicalKeyAltRight, AvnKeyRightAlt, 0 },
{ 67, "*" }, { 0x33, AvnPhysicalKeyBackspace, AvnKeyBack, kBackspaceCharCode },
{ 69, "+" }, { 0x39, AvnPhysicalKeyCapsLock, AvnKeyCapsLock, 0 },
{ 70, "" }, { 0x6E, AvnPhysicalKeyContextMenu, AvnKeyApps, 0 },
{ 71, "" }, { 0x3B, AvnPhysicalKeyControlLeft, AvnKeyLeftCtrl, 0 },
{ 72, "" }, { 0x3E, AvnPhysicalKeyControlRight, AvnKeyRightCtrl, 0 },
{ 75, "/" }, { 0x24, AvnPhysicalKeyEnter, AvnKeyEnter, kReturnCharCode },
{ 76, "" }, { 0x37, AvnPhysicalKeyMetaLeft, AvnKeyLWin, 0 },
{ 77, "" }, { 0x36, AvnPhysicalKeyMetaRight, AvnKeyRWin, 0 },
{ 78, "-" }, { 0x38, AvnPhysicalKeyShiftLeft, AvnKeyLeftShift, 0 },
{ 81, "=" }, { 0x3C, AvnPhysicalKeyShiftRight, AvnKeyRightShift, 0 },
{ 82, "0" }, { 0x31, AvnPhysicalKeySpace, AvnKeySpace, kSpaceCharCode },
{ 83, "1" }, { 0x30, AvnPhysicalKeyTab, AvnKeyTab, kTabCharCode },
{ 84, "2" }, //{ , AvnPhysicalKeyConvert, 0 },
{ 85, "3" }, //{ , AvnPhysicalKeyKanaMode, 0 },
{ 86, "4" }, { 0x68, AvnPhysicalKeyLang1, AvnKeyKanaMode, 0 },
{ 87, "5" }, { 0x66, AvnPhysicalKeyLang2, AvnKeyHanjaMode, 0 },
{ 88, "6" }, //{ , AvnPhysicalKeyLang3, 0 },
{ 89, "7" }, //{ , AvnPhysicalKeyLang4, 0 },
{ 91, "8" }, //{ , AvnPhysicalKeyLang5, 0 },
{ 92, "9" } //{ , AvnPhysicalKeyNonConvert, 0 },
// Control Pad Section
{ 0x75, AvnPhysicalKeyDelete, AvnKeyDelete, NSDeleteFunctionKey },
{ 0x77, AvnPhysicalKeyEnd, AvnKeyEnd, NSEndFunctionKey },
//{ , AvnPhysicalKeyHelp, 0 },
{ 0x73, AvnPhysicalKeyHome, AvnKeyHome, NSHomeFunctionKey },
{ 0x72, AvnPhysicalKeyInsert, AvnKeyInsert, NSInsertFunctionKey },
{ 0x79, AvnPhysicalKeyPageDown, AvnKeyPageDown, NSPageDownFunctionKey },
{ 0x74, AvnPhysicalKeyPageUp, AvnKeyPageUp, NSPageUpFunctionKey },
// Arrow Pad Section
{ 0x7D, AvnPhysicalKeyArrowDown, AvnKeyDown, NSDownArrowFunctionKey },
{ 0x7B, AvnPhysicalKeyArrowLeft, AvnKeyLeft, NSLeftArrowFunctionKey },
{ 0x7C, AvnPhysicalKeyArrowRight, AvnKeyRight, NSRightArrowFunctionKey },
{ 0x7E, AvnPhysicalKeyArrowUp, AvnKeyUp, NSUpArrowFunctionKey },
// Numpad Section
{ 0x47, AvnPhysicalKeyNumLock, AvnKeyClear, kClearCharCode },
{ 0x52, AvnPhysicalKeyNumPad0, AvnKeyNumPad0, '0' },
{ 0x53, AvnPhysicalKeyNumPad1, AvnKeyNumPad1, '1' },
{ 0x54, AvnPhysicalKeyNumPad2, AvnKeyNumPad2, '2' },
{ 0x55, AvnPhysicalKeyNumPad3, AvnKeyNumPad3, '3' },
{ 0x56, AvnPhysicalKeyNumPad4, AvnKeyNumPad4, '4' },
{ 0x57, AvnPhysicalKeyNumPad5, AvnKeyNumPad5, '5' },
{ 0x58, AvnPhysicalKeyNumPad6, AvnKeyNumPad6, '6' },
{ 0x59, AvnPhysicalKeyNumPad7, AvnKeyNumPad7, '7' },
{ 0x5B, AvnPhysicalKeyNumPad8, AvnKeyNumPad8, '8' },
{ 0x5C, AvnPhysicalKeyNumPad9, AvnKeyNumPad9, '9' },
{ 0x45, AvnPhysicalKeyNumPadAdd, AvnKeyAdd, '+' },
//{ , AvnPhysicalKeyNumPadClear, 0 },
{ 0x5F, AvnPhysicalKeyNumPadComma, AvnKeyAbntC2, 0 },
{ 0x41, AvnPhysicalKeyNumPadDecimal, AvnKeyDecimal, '.' },
{ 0x4B, AvnPhysicalKeyNumPadDivide, AvnKeyDivide, '/' },
{ 0x4C, AvnPhysicalKeyNumPadEnter, AvnKeyEnter, kReturnCharCode },
{ 0x51, AvnPhysicalKeyNumPadEqual, AvnKeyOemPlus, '=' },
{ 0x43, AvnPhysicalKeyNumPadMultiply, AvnKeyMultiply, '*' },
//{ , AvnPhysicalKeyNumPadParenLeft, 0 },
//{ , AvnPhysicalKeyNumPadParenRight, 0 },
{ 0x4E, AvnPhysicalKeyNumPadSubtract, AvnKeySubtract, '-' },
// Function Section
{ 0x35, AvnPhysicalKeyEscape, AvnKeyEscape, kEscapeCharCode },
{ 0x7A, AvnPhysicalKeyF1, AvnKeyF1, NSF1FunctionKey },
{ 0x78, AvnPhysicalKeyF2, AvnKeyF2, NSF2FunctionKey },
{ 0x63, AvnPhysicalKeyF3, AvnKeyF3, NSF3FunctionKey },
{ 0x76, AvnPhysicalKeyF4, AvnKeyF4, NSF4FunctionKey },
{ 0x60, AvnPhysicalKeyF5, AvnKeyF5, NSF5FunctionKey },
{ 0x61, AvnPhysicalKeyF6, AvnKeyF6, NSF6FunctionKey },
{ 0x62, AvnPhysicalKeyF7, AvnKeyF7, NSF7FunctionKey },
{ 0x64, AvnPhysicalKeyF8, AvnKeyF8, NSF8FunctionKey },
{ 0x65, AvnPhysicalKeyF9, AvnKeyF9, NSF9FunctionKey },
{ 0x6D, AvnPhysicalKeyF10, AvnKeyF10, NSF10FunctionKey },
{ 0x67, AvnPhysicalKeyF11, AvnKeyF11, NSF11FunctionKey },
{ 0x6F, AvnPhysicalKeyF12, AvnKeyF12, NSF12FunctionKey },
{ 0x69, AvnPhysicalKeyF13, AvnKeyF13, NSF13FunctionKey },
{ 0x6B, AvnPhysicalKeyF14, AvnKeyF14, NSF14FunctionKey },
{ 0x71, AvnPhysicalKeyF15, AvnKeyF15, NSF15FunctionKey },
{ 0x6A, AvnPhysicalKeyF16, AvnKeyF16, NSF16FunctionKey },
{ 0x40, AvnPhysicalKeyF17, AvnKeyF17, NSF17FunctionKey },
{ 0x4F, AvnPhysicalKeyF18, AvnKeyF18, NSF18FunctionKey },
{ 0x50, AvnPhysicalKeyF19, AvnKeyF19, NSF19FunctionKey },
{ 0x5A, AvnPhysicalKeyF20, AvnKeyF20, NSF20FunctionKey },
//{ , AvnPhysicalKeyF21, 0 },
//{ , AvnPhysicalKeyF22, 0 },
//{ , AvnPhysicalKeyF23, 0 },
//{ , AvnPhysicalKeyF24, 0 },
//{ , AvnPhysicalKeyPrintScreen, 0 },
//{ , AvnPhysicalKeyScrollLock, 0 },
//{ , AvnPhysicalKeyPause, 0 },
// Media Keys
//{ , AvnPhysicalKeyBrowserBack, 0 },
//{ , AvnPhysicalKeyBrowserFavorites, 0 },
//{ , AvnPhysicalKeyBrowserForward, 0 },
//{ , AvnPhysicalKeyBrowserHome, 0 },
//{ , AvnPhysicalKeyBrowserRefresh, 0 },
//{ , AvnPhysicalKeyBrowserSearch, 0 },
//{ , AvnPhysicalKeyBrowserStop, 0 },
//{ , AvnPhysicalKeyEject, 0 },
//{ , AvnPhysicalKeyLaunchApp1, 0 },
//{ , AvnPhysicalKeyLaunchApp2, 0 },
//{ , AvnPhysicalKeyLaunchMail, 0 },
//{ , AvnPhysicalKeyMediaPlayPause, 0 },
//{ , AvnPhysicalKeyMediaSelect, 0 },
//{ , AvnPhysicalKeyMediaStop, 0 },
//{ , AvnPhysicalKeyMediaTrackNext, 0 },
//{ , AvnPhysicalKeyMediaTrackPrevious, 0 },
//{ , AvnPhysicalKeyPower, 0 },
//{ , AvnPhysicalKeySleep, 0 },
{ 0x49, AvnPhysicalKeyAudioVolumeDown, AvnKeyVolumeDown, 0 },
{ 0x4A, AvnPhysicalKeyAudioVolumeMute, AvnKeyVolumeMute, 0 },
{ 0x48, AvnPhysicalKeyAudioVolumeUp, AvnKeyVolumeUp, 0 },
//{ , AvnPhysicalKeyWakeUp, 0 },
// Legacy Keys
//{ , AvnPhysicalKeyAgain, 0 },
//{ , AvnPhysicalKeyCopy, 0 },
//{ , AvnPhysicalKeyCut, 0 },
//{ , AvnPhysicalKeyFind, 0 },
//{ , AvnPhysicalKeyOpen, 0 },
//{ , AvnPhysicalKeyPaste, 0 },
//{ , AvnPhysicalKeyProps, 0 },
//{ , AvnPhysicalKeySelect, 0 },
//{ , AvnPhysicalKeyUndo, 0 }
}; };
// converts from ansi virtualkeys to AvnKeys. std::unordered_map<uint16_t, AvnKey> virtualKeyFromChar =
std::map<int, AvnKey> s_KeyMap = {
{ // Alphabetic keys
{kVK_ANSI_A, A}, { 'A', AvnKeyA },
{kVK_ANSI_S, S}, { 'B', AvnKeyB },
{kVK_ANSI_D, D}, { 'C', AvnKeyC },
{kVK_ANSI_F, F}, { 'D', AvnKeyD },
{kVK_ANSI_H, H}, { 'E', AvnKeyE },
{kVK_ANSI_G, G}, { 'F', AvnKeyF },
{kVK_ANSI_Z, Z}, { 'G', AvnKeyG },
{kVK_ANSI_X, X}, { 'H', AvnKeyH },
{kVK_ANSI_C, C}, { 'I', AvnKeyI },
{kVK_ANSI_V, V}, { 'J', AvnKeyJ },
{kVK_ANSI_B, B}, { 'K', AvnKeyK },
{kVK_ANSI_Q, Q}, { 'L', AvnKeyL },
{kVK_ANSI_W, W}, { 'M', AvnKeyM },
{kVK_ANSI_E, E}, { 'N', AvnKeyN },
{kVK_ANSI_R, R}, { 'O', AvnKeyO },
{kVK_ANSI_Y, Y}, { 'P', AvnKeyP },
{kVK_ANSI_T, T}, { 'Q', AvnKeyQ },
{kVK_ANSI_1, D1}, { 'R', AvnKeyR },
{kVK_ANSI_2, D2}, { 'S', AvnKeyS },
{kVK_ANSI_3, D3}, { 'T', AvnKeyT },
{kVK_ANSI_4, D4}, { 'U', AvnKeyU },
{kVK_ANSI_6, D6}, { 'V', AvnKeyV },
{kVK_ANSI_5, D5}, { 'W', AvnKeyW },
{kVK_ANSI_Equal, OemPlus}, { 'X', AvnKeyX },
{kVK_ANSI_9, D9}, { 'Y', AvnKeyY },
{kVK_ANSI_7, D7}, { 'Z', AvnKeyZ },
{kVK_ANSI_Minus, OemMinus}, { 'a', AvnKeyA },
{kVK_ANSI_8, D8}, { 'b', AvnKeyB },
{kVK_ANSI_0, D0}, { 'c', AvnKeyC },
{kVK_ANSI_RightBracket, OemCloseBrackets}, { 'd', AvnKeyD },
{kVK_ANSI_O, O}, { 'e', AvnKeyE },
{kVK_ANSI_U, U}, { 'f', AvnKeyF },
{kVK_ANSI_LeftBracket, OemOpenBrackets}, { 'g', AvnKeyG },
{kVK_ANSI_I, I}, { 'h', AvnKeyH },
{kVK_ANSI_P, P}, { 'i', AvnKeyI },
{kVK_ANSI_L, L}, { 'j', AvnKeyJ },
{kVK_ANSI_J, J}, { 'k', AvnKeyK },
{kVK_ANSI_Quote, OemQuotes}, { 'l', AvnKeyL },
{kVK_ANSI_K, AvnKeyK}, { 'm', AvnKeyM },
{kVK_ANSI_Semicolon, OemSemicolon}, { 'n', AvnKeyN },
{kVK_ANSI_Backslash, OemBackslash}, { 'o', AvnKeyO },
{kVK_ANSI_Comma, OemComma}, { 'p', AvnKeyP },
{kVK_ANSI_Slash, Oem2}, { 'q', AvnKeyQ },
{kVK_ANSI_N, N}, { 'r', AvnKeyR },
{kVK_ANSI_M, M}, { 's', AvnKeyS },
{kVK_ANSI_Period, OemPeriod}, { 't', AvnKeyT },
{kVK_ANSI_Grave, OemTilde}, { 'u', AvnKeyU },
{kVK_ANSI_KeypadDecimal, Decimal}, { 'v', AvnKeyV },
{kVK_ANSI_KeypadMultiply, Multiply}, { 'w', AvnKeyW },
{kVK_ANSI_KeypadPlus, OemPlus}, { 'x', AvnKeyX },
{kVK_ANSI_KeypadClear, AvnKeyClear}, { 'y', AvnKeyY },
{kVK_ANSI_KeypadDivide, Divide}, { 'z', AvnKeyZ },
{kVK_ANSI_KeypadEnter, AvnKeyEnter},
{kVK_ANSI_KeypadMinus, OemMinus}, // Punctuation: US specific mappings (same as Chromium)
{kVK_ANSI_KeypadEquals, OemPlus}, { ';', AvnKeyOem1 },
{kVK_ANSI_Keypad0, NumPad0}, { ':', AvnKeyOem1 },
{kVK_ANSI_Keypad1, NumPad1}, { '=', AvnKeyOemPlus },
{kVK_ANSI_Keypad2, NumPad2}, { '+', AvnKeyOemPlus },
{kVK_ANSI_Keypad3, NumPad3}, { ',', AvnKeyOemComma },
{kVK_ANSI_Keypad4, NumPad4}, { '<', AvnKeyOemComma },
{kVK_ANSI_Keypad5, NumPad5}, { '-', AvnKeyOemMinus },
{kVK_ANSI_Keypad6, NumPad6}, { '_', AvnKeyOemMinus },
{kVK_ANSI_Keypad7, NumPad7}, { '.', AvnKeyOemPeriod },
{kVK_ANSI_Keypad8, NumPad8}, { '>', AvnKeyOemPeriod },
{kVK_ANSI_Keypad9, NumPad9}, { '/', AvnKeyOem2 },
{kVK_Return, AvnKeyReturn}, { '?', AvnKeyOem2 },
{kVK_Tab, AvnKeyTab}, { '`', AvnKeyOem3 },
{kVK_Space, Space}, { '~', AvnKeyOem3 },
{kVK_Delete, AvnKeyBack}, { '[', AvnKeyOem4 },
{kVK_Escape, Escape}, { '{', AvnKeyOem4 },
{kVK_Command, LWin}, { '\\', AvnKeyOem5 },
{kVK_Shift, LeftShift}, { '|', AvnKeyOem5 },
{kVK_CapsLock, AvnKeyCapsLock}, { ']', AvnKeyOem6 },
{kVK_Option, LeftAlt}, { '}', AvnKeyOem6 },
{kVK_Control, LeftCtrl}, { '\'', AvnKeyOem7 },
{kVK_RightCommand, RWin}, { '"', AvnKeyOem7 },
{kVK_RightShift, RightShift},
{kVK_RightOption, RightAlt}, // Apple function keys
{kVK_RightControl, RightCtrl}, // https://developer.apple.com/documentation/appkit/1535851-function-key_unicode_values
//{kVK_Function, ?}, { NSDeleteFunctionKey, AvnKeyDelete },
{kVK_F17, F17}, { NSUpArrowFunctionKey, AvnKeyUp },
{kVK_VolumeUp, VolumeUp}, { NSLeftArrowFunctionKey, AvnKeyLeft },
{kVK_VolumeDown, VolumeDown}, { NSRightArrowFunctionKey, AvnKeyRight },
{kVK_Mute, VolumeMute}, { NSPageUpFunctionKey, AvnKeyPageUp },
{kVK_F18, F18}, { NSPageDownFunctionKey, AvnKeyPageDown },
{kVK_F19, F19}, { NSHomeFunctionKey, AvnKeyHome },
{kVK_F20, F20}, { NSEndFunctionKey, AvnKeyEnd },
{kVK_F5, F5}, { NSClearLineFunctionKey, AvnKeyClear },
{kVK_F6, F6}, { NSExecuteFunctionKey, AvnKeyExecute },
{kVK_F7, F7}, { NSHelpFunctionKey, AvnKeyHelp },
{kVK_F3, F3}, { NSInsertFunctionKey, AvnKeyInsert },
{kVK_F8, F8}, { NSMenuFunctionKey, AvnKeyApps },
{kVK_F9, F9}, { NSPauseFunctionKey, AvnKeyPause },
{kVK_F11, F11}, { NSPrintFunctionKey, AvnKeyPrint },
{kVK_F13, F13}, { NSPrintScreenFunctionKey, AvnKeyPrintScreen },
{kVK_F16, F16}, { NSScrollLockFunctionKey, AvnKeyScroll },
{kVK_F14, F14}, { NSF1FunctionKey, AvnKeyF1 },
{kVK_F10, F10}, { NSF2FunctionKey, AvnKeyF2 },
{kVK_F12, F12}, { NSF3FunctionKey, AvnKeyF3 },
{kVK_F15, F15}, { NSF4FunctionKey, AvnKeyF4 },
{kVK_Help, Help}, { NSF5FunctionKey, AvnKeyF5 },
{kVK_Home, Home}, { NSF6FunctionKey, AvnKeyF6 },
{kVK_PageUp, PageUp}, { NSF7FunctionKey, AvnKeyF7 },
{kVK_ForwardDelete, Delete}, { NSF8FunctionKey, AvnKeyF8 },
{kVK_F4, F4}, { NSF9FunctionKey, AvnKeyF9 },
{kVK_End, End}, { NSF10FunctionKey, AvnKeyF10 },
{kVK_F2, F2}, { NSF11FunctionKey, AvnKeyF11 },
{kVK_PageDown, PageDown}, { NSF12FunctionKey, AvnKeyF12 },
{kVK_F1, F1}, { NSF13FunctionKey, AvnKeyF13 },
{kVK_LeftArrow, Left}, { NSF14FunctionKey, AvnKeyF14 },
{kVK_RightArrow, Right}, { NSF15FunctionKey, AvnKeyF15 },
{kVK_DownArrow, Down}, { NSF16FunctionKey, AvnKeyF16 },
{kVK_UpArrow, Up}, { NSF17FunctionKey, AvnKeyF17 },
{kVK_JIS_Kana, AvnKeyKanaMode}, { NSF18FunctionKey, AvnKeyF18 },
{ NSF19FunctionKey, AvnKeyF19 },
{ NSF20FunctionKey, AvnKeyF20 },
{ NSF21FunctionKey, AvnKeyF21 },
{ NSF22FunctionKey, AvnKeyF22 },
{ NSF23FunctionKey, AvnKeyF23 },
{ NSF24FunctionKey, AvnKeyF24 }
}; };
static std::map<AvnKey, int> BuildAvnKeyMap () typedef std::array<AvnPhysicalKey, 0x7F> PhysicalKeyArray;
static PhysicalKeyArray BuildPhysicalKeyFromScanCode()
{ {
std::map<AvnKey, int> result; PhysicalKeyArray result {};
for( auto it = s_KeyMap.begin(); it != s_KeyMap.end(); ++it ) for (auto& keyInfo : keyInfos)
{ {
int key = it->first; result[keyInfo.scanCode] = keyInfo.physicalKey;
AvnKey value = it->second;
result[value] = key;
} }
return result;
}
PhysicalKeyArray physicalKeyFromScanCode = BuildPhysicalKeyFromScanCode();
static std::unordered_map<AvnPhysicalKey, AvnKey, std::hash<int>> BuildQwertyVirtualKeyFromPhysicalKey()
{
std::unordered_map<AvnPhysicalKey, AvnKey, std::hash<int>> result;
result.reserve(sizeof(keyInfos) / sizeof(keyInfos[0]));
for (auto& keyInfo : keyInfos)
{
result[keyInfo.physicalKey] = keyInfo.qwertyKey;
}
return result;
}
std::unordered_map<AvnPhysicalKey, AvnKey, std::hash<int>> qwertyVirtualKeyFromPhysicalKey = BuildQwertyVirtualKeyFromPhysicalKey();
static std::unordered_map<AvnKey, uint16_t, std::hash<int>> BuildMenuCharFromVirtualKey()
{
std::unordered_map<AvnKey, uint16_t, std::hash<int>> result;
result.reserve(100);
for (auto& keyInfo : keyInfos)
{
if (keyInfo.menuChar != 0)
result[keyInfo.qwertyKey] = keyInfo.menuChar;
}
return result; return result;
} }
// Converts AvnKeys to Ansi VirtualKeys std::unordered_map<AvnKey, uint16_t, std::hash<int>> menuCharFromVirtualKey = BuildMenuCharFromVirtualKey();
std::map<AvnKey, int> s_AvnKeyMap = BuildAvnKeyMap();
static bool IsNumpadOrNumericKey(AvnPhysicalKey physicalKey)
{
return (physicalKey >= AvnPhysicalKeyDigit0 && physicalKey <= AvnPhysicalKeyDigit9)
|| (physicalKey >= AvnPhysicalKeyNumLock && physicalKey <= AvnPhysicalKeyNumPadSubtract);
}
AvnPhysicalKey PhysicalKeyFromScanCode(uint16_t scanCode)
{
return scanCode < physicalKeyFromScanCode.size() ? physicalKeyFromScanCode[scanCode] : AvnPhysicalKeyNone;
}
static bool IsAllowedAsciiChar(UniChar c)
{
if (c < 0x20)
{
switch (c)
{
case kBackspaceCharCode:
case kReturnCharCode:
case kTabCharCode:
case kEscapeCharCode:
return true;
default:
return false;
}
}
if (c == kDeleteCharCode)
return false;
return true;
}
static UniCharCount CharsFromScanCode(UInt16 scanCode, NSEventModifierFlags modifierFlags, UInt16 keyAction, UniChar* buffer, UniCharCount bufferSize)
{
auto currentKeyboard = TISCopyCurrentKeyboardInputSource();
if (!currentKeyboard)
return 0;
auto layoutData = static_cast<CFDataRef>(TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData));
if (!layoutData)
return 0;
auto* keyboardLayout = reinterpret_cast<const UCKeyboardLayout*>(CFDataGetBytePtr(layoutData));
UInt32 deadKeyState = 0;
UniCharCount length = 0;
int glyphModifiers = 0;
if (modifierFlags & NSEventModifierFlagShift)
glyphModifiers |= shiftKey;
if (modifierFlags & NSEventModifierFlagCapsLock)
glyphModifiers |= alphaLock;
if (modifierFlags & NSEventModifierFlagOption)
glyphModifiers |= optionKey;
auto result = UCKeyTranslate(
keyboardLayout,
scanCode,
keyAction,
(glyphModifiers >> 8) & 0xFF,
LMGetKbdType(),
kUCKeyTranslateNoDeadKeysBit,
&deadKeyState,
bufferSize,
&length,
buffer);
if (result != noErr)
return 0;
if (deadKeyState)
{
// translate a space with dead key state to get the dead key itself
result = UCKeyTranslate(
keyboardLayout,
kVK_Space,
keyAction,
0,
LMGetKbdType(),
kUCKeyTranslateNoDeadKeysBit,
&deadKeyState,
bufferSize,
&length,
buffer);
if (result != noErr)
return 0;
}
if (length == 1 && buffer[0] <= 0x7F && !IsAllowedAsciiChar(buffer[0]))
return 0;
return length;
}
AvnKey VirtualKeyFromScanCode(uint16_t scanCode, NSEventModifierFlags modifierFlags)
{
auto physicalKey = PhysicalKeyFromScanCode(scanCode);
if (!IsNumpadOrNumericKey(physicalKey))
{
const UniCharCount charCount = 4;
UniChar chars[charCount];
auto length = CharsFromScanCode(scanCode, modifierFlags, kUCKeyActionDown, chars, charCount);
if (length > 0)
{
auto it = virtualKeyFromChar.find(chars[0]);
if (it != virtualKeyFromChar.end())
return it->second;
}
}
auto it = qwertyVirtualKeyFromPhysicalKey.find(physicalKey);
return it == qwertyVirtualKeyFromPhysicalKey.end() ? AvnKeyNone : it->second;
}
NSString* KeySymbolFromScanCode(uint16_t scanCode, NSEventModifierFlags modifierFlags)
{
auto physicalKey = PhysicalKeyFromScanCode(scanCode);
const UniCharCount charCount = 4;
UniChar chars[charCount];
auto length = CharsFromScanCode(scanCode, modifierFlags, kUCKeyActionDisplay, chars, charCount);
if (length > 0)
return [NSString stringWithCharacters:chars length:length];
auto it = qwertyVirtualKeyFromPhysicalKey.find(physicalKey);
if (it == qwertyVirtualKeyFromPhysicalKey.end())
return nullptr;
auto menuChar = MenuCharFromVirtualKey(it->second);
return menuChar == 0 || menuChar > 0x7E ? nullptr : [NSString stringWithCharacters:&menuChar length:1];
}
uint16_t MenuCharFromVirtualKey(AvnKey key)
{
auto it = menuCharFromVirtualKey.find(key);
return it == menuCharFromVirtualKey.end() ? 0 : it->second;
}

23
native/Avalonia.Native/src/OSX/menu.mm

@ -147,34 +147,17 @@ HRESULT AvnAppMenuItem::SetGesture (AvnKey key, AvnInputModifiers modifiers)
if (modifiers & Windows) if (modifiers & Windows)
flags |= NSEventModifierFlagCommand; flags |= NSEventModifierFlagCommand;
auto it = s_UnicodeKeyMap.find(key); auto menuChar = MenuCharFromVirtualKey(key);
if(it != s_UnicodeKeyMap.end()) if (menuChar != 0)
{ {
auto keyString= [NSString stringWithFormat:@"%C", (unsigned short)it->second]; auto keyString = [NSString stringWithCharacters:&menuChar length:1];
[_native setKeyEquivalent: keyString]; [_native setKeyEquivalent: keyString];
[_native setKeyEquivalentModifierMask:flags]; [_native setKeyEquivalentModifierMask:flags];
return S_OK; return S_OK;
} }
else
{
auto it = s_AvnKeyMap.find(key); // check if a virtual key is mapped.
if(it != s_AvnKeyMap.end())
{
auto it1 = s_QwertyKeyMap.find(it->second); // convert virtual key to qwerty string.
if(it1 != s_QwertyKeyMap.end())
{
[_native setKeyEquivalent: [NSString stringWithUTF8String: it1->second]];
[_native setKeyEquivalentModifierMask:flags];
return S_OK;
}
}
}
} }
// Nothing matched... clear. // Nothing matched... clear.

5
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidKeyboardEventsHelper.cs

@ -54,7 +54,10 @@ namespace Avalonia.Android.Platform.Specific.Helpers
Convert.ToUInt64(e.EventTime), Convert.ToUInt64(e.EventTime),
_view.InputRoot, _view.InputRoot,
e.Action == KeyEventActions.Down ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, e.Action == KeyEventActions.Down ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
AndroidKeyboardDevice.ConvertKey(e.KeyCode), GetModifierKeys(e)); AndroidKeyboardDevice.ConvertKey(e.KeyCode),
GetModifierKeys(e),
PhysicalKey.None,
e.DisplayLabel == '\0' ? null : new string(e.DisplayLabel, 1));
_view.Input(rawKeyEvent); _view.Input(rawKeyEvent);

76
src/Avalonia.Base/Input/KeyEventArgs.cs

@ -1,12 +1,74 @@
using System;
using Avalonia.Interactivity; using Avalonia.Interactivity;
namespace Avalonia.Input namespace Avalonia.Input;
/// <summary>
/// Provides information specific to a keyboard event.
/// </summary>
public class KeyEventArgs : RoutedEventArgs
{ {
public class KeyEventArgs : RoutedEventArgs /// <summary>
{ /// <para>
public Key Key { get; init; } /// Gets the virtual-key for the associated event.<br/>
/// A given physical key can result in different virtual keys depending on the current keyboard layout.<br/>
/// This is the key that is generally referred to when creating keyboard shortcuts.
/// </para>
/// <para>
/// For example, when pressing the key located at the <c>Z</c> position on standard US English QWERTY keyboard,
/// this property returns:<br/>
/// - <see cref="Input.Key.Z"/> for an English (QWERTY) layout<br/>
/// - <see cref="Input.Key.W"/> for a French (AZERTY) layout<br/>
/// - <see cref="Input.Key.Y"/> for a German (QWERTZ) layout<br/>
/// - <see cref="Input.Key.Z"/> for a Russian (JCUKEN) layout
/// </para>
/// </summary>
/// <remarks>
/// This property should be used for letter-related shortcuts only.<br/>
/// Prefer using <see cref="PhysicalKey"/> if you need to refer to a key given its position on the keyboard (a
/// common usage is moving the player with WASD-like keys in games), or <see cref="KeySymbol"/> if you want to know
/// which character the key will output.<br/>
/// Avoid using this for shortcuts related to punctuation keys, as they differ wildly depending on keyboard layouts.
/// </remarks>
/// <seealso cref="PhysicalKey"/>
/// <seealso cref="KeySymbol"/>
public Key Key { get; init; }
/// <summary>
/// Gets the key modifiers for the associated event.
/// </summary>
public KeyModifiers KeyModifiers { get; init; }
/// <summary>
/// <para>
/// Gets the physical key for the associated event.
/// </para>
/// <para>
/// This value is independent of the current keyboard layout and usually correspond to the key printed on a standard
/// US English QWERTY keyboard.
/// </para>
/// </summary>
/// <remarks>
/// Use this property if you need to refer to a key given its position on the keyboard (a common usage is moving the
/// player with WASD-like keys).
/// </remarks>
/// <seealso cref="Key"/>
/// <seealso cref="KeySymbol"/>
public PhysicalKey PhysicalKey { get; init; }
public KeyModifiers KeyModifiers { get; init; } /// <summary>
} /// <para>
/// Gets the unicode symbol of the key, or null if none is applicable.
/// </para>
/// <para>
/// For example, when pressing the key located at the <c>Z</c> position on standard US English QWERTY keyboard,
/// this property returns:<br/>
/// - <c>z</c> for an English (QWERTY) layout<br/>
/// - <c>w</c> for a French (AZERTY) layout<br/>
/// - <c>y</c> for a German (QWERTZ) layout<br/>
/// - <c>я</c> for a Russian (JCUKEN) layout
/// </para>
/// </summary>
/// <see cref="Key"/>
/// <see cref="PhysicalKey"/>
public string? KeySymbol { get; init; }
} }

26
src/Avalonia.Base/Input/KeySymbolHelper.cs

@ -0,0 +1,26 @@
namespace Avalonia.Input;
internal static class KeySymbolHelper
{
public static bool IsAllowedAsciiKeySymbol(char c)
{
if (c < 0x20)
{
switch (c)
{
case '\b': // backspace
case '\t': // tab
case '\r': // return
case (char)0x1B: // escape
return true;
default:
return false;
}
}
if (c == 0x07) // delete
return false;
return true;
}
}

6
src/Avalonia.Base/Input/KeyboardDevice.cs

@ -189,12 +189,14 @@ namespace Avalonia.Input
? InputElement.KeyDownEvent ? InputElement.KeyDownEvent
: InputElement.KeyUpEvent; : InputElement.KeyUpEvent;
KeyEventArgs ev = new KeyEventArgs var ev = new KeyEventArgs
{ {
RoutedEvent = routedEvent, RoutedEvent = routedEvent,
Key = keyInput.Key, Key = keyInput.Key,
KeyModifiers = keyInput.Modifiers.ToKeyModifiers(), KeyModifiers = keyInput.Modifiers.ToKeyModifiers(),
Source = element, PhysicalKey = keyInput.PhysicalKey,
KeySymbol = keyInput.KeySymbol,
Source = element
}; };
var currentHandler = element as Visual; var currentHandler = element as Visual;

944
src/Avalonia.Base/Input/PhysicalKey.cs

@ -0,0 +1,944 @@
#if AVALONIA_REMOTE_PROTOCOL
namespace Avalonia.Remote.Protocol.Input;
#else
namespace Avalonia.Input;
#endif
/// <summary>
/// Represents a keyboard physical key.<br/>
/// </summary>
/// <remarks>
/// The names follow the W3C codes: https://www.w3.org/TR/uievents-code/
/// </remarks>
public enum PhysicalKey
{
/// <summary>
/// Represents no key.
/// </summary>
None = 0,
// ###################
// Writing System Keys
// ###################
/// <summary>
/// <c>`~</c> on a US keyboard.
/// This is the <c>半角/全角/漢字</c> (hankaku/zenkaku/kanji) key on Japanese keyboards.
/// </summary>
Backquote = 1,
/// <summary>
/// Used for both the US <c>\|</c> (on the 101-key layout) and also for the key located between the <c>"</c> and
/// <c>Enter</c> keys on row C of the 102-, 104- and 106-key layouts.
/// <c>#~</c> on a UK (102) keyboard.
/// </summary>
Backslash = 2,
/// <summary>
/// <c>[{</c> on a US keyboard.
/// </summary>
BracketLeft = 3,
/// <summary>
/// <c>]}</c> on a US keyboard.
/// </summary>
BracketRight = 4,
/// <summary>
/// <c>,&lt;</c> on a US keyboard.
/// </summary>
Comma = 5,
/// <summary>
/// <c>0)</c> on a US keyboard.
/// </summary>
Digit0 = 6,
/// <summary>
/// <c>1!</c> on a US keyboard.
/// </summary>
Digit1 = 7,
/// <summary>
/// <c>2@</c> on a US keyboard.
/// </summary>
Digit2 = 8,
/// <summary>
/// <c>3#</c> on a US keyboard.
/// </summary>
Digit3 = 9,
/// <summary>
/// <c>4$</c> on a US keyboard.
/// </summary>
Digit4 = 10,
/// <summary>
/// <c>5%</c> on a US keyboard.
/// </summary>
Digit5 = 11,
/// <summary>
/// <c>6^</c> on a US keyboard.
/// </summary>
Digit6 = 12,
/// <summary>
/// <c>7&amp;</c> on a US keyboard.
/// </summary>
Digit7 = 13,
/// <summary>
/// <c>8*</c> on a US keyboard.
/// </summary>
Digit8 = 14,
/// <summary>
/// <c>9(</c> on a US keyboard.
/// </summary>
Digit9 = 15,
/// <summary>
/// <c>=+</c> on a US keyboard.
/// </summary>
Equal = 16,
/// <summary>
/// Located between the left <c>Shift</c> and <c>Z</c> keys.
/// <c>\|</c> on a UK keyboard.
/// </summary>
IntlBackslash = 17,
/// <summary>
/// Located between the <c>/</c> and right <c>Shift</c> keys.
/// <c>\ろ</c> (ro) on a Japanese keyboard.
/// </summary>
IntlRo = 18,
/// <summary>
/// Located between the <c>=</c> and <c>Backspace</c> keys.
/// <c>¥</c> (yen) on a Japanese keyboard.
/// <c>\/</c> on a Russian keyboard.
/// </summary>
IntlYen = 19,
/// <summary>
/// <c>a</c> on a US keyboard.
/// <c>q</c> on an AZERTY (e.g., French) keyboard.
/// </summary>
A = 20,
/// <summary>
/// <c>b</c> on a US keyboard.
/// </summary>
B = 21,
/// <summary>
/// <c>c</c> on a US keyboard.
/// </summary>
C = 22,
/// <summary>
/// <c>d</c> on a US keyboard.
/// </summary>
D = 23,
/// <summary>
/// <c>e</c> on a US keyboard.
/// </summary>
E = 24,
/// <summary>
/// <c>f</c> on a US keyboard.
/// </summary>
F = 25,
/// <summary>
/// <c>g</c> on a US keyboard.
/// </summary>
G = 26,
/// <summary>
/// <c>h</c> on a US keyboard.
/// </summary>
H = 27,
/// <summary>
/// <c>i</c> on a US keyboard.
/// </summary>
I = 28,
/// <summary>
/// <c>j</c> on a US keyboard.
/// </summary>
J = 29,
/// <summary>
/// <c>k</c> on a US keyboard.
/// </summary>
K = 30,
/// <summary>
/// <c>l</c> on a US keyboard.
/// </summary>
L = 31,
/// <summary>
/// <c>m</c> on a US keyboard.
/// </summary>
M = 32,
/// <summary>
/// <c>n</c> on a US keyboard.
/// </summary>
N = 33,
/// <summary>
/// <c>o</c> on a US keyboard.
/// </summary>
O = 34,
/// <summary>
/// <c>p</c> on a US keyboard.
/// </summary>
P = 35,
/// <summary>
/// <c>q</c> on a US keyboard.
/// <c>a</c> on an AZERTY (e.g., French) keyboard.
/// </summary>
Q = 36,
/// <summary>
/// <c>r</c> on a US keyboard.
/// </summary>
R = 37,
/// <summary>
/// <c>s</c> on a US keyboard.
/// </summary>
S = 38,
/// <summary>
/// <c>t</c> on a US keyboard.
/// </summary>
T = 39,
/// <summary>
/// <c>u</c> on a US keyboard.
/// </summary>
U = 40,
/// <summary>
/// <c>v</c> on a US keyboard.
/// </summary>
V = 41,
/// <summary>
/// <c>w</c> on a US keyboard.
/// <c>z</c> on an AZERTY (e.g., French) keyboard.
/// </summary>
W = 42,
/// <summary>
/// <c>x</c> on a US keyboard.
/// </summary>
X = 43,
/// <summary>
/// <c>y</c> on a US keyboard.
/// <c>z</c> on a QWERTZ (e.g., German) keyboard.
/// </summary>
Y = 44,
/// <summary>
/// <c>z</c> on a US keyboard.
/// <c>w</c> on an AZERTY (e.g., French) keyboard.
/// <c>y</c> on a QWERTZ (e.g., German) keyboard.
/// </summary>
Z = 45,
/// <summary>
/// <c>-_</c> on a US keyboard.
/// </summary>
Minus = 46,
/// <summary>
/// <c>.&gt;</c> on a US keyboard.
/// </summary>
Period = 47,
/// <summary>
/// <c>'"</c> on a US keyboard.
/// </summary>
Quote = 48,
/// <summary>
/// <c>;:</c> on a US keyboard.
/// </summary>
Semicolon = 49,
/// <summary>
/// <c>/?</c> on a US keyboard.
/// </summary>
Slash = 50,
// ###############
// Functional keys
// ###############
/// <summary>
/// <c>Alt</c>, <c>Option</c> or <c>⌥</c>.
/// </summary>
AltLeft = 51,
/// <summary>
/// <c>Alt</c>, <c>Option</c> or <c>⌥</c>.
/// This is labelled <c>AltGr</c> key on many keyboard layouts.
/// </summary>
AltRight = 52,
/// <summary>
/// <c>Backspace</c> or <c>⌫</c>.
/// Labelled <c>Delete</c> on Apple keyboards.
/// </summary>
Backspace = 53,
/// <summary>
/// <c>CapsLock</c> or <c>⇪</c>.
/// </summary>
CapsLock = 54,
/// <summary>
/// The application context menu key, which is typically found between the right <c>Meta</c> key
/// and the right <c>Control</c> key.
/// </summary>
ContextMenu = 55,
/// <summary>
/// <c>Control</c> or <c>⌃</c>.
/// </summary>
ControlLeft = 56,
/// <summary>
/// <c>Control</c> or <c>⌃</c>.
/// </summary>
ControlRight = 57,
/// <summary>
/// <c>Enter</c> or <c>↵</c>.
/// Labelled <c>Return</c> on Apple keyboards.
/// </summary>
Enter = 58,
/// <summary>
/// The <c>⊞</c> (Windows), <c>⌘</c>, <c>Command</c> or other OS symbol key.
/// </summary>
MetaLeft = 59,
/// <summary>
/// The <c>⊞</c> (Windows), <c>⌘</c>, <c>Command</c> or other OS symbol key.
/// </summary>
MetaRight = 60,
/// <summary>
/// <c>Shift</c> or <c>⇧</c>.
/// </summary>
ShiftLeft = 61,
/// <summary>
/// <c>Shift</c> or <c>⇧</c>.
/// </summary>
ShiftRight = 62,
/// <summary>
/// <c> </c> (space).
/// </summary>
Space = 63,
/// <summary>
/// <c>Tab</c> or <c>⇥</c>.
/// </summary>
Tab = 64,
/// <summary>
/// Japanese: <c>変換</c> (henkan).
/// </summary>
Convert = 65,
/// <summary>
/// Japanese: <c>カタカナ/ひらがな/ローマ字</c> (katakana/hiragana/romaji).
/// </summary>
KanaMode = 66,
/// <summary>
/// Korean: HangulMode <c>한/영</c> (han/yeong).
/// Japanese (Mac keyboard): <c>かな</c> (kana).
/// </summary>
Lang1 = 67,
/// <summary>
/// Korean: Hanja <c>한자</c> (hanja).
/// Japanese (Mac keyboard): <c>英数</c> (eisu).
/// </summary>
Lang2 = 68,
/// <summary>
/// Japanese (word-processing keyboard): Katakana.
/// </summary>
Lang3 = 69,
/// <summary>
/// Japanese (word-processing keyboard): Hiragana.
/// </summary>
Lang4 = 70,
/// <summary>
/// Japanese (word-processing keyboard): Zenkaku/Hankaku.
/// </summary>
Lang5 = 71,
/// <summary>
/// Japanese: <c>無変換</c> (muhenkan).
/// </summary>
NonConvert = 72,
// ###################
// Control Pad Section
// ###################
/// <summary>
/// <c>⌦</c>. The forward delete key.
/// Note that on Apple keyboards, the key labelled <c>Delete</c> on the main part of the keyboard is
/// <see cref="Backspace"/>.
/// </summary>
Delete = 73,
/// <summary>
/// <c>End</c> or <c>↘</c>.
/// </summary>
End = 74,
/// <summary>
/// <c>Help</c>.
/// Not present on standard PC keyboards.
/// </summary>
Help = 75,
/// <summary>
/// <c>Home</c> or <c>↖</c>.
/// </summary>
Home = 76,
/// <summary>
/// <c>Insert</c> or <c>Ins</c>.
/// Not present on Apple keyboards.
/// </summary>
Insert = 77,
/// <summary>
/// <c>Page Down</c>, <c>PgDn</c> or <c>⇟</c>.
/// </summary>
PageDown = 78,
/// <summary>
/// <c>Page Up</c>, <c>PgUp</c> or <c>⇞</c>.
/// </summary>
PageUp = 79,
// #################
// Arrow Pad Section
// #################
/// <summary>
/// <c>↓</c>.
/// </summary>
ArrowDown = 80,
/// <summary>
/// <c>←</c>.
/// </summary>
ArrowLeft = 81,
/// <summary>
/// <c>→</c>.
/// </summary>
ArrowRight = 82,
/// <summary>
/// <c>↑</c>.
/// </summary>
ArrowUp = 83,
// ######################
// Numeric Keypad Section
// ######################
/// <summary>
/// Numeric keypad <c>Num Lock</c>.
/// On the Mac, this is used for the numpad <c>Clear</c> key.
/// </summary>
NumLock = 84,
/// <summary>
/// Numeric keypad <c>0 Ins</c> on a keyboard.
/// <c>0</c> on a phone or remote control.
/// </summary>
NumPad0 = 85,
/// <summary>
/// Numeric keypad <c>1 End</c> on a keyboard.
/// <c>1</c> or <c>1 QZ</c> on a phone or remote control.
/// </summary>
NumPad1 = 86,
/// <summary>
/// Numeric keypad <c>2 ↓</c> on a keyboard.
/// <c>2 ABC</c> on a phone or remote control.
/// </summary>
NumPad2 = 87,
/// <summary>
/// Numeric keypad <c>3 PgDn</c> on a keyboard.
/// <c>3 DEF</c> on a phone or remote control.
/// </summary>
NumPad3 = 88,
/// <summary>
/// Numeric keypad <c>4 ←</c> on a keyboard.
/// <c>4 GHI</c> on a phone or remote control.
/// </summary>
NumPad4 = 89,
/// <summary>
/// Numeric keypad <c>5</c> on a keyboard.
/// <c>5 JKL</c> on a phone or remote control.
/// </summary>
NumPad5 = 90,
/// <summary>
/// Numeric keypad <c>6 →</c> on a keyboard.
/// <c>6 MNO</c> on a phone or remote control.
/// </summary>
NumPad6 = 91,
/// <summary>
/// Numeric keypad <c>7 Home</c> on a keyboard.
/// <c>7 PQRS</c> or <c>7 PRS</c> on a phone or remote control.
/// </summary>
NumPad7 = 92,
/// <summary>
/// Numeric keypad <c>8 ↑</c> on a keyboard.
/// <c>8 TUV</c> on a phone or remote control.
/// </summary>
NumPad8 = 93,
/// <summary>
/// Numeric keypad <c>9 PgUp</c> on a keyboard.
/// <c>9 WXYZ</c> or <c>9 WXY</c> on a phone or remote control.
/// </summary>
NumPad9 = 94,
/// <summary>
/// Numeric keypad <c>+</c>.
/// </summary>
NumPadAdd = 95,
/// <summary>
/// Numeric keypad <c>C</c> or <c>AC</c> (All Clear).
/// Also for use with numpads that have a <c>Clear</c> key that is separate from the <c>NumLock</c> key.
/// On the Mac, the numpad <c>Clear</c> key is <see cref="NumLock"/>.
/// </summary>
NumPadClear = 96,
/// <summary>
/// Numeric keypad <c>,</c> (thousands separator).
/// For locales where the thousands separator is a "." (e.g., Brazil), this key may generate a <c>.</c>.
/// </summary>
NumPadComma = 97,
/// <summary>
/// Numeric keypad <c>. Del</c>.
/// For locales where the decimal separator is "," (e.g., Brazil), this key may generate a <c>,</c>.
/// </summary>
NumPadDecimal = 98,
/// <summary>
/// Numeric keypad <c>/</c>.
/// </summary>
NumPadDivide = 99,
/// <summary>
/// Numeric keypad <c>Enter</c>.
/// </summary>
NumPadEnter = 100,
/// <summary>
/// Numeric keypad <c>=</c>.
/// </summary>
NumPadEqual = 101,
/// <summary>
/// Numeric keypad <c>*</c> on a keyboard.
/// For use with numpads that provide mathematical operations (<c>+</c>, <c>-</c>, <c>*</c> and <c>/</c>).
/// </summary>
NumPadMultiply = 102,
/// <summary>
/// Numeric keypad <c>(</c>.
/// Found on the Microsoft Natural Keyboard.
/// </summary>
NumPadParenLeft = 103,
/// <summary>
/// Numeric keypad <c>)</c>.
/// Found on the Microsoft Natural Keyboard.
/// </summary>
NumPadParenRight = 104,
/// <summary>
/// Numeric keypad <c>-</c>.
/// </summary>
NumPadSubtract = 105,
// ################
// Function Section
// ################
/// <summary>
/// <c>Esc</c> or <c>⎋</c>.
/// </summary>
Escape = 106,
/// <summary>
/// <c>F1</c>.
/// </summary>
F1 = 107,
/// <summary>
/// <c>F2</c>.
/// </summary>
F2 = 108,
/// <summary>
/// <c>F3</c>.
/// </summary>
F3 = 109,
/// <summary>
/// <c>F4</c>.
/// </summary>
F4 = 110,
/// <summary>
/// <c>F5</c>.
/// </summary>
F5 = 111,
/// <summary>
/// <c>F6</c>.
/// </summary>
F6 = 112,
/// <summary>
/// <c>F7</c>.
/// </summary>
F7 = 113,
/// <summary>
/// <c>F8</c>.
/// </summary>
F8 = 114,
/// <summary>
/// <c>F9</c>.
/// </summary>
F9 = 115,
/// <summary>
/// <c>F10</c>.
/// </summary>
F10 = 116,
/// <summary>
/// <c>F11</c>.
/// </summary>
F11 = 117,
/// <summary>
/// <c>F12</c>.
/// </summary>
F12 = 118,
/// <summary>
/// <c>F13</c>.
/// </summary>
F13 = 119,
/// <summary>
/// <c>F14</c>.
/// </summary>
F14 = 120,
/// <summary>
/// <c>F15</c>.
/// </summary>
F15 = 121,
/// <summary>
/// <c>F16</c>.
/// </summary>
F16 = 122,
/// <summary>
/// <c>F17</c>.
/// </summary>
F17 = 123,
/// <summary>
/// <c>F18</c>.
/// </summary>
F18 = 124,
/// <summary>
/// <c>F19</c>.
/// </summary>
F19 = 125,
/// <summary>
/// <c>F20</c>.
/// </summary>
F20 = 126,
/// <summary>
/// <c>F21</c>.
/// </summary>
F21 = 127,
/// <summary>
/// <c>F22</c>.
/// </summary>
F22 = 128,
/// <summary>
/// <c>F23</c>.
/// </summary>
F23 = 129,
/// <summary>
/// <c>F24</c>.
/// </summary>
F24 = 130,
/// <summary>
/// <c>PrtScr SysRq</c> or <c>Print Screen</c>.
/// </summary>
PrintScreen = 131,
/// <summary>
/// <c>Scroll Lock</c>.
/// </summary>
ScrollLock = 132,
/// <summary>
/// <c>Pause Break</c>.
/// </summary>
Pause = 133,
// ##########
// Media Keys
// ##########
/// <summary>
/// Browser <c>Back</c>.
/// Some laptops place this key to the left of the <c>↑</c> key.
/// </summary>
BrowserBack = 134,
/// <summary>
/// Browser <c>Favorites</c>.
/// </summary>
BrowserFavorites = 135,
/// <summary>
/// Browser <c>Forward</c>.
/// Some laptops place this key to the right of the <c>↑</c> key.
/// </summary>
BrowserForward = 136,
/// <summary>
/// Browser <c>Home</c>.
/// </summary>
BrowserHome = 137,
/// <summary>
/// Browser <c>Refresh</c>.
/// </summary>
BrowserRefresh = 138,
/// <summary>
/// Browser <c>Search</c>.
/// </summary>
BrowserSearch = 139,
/// <summary>
/// Browser <c>Stop</c>.
/// </summary>
BrowserStop = 140,
/// <summary>
/// <c>Eject</c> or <c>⏏</c>.
/// This key is placed in the function section on some Apple keyboards.
/// </summary>
Eject = 141,
/// <summary>
/// <c>App 1</c>.
/// Sometimes labelled <c>My Computer</c> on the keyboard.
/// </summary>
LaunchApp1 = 142,
/// <summary>
/// <c>App 2</c>.
/// Sometimes labelled <c>Calculator</c> on the keyboard.
/// </summary>
LaunchApp2 = 143,
/// <summary>
/// <c>Mail</c>.
/// </summary>
LaunchMail = 144,
/// <summary>
/// Media <c>Play/Pause</c> or <c>⏵⏸</c>.
/// </summary>
MediaPlayPause = 145,
/// <summary>
/// Media <c>Select</c>.
/// </summary>
MediaSelect = 146,
/// <summary>
/// Media <c>Stop</c> or <c>⏹</c>.
/// </summary>
MediaStop = 147,
/// <summary>
/// Media <c>Next</c> or <c>⏭</c>.
/// </summary>
MediaTrackNext = 148,
/// <summary>
/// Media <c>Previous</c> or <c>⏮</c>.
/// </summary>
MediaTrackPrevious = 149,
/// <summary>
/// <c>Power</c>.
/// </summary>
Power = 150,
/// <summary>
/// <c>Sleep</c>.
/// </summary>
Sleep = 151,
/// <summary>
/// <c>Volume Down</c>.
/// </summary>
AudioVolumeDown = 152,
/// <summary>
/// <c>Mute</c>.
/// </summary>
AudioVolumeMute = 153,
/// <summary>
/// <c>Volume Up</c>.
/// </summary>
AudioVolumeUp = 154,
/// <summary>
/// <c>Wake Up</c>.
/// </summary>
WakeUp = 155,
// ###########
// Legacy Keys
// ###########
/// <summary>
/// <c>Again</c>.
/// Legacy.
/// Found on Sun’s USB keyboard.
/// </summary>
Again = 156,
/// <summary>
/// <c>Copy</c>.
/// Legacy.
/// Found on Sun’s USB keyboard.
/// </summary>
Copy = 157,
/// <summary>
/// <c>Cut</c>.
/// Legacy.
/// Found on Sun’s USB keyboard.
/// </summary>
Cut = 158,
/// <summary>
/// <c>Find</c>.
/// Legacy.
/// Found on Sun’s USB keyboard.
/// </summary>
Find = 159,
/// <summary>
/// <c>Open</c>.
/// Legacy.
/// Found on Sun’s USB keyboard.
/// </summary>
Open = 160,
/// <summary>
/// <c>Paste</c>.
/// Legacy.
/// Found on Sun’s USB keyboard.
/// </summary>
Paste = 161,
/// <summary>
/// <c>Props</c>.
/// Legacy.
/// Found on Sun’s USB keyboard.
/// </summary>
Props = 162,
/// <summary>
/// <c>Select</c>.
/// Legacy.
/// Found on Sun’s USB keyboard.
/// </summary>
Select = 163,
/// <summary>
/// <c>Undo</c>.
/// Legacy.
/// Found on Sun’s USB keyboard.
/// </summary>
Undo = 164
}

394
src/Avalonia.Base/Input/PhysicalKeyExtensions.cs

@ -0,0 +1,394 @@
namespace Avalonia.Input;
/// <summary>
/// Contains extension methods related to <see cref="PhysicalKey"/>.
/// </summary>
public static class PhysicalKeyExtensions
{
/// <summary>
/// Maps a physical key to a corresponding key, if possible, on a QWERTY keyboard.
/// </summary>
/// <param name="physicalKey">the physical key to map.</param>
/// <returns>The key corresponding to <paramref name="physicalKey"/>, or <see cref="Key.None"/>.</returns>
public static Key ToQwertyKey(this PhysicalKey physicalKey)
=> physicalKey switch
{
PhysicalKey.None => Key.None,
// Writing System Keys
PhysicalKey.Backquote => Key.Oem3,
PhysicalKey.Backslash => Key.Oem5,
PhysicalKey.BracketLeft => Key.Oem4,
PhysicalKey.BracketRight => Key.Oem6,
PhysicalKey.Comma => Key.OemComma,
PhysicalKey.Digit0 => Key.D0,
PhysicalKey.Digit1 => Key.D1,
PhysicalKey.Digit2 => Key.D2,
PhysicalKey.Digit3 => Key.D3,
PhysicalKey.Digit4 => Key.D4,
PhysicalKey.Digit5 => Key.D5,
PhysicalKey.Digit6 => Key.D6,
PhysicalKey.Digit7 => Key.D7,
PhysicalKey.Digit8 => Key.D8,
PhysicalKey.Digit9 => Key.D9,
PhysicalKey.Equal => Key.OemMinus,
PhysicalKey.IntlBackslash => Key.Oem102,
PhysicalKey.IntlRo => Key.Oem102,
PhysicalKey.IntlYen => Key.Oem5,
PhysicalKey.A => Key.A,
PhysicalKey.B => Key.B,
PhysicalKey.C => Key.C,
PhysicalKey.D => Key.D,
PhysicalKey.E => Key.E,
PhysicalKey.F => Key.F,
PhysicalKey.G => Key.G,
PhysicalKey.H => Key.H,
PhysicalKey.I => Key.I,
PhysicalKey.J => Key.J,
PhysicalKey.K => Key.K,
PhysicalKey.L => Key.L,
PhysicalKey.M => Key.M,
PhysicalKey.N => Key.N,
PhysicalKey.O => Key.O,
PhysicalKey.P => Key.P,
PhysicalKey.Q => Key.Q,
PhysicalKey.R => Key.R,
PhysicalKey.S => Key.S,
PhysicalKey.T => Key.T,
PhysicalKey.U => Key.U,
PhysicalKey.V => Key.V,
PhysicalKey.W => Key.W,
PhysicalKey.X => Key.X,
PhysicalKey.Y => Key.Y,
PhysicalKey.Z => Key.Z,
PhysicalKey.Minus => Key.OemMinus,
PhysicalKey.Period => Key.OemPeriod,
PhysicalKey.Quote => Key.Oem7,
PhysicalKey.Semicolon => Key.Oem1,
PhysicalKey.Slash => Key.Oem2,
// Functional Keys
PhysicalKey.AltLeft => Key.LeftAlt,
PhysicalKey.AltRight => Key.RightAlt,
PhysicalKey.Backspace => Key.Back,
PhysicalKey.CapsLock => Key.CapsLock,
PhysicalKey.ContextMenu => Key.Apps,
PhysicalKey.ControlLeft => Key.LeftCtrl,
PhysicalKey.ControlRight => Key.RightCtrl,
PhysicalKey.Enter => Key.Enter,
PhysicalKey.MetaLeft => Key.LWin,
PhysicalKey.MetaRight => Key.RWin,
PhysicalKey.ShiftLeft => Key.LeftShift,
PhysicalKey.ShiftRight => Key.RightShift,
PhysicalKey.Space => Key.Space,
PhysicalKey.Tab => Key.Tab,
PhysicalKey.Convert => Key.ImeConvert,
PhysicalKey.KanaMode => Key.KanaMode,
PhysicalKey.Lang1 => Key.HangulMode,
PhysicalKey.Lang2 => Key.HanjaMode,
PhysicalKey.Lang3 => Key.DbeKatakana,
PhysicalKey.Lang4 => Key.DbeHiragana,
PhysicalKey.Lang5 => Key.OemAuto,
PhysicalKey.NonConvert => Key.ImeNonConvert,
// Control Pad Section
PhysicalKey.Delete => Key.Delete,
PhysicalKey.End => Key.End,
PhysicalKey.Help => Key.Help,
PhysicalKey.Home => Key.Home,
PhysicalKey.Insert => Key.Insert,
PhysicalKey.PageDown => Key.PageDown,
PhysicalKey.PageUp => Key.PageUp,
// Arrow Pad Section
PhysicalKey.ArrowDown => Key.Down,
PhysicalKey.ArrowLeft => Key.Left,
PhysicalKey.ArrowRight => Key.Right,
PhysicalKey.ArrowUp => Key.Up,
// Numpad Section
PhysicalKey.NumLock => Key.NumLock,
PhysicalKey.NumPad0 => Key.NumPad0,
PhysicalKey.NumPad1 => Key.NumPad1,
PhysicalKey.NumPad2 => Key.NumPad2,
PhysicalKey.NumPad3 => Key.NumPad3,
PhysicalKey.NumPad4 => Key.NumPad4,
PhysicalKey.NumPad5 => Key.NumPad5,
PhysicalKey.NumPad6 => Key.NumPad6,
PhysicalKey.NumPad7 => Key.NumPad7,
PhysicalKey.NumPad8 => Key.NumPad8,
PhysicalKey.NumPad9 => Key.NumPad9,
PhysicalKey.NumPadAdd => Key.Add,
PhysicalKey.NumPadClear => Key.Clear,
PhysicalKey.NumPadComma => Key.AbntC2,
PhysicalKey.NumPadDecimal => Key.Decimal,
PhysicalKey.NumPadDivide => Key.Divide,
PhysicalKey.NumPadEnter => Key.Enter,
PhysicalKey.NumPadEqual => Key.OemPlus,
PhysicalKey.NumPadMultiply => Key.Multiply,
PhysicalKey.NumPadParenLeft => Key.Oem4,
PhysicalKey.NumPadParenRight => Key.Oem6,
PhysicalKey.NumPadSubtract => Key.Subtract,
// Function Section
PhysicalKey.Escape => Key.Escape,
PhysicalKey.F1 => Key.F1,
PhysicalKey.F2 => Key.F2,
PhysicalKey.F3 => Key.F3,
PhysicalKey.F4 => Key.F4,
PhysicalKey.F5 => Key.F5,
PhysicalKey.F6 => Key.F6,
PhysicalKey.F7 => Key.F7,
PhysicalKey.F8 => Key.F8,
PhysicalKey.F9 => Key.F9,
PhysicalKey.F10 => Key.F10,
PhysicalKey.F11 => Key.F11,
PhysicalKey.F12 => Key.F12,
PhysicalKey.F13 => Key.F13,
PhysicalKey.F14 => Key.F14,
PhysicalKey.F15 => Key.F15,
PhysicalKey.F16 => Key.F16,
PhysicalKey.F17 => Key.F17,
PhysicalKey.F18 => Key.F18,
PhysicalKey.F19 => Key.F19,
PhysicalKey.F20 => Key.F20,
PhysicalKey.F21 => Key.F21,
PhysicalKey.F22 => Key.F22,
PhysicalKey.F23 => Key.F23,
PhysicalKey.F24 => Key.F24,
PhysicalKey.PrintScreen => Key.PrintScreen,
PhysicalKey.ScrollLock => Key.Scroll,
PhysicalKey.Pause => Key.Pause,
// Media Keys
PhysicalKey.BrowserBack => Key.BrowserBack,
PhysicalKey.BrowserFavorites => Key.BrowserFavorites,
PhysicalKey.BrowserForward => Key.BrowserForward,
PhysicalKey.BrowserHome => Key.BrowserHome,
PhysicalKey.BrowserRefresh => Key.BrowserRefresh,
PhysicalKey.BrowserSearch => Key.BrowserSearch,
PhysicalKey.BrowserStop => Key.BrowserStop,
PhysicalKey.Eject => Key.None,
PhysicalKey.LaunchApp1 => Key.LaunchApplication1,
PhysicalKey.LaunchApp2 => Key.LaunchApplication2,
PhysicalKey.LaunchMail => Key.LaunchMail,
PhysicalKey.MediaPlayPause => Key.MediaPlayPause,
PhysicalKey.MediaSelect => Key.SelectMedia,
PhysicalKey.MediaStop => Key.MediaStop,
PhysicalKey.MediaTrackNext => Key.MediaNextTrack,
PhysicalKey.MediaTrackPrevious => Key.MediaPreviousTrack,
PhysicalKey.Power => Key.None,
PhysicalKey.Sleep => Key.Sleep,
PhysicalKey.AudioVolumeDown => Key.VolumeDown,
PhysicalKey.AudioVolumeMute => Key.VolumeMute,
PhysicalKey.AudioVolumeUp => Key.VolumeUp,
PhysicalKey.WakeUp => Key.None,
// Legacy Keys
PhysicalKey.Again => Key.None,
PhysicalKey.Copy => Key.OemCopy,
PhysicalKey.Cut => Key.None,
PhysicalKey.Find => Key.None,
PhysicalKey.Open => Key.None,
PhysicalKey.Paste => Key.None,
PhysicalKey.Props => Key.None,
PhysicalKey.Select => Key.Select,
PhysicalKey.Undo => Key.None,
_ => Key.None
};
/// <summary>
/// Maps a physical key to a corresponding key symbol, if possible, on a QWERTY keyboard.
/// </summary>
/// <param name="physicalKey">the physical key to map.</param>
/// <param name="useShiftModifier">Indicates whether the Shift key is considered pressed.</param>
/// <returns>The key corresponding to <paramref name="physicalKey"/>, or <see cref="Key.None"/>.</returns>
public static string? ToQwertyKeySymbol(this PhysicalKey physicalKey, bool useShiftModifier = false)
=> physicalKey switch
{
PhysicalKey.None => null,
// Writing System Keys
PhysicalKey.Backquote => useShiftModifier ? "~" : "`",
PhysicalKey.Backslash => useShiftModifier ? "|" : "\\",
PhysicalKey.BracketLeft => useShiftModifier ? "{" : "[",
PhysicalKey.BracketRight => useShiftModifier ? "}" : "]",
PhysicalKey.Comma => useShiftModifier ? "<" : ",",
PhysicalKey.Digit0 => useShiftModifier ? ")" : "0",
PhysicalKey.Digit1 => useShiftModifier ? "!" : "1",
PhysicalKey.Digit2 => useShiftModifier ? "@" : "2",
PhysicalKey.Digit3 => useShiftModifier ? "#" : "3",
PhysicalKey.Digit4 => useShiftModifier ? "$" : "4",
PhysicalKey.Digit5 => useShiftModifier ? "%" : "5",
PhysicalKey.Digit6 => useShiftModifier ? "^" : "6",
PhysicalKey.Digit7 => useShiftModifier ? "&" : "7",
PhysicalKey.Digit8 => useShiftModifier ? "*" : "8",
PhysicalKey.Digit9 => useShiftModifier ? "(" : "9",
PhysicalKey.Equal => useShiftModifier ? "+" : "=",
PhysicalKey.IntlBackslash => useShiftModifier ? "|" : "\\",
PhysicalKey.IntlRo => null,
PhysicalKey.IntlYen => null,
PhysicalKey.A => useShiftModifier ? "A" : "a",
PhysicalKey.B => useShiftModifier ? "B" : "b",
PhysicalKey.C => useShiftModifier ? "C" : "c",
PhysicalKey.D => useShiftModifier ? "D" : "d",
PhysicalKey.E => useShiftModifier ? "E" : "e",
PhysicalKey.F => useShiftModifier ? "F" : "f",
PhysicalKey.G => useShiftModifier ? "G" : "g",
PhysicalKey.H => useShiftModifier ? "H" : "h",
PhysicalKey.I => useShiftModifier ? "I" : "i",
PhysicalKey.J => useShiftModifier ? "J" : "j",
PhysicalKey.K => useShiftModifier ? "K" : "k",
PhysicalKey.L => useShiftModifier ? "L" : "l",
PhysicalKey.M => useShiftModifier ? "M" : "m",
PhysicalKey.N => useShiftModifier ? "N" : "n",
PhysicalKey.O => useShiftModifier ? "O" : "o",
PhysicalKey.P => useShiftModifier ? "P" : "p",
PhysicalKey.Q => useShiftModifier ? "Q" : "q",
PhysicalKey.R => useShiftModifier ? "R" : "r",
PhysicalKey.S => useShiftModifier ? "S" : "s",
PhysicalKey.T => useShiftModifier ? "T" : "t",
PhysicalKey.U => useShiftModifier ? "U" : "u",
PhysicalKey.V => useShiftModifier ? "V" : "v",
PhysicalKey.W => useShiftModifier ? "W" : "w",
PhysicalKey.X => useShiftModifier ? "X" : "x",
PhysicalKey.Y => useShiftModifier ? "Y" : "y",
PhysicalKey.Z => useShiftModifier ? "Z" : "z",
PhysicalKey.Minus => useShiftModifier ? "_" : "-",
PhysicalKey.Period => useShiftModifier ? ">" : ".",
PhysicalKey.Quote => useShiftModifier ? "\"" : "'",
PhysicalKey.Semicolon => useShiftModifier ? ":" : ";",
PhysicalKey.Slash => useShiftModifier ? "?" : "/",
// Functional Keys
PhysicalKey.AltLeft => null,
PhysicalKey.AltRight => null,
PhysicalKey.Backspace => "\b",
PhysicalKey.CapsLock => null,
PhysicalKey.ContextMenu => null,
PhysicalKey.ControlLeft => null,
PhysicalKey.ControlRight => null,
PhysicalKey.Enter => "\r",
PhysicalKey.MetaLeft => null,
PhysicalKey.MetaRight => null,
PhysicalKey.ShiftLeft => null,
PhysicalKey.ShiftRight => null,
PhysicalKey.Space => " ",
PhysicalKey.Tab => "\t",
PhysicalKey.Convert => null,
PhysicalKey.KanaMode => null,
PhysicalKey.Lang1 => null,
PhysicalKey.Lang2 => null,
PhysicalKey.Lang3 => null,
PhysicalKey.Lang4 => null,
PhysicalKey.Lang5 => null,
PhysicalKey.NonConvert => null,
// Control Pad Section
PhysicalKey.Delete => null,
PhysicalKey.End => null,
PhysicalKey.Help => null,
PhysicalKey.Home => null,
PhysicalKey.Insert => null,
PhysicalKey.PageDown => null,
PhysicalKey.PageUp => null,
// Arrow Pad Section
PhysicalKey.ArrowDown => null,
PhysicalKey.ArrowLeft => null,
PhysicalKey.ArrowRight => null,
PhysicalKey.ArrowUp => null,
// Numpad Section
PhysicalKey.NumLock => null,
PhysicalKey.NumPad0 => "0",
PhysicalKey.NumPad1 => "1",
PhysicalKey.NumPad2 => "2",
PhysicalKey.NumPad3 => "3",
PhysicalKey.NumPad4 => "4",
PhysicalKey.NumPad5 => "5",
PhysicalKey.NumPad6 => "6",
PhysicalKey.NumPad7 => "7",
PhysicalKey.NumPad8 => "8",
PhysicalKey.NumPad9 => "9",
PhysicalKey.NumPadAdd => "+",
PhysicalKey.NumPadClear => null,
PhysicalKey.NumPadComma => ",",
PhysicalKey.NumPadDecimal => ".",
PhysicalKey.NumPadDivide => "/",
PhysicalKey.NumPadEnter => "\r",
PhysicalKey.NumPadEqual => "=",
PhysicalKey.NumPadMultiply => "*",
PhysicalKey.NumPadParenLeft => "(",
PhysicalKey.NumPadParenRight => ")",
PhysicalKey.NumPadSubtract => "-",
// Function Section
PhysicalKey.Escape => "\u001B",
PhysicalKey.F1 => null,
PhysicalKey.F2 => null,
PhysicalKey.F3 => null,
PhysicalKey.F4 => null,
PhysicalKey.F5 => null,
PhysicalKey.F6 => null,
PhysicalKey.F7 => null,
PhysicalKey.F8 => null,
PhysicalKey.F9 => null,
PhysicalKey.F10 => null,
PhysicalKey.F11 => null,
PhysicalKey.F12 => null,
PhysicalKey.F13 => null,
PhysicalKey.F14 => null,
PhysicalKey.F15 => null,
PhysicalKey.F16 => null,
PhysicalKey.F17 => null,
PhysicalKey.F18 => null,
PhysicalKey.F19 => null,
PhysicalKey.F20 => null,
PhysicalKey.F21 => null,
PhysicalKey.F22 => null,
PhysicalKey.F23 => null,
PhysicalKey.F24 => null,
PhysicalKey.PrintScreen => null,
PhysicalKey.ScrollLock => null,
PhysicalKey.Pause => null,
// Media Keys
PhysicalKey.BrowserBack => null,
PhysicalKey.BrowserFavorites => null,
PhysicalKey.BrowserForward => null,
PhysicalKey.BrowserHome => null,
PhysicalKey.BrowserRefresh => null,
PhysicalKey.BrowserSearch => null,
PhysicalKey.BrowserStop => null,
PhysicalKey.Eject => null,
PhysicalKey.LaunchApp1 => null,
PhysicalKey.LaunchApp2 => null,
PhysicalKey.LaunchMail => null,
PhysicalKey.MediaPlayPause => null,
PhysicalKey.MediaSelect => null,
PhysicalKey.MediaStop => null,
PhysicalKey.MediaTrackNext => null,
PhysicalKey.MediaTrackPrevious => null,
PhysicalKey.Power => null,
PhysicalKey.Sleep => null,
PhysicalKey.AudioVolumeDown => null,
PhysicalKey.AudioVolumeMute => null,
PhysicalKey.AudioVolumeUp => null,
PhysicalKey.WakeUp => null,
// Legacy Keys
PhysicalKey.Again => null,
PhysicalKey.Copy => null,
PhysicalKey.Cut => null,
PhysicalKey.Find => null,
PhysicalKey.Open => null,
PhysicalKey.Paste => null,
PhysicalKey.Props => null,
PhysicalKey.Select => null,
PhysicalKey.Undo => null,
_ => null
};
}

29
src/Avalonia.Base/Input/Raw/RawKeyEventArgs.cs

@ -1,3 +1,4 @@
using System;
using Avalonia.Metadata; using Avalonia.Metadata;
namespace Avalonia.Input.Raw namespace Avalonia.Input.Raw
@ -11,23 +12,47 @@ namespace Avalonia.Input.Raw
[PrivateApi] [PrivateApi]
public class RawKeyEventArgs : RawInputEventArgs public class RawKeyEventArgs : RawInputEventArgs
{ {
[Obsolete("Use the overload that takes a physical key and key symbol instead.")]
public RawKeyEventArgs( public RawKeyEventArgs(
IKeyboardDevice device, IKeyboardDevice device,
ulong timestamp, ulong timestamp,
IInputRoot root, IInputRoot root,
RawKeyEventType type, RawKeyEventType type,
Key key, RawInputModifiers modifiers) Key key,
: base(device, timestamp, root) RawInputModifiers modifiers)
: this(device, timestamp, root, type, key, modifiers, PhysicalKey.None, null)
{ {
Key = key; Key = key;
Type = type; Type = type;
Modifiers = modifiers; Modifiers = modifiers;
} }
public RawKeyEventArgs(
IInputDevice device,
ulong timestamp,
IInputRoot root,
RawKeyEventType type,
Key key,
RawInputModifiers modifiers,
PhysicalKey physicalKey,
string? keySymbol)
: base(device, timestamp, root)
{
Key = key;
Modifiers = modifiers;
Type = type;
PhysicalKey = physicalKey;
KeySymbol = keySymbol;
}
public Key Key { get; set; } public Key Key { get; set; }
public RawInputModifiers Modifiers { get; set; } public RawInputModifiers Modifiers { get; set; }
public RawKeyEventType Type { get; set; } public RawKeyEventType Type { get; set; }
public PhysicalKey PhysicalKey { get; set; }
public string? KeySymbol { get; set; }
} }
} }

5
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@ -12,6 +12,7 @@ using Avalonia.Remote.Protocol.Input;
using Avalonia.Remote.Protocol.Viewport; using Avalonia.Remote.Protocol.Viewport;
using Avalonia.Threading; using Avalonia.Threading;
using Key = Avalonia.Input.Key; using Key = Avalonia.Input.Key;
using PhysicalKey = Avalonia.Input.PhysicalKey;
using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat; using ProtocolPixelFormat = Avalonia.Remote.Protocol.Viewport.PixelFormat;
using ProtocolMouseButton = Avalonia.Remote.Protocol.Input.MouseButton; using ProtocolMouseButton = Avalonia.Remote.Protocol.Input.MouseButton;
@ -226,7 +227,9 @@ namespace Avalonia.Controls.Remote.Server
InputRoot!, InputRoot!,
key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, key.IsDown ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp,
(Key)key.Key, (Key)key.Key,
GetAvaloniaRawInputModifiers(key.Modifiers))); GetAvaloniaRawInputModifiers(key.Modifiers),
(PhysicalKey)key.PhysicalKey,
key.KeySymbol));
}, DispatcherPriority.Input); }, DispatcherPriority.Input);
break; break;

4
src/Avalonia.Controls/TopLevel.cs

@ -258,7 +258,9 @@ namespace Avalonia.Controls
var keyEvent = new KeyEventArgs() var keyEvent = new KeyEventArgs()
{ {
KeyModifiers = (KeyModifiers)rawKeyEventArgs.Modifiers, KeyModifiers = (KeyModifiers)rawKeyEventArgs.Modifiers,
Key = rawKeyEventArgs.Key Key = rawKeyEventArgs.Key,
PhysicalKey = rawKeyEventArgs.PhysicalKey,
KeySymbol = rawKeyEventArgs.KeySymbol
}; };
backRequested = keymap.Any( key => key.Matches(keyEvent)); backRequested = keymap.Any( key => key.Matches(keyEvent));

22
src/Avalonia.Native/WindowImplBase.cs

@ -247,9 +247,9 @@ namespace Avalonia.Native
_parent.RawMouseEvent(type, timeStamp, modifiers, point, delta); _parent.RawMouseEvent(type, timeStamp, modifiers, point, delta);
} }
int IAvnWindowBaseEvents.RawKeyEvent(AvnRawKeyEventType type, ulong timeStamp, AvnInputModifiers modifiers, uint key) int IAvnWindowBaseEvents.RawKeyEvent(AvnRawKeyEventType type, ulong timeStamp, AvnInputModifiers modifiers, AvnKey key, AvnPhysicalKey physicalKey, string keySymbol)
{ {
return _parent.RawKeyEvent(type, timeStamp, modifiers, key).AsComBool(); return _parent.RawKeyEvent(type, timeStamp, modifiers, key, physicalKey, keySymbol).AsComBool();
} }
int IAvnWindowBaseEvents.RawTextInputEvent(ulong timeStamp, string text) int IAvnWindowBaseEvents.RawTextInputEvent(ulong timeStamp, string text)
@ -322,14 +322,28 @@ namespace Avalonia.Native
return args.Handled; return args.Handled;
} }
public bool RawKeyEvent(AvnRawKeyEventType type, ulong timeStamp, AvnInputModifiers modifiers, uint key) public bool RawKeyEvent(
AvnRawKeyEventType type,
ulong timeStamp,
AvnInputModifiers modifiers,
AvnKey key,
AvnPhysicalKey physicalKey,
string keySymbol)
{ {
if (_inputRoot is null) if (_inputRoot is null)
return false; return false;
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
var args = new RawKeyEventArgs(_keyboard, timeStamp, _inputRoot, (RawKeyEventType)type, (Key)key, (RawInputModifiers)modifiers); var args = new RawKeyEventArgs(
_keyboard,
timeStamp,
_inputRoot,
(RawKeyEventType)type,
(Key)key,
(RawInputModifiers)modifiers,
(PhysicalKey)physicalKey,
keySymbol);
Input?.Invoke(args); Input?.Invoke(args);

549
src/Avalonia.Native/avn.idl

@ -26,195 +26,369 @@ enum AvnKey
AvnKeyJunjaMode = 10, AvnKeyJunjaMode = 10,
AvnKeyFinalMode = 11, AvnKeyFinalMode = 11,
AvnKeyKanjiMode = 12, AvnKeyKanjiMode = 12,
HanjaMode = 12, AvnKeyHanjaMode = 12,
Escape = 13, AvnKeyEscape = 13,
ImeConvert = 14, AvnKeyImeConvert = 14,
ImeNonConvert = 15, AvnKeyImeNonConvert = 15,
ImeAccept = 16, AvnKeyImeAccept = 16,
ImeModeChange = 17, AvnKeyImeModeChange = 17,
Space = 18, AvnKeySpace = 18,
PageUp = 19, AvnKeyPageUp = 19,
Prior = 19, AvnKeyPrior = 19,
PageDown = 20, AvnKeyPageDown = 20,
Next = 20, AvnKeyNext = 20,
End = 21, AvnKeyEnd = 21,
Home = 22, AvnKeyHome = 22,
Left = 23, AvnKeyLeft = 23,
Up = 24, AvnKeyUp = 24,
Right = 25, AvnKeyRight = 25,
Down = 26, AvnKeyDown = 26,
Select = 27, AvnKeySelect = 27,
Print = 28, AvnKeyPrint = 28,
Execute = 29, AvnKeyExecute = 29,
Snapshot = 30, AvnKeySnapshot = 30,
PrintScreen = 30, AvnKeyPrintScreen = 30,
Insert = 31, AvnKeyInsert = 31,
Delete = 32, AvnKeyDelete = 32,
Help = 33, AvnKeyHelp = 33,
D0 = 34, AvnKeyD0 = 34,
D1 = 35, AvnKeyD1 = 35,
D2 = 36, AvnKeyD2 = 36,
D3 = 37, AvnKeyD3 = 37,
D4 = 38, AvnKeyD4 = 38,
D5 = 39, AvnKeyD5 = 39,
D6 = 40, AvnKeyD6 = 40,
D7 = 41, AvnKeyD7 = 41,
D8 = 42, AvnKeyD8 = 42,
D9 = 43, AvnKeyD9 = 43,
A = 44, AvnKeyA = 44,
B = 45, AvnKeyB = 45,
C = 46, AvnKeyC = 46,
D = 47, AvnKeyD = 47,
E = 48, AvnKeyE = 48,
F = 49, AvnKeyF = 49,
G = 50, AvnKeyG = 50,
H = 51, AvnKeyH = 51,
I = 52, AvnKeyI = 52,
J = 53, AvnKeyJ = 53,
AvnKeyK = 54, AvnKeyK = 54,
L = 55, AvnKeyL = 55,
M = 56, AvnKeyM = 56,
N = 57, AvnKeyN = 57,
O = 58, AvnKeyO = 58,
P = 59, AvnKeyP = 59,
Q = 60, AvnKeyQ = 60,
R = 61, AvnKeyR = 61,
S = 62, AvnKeyS = 62,
T = 63, AvnKeyT = 63,
U = 64, AvnKeyU = 64,
V = 65, AvnKeyV = 65,
W = 66, AvnKeyW = 66,
X = 67, AvnKeyX = 67,
Y = 68, AvnKeyY = 68,
Z = 69, AvnKeyZ = 69,
LWin = 70, AvnKeyLWin = 70,
RWin = 71, AvnKeyRWin = 71,
Apps = 72, AvnKeyApps = 72,
Sleep = 73, AvnKeySleep = 73,
NumPad0 = 74, AvnKeyNumPad0 = 74,
NumPad1 = 75, AvnKeyNumPad1 = 75,
NumPad2 = 76, AvnKeyNumPad2 = 76,
NumPad3 = 77, AvnKeyNumPad3 = 77,
NumPad4 = 78, AvnKeyNumPad4 = 78,
NumPad5 = 79, AvnKeyNumPad5 = 79,
NumPad6 = 80, AvnKeyNumPad6 = 80,
NumPad7 = 81, AvnKeyNumPad7 = 81,
NumPad8 = 82, AvnKeyNumPad8 = 82,
NumPad9 = 83, AvnKeyNumPad9 = 83,
Multiply = 84, AvnKeyMultiply = 84,
Add = 85, AvnKeyAdd = 85,
Separator = 86, AvnKeySeparator = 86,
Subtract = 87, AvnKeySubtract = 87,
Decimal = 88, AvnKeyDecimal = 88,
Divide = 89, AvnKeyDivide = 89,
F1 = 90, AvnKeyF1 = 90,
F2 = 91, AvnKeyF2 = 91,
F3 = 92, AvnKeyF3 = 92,
F4 = 93, AvnKeyF4 = 93,
F5 = 94, AvnKeyF5 = 94,
F6 = 95, AvnKeyF6 = 95,
F7 = 96, AvnKeyF7 = 96,
F8 = 97, AvnKeyF8 = 97,
F9 = 98, AvnKeyF9 = 98,
F10 = 99, AvnKeyF10 = 99,
F11 = 100, AvnKeyF11 = 100,
F12 = 101, AvnKeyF12 = 101,
F13 = 102, AvnKeyF13 = 102,
F14 = 103, AvnKeyF14 = 103,
F15 = 104, AvnKeyF15 = 104,
F16 = 105, AvnKeyF16 = 105,
F17 = 106, AvnKeyF17 = 106,
F18 = 107, AvnKeyF18 = 107,
F19 = 108, AvnKeyF19 = 108,
F20 = 109, AvnKeyF20 = 109,
F21 = 110, AvnKeyF21 = 110,
F22 = 111, AvnKeyF22 = 111,
F23 = 112, AvnKeyF23 = 112,
F24 = 113, AvnKeyF24 = 113,
NumLock = 114, AvnKeyNumLock = 114,
Scroll = 115, AvnKeyScroll = 115,
LeftShift = 116, AvnKeyLeftShift = 116,
RightShift = 117, AvnKeyRightShift = 117,
LeftCtrl = 118, AvnKeyLeftCtrl = 118,
RightCtrl = 119, AvnKeyRightCtrl = 119,
LeftAlt = 120, AvnKeyLeftAlt = 120,
RightAlt = 121, AvnKeyRightAlt = 121,
BrowserBack = 122, AvnKeyBrowserBack = 122,
BrowserForward = 123, AvnKeyBrowserForward = 123,
BrowserRefresh = 124, AvnKeyBrowserRefresh = 124,
BrowserStop = 125, AvnKeyBrowserStop = 125,
BrowserSearch = 126, AvnKeyBrowserSearch = 126,
BrowserFavorites = 127, AvnKeyBrowserFavorites = 127,
BrowserHome = 128, AvnKeyBrowserHome = 128,
VolumeMute = 129, AvnKeyVolumeMute = 129,
VolumeDown = 130, AvnKeyVolumeDown = 130,
VolumeUp = 131, AvnKeyVolumeUp = 131,
MediaNextTrack = 132, AvnKeyMediaNextTrack = 132,
MediaPreviousTrack = 133, AvnKeyMediaPreviousTrack = 133,
MediaStop = 134, AvnKeyMediaStop = 134,
MediaPlayPause = 135, AvnKeyMediaPlayPause = 135,
LaunchMail = 136, AvnKeyLaunchMail = 136,
SelectMedia = 137, AvnKeySelectMedia = 137,
LaunchApplication1 = 138, AvnKeyLaunchApplication1 = 138,
LaunchApplication2 = 139, AvnKeyLaunchApplication2 = 139,
OemSemicolon = 140, AvnKeyOemSemicolon = 140,
Oem1 = 140, AvnKeyOem1 = 140,
OemPlus = 141, AvnKeyOemPlus = 141,
OemComma = 142, AvnKeyOemComma = 142,
OemMinus = 143, AvnKeyOemMinus = 143,
OemPeriod = 144, AvnKeyOemPeriod = 144,
OemQuestion = 145, AvnKeyOemQuestion = 145,
Oem2 = 145, AvnKeyOem2 = 145,
OemTilde = 146, AvnKeyOemTilde = 146,
Oem3 = 146, AvnKeyOem3 = 146,
AbntC1 = 147, AvnKeyAbntC1 = 147,
AbntC2 = 148, AvnKeyAbntC2 = 148,
OemOpenBrackets = 149, AvnKeyOemOpenBrackets = 149,
Oem4 = 149, AvnKeyOem4 = 149,
OemPipe = 150, AvnKeyOemPipe = 150,
Oem5 = 150, AvnKeyOem5 = 150,
OemCloseBrackets = 151, AvnKeyOemCloseBrackets = 151,
Oem6 = 151, AvnKeyOem6 = 151,
OemQuotes = 152, AvnKeyOemQuotes = 152,
Oem7 = 152, AvnKeyOem7 = 152,
Oem8 = 153, AvnKeyOem8 = 153,
OemBackslash = 154, AvnKeyOemBackslash = 154,
Oem102 = 154, AvnKeyOem102 = 154,
ImeProcessed = 155, AvnKeyImeProcessed = 155,
System = 156, AvnKeySystem = 156,
OemAttn = 157, AvnKeyOemAttn = 157,
DbeAlphanumeric = 157, AvnKeyDbeAlphanumeric = 157,
OemFinish = 158, AvnKeyOemFinish = 158,
DbeKatakana = 158, AvnKeyDbeKatakana = 158,
DbeHiragana = 159, AvnKeyDbeHiragana = 159,
OemCopy = 159, AvnKeyOemCopy = 159,
DbeSbcsChar = 160, AvnKeyDbeSbcsChar = 160,
OemAuto = 160, AvnKeyOemAuto = 160,
DbeDbcsChar = 161, AvnKeyDbeDbcsChar = 161,
OemEnlw = 161, AvnKeyOemEnlw = 161,
OemBackTab = 162, AvnKeyOemBackTab = 162,
DbeRoman = 162, AvnKeyDbeRoman = 162,
DbeNoRoman = 163, AvnKeyDbeNoRoman = 163,
Attn = 163, AvnKeyAttn = 163,
CrSel = 164, AvnKeyCrSel = 164,
DbeEnterWordRegisterMode = 164, AvnKeyDbeEnterWordRegisterMode = 164,
ExSel = 165, AvnKeyExSel = 165,
DbeEnterImeConfigureMode = 165, AvnKeyDbeEnterImeConfigureMode = 165,
EraseEof = 166, AvnKeyEraseEof = 166,
DbeFlushString = 166, AvnKeyDbeFlushString = 166,
Play = 167, AvnKeyPlay = 167,
DbeCodeInput = 167, AvnKeyDbeCodeInput = 167,
DbeNoCodeInput = 168, AvnKeyDbeNoCodeInput = 168,
Zoom = 168, AvnKeyZoom = 168,
NoName = 169, AvnKeyNoName = 169,
DbeDetermineString = 169, AvnKeyDbeDetermineString = 169,
DbeEnterDialogConversionMode = 170, AvnKeyDbeEnterDialogConversionMode = 170,
Pa1 = 170, AvnKeyPa1 = 170,
OemClear = 171, AvnKeyOemClear = 171,
DeadCharProcessed = 172, AvnKeyDeadCharProcessed = 172,
AvnKeyFnLeftArrow = 10001,
AvnKeyFnRightArrow = 10002,
AvnKeyFnUpArrow = 10003,
AvnKeyFnDownArrow = 10004,
}; };
enum SystemDecorations { enum AvnPhysicalKey
{
AvnPhysicalKeyNone = 0,
AvnPhysicalKeyBackquote = 1,
AvnPhysicalKeyBackslash = 2,
AvnPhysicalKeyBracketLeft = 3,
AvnPhysicalKeyBracketRight = 4,
AvnPhysicalKeyComma = 5,
AvnPhysicalKeyDigit0 = 6,
AvnPhysicalKeyDigit1 = 7,
AvnPhysicalKeyDigit2 = 8,
AvnPhysicalKeyDigit3 = 9,
AvnPhysicalKeyDigit4 = 10,
AvnPhysicalKeyDigit5 = 11,
AvnPhysicalKeyDigit6 = 12,
AvnPhysicalKeyDigit7 = 13,
AvnPhysicalKeyDigit8 = 14,
AvnPhysicalKeyDigit9 = 15,
AvnPhysicalKeyEqual = 16,
AvnPhysicalKeyIntlBackslash = 17,
AvnPhysicalKeyIntlRo = 18,
AvnPhysicalKeyIntlYen = 19,
AvnPhysicalKeyA = 20,
AvnPhysicalKeyB = 21,
AvnPhysicalKeyC = 22,
AvnPhysicalKeyD = 23,
AvnPhysicalKeyE = 24,
AvnPhysicalKeyF = 25,
AvnPhysicalKeyG = 26,
AvnPhysicalKeyH = 27,
AvnPhysicalKeyI = 28,
AvnPhysicalKeyJ = 29,
AvnPhysicalKeyK = 30,
AvnPhysicalKeyL = 31,
AvnPhysicalKeyM = 32,
AvnPhysicalKeyN = 33,
AvnPhysicalKeyO = 34,
AvnPhysicalKeyP = 35,
AvnPhysicalKeyQ = 36,
AvnPhysicalKeyR = 37,
AvnPhysicalKeyS = 38,
AvnPhysicalKeyT = 39,
AvnPhysicalKeyU = 40,
AvnPhysicalKeyV = 41,
AvnPhysicalKeyW = 42,
AvnPhysicalKeyX = 43,
AvnPhysicalKeyY = 44,
AvnPhysicalKeyZ = 45,
AvnPhysicalKeyMinus = 46,
AvnPhysicalKeyPeriod = 47,
AvnPhysicalKeyQuote = 48,
AvnPhysicalKeySemicolon = 49,
AvnPhysicalKeySlash = 50,
AvnPhysicalKeyAltLeft = 51,
AvnPhysicalKeyAltRight = 52,
AvnPhysicalKeyBackspace = 53,
AvnPhysicalKeyCapsLock = 54,
AvnPhysicalKeyContextMenu = 55,
AvnPhysicalKeyControlLeft = 56,
AvnPhysicalKeyControlRight = 57,
AvnPhysicalKeyEnter = 58,
AvnPhysicalKeyMetaLeft = 59,
AvnPhysicalKeyMetaRight = 60,
AvnPhysicalKeyShiftLeft = 61,
AvnPhysicalKeyShiftRight = 62,
AvnPhysicalKeySpace = 63,
AvnPhysicalKeyTab = 64,
AvnPhysicalKeyConvert = 65,
AvnPhysicalKeyKanaMode = 66,
AvnPhysicalKeyLang1 = 67,
AvnPhysicalKeyLang2 = 68,
AvnPhysicalKeyLang3 = 69,
AvnPhysicalKeyLang4 = 70,
AvnPhysicalKeyLang5 = 71,
AvnPhysicalKeyNonConvert = 72,
AvnPhysicalKeyDelete = 73,
AvnPhysicalKeyEnd = 74,
AvnPhysicalKeyHelp = 75,
AvnPhysicalKeyHome = 76,
AvnPhysicalKeyInsert = 77,
AvnPhysicalKeyPageDown = 78,
AvnPhysicalKeyPageUp = 79,
AvnPhysicalKeyArrowDown = 80,
AvnPhysicalKeyArrowLeft = 81,
AvnPhysicalKeyArrowRight = 82,
AvnPhysicalKeyArrowUp = 83,
AvnPhysicalKeyNumLock = 84,
AvnPhysicalKeyNumPad0 = 85,
AvnPhysicalKeyNumPad1 = 86,
AvnPhysicalKeyNumPad2 = 87,
AvnPhysicalKeyNumPad3 = 88,
AvnPhysicalKeyNumPad4 = 89,
AvnPhysicalKeyNumPad5 = 90,
AvnPhysicalKeyNumPad6 = 91,
AvnPhysicalKeyNumPad7 = 92,
AvnPhysicalKeyNumPad8 = 93,
AvnPhysicalKeyNumPad9 = 94,
AvnPhysicalKeyNumPadAdd = 95,
AvnPhysicalKeyNumPadClear = 96,
AvnPhysicalKeyNumPadComma = 97,
AvnPhysicalKeyNumPadDecimal = 98,
AvnPhysicalKeyNumPadDivide = 99,
AvnPhysicalKeyNumPadEnter = 100,
AvnPhysicalKeyNumPadEqual = 101,
AvnPhysicalKeyNumPadMultiply = 102,
AvnPhysicalKeyNumPadParenLeft = 103,
AvnPhysicalKeyNumPadParenRight = 104,
AvnPhysicalKeyNumPadSubtract = 105,
AvnPhysicalKeyEscape = 106,
AvnPhysicalKeyF1 = 107,
AvnPhysicalKeyF2 = 108,
AvnPhysicalKeyF3 = 109,
AvnPhysicalKeyF4 = 110,
AvnPhysicalKeyF5 = 111,
AvnPhysicalKeyF6 = 112,
AvnPhysicalKeyF7 = 113,
AvnPhysicalKeyF8 = 114,
AvnPhysicalKeyF9 = 115,
AvnPhysicalKeyF10 = 116,
AvnPhysicalKeyF11 = 117,
AvnPhysicalKeyF12 = 118,
AvnPhysicalKeyF13 = 119,
AvnPhysicalKeyF14 = 120,
AvnPhysicalKeyF15 = 121,
AvnPhysicalKeyF16 = 122,
AvnPhysicalKeyF17 = 123,
AvnPhysicalKeyF18 = 124,
AvnPhysicalKeyF19 = 125,
AvnPhysicalKeyF20 = 126,
AvnPhysicalKeyF21 = 127,
AvnPhysicalKeyF22 = 128,
AvnPhysicalKeyF23 = 129,
AvnPhysicalKeyF24 = 130,
AvnPhysicalKeyPrintScreen = 131,
AvnPhysicalKeyScrollLock = 132,
AvnPhysicalKeyPause = 133,
AvnPhysicalKeyBrowserBack = 134,
AvnPhysicalKeyBrowserFavorites = 135,
AvnPhysicalKeyBrowserForward = 136,
AvnPhysicalKeyBrowserHome = 137,
AvnPhysicalKeyBrowserRefresh = 138,
AvnPhysicalKeyBrowserSearch = 139,
AvnPhysicalKeyBrowserStop = 140,
AvnPhysicalKeyEject = 141,
AvnPhysicalKeyLaunchApp1 = 142,
AvnPhysicalKeyLaunchApp2 = 143,
AvnPhysicalKeyLaunchMail = 144,
AvnPhysicalKeyMediaPlayPause = 145,
AvnPhysicalKeyMediaSelect = 146,
AvnPhysicalKeyMediaStop = 147,
AvnPhysicalKeyMediaTrackNext = 148,
AvnPhysicalKeyMediaTrackPrevious = 149,
AvnPhysicalKeyPower = 150,
AvnPhysicalKeySleep = 151,
AvnPhysicalKeyAudioVolumeDown = 152,
AvnPhysicalKeyAudioVolumeMute = 153,
AvnPhysicalKeyAudioVolumeUp = 154,
AvnPhysicalKeyWakeUp = 155,
AvnPhysicalKeyAgain = 156,
AvnPhysicalKeyCopy = 157,
AvnPhysicalKeyCut = 158,
AvnPhysicalKeyFind = 159,
AvnPhysicalKeyOpen = 160,
AvnPhysicalKeyPaste = 161,
AvnPhysicalKeyProps = 162,
AvnPhysicalKeySelect = 163,
AvnPhysicalKeyUndo = 164
}
enum SystemDecorations
{
SystemDecorationsNone = 0, SystemDecorationsNone = 0,
SystemDecorationsBorderOnly = 1, SystemDecorationsBorderOnly = 1,
SystemDecorationsFull = 2, SystemDecorationsFull = 2,
@ -592,7 +766,8 @@ interface IAvnWindowBaseEvents : IUnknown
AvnInputModifiers modifiers, AvnInputModifiers modifiers,
AvnPoint point, AvnPoint point,
AvnVector delta); AvnVector delta);
bool RawKeyEvent(AvnRawKeyEventType type, u_int64_t timeStamp, AvnInputModifiers modifiers, uint key); bool RawKeyEvent(AvnRawKeyEventType type, u_int64_t timeStamp, AvnInputModifiers modifiers,
AvnKey key, AvnPhysicalKey physicalKey, [const] char* keySymbol);
bool RawTextInputEvent(u_int64_t timeStamp, [const] char* text); bool RawTextInputEvent(u_int64_t timeStamp, [const] char* text);
void ScalingChanged(double scaling); void ScalingChanged(double scaling);
void RunRenderPriorityJobs(); void RunRenderPriorityJobs();

2
src/Avalonia.Native/regen.sh

@ -1,2 +0,0 @@
#!/bin/sh
dotnet msbuild /t:Clean,GenerateSharpGenBindings

1
src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj

@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\Avalonia.Base\Input\Key.cs" /> <Compile Include="..\Avalonia.Base\Input\Key.cs" />
<Compile Include="..\Avalonia.Base\Input\PhysicalKey.cs" />
<Compile Include="..\Avalonia.Base\Compatibility\TrimmingAttributes.cs" Visible="False" /> <Compile Include="..\Avalonia.Base\Compatibility\TrimmingAttributes.cs" Visible="False" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />

11
src/Avalonia.Remote.Protocol/InputMessages.cs

@ -1,4 +1,7 @@
using System; #nullable enable
using System;
/* /*
We are keeping copies of core events here, so they can be used We are keeping copies of core events here, so they can be used
without referencing Avalonia itself, e. g. from projects that without referencing Avalonia itself, e. g. from projects that
@ -34,7 +37,7 @@ namespace Avalonia.Remote.Protocol.Input
public abstract class InputEventMessageBase public abstract class InputEventMessageBase
{ {
public InputModifiers[] Modifiers { get; set; } public InputModifiers[]? Modifiers { get; set; }
} }
public abstract class PointerEventMessageBase : InputEventMessageBase public abstract class PointerEventMessageBase : InputEventMessageBase
@ -73,12 +76,14 @@ namespace Avalonia.Remote.Protocol.Input
{ {
public bool IsDown { get; set; } public bool IsDown { get; set; }
public Key Key { get; set; } public Key Key { get; set; }
public PhysicalKey PhysicalKey { get; set; }
public string? KeySymbol { get; set; }
} }
[AvaloniaRemoteMessageGuid("C174102E-7405-4594-916F-B10B8248A17D")] [AvaloniaRemoteMessageGuid("C174102E-7405-4594-916F-B10B8248A17D")]
public class TextInputEventMessage : InputEventMessageBase public class TextInputEventMessage : InputEventMessageBase
{ {
public string Text { get; set; } public string Text { get; set; } = string.Empty;
} }
} }

25
src/Avalonia.X11/X11Info.cs

@ -1,6 +1,4 @@
using System; using System;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using static Avalonia.X11.XLib; using static Avalonia.X11.XLib;
// ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable UnusedAutoPropertyAccessor.Local
@ -30,10 +28,13 @@ namespace Avalonia.X11
public Version XInputVersion { get; } public Version XInputVersion { get; }
public IntPtr LastActivityTimestamp { get; set; } public IntPtr LastActivityTimestamp { get; set; }
public XVisualInfo? TransparentVisualInfo { get; set; } public XVisualInfo? TransparentVisualInfo { get; }
public bool HasXim { get; set; } public bool HasXim { get; }
public bool HasXSync { get; set; } public bool HasXSync { get; }
public IntPtr DefaultFontSet { get; set; }
public IntPtr DefaultFontSet { get; }
public bool HasXkb { get; }
[DllImport("libc")] [DllImport("libc")]
private static extern void setlocale(int type, string s); private static extern void setlocale(int type, string s);
@ -124,6 +125,18 @@ namespace Avalonia.X11
{ {
//Ignore, XSync is not supported //Ignore, XSync is not supported
} }
try
{
var xkbMajor = 1;
var xkbMinor = 0;
HasXkb = XkbLibraryVersion(ref xkbMajor, ref xkbMinor)
&& XkbQueryExtension(display, out _, out _, out _, ref xkbMajor, ref xkbMinor);
}
catch
{
// Ignore, XKB is not supported
}
} }
} }
} }

209
src/Avalonia.X11/X11KeyTransform.cs

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Input; using Avalonia.Input;
@ -6,7 +5,198 @@ namespace Avalonia.X11
{ {
internal static class X11KeyTransform internal static class X11KeyTransform
{ {
private static readonly Dictionary<X11Key, Key> KeyDic = new Dictionary<X11Key, Key> // X scan code to physical key map.
// https://github.com/chromium/chromium/blob/main/ui/events/keycodes/dom/dom_code_data.inc
// This list has the same order as the PhysicalKey enum.
private static readonly Dictionary<byte, PhysicalKey> s_physicalKeyFromScanCode = new(162)
{
// Writing System Keys
{ 0x31, PhysicalKey.Backquote },
{ 0x33, PhysicalKey.Backslash },
{ 0x22, PhysicalKey.BracketLeft },
{ 0x23, PhysicalKey.BracketRight },
{ 0x3B, PhysicalKey.Comma },
{ 0x13, PhysicalKey.Digit0 },
{ 0x0A, PhysicalKey.Digit1 },
{ 0x0B, PhysicalKey.Digit2 },
{ 0x0C, PhysicalKey.Digit3 },
{ 0x0D, PhysicalKey.Digit4 },
{ 0x0E, PhysicalKey.Digit5 },
{ 0x0F, PhysicalKey.Digit6 },
{ 0x10, PhysicalKey.Digit7 },
{ 0x11, PhysicalKey.Digit8 },
{ 0x12, PhysicalKey.Digit9 },
{ 0x15, PhysicalKey.Equal },
{ 0x5E, PhysicalKey.IntlBackslash },
{ 0x61, PhysicalKey.IntlRo },
{ 0x84, PhysicalKey.IntlYen },
{ 0x26, PhysicalKey.A },
{ 0x38, PhysicalKey.B },
{ 0x36, PhysicalKey.C },
{ 0x28, PhysicalKey.D },
{ 0x1A, PhysicalKey.E },
{ 0x29, PhysicalKey.F },
{ 0x2A, PhysicalKey.G },
{ 0x2B, PhysicalKey.H },
{ 0x1F, PhysicalKey.I },
{ 0x2C, PhysicalKey.J },
{ 0x2D, PhysicalKey.K },
{ 0x2E, PhysicalKey.L },
{ 0x3A, PhysicalKey.M },
{ 0x39, PhysicalKey.N },
{ 0x20, PhysicalKey.O },
{ 0x21, PhysicalKey.P },
{ 0x18, PhysicalKey.Q },
{ 0x1B, PhysicalKey.R },
{ 0x27, PhysicalKey.S },
{ 0x1C, PhysicalKey.T },
{ 0x1E, PhysicalKey.U },
{ 0x37, PhysicalKey.V },
{ 0x19, PhysicalKey.W },
{ 0x35, PhysicalKey.X },
{ 0x1D, PhysicalKey.Y },
{ 0x34, PhysicalKey.Z },
{ 0x14, PhysicalKey.Minus },
{ 0x3C, PhysicalKey.Period },
{ 0x30, PhysicalKey.Quote },
{ 0x2F, PhysicalKey.Semicolon },
{ 0x3D, PhysicalKey.Slash },
// Functional Keys
{ 0x40, PhysicalKey.AltLeft },
{ 0x6C, PhysicalKey.AltRight },
{ 0x16, PhysicalKey.Backspace },
{ 0x42, PhysicalKey.CapsLock },
{ 0x87, PhysicalKey.ContextMenu },
{ 0x25, PhysicalKey.ControlLeft },
{ 0x69, PhysicalKey.ControlRight },
{ 0x24, PhysicalKey.Enter },
{ 0x85, PhysicalKey.MetaLeft },
{ 0x86, PhysicalKey.MetaRight },
{ 0x32, PhysicalKey.ShiftLeft },
{ 0x3E, PhysicalKey.ShiftRight },
{ 0x41, PhysicalKey.Space },
{ 0x17, PhysicalKey.Tab },
{ 0x64, PhysicalKey.Convert },
{ 0x65, PhysicalKey.KanaMode },
{ 0x82, PhysicalKey.Lang1 },
{ 0x83, PhysicalKey.Lang2 },
{ 0x62, PhysicalKey.Lang3 },
{ 0x63, PhysicalKey.Lang4 },
{ 0x5D, PhysicalKey.Lang5 },
{ 0x66, PhysicalKey.NonConvert },
// Control Pad Section
{ 0x77, PhysicalKey.Delete },
{ 0x73, PhysicalKey.End },
{ 0x92, PhysicalKey.Help },
{ 0x6E, PhysicalKey.Home },
{ 0x76, PhysicalKey.Insert },
{ 0x75, PhysicalKey.PageDown },
{ 0x70, PhysicalKey.PageUp },
// Arrow Pad Section
{ 0x74, PhysicalKey.ArrowDown },
{ 0x71, PhysicalKey.ArrowLeft },
{ 0x72, PhysicalKey.ArrowRight },
{ 0x6F, PhysicalKey.ArrowUp },
// Numpad Section
{ 0x4D, PhysicalKey.NumLock },
{ 0x5A, PhysicalKey.NumPad0 },
{ 0x57, PhysicalKey.NumPad1 },
{ 0x58, PhysicalKey.NumPad2 },
{ 0x59, PhysicalKey.NumPad3 },
{ 0x53, PhysicalKey.NumPad4 },
{ 0x54, PhysicalKey.NumPad5 },
{ 0x55, PhysicalKey.NumPad6 },
{ 0x4F, PhysicalKey.NumPad7 },
{ 0x50, PhysicalKey.NumPad8 },
{ 0x51, PhysicalKey.NumPad9 },
{ 0x56, PhysicalKey.NumPadAdd },
//{ , PhysicalKey.NumPadClear },
{ 0x81, PhysicalKey.NumPadComma },
{ 0x5B, PhysicalKey.NumPadDecimal },
{ 0x6A, PhysicalKey.NumPadDivide },
{ 0x68, PhysicalKey.NumPadEnter },
{ 0x7D, PhysicalKey.NumPadEqual },
{ 0x3F, PhysicalKey.NumPadMultiply },
{ 0xBB, PhysicalKey.NumPadParenLeft },
{ 0xBC, PhysicalKey.NumPadParenRight },
{ 0x52, PhysicalKey.NumPadSubtract },
// Function Section
{ 0x09, PhysicalKey.Escape },
{ 0x43, PhysicalKey.F1 },
{ 0x44, PhysicalKey.F2 },
{ 0x45, PhysicalKey.F3 },
{ 0x46, PhysicalKey.F4 },
{ 0x47, PhysicalKey.F5 },
{ 0x48, PhysicalKey.F6 },
{ 0x49, PhysicalKey.F7 },
{ 0x4A, PhysicalKey.F8 },
{ 0x4B, PhysicalKey.F9 },
{ 0x4C, PhysicalKey.F10 },
{ 0x5F, PhysicalKey.F11 },
{ 0x60, PhysicalKey.F12 },
{ 0xBF, PhysicalKey.F13 },
{ 0xC0, PhysicalKey.F14 },
{ 0xC1, PhysicalKey.F15 },
{ 0xC2, PhysicalKey.F16 },
{ 0xC3, PhysicalKey.F17 },
{ 0xC4, PhysicalKey.F18 },
{ 0xC5, PhysicalKey.F19 },
{ 0xC6, PhysicalKey.F20 },
{ 0xC7, PhysicalKey.F21 },
{ 0xC8, PhysicalKey.F22 },
{ 0xC9, PhysicalKey.F23 },
{ 0xCA, PhysicalKey.F24 },
{ 0x6B, PhysicalKey.PrintScreen },
{ 0x4E, PhysicalKey.ScrollLock },
{ 0x7F, PhysicalKey.Pause },
// Media Keys
{ 0xA6, PhysicalKey.BrowserBack },
{ 0xA4, PhysicalKey.BrowserFavorites },
{ 0xA7, PhysicalKey.BrowserForward },
{ 0xB4, PhysicalKey.BrowserHome },
{ 0xB5, PhysicalKey.BrowserRefresh },
{ 0xE1, PhysicalKey.BrowserSearch },
{ 0x88, PhysicalKey.BrowserStop },
{ 0xA9, PhysicalKey.Eject },
{ 0x98, PhysicalKey.LaunchApp1 },
{ 0x94, PhysicalKey.LaunchApp2 },
{ 0xA3, PhysicalKey.LaunchMail },
{ 0xAC, PhysicalKey.MediaPlayPause },
{ 0xB3, PhysicalKey.MediaSelect },
{ 0xAE, PhysicalKey.MediaStop },
{ 0xAB, PhysicalKey.MediaTrackNext },
{ 0xAD, PhysicalKey.MediaTrackPrevious },
{ 0x7C, PhysicalKey.Power },
{ 0x96, PhysicalKey.Sleep },
{ 0x7A, PhysicalKey.AudioVolumeDown },
{ 0x79, PhysicalKey.AudioVolumeMute },
{ 0x7B, PhysicalKey.AudioVolumeUp },
{ 0x97, PhysicalKey.WakeUp },
// Legacy Keys
{ 0x89, PhysicalKey.Again },
{ 0x8D, PhysicalKey.Copy },
{ 0x91, PhysicalKey.Cut },
{ 0x90, PhysicalKey.Find },
{ 0x8E, PhysicalKey.Open },
{ 0x8F, PhysicalKey.Paste },
//{ , PhysicalKey.Props },
{ 0x8C, PhysicalKey.Select },
{ 0x8B, PhysicalKey.Undo }
};
public static PhysicalKey PhysicalKeyFromScanCode(int scanCode)
=> scanCode is > 0 and <= 255 && s_physicalKeyFromScanCode.TryGetValue((byte)scanCode, out var result) ?
result :
PhysicalKey.None;
private static readonly Dictionary<X11Key, Key> s_keyFromX11Key = new(180)
{ {
{X11Key.Cancel, Key.Cancel}, {X11Key.Cancel, Key.Cancel},
{X11Key.BackSpace, Key.Back}, {X11Key.BackSpace, Key.Back},
@ -198,26 +388,15 @@ namespace Avalonia.X11
{X11Key.grave, Key.OemTilde}, {X11Key.grave, Key.OemTilde},
{X11Key.asciitilde, Key.OemTilde}, {X11Key.asciitilde, Key.OemTilde},
{X11Key.XK_1, Key.D1}, {X11Key.XK_1, Key.D1},
{X11Key.exclam, Key.D1},
{X11Key.XK_2, Key.D2}, {X11Key.XK_2, Key.D2},
{X11Key.at, Key.D2},
{X11Key.XK_3, Key.D3}, {X11Key.XK_3, Key.D3},
{X11Key.numbersign, Key.D3},
{X11Key.XK_4, Key.D4}, {X11Key.XK_4, Key.D4},
{X11Key.dollar, Key.D4},
{X11Key.XK_5, Key.D5}, {X11Key.XK_5, Key.D5},
{X11Key.percent, Key.D5},
{X11Key.XK_6, Key.D6}, {X11Key.XK_6, Key.D6},
{X11Key.asciicircum, Key.D6},
{X11Key.XK_7, Key.D7}, {X11Key.XK_7, Key.D7},
{X11Key.ampersand, Key.D7},
{X11Key.XK_8, Key.D8}, {X11Key.XK_8, Key.D8},
{X11Key.asterisk, Key.D8},
{X11Key.XK_9, Key.D9}, {X11Key.XK_9, Key.D9},
{X11Key.parenleft, Key.D9},
{X11Key.XK_0, Key.D0}, {X11Key.XK_0, Key.D0},
{X11Key.parenright, Key.D0},
//{ X11Key.?, Key.AbntC1 } //{ X11Key.?, Key.AbntC1 }
//{ X11Key.?, Key.AbntC2 } //{ X11Key.?, Key.AbntC2 }
//{ X11Key.?, Key.Oem8 } //{ X11Key.?, Key.Oem8 }
@ -242,8 +421,8 @@ namespace Avalonia.X11
//{ X11Key.?, Key.DeadCharProcessed } //{ X11Key.?, Key.DeadCharProcessed }
}; };
public static Key ConvertKey(X11Key key) public static Key KeyFromX11Key(X11Key key)
=> KeyDic.TryGetValue(key, out var result) ? result : Key.None; => s_keyFromX11Key.TryGetValue(key, out var result) ? result : Key.None;
} }
} }

256
src/Avalonia.X11/X11Window.Ime.cs

@ -1,3 +1,5 @@
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -13,12 +15,11 @@ namespace Avalonia.X11
{ {
internal partial class X11Window internal partial class X11Window
{ {
private ITextInputMethodImpl _ime; private ITextInputMethodImpl? _ime;
private IX11InputMethodControl _imeControl; private IX11InputMethodControl? _imeControl;
private bool _processingIme; private bool _processingIme;
private Queue<(RawKeyEventArgs args, XEvent xev, int keyval, int keycode)> _imeQueue = private readonly Queue<(RawKeyEventArgs args, XEvent xev, int keyval, int keycode)> _imeQueue = new();
new Queue<(RawKeyEventArgs args, XEvent xev, int keyVal, int keyCode)>();
private unsafe void CreateIC() private unsafe void CreateIC()
{ {
@ -79,94 +80,211 @@ namespace Avalonia.X11
(_ime, _imeControl) = ime.Value; (_ime, _imeControl) = ime.Value;
_imeControl.Commit += s => _imeControl.Commit += s =>
ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(), ScheduleInput(new RawTextInputEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(),
_inputRoot, s)); InputRoot, s));
_imeControl.ForwardKey += ev => _imeControl.ForwardKey += OnImeControlForwardKey;
{
ScheduleInput(new RawKeyEventArgs(_keyboard, (ulong)_x11.LastActivityTimestamp.ToInt64(),
_inputRoot, ev.Type, X11KeyTransform.ConvertKey((X11Key)ev.KeyVal),
(RawInputModifiers)ev.Modifiers));
};
} }
} }
private void OnImeControlForwardKey(X11InputMethodForwardedKey forwardedKey)
{
var x11Key = (X11Key)forwardedKey.KeyVal;
var keySymbol = _x11.HasXkb ? GetKeySymbolXkb(x11Key) : GetKeySymbolXCore(x11Key);
ScheduleInput(new RawKeyEventArgs(
_keyboard,
(ulong)_x11.LastActivityTimestamp.ToInt64(),
InputRoot,
forwardedKey.Type,
X11KeyTransform.KeyFromX11Key(x11Key),
(RawInputModifiers)forwardedKey.Modifiers,
PhysicalKey.None,
keySymbol));
}
private void UpdateImePosition() => _imeControl?.UpdateWindowInfo(_position ?? default, RenderScaling); private void UpdateImePosition() => _imeControl?.UpdateWindowInfo(_position ?? default, RenderScaling);
private void HandleKeyEvent(ref XEvent ev) private void HandleKeyEvent(ref XEvent ev)
{ {
var index = ev.KeyEvent.state.HasAllFlags(XModifierMask.ShiftMask); var physicalKey = X11KeyTransform.PhysicalKeyFromScanCode(ev.KeyEvent.keycode);
var (x11Key, key, symbol) = LookupKey(ref ev.KeyEvent, physicalKey);
// We need the latin key, since it's mainly used for hotkeys, we use a different API for text anyway
var key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 1 : 0).ToInt32();
// Manually switch the Shift index for the keypad,
// there should be a proper way to do this
if (ev.KeyEvent.state.HasAllFlags(XModifierMask.Mod2Mask)
&& key > X11Key.Num_Lock && key <= X11Key.KP_9)
key = (X11Key)XKeycodeToKeysym(_x11.Display, ev.KeyEvent.keycode, index ? 0 : 1).ToInt32();
var convertedKey = X11KeyTransform.ConvertKey(key);
var modifiers = TranslateModifiers(ev.KeyEvent.state); var modifiers = TranslateModifiers(ev.KeyEvent.state);
var timestamp = (ulong)ev.KeyEvent.time.ToInt64(); var timestamp = (ulong)ev.KeyEvent.time.ToInt64();
RawKeyEventArgs args =
ev.type == XEventName.KeyPress var args = ev.type == XEventName.KeyPress ?
? new RawKeyEventArgsWithText(_keyboard, timestamp, _inputRoot, RawKeyEventType.KeyDown, new RawKeyEventArgsWithText(
convertedKey, modifiers, TranslateEventToString(ref ev)) _keyboard,
: new RawKeyEventArgs(_keyboard, timestamp, _inputRoot, RawKeyEventType.KeyUp, convertedKey, timestamp,
modifiers); InputRoot,
RawKeyEventType.KeyDown,
ScheduleKeyInput(args, ref ev, (int)key, ev.KeyEvent.keycode); key,
modifiers,
physicalKey,
symbol,
TranslateEventToString(ref ev, symbol)) :
new RawKeyEventArgs(
_keyboard,
timestamp,
InputRoot,
RawKeyEventType.KeyUp,
key,
modifiers,
physicalKey,
symbol);
ScheduleKeyInput(args, ref ev, (int)x11Key, ev.KeyEvent.keycode);
} }
private void TriggerClassicTextInputEvent(ref XEvent ev) private (X11Key x11Key, Key key, string? symbol) LookupKey(ref XKeyEvent keyEvent, PhysicalKey physicalKey)
{ {
var text = TranslateEventToString(ref ev); var (x11Key, key, symbol) = _x11.HasXkb ? LookUpKeyXkb(ref keyEvent) : LookupKeyXCore(ref keyEvent);
if (text != null)
ScheduleInput( // Always use digits keys if possible, matching Windows/macOS.
new RawTextInputEventArgs(_keyboard, (ulong)ev.KeyEvent.time.ToInt64(), _inputRoot, text), if (physicalKey is >= PhysicalKey.Digit0 and <= PhysicalKey.Digit9)
ref ev); key = physicalKey.ToQwertyKey();
// No key sym matched a key (e.g. non-latin keyboard without US fallback): fallback to a basic QWERTY map.
if (x11Key != 0 && key == Key.None)
key = physicalKey.ToQwertyKey();
return (x11Key, key, symbol);
} }
private const int ImeBufferSize = 64 * 1024; private (X11Key x11Key, Key key, string? symbol) LookUpKeyXkb(ref XKeyEvent keyEvent)
[ThreadStatic] private static IntPtr ImeBuffer; {
// First lookup using the current keyboard layout group (contained in state).
var state = (int)keyEvent.state;
if (!XkbLookupKeySym(_x11.Display, keyEvent.keycode, state, out _, out var originalKeySym))
return (0, Key.None, null);
private unsafe string TranslateEventToString(ref XEvent ev) var x11Key = (X11Key)originalKeySym;
var symbol = GetKeySymbolXkb(x11Key);
var key = X11KeyTransform.KeyFromX11Key(x11Key);
if (key != Key.None)
return (x11Key, key, symbol);
var originalGroup = XkbGetGroupForCoreState(state);
// We got a KeySym that doesn't match a key: try the other groups.
// This is needed to get a latin key for non-latin keyboard layouts.
for (var group = 0; group < 4; ++group)
{
if (group == originalGroup)
continue;
var newState = XkbSetGroupForCoreState(state, group);
if (XkbLookupKeySym(_x11.Display, keyEvent.keycode, newState, out _, out var groupKeySym))
{
key = X11KeyTransform.KeyFromX11Key((X11Key)groupKeySym);
if (key != Key.None)
return (x11Key, key, symbol);
}
}
return (x11Key, Key.None, null);
}
private unsafe string? GetKeySymbolXkb(X11Key x11Key)
{ {
if (ImeBuffer == IntPtr.Zero) var keySym = (nint)x11Key;
ImeBuffer = Marshal.AllocHGlobal(ImeBufferSize); const int bufferSize = 4;
var buffer = stackalloc byte[bufferSize];
IntPtr istatus; var length = XkbTranslateKeySym(_x11.Display, ref keySym, 0, buffer, bufferSize, out var extraSize);
int len;
if(_xic != IntPtr.Zero) if (length == 0)
len = Xutf8LookupString(_xic, ref ev, ImeBuffer.ToPointer(), return null;
ImeBufferSize, out _, out istatus);
else if (length == 1 && !KeySymbolHelper.IsAllowedAsciiKeySymbol((char)buffer[0]))
len = XLookupString(ref ev, ImeBuffer.ToPointer(), ImeBufferSize, return null;
out _, out istatus);
if (len == 0) if (extraSize <= 0)
return Encoding.UTF8.GetString(buffer, length);
// A symbol should normally fit in 4 bytes, so this path isn't expected to be taken.
var heapBuffer = new byte[length + extraSize];
fixed (byte* heapBufferPtr = heapBuffer)
length = XkbTranslateKeySym(_x11.Display, ref keySym, 0, heapBufferPtr, heapBuffer.Length, out _);
return Encoding.UTF8.GetString(heapBuffer, 0, length);
}
private static unsafe (X11Key x11Key, Key key, string? symbol) LookupKeyXCore(ref XKeyEvent keyEvent)
{
const int bufferSize = 4;
var buffer = stackalloc byte[bufferSize];
// We don't have Xkb enabled, which should be rare: use XLookupString which will map to the first keyboard
// while handling modifiers for us (XKeycodeToKeysym doesn't).
var length = XLookupString(ref keyEvent, buffer, bufferSize, out var keySym, IntPtr.Zero);
var x11Key = (X11Key)keySym;
var key = X11KeyTransform.KeyFromX11Key(x11Key);
var symbol = length switch
{
0 => null,
1 when !KeySymbolHelper.IsAllowedAsciiKeySymbol((char)buffer[0]) => null,
_ => Encoding.UTF8.GetString(buffer, length)
};
return (x11Key, key, symbol);
}
private static unsafe string? GetKeySymbolXCore(X11Key x11Key)
{
var bytes = XKeysymToString((nint)x11Key);
if (bytes is null)
return null; return null;
var status = (XLookupStatus)istatus; var length = 0;
for (var p = bytes; *p != 0; ++p)
++length;
string text; if (length == 0)
if (status == XLookupStatus.XBufferOverflow)
return null; return null;
return Encoding.UTF8.GetString(bytes, length);
}
private const int ImeBufferSize = 64 * 1024;
[ThreadStatic] private static IntPtr ImeBuffer;
private unsafe string? TranslateEventToString(ref XEvent ev, string? symbol)
{
string? text;
if (!_x11.HasXkb && _xic == IntPtr.Zero)
text = symbol; // We already got the symbol from XLookupString, no need to call it again.
else else
text = Encoding.UTF8.GetString((byte*)ImeBuffer.ToPointer(), len); {
if (ImeBuffer == IntPtr.Zero)
ImeBuffer = Marshal.AllocHGlobal(ImeBufferSize);
var imeBufferPtr = (byte*)ImeBuffer.ToPointer();
XLookupStatus status = 0;
var len = _xic == IntPtr.Zero ?
XLookupString(ref ev.KeyEvent, imeBufferPtr, ImeBufferSize, out _, IntPtr.Zero) :
Xutf8LookupString(_xic, ref ev.KeyEvent, imeBufferPtr, ImeBufferSize, out _, out status);
if (len == 0 || status == XLookupStatus.XBufferOverflow)
return null;
text = Encoding.UTF8.GetString(imeBufferPtr, len);
}
if (text == null) if (text is null)
return null; return null;
if (text.Length == 1) if (text.Length == 1)
{ {
if (text[0] < ' ' || text[0] == 0x7f) //Control codes or DEL if (text[0] < ' ' || text[0] == 0x7f) // Control codes or DEL
return null; return null;
} }
return text; return text;
} }
private void ScheduleKeyInput(RawKeyEventArgs args, ref XEvent xev, int keyval, int keycode) private void ScheduleKeyInput(RawKeyEventArgs args, ref XEvent xev, int keyval, int keycode)
{ {
_x11.LastActivityTimestamp = xev.ButtonEvent.time; _x11.LastActivityTimestamp = xev.ButtonEvent.time;
@ -212,14 +330,22 @@ namespace Avalonia.X11
// This class is used to attach the text value of the key to an asynchronously dispatched KeyDown event // This class is used to attach the text value of the key to an asynchronously dispatched KeyDown event
private class RawKeyEventArgsWithText : RawKeyEventArgs private class RawKeyEventArgsWithText : RawKeyEventArgs
{ {
public RawKeyEventArgsWithText(IKeyboardDevice device, ulong timestamp, IInputRoot root, public RawKeyEventArgsWithText(
RawKeyEventType type, Key key, RawInputModifiers modifiers, string text) : IKeyboardDevice device,
base(device, timestamp, root, type, key, modifiers) ulong timestamp,
IInputRoot root,
RawKeyEventType type,
Key key,
RawInputModifiers modifiers,
PhysicalKey physicalKey,
string? keySymbol,
string? text)
: base(device, timestamp, root, type, key, modifiers, physicalKey, keySymbol)
{ {
Text = text; Text = text;
} }
public string Text { get; } public string? Text { get; }
} }
} }
} }

3
src/Avalonia.X11/X11Window.cs

@ -838,7 +838,8 @@ namespace Avalonia.X11
} }
public IInputRoot? InputRoot => _inputRoot; public IInputRoot InputRoot
=> _inputRoot ?? throw new InvalidOperationException($"{nameof(SetInputRoot)} must have been called");
public void SetInputRoot(IInputRoot inputRoot) public void SetInputRoot(IInputRoot inputRoot)
{ {

32
src/Avalonia.X11/XLib.cs

@ -465,19 +465,31 @@ namespace Avalonia.X11
} }
[DllImport (libX11)] [DllImport (libX11)]
public static extern unsafe int XLookupString(ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status); public static extern int XLookupString(ref XKeyEvent xevent, byte* buffer, int num_bytes, out nint keysym, IntPtr composeStatus);
[DllImport (libX11)] [DllImport (libX11)]
public static extern unsafe int Xutf8LookupString(IntPtr xic, ref XEvent xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status); public static extern int Xutf8LookupString(IntPtr xic, ref XKeyEvent xevent, byte* buffer, int num_bytes, out nint keysym, out XLookupStatus status);
[DllImport (libX11)] [DllImport (libX11)]
public static extern unsafe int Xutf8LookupString(IntPtr xic, XEvent* xevent, void* buffer, int num_bytes, out IntPtr keysym, out IntPtr status); public static extern byte* XKeysymToString(nint keysym);
[DllImport (libX11)]
public static extern bool XkbLibraryVersion(ref int libMajor, ref int libMinor);
[DllImport (libX11)]
public static extern bool XkbQueryExtension(IntPtr display, out int *opcode, out int eventBase, out int *errorBase, ref int major, ref int minor);
[DllImport (libX11)]
public static extern bool XkbIgnoreExtension(bool ignore);
[DllImport (libX11)] [DllImport (libX11)]
public static extern unsafe IntPtr XKeycodeToKeysym(IntPtr display, int keycode, int index); public static extern bool XkbLookupKeySym(IntPtr display, int keycode, int modifiers, out XModifierMask consumedModifiers, out nint keysym);
[DllImport (libX11)]
public static extern int XkbTranslateKeySym(IntPtr display, ref nint keySym, int modifiers, byte* buffer, int bufferSize, out int extraSize);
[DllImport (libX11)] [DllImport (libX11)]
public static extern unsafe IntPtr XSetLocaleModifiers(string modifiers); public static extern IntPtr XSetLocaleModifiers(string modifiers);
[DllImport (libX11)] [DllImport (libX11)]
public static extern IntPtr XOpenIM (IntPtr display, IntPtr rdb, IntPtr res_name, IntPtr res_class); public static extern IntPtr XOpenIM (IntPtr display, IntPtr rdb, IntPtr res_name, IntPtr res_class);
@ -731,5 +743,11 @@ namespace Avalonia.X11
plat.Windows[win] = handler; plat.Windows[win] = handler;
return win; return win;
} }
public static int XkbGetGroupForCoreState(int state)
=> (state >> 13) & 0x3;
public static int XkbSetGroupForCoreState(int state, int newGroup)
=> (state & ~(0x3 << 13)) | ((newGroup & 0x3) << 13);
} }
} }

48
src/Browser/Avalonia.Browser/BrowserTopLevelImpl.cs

@ -13,7 +13,6 @@ using Avalonia.Input.Raw;
using Avalonia.Input.TextInput; using Avalonia.Input.TextInput;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
[assembly: SupportedOSPlatform("browser")] [assembly: SupportedOSPlatform("browser")]
@ -131,32 +130,29 @@ namespace Avalonia.Browser
return false; return false;
} }
public bool RawKeyboardEvent(RawKeyEventType type, string code, string key, RawInputModifiers modifiers) public bool RawKeyboardEvent(RawKeyEventType type, string domCode, string domKey, RawInputModifiers modifiers)
{ {
if (Keycodes.KeyCodes.TryGetValue(code, out var avkey)) if (_inputRoot is null)
{ return false;
if (_inputRoot is { })
{ var physicalKey = KeyInterop.PhysicalKeyFromDomCode(domCode);
var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); var key = KeyInterop.KeyFromDomKey(domKey, physicalKey);
var keySymbol = KeyInterop.KeySymbolFromDomKey(domKey);
Input?.Invoke(args);
var args = new RawKeyEventArgs(
return args.Handled; KeyboardDevice,
} Timestamp,
} _inputRoot,
else if (Keycodes.KeyCodes.TryGetValue(key, out avkey)) type,
{ key,
if (_inputRoot is { }) modifiers,
{ physicalKey,
var args = new RawKeyEventArgs(KeyboardDevice, Timestamp, _inputRoot, type, avkey, modifiers); keySymbol
);
Input?.Invoke(args);
Input?.Invoke(args);
return args.Handled;
} return args.Handled;
}
return false;
} }
public bool RawTextEvent(string text) public bool RawTextEvent(string text)

435
src/Browser/Avalonia.Browser/KeyInterop.cs

@ -0,0 +1,435 @@
using System.Collections.Generic;
using Avalonia.Input;
namespace Avalonia.Browser
{
internal static class KeyInterop
{
// https://www.w3.org/TR/uievents-code/
// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_code_values
// This list has the same order as the PhysicalKey enum.
private static readonly Dictionary<string, PhysicalKey> s_physicalKeyFromDomCode = new()
{
{ "Unidentified", PhysicalKey.None },
// Writing System Keys
{ "Backquote", PhysicalKey.Backquote },
{ "Backslash", PhysicalKey.Backslash },
{ "BracketLeft", PhysicalKey.BracketLeft },
{ "BracketRight", PhysicalKey.BracketRight },
{ "Comma", PhysicalKey.Comma },
{ "Digit0", PhysicalKey.Digit0 },
{ "Digit1", PhysicalKey.Digit1 },
{ "Digit2", PhysicalKey.Digit2 },
{ "Digit3", PhysicalKey.Digit3 },
{ "Digit4", PhysicalKey.Digit4 },
{ "Digit5", PhysicalKey.Digit5 },
{ "Digit6", PhysicalKey.Digit6 },
{ "Digit7", PhysicalKey.Digit7 },
{ "Digit8", PhysicalKey.Digit8 },
{ "Digit9", PhysicalKey.Digit9 },
{ "Equal", PhysicalKey.Equal },
{ "IntlBackslash", PhysicalKey.IntlBackslash },
{ "IntlRo", PhysicalKey.IntlRo },
{ "IntlYen", PhysicalKey.IntlYen },
{ "KeyA", PhysicalKey.A },
{ "KeyB", PhysicalKey.B },
{ "KeyC", PhysicalKey.C },
{ "KeyD", PhysicalKey.D },
{ "KeyE", PhysicalKey.E },
{ "KeyF", PhysicalKey.F },
{ "KeyG", PhysicalKey.G },
{ "KeyH", PhysicalKey.H },
{ "KeyI", PhysicalKey.I },
{ "KeyJ", PhysicalKey.J },
{ "KeyK", PhysicalKey.K },
{ "KeyL", PhysicalKey.L },
{ "KeyM", PhysicalKey.M },
{ "KeyN", PhysicalKey.N },
{ "KeyO", PhysicalKey.O },
{ "KeyP", PhysicalKey.P },
{ "KeyQ", PhysicalKey.Q },
{ "KeyR", PhysicalKey.R },
{ "KeyS", PhysicalKey.S },
{ "KeyT", PhysicalKey.T },
{ "KeyU", PhysicalKey.U },
{ "KeyV", PhysicalKey.V },
{ "KeyW", PhysicalKey.W },
{ "KeyX", PhysicalKey.X },
{ "KeyY", PhysicalKey.Y },
{ "KeyZ", PhysicalKey.Z },
{ "Minus", PhysicalKey.Minus },
{ "Period", PhysicalKey.Period },
{ "Quote", PhysicalKey.Quote },
{ "Semicolon", PhysicalKey.Semicolon },
{ "Slash", PhysicalKey.Slash },
// Functional Keys
{ "AltLeft", PhysicalKey.AltLeft },
{ "AltRight", PhysicalKey.AltRight },
{ "Backspace", PhysicalKey.Backspace },
{ "CapsLock", PhysicalKey.CapsLock },
{ "ContextMenu", PhysicalKey.ContextMenu },
{ "ControlLeft", PhysicalKey.ControlLeft },
{ "ControlRight", PhysicalKey.ControlRight },
{ "Enter", PhysicalKey.Enter },
{ "MetaLeft", PhysicalKey.MetaLeft },
{ "OSLeft", PhysicalKey.MetaLeft },
{ "MetaRight", PhysicalKey.MetaRight },
{ "OSRight", PhysicalKey.MetaRight },
{ "ShiftLeft", PhysicalKey.ShiftLeft },
{ "ShiftRight", PhysicalKey.ShiftRight },
{ "Space", PhysicalKey.Space },
{ "Tab", PhysicalKey.Tab },
{ "Convert", PhysicalKey.Convert },
{ "KanaMode", PhysicalKey.KanaMode },
{ "Lang1", PhysicalKey.Lang1 },
{ "Lang2", PhysicalKey.Lang2 },
{ "Lang3", PhysicalKey.Lang3 },
{ "Lang4", PhysicalKey.Lang4 },
{ "Lang5", PhysicalKey.Lang5 },
{ "NonConvert", PhysicalKey.NonConvert },
// Control Pad Section
{ "Delete", PhysicalKey.Delete },
{ "End", PhysicalKey.End },
{ "Help", PhysicalKey.Help },
{ "Home", PhysicalKey.Home },
{ "Insert", PhysicalKey.Insert },
{ "PageDown", PhysicalKey.PageDown },
{ "PageUp", PhysicalKey.PageUp },
// Arrow Pad Section
{ "ArrowDown", PhysicalKey.ArrowDown },
{ "ArrowLeft", PhysicalKey.ArrowLeft },
{ "ArrowRight", PhysicalKey.ArrowRight },
{ "ArrowUp", PhysicalKey.ArrowUp },
// Numpad Section
{ "NumLock", PhysicalKey.NumLock },
{ "Numpad0", PhysicalKey.NumPad0 },
{ "Numpad1", PhysicalKey.NumPad1 },
{ "Numpad2", PhysicalKey.NumPad2 },
{ "Numpad3", PhysicalKey.NumPad3 },
{ "Numpad4", PhysicalKey.NumPad4 },
{ "Numpad5", PhysicalKey.NumPad5 },
{ "Numpad6", PhysicalKey.NumPad6 },
{ "Numpad7", PhysicalKey.NumPad7 },
{ "Numpad8", PhysicalKey.NumPad8 },
{ "Numpad9", PhysicalKey.NumPad9 },
{ "NumpadAdd", PhysicalKey.NumPadAdd },
{ "NumpadClear", PhysicalKey.NumPadClear },
{ "NumpadComma", PhysicalKey.NumPadComma },
{ "NumpadDecimal", PhysicalKey.NumPadDecimal },
{ "NumpadDivide", PhysicalKey.NumPadDivide },
{ "NumpadEnter", PhysicalKey.NumPadEnter },
{ "NumpadEqual", PhysicalKey.NumPadEqual },
{ "NumpadMultiply", PhysicalKey.NumPadMultiply },
{ "NumpadParenLeft", PhysicalKey.NumPadParenLeft },
{ "NumpadParenRight", PhysicalKey.NumPadParenRight },
{ "NumpadSubtract", PhysicalKey.NumPadSubtract },
// Function Section
{ "Escape", PhysicalKey.Escape },
{ "F1", PhysicalKey.F1 },
{ "F2", PhysicalKey.F2 },
{ "F3", PhysicalKey.F3 },
{ "F4", PhysicalKey.F4 },
{ "F5", PhysicalKey.F5 },
{ "F6", PhysicalKey.F6 },
{ "F7", PhysicalKey.F7 },
{ "F8", PhysicalKey.F8 },
{ "F9", PhysicalKey.F9 },
{ "F10", PhysicalKey.F10 },
{ "F11", PhysicalKey.F11 },
{ "F12", PhysicalKey.F12 },
{ "F13", PhysicalKey.F13 },
{ "F14", PhysicalKey.F14 },
{ "F15", PhysicalKey.F15 },
{ "F16", PhysicalKey.F16 },
{ "F17", PhysicalKey.F17 },
{ "F18", PhysicalKey.F18 },
{ "F19", PhysicalKey.F19 },
{ "F20", PhysicalKey.F20 },
{ "F21", PhysicalKey.F21 },
{ "F22", PhysicalKey.F22 },
{ "F23", PhysicalKey.F23 },
{ "F24", PhysicalKey.F24 },
{ "PrintScreen", PhysicalKey.PrintScreen },
{ "ScrollLock", PhysicalKey.ScrollLock },
{ "Pause", PhysicalKey.Pause },
// Media Keys
{ "BrowserBack", PhysicalKey.BrowserBack },
{ "BrowserFavorites", PhysicalKey.BrowserFavorites },
{ "BrowserForward", PhysicalKey.BrowserForward },
{ "BrowserHome", PhysicalKey.BrowserHome },
{ "BrowserRefresh", PhysicalKey.BrowserRefresh },
{ "BrowserSearch", PhysicalKey.BrowserSearch },
{ "BrowserStop", PhysicalKey.BrowserStop },
{ "Abort", PhysicalKey.BrowserStop },
{ "Eject", PhysicalKey.Eject },
{ "LaunchApp1", PhysicalKey.LaunchApp1 },
{ "LaunchApp2", PhysicalKey.LaunchApp2 },
{ "LaunchMail", PhysicalKey.LaunchMail },
{ "MediaPlayPause", PhysicalKey.MediaPlayPause },
{ "MediaSelect", PhysicalKey.MediaSelect },
{ "MediaStop", PhysicalKey.MediaStop },
{ "MediaTrackNext", PhysicalKey.MediaTrackNext },
{ "MediaTrackPrevious", PhysicalKey.MediaTrackPrevious },
{ "Power", PhysicalKey.Power },
{ "Sleep", PhysicalKey.Sleep },
{ "AudioVolumeDown", PhysicalKey.AudioVolumeDown },
{ "VolumeDown", PhysicalKey.AudioVolumeDown },
{ "AudioVolumeMute", PhysicalKey.AudioVolumeMute },
{ "VolumeMute", PhysicalKey.AudioVolumeMute },
{ "AudioVolumeUp", PhysicalKey.AudioVolumeUp },
{ "VolumeUp", PhysicalKey.AudioVolumeUp },
{ "WakeUp", PhysicalKey.WakeUp },
// Legacy Keys
{ "Copy", PhysicalKey.Copy },
{ "Cut", PhysicalKey.Cut },
{ "Find", PhysicalKey.Find },
{ "Open", PhysicalKey.Open },
{ "Paste", PhysicalKey.Paste },
{ "Props", PhysicalKey.Props },
{ "Select", PhysicalKey.Select },
{ "Undo", PhysicalKey.Undo }
};
public static PhysicalKey PhysicalKeyFromDomCode(string? domCode)
=> !string.IsNullOrEmpty(domCode) && s_physicalKeyFromDomCode.TryGetValue(domCode, out var physicalKey) ?
physicalKey :
PhysicalKey.None;
// https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values
private static readonly Dictionary<string, Key> s_keyFromDomKey = new()
{
// Alphabetic keys
{ "A", Key.A },
{ "B", Key.B },
{ "C", Key.C },
{ "D", Key.D },
{ "E", Key.E },
{ "F", Key.F },
{ "G", Key.G },
{ "H", Key.H },
{ "I", Key.I },
{ "J", Key.J },
{ "K", Key.K },
{ "L", Key.L },
{ "M", Key.M },
{ "N", Key.N },
{ "O", Key.O },
{ "P", Key.P },
{ "Q", Key.Q },
{ "R", Key.R },
{ "S", Key.S },
{ "T", Key.T },
{ "U", Key.U },
{ "V", Key.V },
{ "W", Key.W },
{ "X", Key.X },
{ "Y", Key.Y },
{ "Z", Key.Z },
{ "a", Key.A },
{ "b", Key.B },
{ "c", Key.C },
{ "d", Key.D },
{ "e", Key.E },
{ "f", Key.F },
{ "g", Key.G },
{ "h", Key.H },
{ "i", Key.I },
{ "j", Key.J },
{ "k", Key.K },
{ "l", Key.L },
{ "m", Key.M },
{ "n", Key.N },
{ "o", Key.O },
{ "p", Key.P },
{ "q", Key.Q },
{ "r", Key.R },
{ "s", Key.S },
{ "t", Key.T },
{ "u", Key.U },
{ "v", Key.V },
{ "w", Key.W },
{ "x", Key.X },
{ "y", Key.Y },
{ "z", Key.Z },
// Modifier keys (left/right keys are handled separately)
{ "AltGr", Key.RightAlt },
{ "CapsLock", Key.CapsLock },
{ "NumLock", Key.NumLock },
{ "ScrollLock", Key.Scroll },
// Whitespace keys
{ "Enter", Key.Enter },
{ "Tab", Key.Tab },
{ " ", Key.Space },
// Navigation keys
{ "ArrowDown", Key.Down },
{ "ArrowLeft", Key.Left },
{ "ArrowRight", Key.Right },
{ "ArrowUp", Key.Up },
{ "End", Key.End },
{ "Home", Key.Home },
{ "PageDown", Key.PageDown },
{ "PageUp", Key.PageUp },
// Editing keys
{ "Backspace", Key.Back },
{ "Clear", Key.Clear },
{ "CrSel", Key.CrSel },
{ "Delete", Key.Delete },
{ "EraseEof", Key.EraseEof },
{ "ExSel", Key.ExSel },
{ "Insert", Key.Insert },
// UI keys
{ "Accept", Key.ImeAccept },
{ "Attn", Key.OemAttn },
{ "Cancel", Key.Cancel },
{ "ContextMenu", Key.Apps },
{ "Escape", Key.Escape },
{ "Execute", Key.Execute },
{ "Finish", Key.OemFinish },
{ "Help", Key.Help },
{ "Pause", Key.Pause },
{ "Play", Key.Play },
{ "Select", Key.Select },
{ "ZoomIn", Key.Zoom },
// Device keys
{ "PrintScreen", Key.PrintScreen },
// IME keys
{ "Convert", Key.ImeConvert },
{ "FinalMode", Key.FinalMode },
{ "ModeChange", Key.ImeModeChange },
{ "NonConvert", Key.ImeNonConvert },
{ "Process", Key.ImeProcessed },
{ "HangulMode", Key.HangulMode },
{ "HanjaMode", Key.HanjaMode },
{ "JunjaMode", Key.JunjaMode },
{ "Hankaku", Key.OemAuto },
{ "Hiragana", Key.DbeHiragana },
{ "KanaMode", Key.KanaMode },
{ "KanjiMode", Key.KanjiMode },
{ "Katakana", Key.DbeKatakana },
{ "Romaji", Key.OemBackTab },
{ "Zenkaku", Key.OemEnlw },
// Function keys
{ "F1", Key.F1 },
{ "F2", Key.F2 },
{ "F3", Key.F3 },
{ "F4", Key.F4 },
{ "F5", Key.F5 },
{ "F6", Key.F6 },
{ "F7", Key.F7 },
{ "F8", Key.F8 },
{ "F9", Key.F9 },
{ "F10", Key.F10 },
{ "F11", Key.F11 },
{ "F12", Key.F12 },
{ "F13", Key.F13 },
{ "F14", Key.F14 },
{ "F15", Key.F15 },
{ "F16", Key.F16 },
{ "F17", Key.F17 },
{ "F18", Key.F18 },
{ "F19", Key.F19 },
{ "F20", Key.F20 },
// Multimedia keys
{ "MediaPlayPause", Key.MediaPlayPause },
{ "MediaStop", Key.MediaStop },
{ "MediaTrackNext", Key.MediaNextTrack },
{ "MediaTrackPrevious", Key.MediaPreviousTrack },
// Audio control keys
{ "AudioVolumeDown", Key.VolumeDown },
{ "AudioVolumeMute", Key.VolumeMute },
{ "AudioVolumeUp", Key.VolumeUp },
// Application selector keys
{ "LaunchCalculator", Key.LaunchApplication2 },
{ "LaunchMail", Key.LaunchMail },
{ "LaunchMyComputer", Key.LaunchApplication1 },
{ "LaunchApplication1", Key.LaunchApplication1 },
{ "LaunchApplication2", Key.LaunchApplication2 },
// Browser control keys
{ "BrowserBack", Key.BrowserBack },
{ "BrowserFavorites", Key.BrowserFavorites },
{ "BrowserForward", Key.BrowserForward },
{ "BrowserHome", Key.BrowserHome },
{ "BrowserRefresh", Key.BrowserRefresh },
{ "BrowserSearch", Key.BrowserSearch },
{ "BrowserStop", Key.BrowserStop },
// Numeric keypad keys
{ "Decimal", Key.Decimal },
{ "Multiply", Key.Multiply },
{ "Add", Key.Add },
{ "Divide", Key.Divide },
{ "Subtract", Key.Subtract },
{ "Separator", Key.Separator },
};
public static Key KeyFromDomKey(string? domKey, PhysicalKey physicalKey)
{
if (string.IsNullOrEmpty(domKey))
return Key.None;
if (s_keyFromDomKey.TryGetValue(domKey, out var key))
return key;
key = domKey switch
{
"Alt" => physicalKey == PhysicalKey.AltRight ? Key.RightAlt : Key.LeftAlt,
"Control" => physicalKey == PhysicalKey.ControlRight ? Key.RightCtrl : Key.LeftCtrl,
"Shift" => physicalKey == PhysicalKey.ShiftRight ? Key.RightShift : Key.LeftShift,
"Meta" => physicalKey == PhysicalKey.MetaRight ? Key.RWin : Key.LWin,
"0" => physicalKey == PhysicalKey.NumPad0 ? Key.NumPad0 : Key.D0,
"1" => physicalKey == PhysicalKey.NumPad1 ? Key.NumPad1 : Key.D1,
"2" => physicalKey == PhysicalKey.NumPad2 ? Key.NumPad2 : Key.D2,
"3" => physicalKey == PhysicalKey.NumPad3 ? Key.NumPad3 : Key.D3,
"4" => physicalKey == PhysicalKey.NumPad4 ? Key.NumPad4 : Key.D4,
"5" => physicalKey == PhysicalKey.NumPad5 ? Key.NumPad5 : Key.D5,
"6" => physicalKey == PhysicalKey.NumPad6 ? Key.NumPad6 : Key.D6,
"7" => physicalKey == PhysicalKey.NumPad7 ? Key.NumPad7 : Key.D7,
"8" => physicalKey == PhysicalKey.NumPad8 ? Key.NumPad8 : Key.D8,
"9" => physicalKey == PhysicalKey.NumPad9 ? Key.NumPad9 : Key.D9,
"+" => physicalKey == PhysicalKey.NumPadAdd ? Key.Add : Key.OemPlus,
"-" => physicalKey == PhysicalKey.NumPadSubtract ? Key.Subtract : Key.OemMinus,
"*" => physicalKey == PhysicalKey.NumPadMultiply ? Key.Multiply : Key.None,
"/" => physicalKey == PhysicalKey.NumPadDivide ? Key.Divide : Key.None,
_ => Key.None
};
if (key != Key.None)
return key;
return physicalKey.ToQwertyKey();
}
public static string? KeySymbolFromDomKey(string? domKey)
{
if (string.IsNullOrEmpty(domKey))
return null;
return domKey.Length switch
{
1 => domKey,
2 when char.IsSurrogatePair(domKey[0], domKey[1]) => domKey,
_ => null
};
}
}
}

129
src/Browser/Avalonia.Browser/Keycodes.cs

@ -1,129 +0,0 @@
using System.Collections.Generic;
using Avalonia.Input;
namespace Avalonia.Browser
{
internal static class Keycodes
{
public static Dictionary<string, Key> KeyCodes = new()
{
{ "Escape", Key.Escape },
{ "Digit1", Key.D1 },
{ "Digit2", Key.D2 },
{ "Digit3", Key.D3 },
{ "Digit4", Key.D4 },
{ "Digit5", Key.D5 },
{ "Digit6", Key.D6 },
{ "Digit7", Key.D7 },
{ "Digit8", Key.D8 },
{ "Digit9", Key.D9 },
{ "Digit0", Key.D0 },
{ "Minus", Key.OemMinus },
//{ "Equal" , Key. },
{ "Backspace", Key.Back },
{ "Tab", Key.Tab },
{ "KeyQ", Key.Q },
{ "KeyW", Key.W },
{ "KeyE", Key.E },
{ "KeyR", Key.R },
{ "KeyT", Key.T },
{ "KeyY", Key.Y },
{ "KeyU", Key.U },
{ "KeyI", Key.I },
{ "KeyO", Key.O },
{ "KeyP", Key.P },
{ "BracketLeft", Key.OemOpenBrackets },
{ "BracketRight", Key.OemCloseBrackets },
{ "Enter", Key.Enter },
{ "ControlLeft", Key.LeftCtrl },
{ "KeyA", Key.A },
{ "KeyS", Key.S },
{ "KeyD", Key.D },
{ "KeyF", Key.F },
{ "KeyG", Key.G },
{ "KeyH", Key.H },
{ "KeyJ", Key.J },
{ "KeyK", Key.K },
{ "KeyL", Key.L },
{ "Semicolon", Key.OemSemicolon },
{ "Quote", Key.OemQuotes },
//{ "Backquote" , Key. },
{ "ShiftLeft", Key.LeftShift },
{ "Backslash", Key.OemBackslash },
{ "KeyZ", Key.Z },
{ "KeyX", Key.X },
{ "KeyC", Key.C },
{ "KeyV", Key.V },
{ "KeyB", Key.B },
{ "KeyN", Key.N },
{ "KeyM", Key.M },
{ "Comma", Key.OemComma },
{ "Period", Key.OemPeriod },
//{ "Slash" , Key. },
{ "ShiftRight", Key.RightShift },
{ "NumpadMultiply", Key.Multiply },
{ "AltLeft", Key.LeftAlt },
{ "Space", Key.Space },
{ "CapsLock", Key.CapsLock },
{ "F1", Key.F1 },
{ "F2", Key.F2 },
{ "F3", Key.F3 },
{ "F4", Key.F4 },
{ "F5", Key.F5 },
{ "F6", Key.F6 },
{ "F7", Key.F7 },
{ "F8", Key.F8 },
{ "F9", Key.F9 },
{ "F10", Key.F10 },
{ "NumLock", Key.NumLock },
{ "ScrollLock", Key.Scroll },
{ "Numpad7", Key.NumPad7 },
{ "Numpad8", Key.NumPad8 },
{ "Numpad9", Key.NumPad9 },
{ "NumpadSubtract", Key.Subtract },
{ "Numpad4", Key.NumPad4 },
{ "Numpad5", Key.NumPad5 },
{ "Numpad6", Key.NumPad6 },
{ "NumpadAdd", Key.Add },
{ "Numpad1", Key.NumPad1 },
{ "Numpad2", Key.NumPad2 },
{ "Numpad3", Key.NumPad3 },
{ "Numpad0", Key.NumPad0 },
{ "NumpadDecimal", Key.Decimal },
{ "Unidentified", Key.NoName },
//{ "IntlBackslash" , Key.bac },
{ "F11", Key.F11 },
{ "F12", Key.F12 },
//{ "IntlRo" , Key.Ro },
//{ "Unidentified" , Key. },
{ "Convert", Key.ImeConvert },
{ "KanaMode", Key.KanaMode },
{ "NonConvert", Key.ImeNonConvert },
//{ "Unidentified" , Key. },
{ "NumpadEnter", Key.Enter },
{ "ControlRight", Key.RightCtrl },
{ "NumpadDivide", Key.Divide },
{ "PrintScreen", Key.PrintScreen },
{ "AltRight", Key.RightAlt },
//{ "Unidentified" , Key. },
{ "Home", Key.Home },
{ "ArrowUp", Key.Up },
{ "PageUp", Key.PageUp },
{ "ArrowLeft", Key.Left },
{ "ArrowRight", Key.Right },
{ "End", Key.End },
{ "ArrowDown", Key.Down },
{ "PageDown", Key.PageDown },
{ "Insert", Key.Insert },
{ "Delete", Key.Delete },
//{ "Unidentified" , Key. },
{ "AudioVolumeMute", Key.VolumeMute },
{ "AudioVolumeDown", Key.VolumeDown },
{ "AudioVolumeUp", Key.VolumeUp },
//{ "NumpadEqual" , Key. },
{ "Pause", Key.Pause },
{ "NumpadComma", Key.OemComma }
};
}
}

408
src/Headless/Avalonia.Headless.Vnc/HeadlessVncFramebufferSource.cs

@ -65,21 +65,21 @@ namespace Avalonia.Headless.Vnc
if (isModifierKey) if (isModifierKey)
return; return;
Key? key = TranslateKey(args.Keysym); var (key, keySymbol) = TranslateKey(args.Keysym);
if (key == null) if (key == Key.None)
return; return;
//we only care about text input on key up if not using Ctrl or Alt //we only care about text input on key up if not using Ctrl or Alt
string? inputText = args.Pressed || _keyState.HasFlag(RawInputModifiers.Control) || _keyState.HasFlag(RawInputModifiers.Alt) string? inputText = args.Pressed || _keyState.HasFlag(RawInputModifiers.Control) || _keyState.HasFlag(RawInputModifiers.Alt)
? null ? null
: KeyToText(args.Keysym); : keySymbol;
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
if (args.Pressed) if (args.Pressed)
Window?.KeyPress(key.Value, _keyState); Window?.KeyPress(key, _keyState, PhysicalKey.None, keySymbol);
else else
Window?.KeyRelease(key.Value, _keyState); Window?.KeyRelease(key, _keyState, PhysicalKey.None, keySymbol);
if (inputText != null) if (inputText != null)
Window?.KeyTextInput(inputText); Window?.KeyTextInput(inputText);
@ -108,217 +108,195 @@ namespace Avalonia.Headless.Vnc
return true; return true;
} }
private static string? KeyToText(KeySym key) private static (Key key, string? symbol) TranslateKey(KeySym key) =>
{
int keyCode = (int)key;
if (key >= KeySym.Space && key <= KeySym.AsciiTilde)
return new string((char)key, 1);
//handle as normal text chars 0-9
if (key >= KeySym.NumPad0 && key <= KeySym.NumPad9)
return new string((char)(key - 65408), 1);
switch (key)
{
case KeySym.NumPadAdd: return "+";
case KeySym.NumPadSubtract: return "-";
case KeySym.NumPadMultiply: return "*";
case KeySym.NumPadDivide: return "/";
case KeySym.NumPadSeparator: return NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
}
return null;
}
private static Key? TranslateKey(KeySym key) =>
key switch key switch
{ {
KeySym.Backspace => Key.Back, KeySym.Backspace => (Key.Back, "\b"),
KeySym.Tab => Key.Tab, KeySym.Tab => (Key.Tab, "\t"),
KeySym.LineFeed => Key.LineFeed, KeySym.LineFeed => (Key.LineFeed, null),
KeySym.Clear => Key.Clear, KeySym.Clear => (Key.Clear, null),
KeySym.Return => Key.Return, KeySym.Return => (Key.Return, "\r"),
KeySym.Pause => Key.Pause, KeySym.Pause => (Key.Pause, null),
KeySym.Escape => Key.Escape, KeySym.Escape => (Key.Escape, "\u001B"),
KeySym.Delete => Key.Delete, KeySym.Delete => (Key.Delete, null),
KeySym.Home => Key.Home, KeySym.Home => (Key.Home, null),
KeySym.Left => Key.Left, KeySym.Left => (Key.Left, null),
KeySym.Up => Key.Up, KeySym.Up => (Key.Up, null),
KeySym.Right => Key.Right, KeySym.Right => (Key.Right, null),
KeySym.Down => Key.Down, KeySym.Down => (Key.Down, null),
KeySym.PageUp => Key.PageUp, KeySym.PageUp => (Key.PageUp, null),
KeySym.PageDown => Key.PageDown, KeySym.PageDown => (Key.PageDown, null),
KeySym.End => Key.End, KeySym.End => (Key.End, null),
KeySym.Begin => Key.Home, KeySym.Begin => (Key.Home, null),
KeySym.Select => Key.Select, KeySym.Select => (Key.Select, null),
KeySym.Print => Key.Print, KeySym.Print => (Key.Print, null),
KeySym.Execute => Key.Execute, KeySym.Execute => (Key.Execute, null),
KeySym.Insert => Key.Insert, KeySym.Insert => (Key.Insert, null),
KeySym.Cancel => Key.Cancel, KeySym.Cancel => (Key.Cancel, null),
KeySym.Help => Key.Help, KeySym.Help => (Key.Help, null),
KeySym.Break => Key.Pause, KeySym.Break => (Key.Pause, null),
KeySym.Num_Lock => Key.NumLock, KeySym.Num_Lock => (Key.NumLock, null),
KeySym.NumPadSpace => Key.Space, KeySym.NumPadSpace => (Key.Space, null),
KeySym.NumPadTab => Key.Tab, KeySym.NumPadTab => (Key.Tab, null),
KeySym.NumPadEnter => Key.Enter, KeySym.NumPadEnter => (Key.Enter, null),
KeySym.NumPadF1 => Key.F1, KeySym.NumPadF1 => (Key.F1, null),
KeySym.NumPadF2 => Key.F2, KeySym.NumPadF2 => (Key.F2, null),
KeySym.NumPadF3 => Key.F3, KeySym.NumPadF3 => (Key.F3, null),
KeySym.NumPadF4 => Key.F4, KeySym.NumPadF4 => (Key.F4, null),
KeySym.NumPadHome => Key.Home, KeySym.NumPadHome => (Key.Home, null),
KeySym.NumPadLeft => Key.Left, KeySym.NumPadLeft => (Key.Left, null),
KeySym.NumPadUp => Key.Up, KeySym.NumPadUp => (Key.Up, null),
KeySym.NumPadRight => Key.Right, KeySym.NumPadRight => (Key.Right, null),
KeySym.NumPadDown => Key.Down, KeySym.NumPadDown => (Key.Down, null),
KeySym.NumPadPageUp => Key.PageUp, KeySym.NumPadPageUp => (Key.PageUp, null),
KeySym.NumPadPageDown => Key.PageDown, KeySym.NumPadPageDown => (Key.PageDown, null),
KeySym.NumPadEnd => Key.End, KeySym.NumPadEnd => (Key.End, null),
KeySym.NumPadBegin => Key.Home, KeySym.NumPadBegin => (Key.Home, null),
KeySym.NumPadInsert => Key.Insert, KeySym.NumPadInsert => (Key.Insert, null),
KeySym.NumPadDelete => Key.Delete, KeySym.NumPadDelete => (Key.Delete, null),
KeySym.NumPadEqual => Key.Return, KeySym.NumPadEqual => (Key.Enter, "="),
KeySym.NumPadMultiply => Key.Multiply, KeySym.NumPadMultiply => (Key.Multiply, "*"),
KeySym.NumPadAdd => Key.Add, KeySym.NumPadAdd => (Key.Add, "+"),
KeySym.NumPadSeparator => Key.Separator, KeySym.NumPadSeparator => (Key.Separator, NumberFormatInfo.CurrentInfo.NumberGroupSeparator),
KeySym.NumPadSubtract => Key.Subtract, KeySym.NumPadSubtract => (Key.Subtract, "-"),
KeySym.NumPadDecimal => Key.Decimal, KeySym.NumPadDecimal => (Key.Decimal, NumberFormatInfo.CurrentInfo.NumberDecimalSeparator),
KeySym.NumPadDivide => Key.Divide, KeySym.NumPadDivide => (Key.Divide, "/"),
KeySym.NumPad0 => Key.NumPad0, KeySym.NumPad0 => (Key.NumPad0, "0"),
KeySym.NumPad1 => Key.NumPad1, KeySym.NumPad1 => (Key.NumPad1, "1"),
KeySym.NumPad2 => Key.NumPad2, KeySym.NumPad2 => (Key.NumPad2, "2"),
KeySym.NumPad3 => Key.NumPad3, KeySym.NumPad3 => (Key.NumPad3, "3"),
KeySym.NumPad4 => Key.NumPad4, KeySym.NumPad4 => (Key.NumPad4, "4"),
KeySym.NumPad5 => Key.NumPad5, KeySym.NumPad5 => (Key.NumPad5, "5"),
KeySym.NumPad6 => Key.NumPad6, KeySym.NumPad6 => (Key.NumPad6, "6"),
KeySym.NumPad7 => Key.NumPad7, KeySym.NumPad7 => (Key.NumPad7, "7"),
KeySym.NumPad8 => Key.NumPad8, KeySym.NumPad8 => (Key.NumPad8, "8"),
KeySym.NumPad9 => Key.NumPad9, KeySym.NumPad9 => (Key.NumPad9, "9"),
KeySym.F1 => Key.F1, KeySym.F1 => (Key.F1, null),
KeySym.F2 => Key.F2, KeySym.F2 => (Key.F2, null),
KeySym.F3 => Key.F3, KeySym.F3 => (Key.F3, null),
KeySym.F4 => Key.F4, KeySym.F4 => (Key.F4, null),
KeySym.F5 => Key.F5, KeySym.F5 => (Key.F5, null),
KeySym.F6 => Key.F6, KeySym.F6 => (Key.F6, null),
KeySym.F7 => Key.F7, KeySym.F7 => (Key.F7, null),
KeySym.F8 => Key.F8, KeySym.F8 => (Key.F8, null),
KeySym.F9 => Key.F9, KeySym.F9 => (Key.F9, null),
KeySym.F10 => Key.F10, KeySym.F10 => (Key.F10, null),
KeySym.F11 => Key.F11, KeySym.F11 => (Key.F11, null),
KeySym.F12 => Key.F12, KeySym.F12 => (Key.F12, null),
KeySym.F13 => Key.F13, KeySym.F13 => (Key.F13, null),
KeySym.F14 => Key.F14, KeySym.F14 => (Key.F14, null),
KeySym.F15 => Key.F15, KeySym.F15 => (Key.F15, null),
KeySym.F16 => Key.F16, KeySym.F16 => (Key.F16, null),
KeySym.F17 => Key.F17, KeySym.F17 => (Key.F17, null),
KeySym.F18 => Key.F18, KeySym.F18 => (Key.F18, null),
KeySym.F19 => Key.F19, KeySym.F19 => (Key.F19, null),
KeySym.F20 => Key.F20, KeySym.F20 => (Key.F20, null),
KeySym.F21 => Key.F21, KeySym.F21 => (Key.F21, null),
KeySym.F22 => Key.F22, KeySym.F22 => (Key.F22, null),
KeySym.F23 => Key.F23, KeySym.F23 => (Key.F23, null),
KeySym.F24 => Key.F24, KeySym.F24 => (Key.F24, null),
KeySym.ShiftLeft => Key.LeftShift, KeySym.ShiftLeft => (Key.LeftShift, null),
KeySym.ShiftRight => Key.RightShift, KeySym.ShiftRight => (Key.RightShift, null),
KeySym.ControlLeft => Key.LeftCtrl, KeySym.ControlLeft => (Key.LeftCtrl, null),
KeySym.ControlRight => Key.RightCtrl, KeySym.ControlRight => (Key.RightCtrl, null),
KeySym.CapsLock => Key.CapsLock, KeySym.CapsLock => (Key.CapsLock, null),
KeySym.AltLeft => Key.LeftAlt, KeySym.AltLeft => (Key.LeftAlt, null),
KeySym.AltRight => Key.RightAlt, KeySym.AltRight => (Key.RightAlt, null),
KeySym.Space => Key.Space, KeySym.Space => (Key.Space, " "),
KeySym.Exclamation => Key.D1, KeySym.Exclamation => (Key.D1, "!"),
KeySym.Quote => Key.D2, KeySym.Quote => (Key.D2, "\""),
KeySym.NumberSign => Key.D3, KeySym.NumberSign => (Key.D3, "#"),
KeySym.Dollar => Key.D4, KeySym.Dollar => (Key.D4, "$"),
KeySym.Percent => Key.D5, KeySym.Percent => (Key.D5, "%"),
KeySym.Ampersand => Key.D7, KeySym.Ampersand => (Key.D7, "&"),
KeySym.Apostrophe => Key.Oem3, KeySym.Apostrophe => (Key.Oem3, "'"),
KeySym.ParenthesisLeft => Key.D9, KeySym.ParenthesisLeft => (Key.D9, "("),
KeySym.ParenthesisRight => Key.D0, KeySym.ParenthesisRight => (Key.D0, ")"),
KeySym.Asterisk => Key.D8, KeySym.Asterisk => (Key.D8, "*"),
KeySym.Plus => Key.OemPlus, KeySym.Plus => (Key.OemPlus, "+"),
KeySym.Comma => Key.OemComma, KeySym.Comma => (Key.OemComma, ","),
KeySym.Minus => Key.OemMinus, KeySym.Minus => (Key.OemMinus, "-"),
KeySym.Period => Key.OemPeriod, KeySym.Period => (Key.OemPeriod, "."),
KeySym.Slash => Key.OemQuestion, KeySym.Slash => (Key.OemQuestion, "/"),
KeySym.D0 => Key.D0, KeySym.D0 => (Key.D0, "0"),
KeySym.D1 => Key.D1, KeySym.D1 => (Key.D1, "1"),
KeySym.D2 => Key.D2, KeySym.D2 => (Key.D2, "2"),
KeySym.D3 => Key.D3, KeySym.D3 => (Key.D3, "3"),
KeySym.D4 => Key.D4, KeySym.D4 => (Key.D4, "4"),
KeySym.D5 => Key.D5, KeySym.D5 => (Key.D5, "5"),
KeySym.D6 => Key.D6, KeySym.D6 => (Key.D6, "6"),
KeySym.D7 => Key.D7, KeySym.D7 => (Key.D7, "7"),
KeySym.D8 => Key.D8, KeySym.D8 => (Key.D8, "8"),
KeySym.D9 => Key.D9, KeySym.D9 => (Key.D9, "9"),
KeySym.Colon => Key.OemSemicolon, KeySym.Colon => (Key.OemSemicolon, ":"),
KeySym.Semicolon => Key.OemSemicolon, KeySym.Semicolon => (Key.OemSemicolon, ";"),
KeySym.Less => Key.OemComma, KeySym.Less => (Key.OemComma, "<"),
KeySym.Equal => Key.OemPlus, KeySym.Equal => (Key.OemPlus, "="),
KeySym.Greater => Key.OemPeriod, KeySym.Greater => (Key.OemPeriod, ">"),
KeySym.Question => Key.OemQuestion, KeySym.Question => (Key.OemQuestion, "?"),
KeySym.At => Key.Oem3, KeySym.At => (Key.Oem3, "@"),
KeySym.A => Key.A, KeySym.A => (Key.A, "A"),
KeySym.B => Key.B, KeySym.B => (Key.B, "B"),
KeySym.C => Key.C, KeySym.C => (Key.C, "C"),
KeySym.D => Key.D, KeySym.D => (Key.D, "D"),
KeySym.E => Key.E, KeySym.E => (Key.E, "E"),
KeySym.F => Key.F, KeySym.F => (Key.F, "F"),
KeySym.G => Key.G, KeySym.G => (Key.G, "G"),
KeySym.H => Key.H, KeySym.H => (Key.H, "H"),
KeySym.I => Key.I, KeySym.I => (Key.I, "I"),
KeySym.J => Key.J, KeySym.J => (Key.J, "J"),
KeySym.K => Key.K, KeySym.K => (Key.K, "K"),
KeySym.L => Key.L, KeySym.L => (Key.L, "L"),
KeySym.M => Key.M, KeySym.M => (Key.M, "M"),
KeySym.N => Key.N, KeySym.N => (Key.N, "N"),
KeySym.O => Key.O, KeySym.O => (Key.O, "O"),
KeySym.P => Key.P, KeySym.P => (Key.P, "P"),
KeySym.Q => Key.Q, KeySym.Q => (Key.Q, "Q"),
KeySym.R => Key.R, KeySym.R => (Key.R, "R"),
KeySym.S => Key.S, KeySym.S => (Key.S, "S"),
KeySym.T => Key.T, KeySym.T => (Key.T, "T"),
KeySym.U => Key.U, KeySym.U => (Key.U, "U"),
KeySym.V => Key.V, KeySym.V => (Key.V, "V"),
KeySym.W => Key.W, KeySym.W => (Key.W, "W"),
KeySym.X => Key.X, KeySym.X => (Key.X, "X"),
KeySym.Y => Key.Y, KeySym.Y => (Key.Y, "Y"),
KeySym.Z => Key.Z, KeySym.Z => (Key.Z, "Z"),
KeySym.BracketLeft => Key.OemOpenBrackets, KeySym.BracketLeft => (Key.OemOpenBrackets, "["),
KeySym.Backslash => Key.OemPipe, KeySym.Backslash => (Key.OemPipe, "\\"),
KeySym.Bracketright => Key.OemCloseBrackets, KeySym.Bracketright => (Key.OemCloseBrackets, "]"),
KeySym.Underscore => Key.OemMinus, KeySym.Underscore => (Key.OemMinus, "_"),
KeySym.Grave => Key.Oem8, KeySym.Grave => (Key.Oem8, "`"),
KeySym.a => Key.A, KeySym.a => (Key.A, "a"),
KeySym.b => Key.B, KeySym.b => (Key.B, "b"),
KeySym.c => Key.C, KeySym.c => (Key.C, "c"),
KeySym.d => Key.D, KeySym.d => (Key.D, "d"),
KeySym.e => Key.E, KeySym.e => (Key.E, "e"),
KeySym.f => Key.F, KeySym.f => (Key.F, "f"),
KeySym.g => Key.G, KeySym.g => (Key.G, "g"),
KeySym.h => Key.H, KeySym.h => (Key.H, "h"),
KeySym.i => Key.I, KeySym.i => (Key.I, "i"),
KeySym.j => Key.J, KeySym.j => (Key.J, "j"),
KeySym.k => Key.K, KeySym.k => (Key.K, "k"),
KeySym.l => Key.L, KeySym.l => (Key.L, "l"),
KeySym.m => Key.M, KeySym.m => (Key.M, "m"),
KeySym.n => Key.M, KeySym.n => (Key.M, "n"),
KeySym.o => Key.O, KeySym.o => (Key.O, "o"),
KeySym.p => Key.P, KeySym.p => (Key.P, "p"),
KeySym.q => Key.Q, KeySym.q => (Key.Q, "q"),
KeySym.r => Key.R, KeySym.r => (Key.R, "r"),
KeySym.s => Key.S, KeySym.s => (Key.S, "s"),
KeySym.t => Key.T, KeySym.t => (Key.T, "t"),
KeySym.u => Key.U, KeySym.u => (Key.U, "u"),
KeySym.v => Key.V, KeySym.v => (Key.V, "v"),
KeySym.w => Key.W, KeySym.w => (Key.W, "w"),
KeySym.x => Key.X, KeySym.x => (Key.X, "x"),
KeySym.y => Key.Y, KeySym.y => (Key.Y, "y"),
KeySym.z => Key.Z, KeySym.z => (Key.Z, "z"),
KeySym.BraceLeft => Key.OemOpenBrackets, KeySym.BraceLeft => (Key.OemOpenBrackets, "{"),
KeySym.Bar => Key.OemPipe, KeySym.Bar => (Key.OemPipe, "|"),
KeySym.BraceRight => Key.OemCloseBrackets, KeySym.BraceRight => (Key.OemCloseBrackets, "}"),
KeySym.AsciiTilde => Key.OemTilde, KeySym.AsciiTilde => (Key.OemTilde, "~"),
_ => null _ => (Key.None, null)
}; };

32
src/Headless/Avalonia.Headless/HeadlessWindowExtensions.cs

@ -43,14 +43,42 @@ public static class HeadlessWindowExtensions
/// <summary> /// <summary>
/// Simulates keyboard press on the headless window/toplevel. /// Simulates keyboard press on the headless window/toplevel.
/// </summary> /// </summary>
[Obsolete("Use the overload that takes a physical key and key symbol instead, or KeyPressQwerty alternatively.")]
public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
RunJobsOnImpl(topLevel, w => w.KeyPress(key, modifiers)); KeyPress(topLevel, key, modifiers, PhysicalKey.None, null);
/// <summary>
/// Simulates keyboard press on the headless window/toplevel.
/// </summary>
public static void KeyPress(this TopLevel topLevel, Key key, RawInputModifiers modifiers, PhysicalKey physicalKey,
string? keySymbol) =>
RunJobsOnImpl(topLevel, w => w.KeyPress(key, modifiers, physicalKey, keySymbol));
/// <summary>
/// Simulates keyboard press on the headless window/toplevel, as if typed on a QWERTY keyboard.
/// </summary>
public static void KeyPressQwerty(this TopLevel topLevel, PhysicalKey physicalKey, RawInputModifiers modifiers) =>
RunJobsOnImpl(topLevel, w => w.KeyPress(physicalKey.ToQwertyKey(), modifiers, physicalKey, physicalKey.ToQwertyKeySymbol()));
/// <summary> /// <summary>
/// Simulates keyboard release on the headless window/toplevel. /// Simulates keyboard release on the headless window/toplevel.
/// </summary> /// </summary>
[Obsolete("Use the overload that takes a physical key and key symbol instead, or KeyReleaseQwerty alternatively.")]
public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) => public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers) =>
RunJobsOnImpl(topLevel, w => w.KeyRelease(key, modifiers)); KeyRelease(topLevel, key, modifiers, PhysicalKey.None, null);
/// <summary>
/// Simulates keyboard release on the headless window/toplevel.
/// </summary>
public static void KeyRelease(this TopLevel topLevel, Key key, RawInputModifiers modifiers, PhysicalKey physicalKey,
string? keySymbol) =>
RunJobsOnImpl(topLevel, w => w.KeyRelease(key, modifiers, physicalKey, keySymbol));
/// <summary>
/// Simulates keyboard release on the headless window/toplevel, as if typed on a QWERTY keyboard.
/// </summary>
public static void KeyReleaseQwerty(this TopLevel topLevel, PhysicalKey physicalKey, RawInputModifiers modifiers) =>
RunJobsOnImpl(topLevel, w => w.KeyRelease(physicalKey.ToQwertyKey(), modifiers, physicalKey, physicalKey.ToQwertyKeySymbol()));
/// <summary> /// <summary>
/// Simulates a text input event on the headless window/toplevel /// Simulates a text input event on the headless window/toplevel

27
src/Headless/Avalonia.Headless/HeadlessWindowImpl.cs

@ -9,11 +9,8 @@ using Avalonia.Input.Platform;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Utilities;
namespace Avalonia.Headless namespace Avalonia.Headless
{ {
@ -271,14 +268,30 @@ namespace Avalonia.Headless
return null; return null;
} }
void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers) void IHeadlessWindow.KeyPress(Key key, RawInputModifiers modifiers, PhysicalKey physicalKey, string? keySymbol)
{ {
Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyDown, key, modifiers)); Input?.Invoke(new RawKeyEventArgs(
_keyboard,
Timestamp,
InputRoot!,
RawKeyEventType.KeyDown,
key,
modifiers,
physicalKey,
keySymbol));
} }
void IHeadlessWindow.KeyRelease(Key key, RawInputModifiers modifiers) void IHeadlessWindow.KeyRelease(Key key, RawInputModifiers modifiers, PhysicalKey physicalKey, string? keySymbol)
{ {
Input?.Invoke(new RawKeyEventArgs(_keyboard, Timestamp, InputRoot!, RawKeyEventType.KeyUp, key, modifiers)); Input?.Invoke(new RawKeyEventArgs(
_keyboard,
Timestamp,
InputRoot!,
RawKeyEventType.KeyUp,
key,
modifiers,
physicalKey,
keySymbol));
} }
void IHeadlessWindow.TextInput(string text) void IHeadlessWindow.TextInput(string text)

6
src/Headless/Avalonia.Headless/IHeadlessWindow.cs

@ -1,16 +1,14 @@
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Utilities;
namespace Avalonia.Headless namespace Avalonia.Headless
{ {
internal interface IHeadlessWindow internal interface IHeadlessWindow
{ {
WriteableBitmap? GetLastRenderedFrame(); WriteableBitmap? GetLastRenderedFrame();
void KeyPress(Key key, RawInputModifiers modifiers); void KeyPress(Key key, RawInputModifiers modifiers, PhysicalKey physicalKey, string? keySymbol);
void KeyRelease(Key key, RawInputModifiers modifiers); void KeyRelease(Key key, RawInputModifiers modifiers, PhysicalKey physicalKey, string? keySymbol);
void TextInput(string text); void TextInput(string text);
void MouseDown(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None); void MouseDown(Point point, MouseButton button, RawInputModifiers modifiers = RawInputModifiers.None);
void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None); void MouseMove(Point point, RawInputModifiers modifiers = RawInputModifiers.None);

8
src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs

@ -327,7 +327,7 @@ namespace Avalonia.Win32.Input
if (Client.SupportsSurroundingText && Client.Selection.Start != Client.Selection.End) if (Client.SupportsSurroundingText && Client.Selection.Start != Client.Selection.End)
{ {
KeyPress(Key.Delete); KeyPress(Key.Delete, PhysicalKey.Delete);
} }
} }
@ -398,15 +398,15 @@ namespace Avalonia.Win32.Input
return (int)(ptr.ToInt64() & 0xffffffff); return (int)(ptr.ToInt64() & 0xffffffff);
} }
private void KeyPress(Key key) private void KeyPress(Key key, PhysicalKey physicalKey)
{ {
if (_parent?.Input != null) if (_parent?.Input != null)
{ {
_parent.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, (ulong)DateTime.Now.Ticks, _parent.Owner, _parent.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, (ulong)DateTime.Now.Ticks, _parent.Owner,
RawKeyEventType.KeyDown, key, RawInputModifiers.None)); RawKeyEventType.KeyDown, key, RawInputModifiers.None, physicalKey, null));
_parent.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, (ulong)DateTime.Now.Ticks, _parent.Owner, _parent.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, (ulong)DateTime.Now.Ticks, _parent.Owner,
RawKeyEventType.KeyUp, key, RawInputModifiers.None)); RawKeyEventType.KeyUp, key, RawInputModifiers.None, physicalKey, null));
} }
} }

816
src/Windows/Avalonia.Win32/Input/KeyInterop.cs

@ -1,363 +1,376 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods;
using static Avalonia.Win32.Interop.UnmanagedMethods.VirtualKeyStates;
namespace Avalonia.Win32.Input namespace Avalonia.Win32.Input
{ {
/// <summary>
/// Contains methods used to translate a Windows virtual/physical key to an Avalonia <see cref="Key"/>.
/// </summary>
public static class KeyInterop public static class KeyInterop
{ {
private static readonly Dictionary<Key, int> s_virtualKeyFromKey = new Dictionary<Key, int> // source: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
private static readonly Dictionary<Key, int> s_virtualKeyFromKey = new(169)
{ {
{ Key.None, 0 }, { Key.Cancel, (int)VK_CANCEL },
{ Key.Cancel, 3 }, { Key.Back, (int)VK_BACK },
{ Key.Back, 8 }, { Key.Tab, (int)VK_TAB },
{ Key.Tab, 9 }, { Key.Clear, (int)VK_CLEAR },
{ Key.LineFeed, 0 }, { Key.Return, (int)VK_RETURN },
{ Key.Clear, 12 }, { Key.Pause, (int)VK_PAUSE },
{ Key.Return, 13 }, { Key.Capital, (int)VK_CAPITAL },
{ Key.Pause, 19 }, { Key.KanaMode, (int)VK_KANA },
{ Key.Capital, 20 }, { Key.JunjaMode, (int)VK_JUNJA },
{ Key.KanaMode, 21 }, { Key.FinalMode, (int)VK_FINAL },
{ Key.JunjaMode, 23 }, { Key.HanjaMode, (int)VK_HANJA },
{ Key.FinalMode, 24 }, { Key.Escape, (int)VK_ESCAPE },
{ Key.HanjaMode, 25 }, { Key.ImeConvert, (int)VK_CONVERT },
{ Key.Escape, 27 }, { Key.ImeNonConvert, (int)VK_NONCONVERT },
{ Key.ImeConvert, 28 }, { Key.ImeAccept, (int)VK_ACCEPT },
{ Key.ImeNonConvert, 29 }, { Key.ImeModeChange, (int)VK_MODECHANGE },
{ Key.ImeAccept, 30 }, { Key.Space, (int)VK_SPACE },
{ Key.ImeModeChange, 31 }, { Key.PageUp, (int)VK_PRIOR },
{ Key.Space, 32 }, { Key.PageDown, (int)VK_NEXT },
{ Key.PageUp, 33 }, { Key.End, (int)VK_END },
{ Key.Next, 34 }, { Key.Home, (int)VK_HOME },
{ Key.End, 35 }, { Key.Left, (int)VK_LEFT },
{ Key.Home, 36 }, { Key.Up, (int)VK_UP },
{ Key.Left, 37 }, { Key.Right, (int)VK_RIGHT },
{ Key.Up, 38 }, { Key.Down, (int)VK_DOWN },
{ Key.Right, 39 }, { Key.Select, (int)VK_SELECT },
{ Key.Down, 40 }, { Key.Print, (int)VK_PRINT },
{ Key.Select, 41 }, { Key.Execute, (int)VK_EXECUTE },
{ Key.Print, 42 }, { Key.Snapshot, (int)VK_SNAPSHOT },
{ Key.Execute, 43 }, { Key.Insert, (int)VK_INSERT },
{ Key.Snapshot, 44 }, { Key.Delete, (int)VK_DELETE },
{ Key.Insert, 45 }, { Key.Help, (int)VK_HELP },
{ Key.Delete, 46 }, { Key.D0, '0' },
{ Key.Help, 47 }, { Key.D1, '1' },
{ Key.D0, 48 }, { Key.D2, '2' },
{ Key.D1, 49 }, { Key.D3, '3' },
{ Key.D2, 50 }, { Key.D4, '4' },
{ Key.D3, 51 }, { Key.D5, '5' },
{ Key.D4, 52 }, { Key.D6, '6' },
{ Key.D5, 53 }, { Key.D7, '7' },
{ Key.D6, 54 }, { Key.D8, '8' },
{ Key.D7, 55 }, { Key.D9, '9' },
{ Key.D8, 56 }, { Key.A, 'A' },
{ Key.D9, 57 }, { Key.B, 'B' },
{ Key.A, 65 }, { Key.C, 'C' },
{ Key.B, 66 }, { Key.D, 'D' },
{ Key.C, 67 }, { Key.E, 'E' },
{ Key.D, 68 }, { Key.F, 'F' },
{ Key.E, 69 }, { Key.G, 'G' },
{ Key.F, 70 }, { Key.H, 'H' },
{ Key.G, 71 }, { Key.I, 'I' },
{ Key.H, 72 }, { Key.J, 'J' },
{ Key.I, 73 }, { Key.K, 'K' },
{ Key.J, 74 }, { Key.L, 'L' },
{ Key.K, 75 }, { Key.M, 'M' },
{ Key.L, 76 }, { Key.N, 'N' },
{ Key.M, 77 }, { Key.O, 'O' },
{ Key.N, 78 }, { Key.P, 'P' },
{ Key.O, 79 }, { Key.Q, 'Q' },
{ Key.P, 80 }, { Key.R, 'R' },
{ Key.Q, 81 }, { Key.S, 'S' },
{ Key.R, 82 }, { Key.T, 'T' },
{ Key.S, 83 }, { Key.U, 'U' },
{ Key.T, 84 }, { Key.V, 'V' },
{ Key.U, 85 }, { Key.W, 'W' },
{ Key.V, 86 }, { Key.X, 'X' },
{ Key.W, 87 }, { Key.Y, 'Y' },
{ Key.X, 88 }, { Key.Z, 'Z' },
{ Key.Y, 89 }, { Key.LWin, (int)VK_LWIN },
{ Key.Z, 90 }, { Key.RWin, (int)VK_RWIN },
{ Key.LWin, 91 }, { Key.Apps, (int)VK_APPS },
{ Key.RWin, 92 }, { Key.Sleep, (int)VK_SLEEP },
{ Key.Apps, 93 }, { Key.NumPad0, (int)VK_NUMPAD0 },
{ Key.Sleep, 95 }, { Key.NumPad1, (int)VK_NUMPAD1 },
{ Key.NumPad0, 96 }, { Key.NumPad2, (int)VK_NUMPAD2 },
{ Key.NumPad1, 97 }, { Key.NumPad3, (int)VK_NUMPAD3 },
{ Key.NumPad2, 98 }, { Key.NumPad4, (int)VK_NUMPAD4 },
{ Key.NumPad3, 99 }, { Key.NumPad5, (int)VK_NUMPAD5 },
{ Key.NumPad4, 100 }, { Key.NumPad6, (int)VK_NUMPAD6 },
{ Key.NumPad5, 101 }, { Key.NumPad7, (int)VK_NUMPAD7 },
{ Key.NumPad6, 102 }, { Key.NumPad8, (int)VK_NUMPAD8 },
{ Key.NumPad7, 103 }, { Key.NumPad9, (int)VK_NUMPAD9 },
{ Key.NumPad8, 104 }, { Key.Multiply, (int)VK_MULTIPLY },
{ Key.NumPad9, 105 }, { Key.Add, (int)VK_ADD },
{ Key.Multiply, 106 }, { Key.Separator, (int)VK_SEPARATOR },
{ Key.Add, 107 }, { Key.Subtract, (int)VK_SUBTRACT },
{ Key.Separator, 108 }, { Key.Decimal, (int)VK_DECIMAL },
{ Key.Subtract, 109 }, { Key.Divide, (int)VK_DIVIDE },
{ Key.Decimal, 110 }, { Key.F1, (int)VK_F1 },
{ Key.Divide, 111 }, { Key.F2, (int)VK_F2 },
{ Key.F1, 112 }, { Key.F3, (int)VK_F3 },
{ Key.F2, 113 }, { Key.F4, (int)VK_F4 },
{ Key.F3, 114 }, { Key.F5, (int)VK_F5 },
{ Key.F4, 115 }, { Key.F6, (int)VK_F6 },
{ Key.F5, 116 }, { Key.F7, (int)VK_F7 },
{ Key.F6, 117 }, { Key.F8, (int)VK_F8 },
{ Key.F7, 118 }, { Key.F9, (int)VK_F9 },
{ Key.F8, 119 }, { Key.F10, (int)VK_F10 },
{ Key.F9, 120 }, { Key.F11, (int)VK_F11 },
{ Key.F10, 121 }, { Key.F12, (int)VK_F12 },
{ Key.F11, 122 }, { Key.F13, (int)VK_F13 },
{ Key.F12, 123 }, { Key.F14, (int)VK_F14 },
{ Key.F13, 124 }, { Key.F15, (int)VK_F15 },
{ Key.F14, 125 }, { Key.F16, (int)VK_F16 },
{ Key.F15, 126 }, { Key.F17, (int)VK_F17 },
{ Key.F16, 127 }, { Key.F18, (int)VK_F18 },
{ Key.F17, 128 }, { Key.F19, (int)VK_F19 },
{ Key.F18, 129 }, { Key.F20, (int)VK_F20 },
{ Key.F19, 130 }, { Key.F21, (int)VK_F21 },
{ Key.F20, 131 }, { Key.F22, (int)VK_F22 },
{ Key.F21, 132 }, { Key.F23, (int)VK_F23 },
{ Key.F22, 133 }, { Key.F24, (int)VK_F24 },
{ Key.F23, 134 }, { Key.NumLock, (int)VK_NUMLOCK },
{ Key.F24, 135 }, { Key.Scroll, (int)VK_SCROLL },
{ Key.NumLock, 144 }, { Key.LeftShift, (int)VK_LSHIFT },
{ Key.Scroll, 145 }, { Key.RightShift, (int)VK_RSHIFT },
{ Key.LeftShift, 160 }, { Key.LeftCtrl, (int)VK_LCONTROL },
{ Key.RightShift, 161 }, { Key.RightCtrl, (int)VK_RCONTROL },
{ Key.LeftCtrl, 162 }, { Key.LeftAlt, (int)VK_LMENU },
{ Key.RightCtrl, 163 }, { Key.RightAlt, (int)VK_RMENU },
{ Key.LeftAlt, 164 }, { Key.BrowserBack, (int)VK_BROWSER_BACK },
{ Key.RightAlt, 165 }, { Key.BrowserForward, (int)VK_BROWSER_FORWARD },
{ Key.BrowserBack, 166 }, { Key.BrowserRefresh, (int)VK_BROWSER_REFRESH },
{ Key.BrowserForward, 167 }, { Key.BrowserStop, (int)VK_BROWSER_STOP },
{ Key.BrowserRefresh, 168 }, { Key.BrowserSearch, (int)VK_BROWSER_SEARCH },
{ Key.BrowserStop, 169 }, { Key.BrowserFavorites, (int)VK_BROWSER_FAVORITES },
{ Key.BrowserSearch, 170 }, { Key.BrowserHome, (int)VK_BROWSER_HOME },
{ Key.BrowserFavorites, 171 }, { Key.VolumeMute, (int)VK_VOLUME_MUTE },
{ Key.BrowserHome, 172 }, { Key.VolumeDown, (int)VK_VOLUME_DOWN },
{ Key.VolumeMute, 173 }, { Key.VolumeUp, (int)VK_VOLUME_UP },
{ Key.VolumeDown, 174 }, { Key.MediaNextTrack, (int)VK_MEDIA_NEXT_TRACK },
{ Key.VolumeUp, 175 }, { Key.MediaPreviousTrack, (int)VK_MEDIA_PREV_TRACK },
{ Key.MediaNextTrack, 176 }, { Key.MediaStop, (int)VK_MEDIA_STOP },
{ Key.MediaPreviousTrack, 177 }, { Key.MediaPlayPause, (int)VK_MEDIA_PLAY_PAUSE },
{ Key.MediaStop, 178 }, { Key.LaunchMail, (int)VK_LAUNCH_MAIL },
{ Key.MediaPlayPause, 179 }, { Key.SelectMedia, (int)VK_LAUNCH_MEDIA_SELECT },
{ Key.LaunchMail, 180 }, { Key.LaunchApplication1, (int)VK_LAUNCH_APP1 },
{ Key.SelectMedia, 181 }, { Key.LaunchApplication2, (int)VK_LAUNCH_APP2 },
{ Key.LaunchApplication1, 182 }, { Key.Oem1, (int)VK_OEM_1 },
{ Key.LaunchApplication2, 183 }, { Key.OemPlus, (int)VK_OEM_PLUS },
{ Key.Oem1, 186 }, { Key.OemComma, (int)VK_OEM_COMMA },
{ Key.OemPlus, 187 }, { Key.OemMinus, (int)VK_OEM_MINUS },
{ Key.OemComma, 188 }, { Key.OemPeriod, (int)VK_OEM_PERIOD },
{ Key.OemMinus, 189 }, { Key.OemQuestion, (int)VK_OEM_2 },
{ Key.OemPeriod, 190 }, { Key.Oem3, (int)VK_OEM_3 },
{ Key.OemQuestion, 191 }, { Key.AbntC1, (int)VK_ABNT_C1 },
{ Key.Oem3, 192 }, { Key.AbntC2, (int)VK_ABNT_C2 },
{ Key.AbntC1, 193 }, { Key.OemOpenBrackets, (int)VK_OEM_4 },
{ Key.AbntC2, 194 }, { Key.Oem5, (int)VK_OEM_5 },
{ Key.OemOpenBrackets, 219 }, { Key.Oem6, (int)VK_OEM_6 },
{ Key.Oem5, 220 }, { Key.OemQuotes, (int)VK_OEM_7 },
{ Key.Oem6, 221 }, { Key.Oem8, (int)VK_OEM_8 },
{ Key.OemQuotes, 222 }, { Key.OemBackslash, (int)VK_OEM_102 },
{ Key.Oem8, 223 }, { Key.ImeProcessed, (int)VK_PROCESSKEY },
{ Key.OemBackslash, 226 }, { Key.OemAttn, (int)VK_OEM_ATTN },
{ Key.ImeProcessed, 229 }, { Key.OemFinish, (int)VK_OEM_FINISH },
{ Key.System, 0 }, { Key.OemCopy, (int)VK_OEM_COPY },
{ Key.OemAttn, 240 }, { Key.DbeSbcsChar, (int)VK_OEM_AUTO },
{ Key.OemFinish, 241 }, { Key.OemEnlw, (int)VK_OEM_ENLW },
{ Key.OemCopy, 242 }, { Key.OemBackTab, (int)VK_OEM_BACKTAB },
{ Key.DbeSbcsChar, 243 }, { Key.DbeNoRoman, (int)VK_ATTN },
{ Key.OemEnlw, 244 }, { Key.DbeEnterWordRegisterMode, (int)VK_CRSEL },
{ Key.OemBackTab, 245 }, { Key.DbeEnterImeConfigureMode, (int)VK_EXSEL },
{ Key.DbeNoRoman, 246 }, { Key.EraseEof, (int)VK_EREOF },
{ Key.DbeEnterWordRegisterMode, 247 }, { Key.Play, (int)VK_PLAY },
{ Key.DbeEnterImeConfigureMode, 248 }, { Key.DbeNoCodeInput, (int)VK_ZOOM },
{ Key.EraseEof, 249 }, { Key.NoName, (int)VK_NONAME },
{ Key.Play, 250 }, { Key.Pa1, (int)VK_PA1 },
{ Key.DbeNoCodeInput, 251 }, { Key.OemClear, (int)VK_OEM_CLEAR }
{ Key.NoName, 252 },
{ Key.Pa1, 253 },
{ Key.OemClear, 254 },
{ Key.DeadCharProcessed, 0 },
}; };
private static readonly Dictionary<int, Key> s_keyFromVirtualKey = new Dictionary<int, Key> private static readonly Dictionary<int, Key> s_keyFromVirtualKey =
s_virtualKeyFromKey.ToDictionary(pair => pair.Value, pair => pair.Key);
// https://learn.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input#scan-codes
// https://github.com/chromium/chromium/blob/main/ui/events/keycodes/dom/dom_code_data.inc
// This list has the same order as the PhysicalKey enum.
private static readonly Dictionary<ushort, PhysicalKey> s_physicalKeyFromExtendedScanCode = new(155)
{ {
{ 0, Key.None }, // Writing System Keys
{ 3, Key.Cancel }, { 0x0029, PhysicalKey.Backquote },
{ 8, Key.Back }, { 0x002B, PhysicalKey.Backslash },
{ 9, Key.Tab }, { 0x001A, PhysicalKey.BracketLeft },
{ 12, Key.Clear }, { 0x001B, PhysicalKey.BracketRight },
{ 13, Key.Return }, { 0x0033, PhysicalKey.Comma },
{ 16, Key.LeftShift}, { 0x000B, PhysicalKey.Digit0 },
{ 17, Key.LeftCtrl}, { 0x0002, PhysicalKey.Digit1 },
{ 18, Key.LeftAlt }, { 0x0003, PhysicalKey.Digit2 },
{ 19, Key.Pause }, { 0x0004, PhysicalKey.Digit3 },
{ 20, Key.Capital }, { 0x0005, PhysicalKey.Digit4 },
{ 21, Key.KanaMode }, { 0x0006, PhysicalKey.Digit5 },
{ 23, Key.JunjaMode }, { 0x0007, PhysicalKey.Digit6 },
{ 24, Key.FinalMode }, { 0x0008, PhysicalKey.Digit7 },
{ 25, Key.HanjaMode }, { 0x0009, PhysicalKey.Digit8 },
{ 27, Key.Escape }, { 0x000A, PhysicalKey.Digit9 },
{ 28, Key.ImeConvert }, { 0x000D, PhysicalKey.Equal },
{ 29, Key.ImeNonConvert }, { 0x0056, PhysicalKey.IntlBackslash },
{ 30, Key.ImeAccept }, { 0x0073, PhysicalKey.IntlRo },
{ 31, Key.ImeModeChange }, { 0x007D, PhysicalKey.IntlYen },
{ 32, Key.Space }, { 0x001E, PhysicalKey.A },
{ 33, Key.PageUp }, { 0x0030, PhysicalKey.B },
{ 34, Key.PageDown }, { 0x002E, PhysicalKey.C },
{ 35, Key.End }, { 0x0020, PhysicalKey.D },
{ 36, Key.Home }, { 0x0012, PhysicalKey.E },
{ 37, Key.Left }, { 0x0021, PhysicalKey.F },
{ 38, Key.Up }, { 0x0022, PhysicalKey.G },
{ 39, Key.Right }, { 0x0023, PhysicalKey.H },
{ 40, Key.Down }, { 0x0017, PhysicalKey.I },
{ 41, Key.Select }, { 0x0024, PhysicalKey.J },
{ 42, Key.Print }, { 0x0025, PhysicalKey.K },
{ 43, Key.Execute }, { 0x0026, PhysicalKey.L },
{ 44, Key.Snapshot }, { 0x0032, PhysicalKey.M },
{ 45, Key.Insert }, { 0x0031, PhysicalKey.N },
{ 46, Key.Delete }, { 0x0018, PhysicalKey.O },
{ 47, Key.Help }, { 0x0019, PhysicalKey.P },
{ 48, Key.D0 }, { 0x0010, PhysicalKey.Q },
{ 49, Key.D1 }, { 0x0013, PhysicalKey.R },
{ 50, Key.D2 }, { 0x001F, PhysicalKey.S },
{ 51, Key.D3 }, { 0x0014, PhysicalKey.T },
{ 52, Key.D4 }, { 0x0016, PhysicalKey.U },
{ 53, Key.D5 }, { 0x002F, PhysicalKey.V },
{ 54, Key.D6 }, { 0x0011, PhysicalKey.W },
{ 55, Key.D7 }, { 0x002D, PhysicalKey.X },
{ 56, Key.D8 }, { 0x0015, PhysicalKey.Y },
{ 57, Key.D9 }, { 0x002C, PhysicalKey.Z },
{ 65, Key.A }, { 0x000C, PhysicalKey.Minus },
{ 66, Key.B }, { 0x0034, PhysicalKey.Period },
{ 67, Key.C }, { 0x0028, PhysicalKey.Quote },
{ 68, Key.D }, { 0x0027, PhysicalKey.Semicolon },
{ 69, Key.E }, { 0x0035, PhysicalKey.Slash },
{ 70, Key.F },
{ 71, Key.G }, // Functional Keys
{ 72, Key.H }, { 0x0038, PhysicalKey.AltLeft },
{ 73, Key.I }, { 0xE038, PhysicalKey.AltRight },
{ 74, Key.J }, { 0x000E, PhysicalKey.Backspace },
{ 75, Key.K }, { 0x003A, PhysicalKey.CapsLock },
{ 76, Key.L }, { 0xE05D, PhysicalKey.ContextMenu },
{ 77, Key.M }, { 0x001D, PhysicalKey.ControlLeft },
{ 78, Key.N }, { 0xE01D, PhysicalKey.ControlRight },
{ 79, Key.O }, { 0x001C, PhysicalKey.Enter },
{ 80, Key.P }, { 0xE05B, PhysicalKey.MetaLeft },
{ 81, Key.Q }, { 0xE05C, PhysicalKey.MetaRight },
{ 82, Key.R }, { 0x002A, PhysicalKey.ShiftLeft },
{ 83, Key.S }, { 0x0036, PhysicalKey.ShiftRight },
{ 84, Key.T }, { 0x0039, PhysicalKey.Space },
{ 85, Key.U }, { 0x000F, PhysicalKey.Tab },
{ 86, Key.V }, { 0x0079, PhysicalKey.Convert },
{ 87, Key.W }, { 0x0070, PhysicalKey.KanaMode },
{ 88, Key.X }, { 0x0072, PhysicalKey.Lang1 },
{ 89, Key.Y }, { 0x0071, PhysicalKey.Lang2 },
{ 90, Key.Z }, { 0x0078, PhysicalKey.Lang3 },
{ 91, Key.LWin }, { 0x0077, PhysicalKey.Lang4 },
{ 92, Key.RWin }, //{ , PhysicalKey.Lang5 }, Not mapped on Windows since it's the same as F24 (see Chromium remarks)
{ 93, Key.Apps }, { 0x007B, PhysicalKey.NonConvert },
{ 95, Key.Sleep },
{ 96, Key.NumPad0 }, // Control Pad Section
{ 97, Key.NumPad1 }, { 0xE053, PhysicalKey.Delete },
{ 98, Key.NumPad2 }, { 0xE04F, PhysicalKey.End },
{ 99, Key.NumPad3 }, { 0xE03B, PhysicalKey.Help },
{ 100, Key.NumPad4 }, { 0xE047, PhysicalKey.Home },
{ 101, Key.NumPad5 }, { 0xE052, PhysicalKey.Insert },
{ 102, Key.NumPad6 }, { 0xE051, PhysicalKey.PageDown },
{ 103, Key.NumPad7 }, { 0xE049, PhysicalKey.PageUp },
{ 104, Key.NumPad8 },
{ 105, Key.NumPad9 }, // Arrow Pad Section
{ 106, Key.Multiply }, { 0xE050, PhysicalKey.ArrowDown },
{ 107, Key.Add }, { 0xE04B, PhysicalKey.ArrowLeft },
{ 108, Key.Separator }, { 0xE04D, PhysicalKey.ArrowRight },
{ 109, Key.Subtract }, { 0xE048, PhysicalKey.ArrowUp },
{ 110, Key.Decimal },
{ 111, Key.Divide }, // Numpad Section
{ 112, Key.F1 }, { 0xE045, PhysicalKey.NumLock },
{ 113, Key.F2 }, { 0x0052, PhysicalKey.NumPad0 },
{ 114, Key.F3 }, { 0x004F, PhysicalKey.NumPad1 },
{ 115, Key.F4 }, { 0x0050, PhysicalKey.NumPad2 },
{ 116, Key.F5 }, { 0x0051, PhysicalKey.NumPad3 },
{ 117, Key.F6 }, { 0x004B, PhysicalKey.NumPad4 },
{ 118, Key.F7 }, { 0x004C, PhysicalKey.NumPad5 },
{ 119, Key.F8 }, { 0x004D, PhysicalKey.NumPad6 },
{ 120, Key.F9 }, { 0x0047, PhysicalKey.NumPad7 },
{ 121, Key.F10 }, { 0x0048, PhysicalKey.NumPad8 },
{ 122, Key.F11 }, { 0x0049, PhysicalKey.NumPad9 },
{ 123, Key.F12 }, { 0x004E, PhysicalKey.NumPadAdd },
{ 124, Key.F13 }, //{ , PhysicalKey.NumPadClear },
{ 125, Key.F14 }, { 0x007E, PhysicalKey.NumPadComma },
{ 126, Key.F15 }, { 0x0053, PhysicalKey.NumPadDecimal },
{ 127, Key.F16 }, { 0xE035, PhysicalKey.NumPadDivide },
{ 128, Key.F17 }, { 0xE01C, PhysicalKey.NumPadEnter },
{ 129, Key.F18 }, { 0x0059, PhysicalKey.NumPadEqual },
{ 130, Key.F19 }, { 0x0037, PhysicalKey.NumPadMultiply },
{ 131, Key.F20 }, //{ , PhysicalKey.NumPadParenLeft },
{ 132, Key.F21 }, //{ , PhysicalKey.NumPadParenRight },
{ 133, Key.F22 }, { 0x004A, PhysicalKey.NumPadSubtract },
{ 134, Key.F23 },
{ 135, Key.F24 }, // Function Section
{ 144, Key.NumLock }, { 0x0001, PhysicalKey.Escape },
{ 145, Key.Scroll }, { 0x003B, PhysicalKey.F1 },
{ 160, Key.LeftShift }, { 0x003C, PhysicalKey.F2 },
{ 161, Key.RightShift }, { 0x003D, PhysicalKey.F3 },
{ 162, Key.LeftCtrl }, { 0x003E, PhysicalKey.F4 },
{ 163, Key.RightCtrl }, { 0x003F, PhysicalKey.F5 },
{ 164, Key.LeftAlt }, { 0x0040, PhysicalKey.F6 },
{ 165, Key.RightAlt }, { 0x0041, PhysicalKey.F7 },
{ 166, Key.BrowserBack }, { 0x0042, PhysicalKey.F8 },
{ 167, Key.BrowserForward }, { 0x0043, PhysicalKey.F9 },
{ 168, Key.BrowserRefresh }, { 0x0044, PhysicalKey.F10 },
{ 169, Key.BrowserStop }, { 0x0057, PhysicalKey.F11 },
{ 170, Key.BrowserSearch }, { 0x0058, PhysicalKey.F12 },
{ 171, Key.BrowserFavorites }, { 0x0064, PhysicalKey.F13 },
{ 172, Key.BrowserHome }, { 0x0065, PhysicalKey.F14 },
{ 173, Key.VolumeMute }, { 0x0066, PhysicalKey.F15 },
{ 174, Key.VolumeDown }, { 0x0067, PhysicalKey.F16 },
{ 175, Key.VolumeUp }, { 0x0068, PhysicalKey.F17 },
{ 176, Key.MediaNextTrack }, { 0x0069, PhysicalKey.F18 },
{ 177, Key.MediaPreviousTrack }, { 0x006A, PhysicalKey.F19 },
{ 178, Key.MediaStop }, { 0x006B, PhysicalKey.F20 },
{ 179, Key.MediaPlayPause }, { 0x006C, PhysicalKey.F21 },
{ 180, Key.LaunchMail }, { 0x006D, PhysicalKey.F22 },
{ 181, Key.SelectMedia }, { 0x006E, PhysicalKey.F23 },
{ 182, Key.LaunchApplication1 }, { 0x0076, PhysicalKey.F24 },
{ 183, Key.LaunchApplication2 }, { 0xE037, PhysicalKey.PrintScreen },
{ 186, Key.Oem1 }, { 0x0046, PhysicalKey.ScrollLock },
{ 187, Key.OemPlus }, { 0x0045, PhysicalKey.Pause },
{ 188, Key.OemComma },
{ 189, Key.OemMinus }, // Media Keys
{ 190, Key.OemPeriod }, { 0xE06A, PhysicalKey.BrowserBack },
{ 191, Key.OemQuestion }, { 0xE066, PhysicalKey.BrowserFavorites },
{ 192, Key.Oem3 }, { 0xE069, PhysicalKey.BrowserForward },
{ 193, Key.AbntC1 }, { 0xE032, PhysicalKey.BrowserHome },
{ 194, Key.AbntC2 }, { 0xE067, PhysicalKey.BrowserRefresh },
{ 219, Key.OemOpenBrackets }, { 0xE065, PhysicalKey.BrowserSearch },
{ 220, Key.Oem5 }, { 0xE068, PhysicalKey.BrowserStop },
{ 221, Key.Oem6 }, { 0xE02C, PhysicalKey.Eject },
{ 222, Key.OemQuotes }, { 0xE06B, PhysicalKey.LaunchApp1 },
{ 223, Key.Oem8 }, { 0xE021, PhysicalKey.LaunchApp2 },
{ 226, Key.OemBackslash }, { 0xE06C, PhysicalKey.LaunchMail },
{ 229, Key.ImeProcessed }, { 0xE022, PhysicalKey.MediaPlayPause },
{ 240, Key.OemAttn }, { 0xE06D, PhysicalKey.MediaSelect },
{ 241, Key.OemFinish }, { 0xE024, PhysicalKey.MediaStop },
{ 242, Key.OemCopy }, { 0xE019, PhysicalKey.MediaTrackNext },
{ 243, Key.DbeSbcsChar }, { 0xE010, PhysicalKey.MediaTrackPrevious },
{ 244, Key.OemEnlw }, { 0xE05E, PhysicalKey.Power },
{ 245, Key.OemBackTab }, { 0xE05F, PhysicalKey.Sleep },
{ 246, Key.DbeNoRoman }, { 0xE02E, PhysicalKey.AudioVolumeDown },
{ 247, Key.DbeEnterWordRegisterMode }, { 0xE020, PhysicalKey.AudioVolumeMute },
{ 248, Key.DbeEnterImeConfigureMode }, { 0xE030, PhysicalKey.AudioVolumeUp },
{ 249, Key.EraseEof }, { 0xE063, PhysicalKey.WakeUp },
{ 250, Key.Play },
{ 251, Key.DbeNoCodeInput }, // Legacy Keys
{ 252, Key.NoName }, { 0xE018, PhysicalKey.Copy },
{ 253, Key.Pa1 }, { 0xE017, PhysicalKey.Cut },
{ 254, Key.OemClear }, //{ , PhysicalKey.Find },
//{ , PhysicalKey.Open },
{ 0xE00A, PhysicalKey.Paste },
//{ , PhysicalKey.Props },
//{ , PhysicalKey.Select },
{ 0xE008, PhysicalKey.Undo },
}; };
/// <summary> /// <summary>
@ -371,56 +384,67 @@ namespace Avalonia.Win32.Input
return (keyData & extendedMask) != 0; return (keyData & extendedMask) != 0;
} }
private static byte GetScanCode(int keyData)
{
// Bits from 16 to 23 represent scan code.
const int scanCodeMask = 0xFF0000;
return (byte)((keyData & scanCodeMask) >> 16);
}
private static int GetVirtualKey(int virtualKey, int keyData) private static int GetVirtualKey(int virtualKey, int keyData)
{ {
// Adapted from https://github.com/dotnet/wpf/blob/master/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/HwndKeyboardInputProvider.cs. // Adapted from https://github.com/dotnet/wpf/blob/master/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/InterOp/HwndKeyboardInputProvider.cs.
if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_SHIFT) if (virtualKey == (int)VK_SHIFT)
{ {
// Bits from 16 to 23 represent scan code. var scanCode = GetScanCode(keyData);
const int scanCodeMask = 0xFF0000;
var scanCode = (keyData & scanCodeMask) >> 16;
virtualKey = (int)UnmanagedMethods.MapVirtualKey((uint)scanCode, (uint)UnmanagedMethods.MapVirtualKeyMapTypes.MAPVK_VSC_TO_VK_EX); virtualKey = (int)MapVirtualKey(scanCode, (uint)MapVirtualKeyMapTypes.MAPVK_VSC_TO_VK_EX);
if (virtualKey == 0) if (virtualKey == 0)
{ {
virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LSHIFT; virtualKey = (int)VK_LSHIFT;
} }
} }
if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_MENU) else if (virtualKey == (int)VK_MENU)
{ {
bool isRight = IsExtended(keyData); bool isRight = IsExtended(keyData);
if (isRight) if (isRight)
{ {
virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_RMENU; virtualKey = (int)VK_RMENU;
} }
else else
{ {
virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LMENU; virtualKey = (int)VK_LMENU;
} }
} }
if (virtualKey == (int)UnmanagedMethods.VirtualKeyStates.VK_CONTROL) else if (virtualKey == (int)VK_CONTROL)
{ {
bool isRight = IsExtended(keyData); bool isRight = IsExtended(keyData);
if (isRight) if (isRight)
{ {
virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_RCONTROL; virtualKey = (int)VK_RCONTROL;
} }
else else
{ {
virtualKey = (int)UnmanagedMethods.VirtualKeyStates.VK_LCONTROL; virtualKey = (int)VK_LCONTROL;
} }
} }
return virtualKey; return virtualKey;
} }
/// <summary>
/// Gets an Avalonia key from a Windows virtual-key and key data.
/// </summary>
/// <param name="virtualKey">The Windows virtual-key.</param>
/// <param name="keyData">The key data (in the same format as lParam for WM_KEYDOWN).</param>
/// <returns>An Avalonia key, or <see cref="Key.None"/> if none matched.</returns>
public static Key KeyFromVirtualKey(int virtualKey, int keyData) public static Key KeyFromVirtualKey(int virtualKey, int keyData)
{ {
virtualKey = GetVirtualKey(virtualKey, keyData); virtualKey = GetVirtualKey(virtualKey, keyData);
@ -430,11 +454,79 @@ namespace Avalonia.Win32.Input
return result; return result;
} }
/// <summary>
/// Gets a Windows virtual-key from an Avalonia key.
/// </summary>
/// <param name="key">The Avalonia key.</param>
/// <returns>A Windows virtual-key code, or 0 if none matched.</returns>
public static int VirtualKeyFromKey(Key key) public static int VirtualKeyFromKey(Key key)
{ {
s_virtualKeyFromKey.TryGetValue(key, out var result); s_virtualKeyFromKey.TryGetValue(key, out var result);
return result; return result;
} }
/// <summary>
/// Gets a physical Avalonia key from a Windows virtual-key and key data.
/// </summary>
/// <param name="virtualKey">The Windows virtual-key.</param>
/// <param name="keyData">The key data (in the same format as lParam for WM_KEYDOWN).</param>
/// <returns>An Avalonia physical key, or <see cref="PhysicalKey.None"/> if none matched.</returns>
public static PhysicalKey PhysicalKeyFromVirtualKey(int virtualKey, int keyData)
{
uint scanCode = GetScanCode(keyData);
if (scanCode == 0U)
{
// in some cases, the scan code contained in the keyData might be zero:
// try to get one from the virtual key instead
scanCode = MapVirtualKey((uint)virtualKey, (uint)MapVirtualKeyMapTypes.MAPVK_VK_TO_VSC);
if (scanCode == 0U)
return PhysicalKey.None;
}
if (IsExtended(keyData))
scanCode |= 0xE000;
return scanCode is > 0 and <= 0xE0FF
&& s_physicalKeyFromExtendedScanCode.TryGetValue((ushort)scanCode, out var result) ?
result :
PhysicalKey.None;
}
/// <summary>
/// Gets a key symbol from a Windows virtual-key and key data.
/// </summary>
/// <param name="virtualKey">The Windows virtual-key.</param>
/// <param name="keyData">The key data (in the same format as lParam for WM_KEYDOWN).</param>
/// <returns>A key symbol, or null if none matched.</returns>
public static unsafe string? GetKeySymbol(int virtualKey, int keyData)
{
const int bufferSize = 4;
const uint doNotChangeKeyboardState = 1U << 2;
fixed (byte* keyStates = stackalloc byte[256])
fixed (char* buffer = stackalloc char[bufferSize])
{
GetKeyboardState(keyStates);
var length = ToUnicodeEx(
(uint)virtualKey,
GetScanCode(keyData),
keyStates,
buffer,
bufferSize,
doNotChangeKeyboardState,
GetKeyboardLayout(0));
return length switch
{
< 0 => new string(buffer, 0, -length), // dead key
0 => null,
1 when !KeySymbolHelper.IsAllowedAsciiKeySymbol(buffer[0]) => null,
2 when buffer[0] == buffer[1] => new string(buffer, 0, 1), // dead key second press repeats symbol
_ => new string(buffer, 0, length)
};
}
}
} }
} }

98
src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs

@ -1,92 +1,50 @@
using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Utilities; using static Avalonia.Win32.Interop.UnmanagedMethods;
using Avalonia.Win32.Interop; using static Avalonia.Win32.Interop.UnmanagedMethods.VirtualKeyStates;
namespace Avalonia.Win32.Input namespace Avalonia.Win32.Input
{ {
internal class WindowsKeyboardDevice : KeyboardDevice internal sealed class WindowsKeyboardDevice : KeyboardDevice
{ {
private readonly byte[] _keyStates = new byte[256]; public new static WindowsKeyboardDevice Instance { get; } = new();
public new static WindowsKeyboardDevice Instance { get; } = new WindowsKeyboardDevice(); public unsafe RawInputModifiers Modifiers
public RawInputModifiers Modifiers
{ {
get get
{ {
UpdateKeyStates(); fixed (byte* keyStates = stackalloc byte[256])
RawInputModifiers result = 0;
if (IsDown(Key.LeftAlt) || IsDown(Key.RightAlt))
{ {
result |= RawInputModifiers.Alt; GetKeyboardState(keyStates);
}
if (IsDown(Key.LeftCtrl) || IsDown(Key.RightCtrl)) var result = RawInputModifiers.None;
{
result |= RawInputModifiers.Control;
}
if (IsDown(Key.LeftShift) || IsDown(Key.RightShift)) if (((keyStates[(int)VK_LMENU] | keyStates[(int)VK_RMENU]) & 0x80) != 0)
{ {
result |= RawInputModifiers.Shift; result |= RawInputModifiers.Alt;
} }
if (IsDown(Key.LWin) || IsDown(Key.RWin)) if (((keyStates[(int)VK_LCONTROL] | keyStates[(int)VK_RCONTROL]) & 0x80) != 0)
{ {
result |= RawInputModifiers.Meta; result |= RawInputModifiers.Control;
} }
return result; if (((keyStates[(int)VK_LSHIFT] | keyStates[(int)VK_RSHIFT]) & 0x80) != 0)
} {
} result |= RawInputModifiers.Shift;
}
public void WindowActivated(Window window) if (((keyStates[(int)VK_LWIN] | keyStates[(int)VK_RWIN]) & 0x80) != 0)
{ {
SetFocusedElement(window, NavigationMethod.Unspecified, KeyModifiers.None); result |= RawInputModifiers.Meta;
} }
public string StringFromVirtualKey(uint virtualKey) return result;
{ }
var result = StringBuilderCache.Acquire(256); }
int length = UnmanagedMethods.ToUnicode(
virtualKey,
0,
_keyStates,
result,
256,
0);
return StringBuilderCache.GetStringAndRelease(result);
}
private void UpdateKeyStates()
{
UnmanagedMethods.GetKeyboardState(_keyStates);
}
private bool IsDown(Key key)
{
return (GetKeyStates(key) & KeyStates.Down) != 0;
} }
private KeyStates GetKeyStates(Key key) private WindowsKeyboardDevice()
{ {
int vk = KeyInterop.VirtualKeyFromKey(key);
byte state = _keyStates[vk];
KeyStates result = 0;
if ((state & 0x80) != 0)
{
result |= KeyStates.Down;
}
if ((state & 0x01) != 0)
{
result |= KeyStates.Toggled;
}
return result;
} }
} }
} }

25
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -366,6 +366,8 @@ namespace Avalonia.Win32.Interop
VK_OEM_PERIOD = 0xBE, VK_OEM_PERIOD = 0xBE,
VK_OEM_2 = 0xBF, VK_OEM_2 = 0xBF,
VK_OEM_3 = 0xC0, VK_OEM_3 = 0xC0,
VK_ABNT_C1 = 0xC1,
VK_ABNT_C2 = 0xC2,
VK_OEM_4 = 0xDB, VK_OEM_4 = 0xDB,
VK_OEM_5 = 0xDC, VK_OEM_5 = 0xDC,
VK_OEM_6 = 0xDD, VK_OEM_6 = 0xDD,
@ -1203,8 +1205,9 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern uint GetDoubleClickTime(); public static extern uint GetDoubleClickTime();
[DllImport("user32.dll")] [DllImport("user32.dll", ExactSpelling = true, SetLastError = true)]
public static extern bool GetKeyboardState(byte[] lpKeyState); [return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetKeyboardState(byte* lpKeyState);
[DllImport("user32.dll", EntryPoint = "MapVirtualKeyW")] [DllImport("user32.dll", EntryPoint = "MapVirtualKeyW")]
public static extern uint MapVirtualKey(uint uCode, uint uMapType); public static extern uint MapVirtualKey(uint uCode, uint uMapType);
@ -1399,15 +1402,15 @@ namespace Avalonia.Win32.Interop
[DllImport("kernel32.dll", SetLastError = true)] [DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer, IntPtr CompletionEvent); public static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer, IntPtr CompletionEvent);
[DllImport("user32.dll")] [DllImport("user32.dll", ExactSpelling = true)]
public static extern int ToUnicode( public static extern int ToUnicodeEx(
uint virtualKeyCode, uint wVirtKey,
uint scanCode, uint wScanCode,
byte[] keyboardState, byte* lpKeyState,
[Out, MarshalAs(UnmanagedType.LPWStr, SizeConst = 64)] char* pwszBuff,
StringBuilder receivingBuffer, int cchBuff,
int bufferSize, uint wFlags,
uint flags); IntPtr dwhkl);
[DllImport("user32.dll", SetLastError = true)] [DllImport("user32.dll", SetLastError = true)]
public static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack); public static extern bool TrackMouseEvent(ref TRACKMOUSEEVENT lpEventTrack);

49
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -151,18 +151,7 @@ namespace Avalonia.Win32
case WindowsMessage.WM_KEYDOWN: case WindowsMessage.WM_KEYDOWN:
case WindowsMessage.WM_SYSKEYDOWN: case WindowsMessage.WM_SYSKEYDOWN:
{ {
var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); e = TryCreateRawKeyEventArgs(RawKeyEventType.KeyDown, timestamp, wParam, lParam);
if (key != Key.None)
{
e = new RawKeyEventArgs(
WindowsKeyboardDevice.Instance,
timestamp,
Owner,
RawKeyEventType.KeyDown,
key,
WindowsKeyboardDevice.Instance.Modifiers);
}
break; break;
} }
@ -181,18 +170,7 @@ namespace Avalonia.Win32
case WindowsMessage.WM_KEYUP: case WindowsMessage.WM_KEYUP:
case WindowsMessage.WM_SYSKEYUP: case WindowsMessage.WM_SYSKEYUP:
{ {
var key = KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)); e = TryCreateRawKeyEventArgs(RawKeyEventType.KeyUp, timestamp, wParam, lParam);
if (key != Key.None)
{
e = new RawKeyEventArgs(
WindowsKeyboardDevice.Instance,
timestamp,
Owner,
RawKeyEventType.KeyUp,
key,
WindowsKeyboardDevice.Instance.Modifiers);
}
break; break;
} }
case WindowsMessage.WM_CHAR: case WindowsMessage.WM_CHAR:
@ -1173,5 +1151,28 @@ namespace Avalonia.Win32
return modifiers; return modifiers;
} }
private RawKeyEventArgs? TryCreateRawKeyEventArgs(RawKeyEventType eventType, ulong timestamp, IntPtr wParam, IntPtr lParam)
{
var virtualKey = ToInt32(wParam);
var keyData = ToInt32(lParam);
var key = KeyInterop.KeyFromVirtualKey(virtualKey, keyData);
var physicalKey = KeyInterop.PhysicalKeyFromVirtualKey(virtualKey, keyData);
if (key == Key.None && physicalKey == PhysicalKey.None)
return null;
var keySymbol = KeyInterop.GetKeySymbol(virtualKey, keyData);
return new RawKeyEventArgs(
WindowsKeyboardDevice.Instance,
timestamp,
Owner,
eventType,
key,
WindowsKeyboardDevice.Instance.Modifiers,
physicalKey,
keySymbol);
}
} }
} }

13
src/iOS/Avalonia.iOS/TextInputResponder.cs

@ -155,14 +155,15 @@ partial class AvaloniaView
} }
} }
private void KeyPress(Key ev) private void KeyPress(Key key, PhysicalKey physicalKey, string? keySymbol)
{ {
Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering key press {key}", ev); Logger.TryGet(LogEventLevel.Debug, ImeLog)?.Log(null, "Triggering key press {key}", key);
_view._topLevelImpl.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot, _view._topLevelImpl.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot,
RawKeyEventType.KeyDown, ev, RawInputModifiers.None)); RawKeyEventType.KeyDown, key, RawInputModifiers.None, physicalKey, keySymbol));
_view._topLevelImpl.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot, _view._topLevelImpl.Input(new RawKeyEventArgs(KeyboardDevice.Instance!, 0, _view.InputRoot,
RawKeyEventType.KeyUp, ev, RawInputModifiers.None)); RawKeyEventType.KeyUp, key, RawInputModifiers.None, physicalKey, keySymbol));
} }
private void TextInput(string text) private void TextInput(string text)
@ -177,7 +178,7 @@ partial class AvaloniaView
if (text == "\n") if (text == "\n")
{ {
KeyPress(Key.Enter); KeyPress(Key.Enter, PhysicalKey.Enter, "\r");
switch (ReturnKeyType) switch (ReturnKeyType)
{ {
@ -194,7 +195,7 @@ partial class AvaloniaView
TextInput(text); TextInput(text);
} }
void IUIKeyInput.DeleteBackward() => KeyPress(Key.Back); void IUIKeyInput.DeleteBackward() => KeyPress(Key.Back, PhysicalKey.Backspace, "\b");
bool IUIKeyInput.HasText => true; bool IUIKeyInput.HasText => true;

12
tests/Avalonia.Base.UnitTests/Input/KeyboardDeviceTests.cs

@ -24,7 +24,9 @@ namespace Avalonia.Base.UnitTests.Input
root.Object, root.Object,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.A, Key.A,
RawInputModifiers.None)); RawInputModifiers.None,
PhysicalKey.A,
"a"));
root.Verify(x => x.RaiseEvent(It.IsAny<KeyEventArgs>())); root.Verify(x => x.RaiseEvent(It.IsAny<KeyEventArgs>()));
} }
@ -51,7 +53,9 @@ namespace Avalonia.Base.UnitTests.Input
root, root,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.A, Key.A,
RawInputModifiers.None)); RawInputModifiers.None,
PhysicalKey.A,
"a"));
Assert.Equal(1, raised); Assert.Equal(1, raised);
} }
@ -123,7 +127,9 @@ namespace Avalonia.Base.UnitTests.Input
root, root,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.O, Key.O,
RawInputModifiers.Control)); RawInputModifiers.Control,
PhysicalKey.O,
"o"));
Assert.Equal(1, raised); Assert.Equal(1, raised);
} }

4
tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs

@ -114,7 +114,9 @@ namespace Avalonia.Controls.UnitTests
root, root,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.F, Key.F,
RawInputModifiers.Control)); RawInputModifiers.Control,
PhysicalKey.F,
"f"));
Assert.True(hotKeyedTextBox.IsFocused); Assert.True(hotKeyedTextBox.IsFocused);
} }

6
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@ -210,7 +210,11 @@ namespace Avalonia.Controls.UnitTests
0, 0,
target, target,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.A, RawInputModifiers.None); Key.A,
RawInputModifiers.None,
PhysicalKey.A,
"a");
impl.Object.Input(input); impl.Object.Input(input);
inputManagerMock.Verify(x => x.ProcessInput(input)); inputManagerMock.Verify(x => x.ProcessInput(input));

24
tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs

@ -88,7 +88,9 @@ namespace Avalonia.Controls.UnitTests.Utils
root, root,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.A, Key.A,
RawInputModifiers.Control)); RawInputModifiers.Control,
PhysicalKey.A,
"a"));
Assert.True(expectedParameter == commandResult, $"{factoryName} HotKey did not carry the CommandParameter."); Assert.True(expectedParameter == commandResult, $"{factoryName} HotKey did not carry the CommandParameter.");
} }
@ -129,7 +131,9 @@ namespace Avalonia.Controls.UnitTests.Utils
root, root,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.A, Key.A,
RawInputModifiers.Control)); RawInputModifiers.Control,
PhysicalKey.A,
"a"));
Assert.True(isExecuted == false, $"{factoryName} Execution raised when IsEnabled is false."); Assert.True(isExecuted == false, $"{factoryName} Execution raised when IsEnabled is false.");
} }
@ -171,7 +175,9 @@ namespace Avalonia.Controls.UnitTests.Utils
root, root,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.A, Key.A,
RawInputModifiers.Control)); RawInputModifiers.Control,
PhysicalKey.A,
"a"));
element.IsEnabled = false; element.IsEnabled = false;
@ -180,7 +186,9 @@ namespace Avalonia.Controls.UnitTests.Utils
root, root,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.A, Key.A,
RawInputModifiers.Control)); RawInputModifiers.Control,
PhysicalKey.A,
"a"));
Assert.True(clickExecutedCount == 1, $"{factoryName} Execution raised when IsEnabled is false."); Assert.True(clickExecutedCount == 1, $"{factoryName} Execution raised when IsEnabled is false.");
@ -229,7 +237,9 @@ namespace Avalonia.Controls.UnitTests.Utils
root, root,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.A, Key.A,
RawInputModifiers.Control)); RawInputModifiers.Control,
PhysicalKey.A,
"a"));
element.IsEnabled = false; element.IsEnabled = false;
@ -238,7 +248,9 @@ namespace Avalonia.Controls.UnitTests.Utils
root, root,
RawKeyEventType.KeyDown, RawKeyEventType.KeyDown,
Key.A, Key.A,
RawInputModifiers.Control)); RawInputModifiers.Control,
PhysicalKey.A,
"a"));
Assert.True(commandExecutedCount == 1, $"{factoryName} Execution raised when IsEnabled is false."); Assert.True(commandExecutedCount == 1, $"{factoryName} Execution raised when IsEnabled is false.");
Assert.True(clickExecutedCount == 0, $"{factoryName} Execution raised event Click."); Assert.True(clickExecutedCount == 0, $"{factoryName} Execution raised event Click.");

Loading…
Cancel
Save