diff --git a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs index 834feb861e..f13694e4fb 100644 --- a/src/Windows/Avalonia.Win32/Input/KeyInterop.cs +++ b/src/Windows/Avalonia.Win32/Input/KeyInterop.cs @@ -493,6 +493,24 @@ namespace Avalonia.Win32.Input PhysicalKey.None; } + /// + /// Gets a key symbol from a Windows virtual-key using MapVirtualKey. + /// Unlike , this does not call ToUnicodeEx and is safe to use + /// during WM_SYSKEYDOWN/UP where ToUnicodeEx would corrupt keyboard state. + /// + /// The Windows virtual-key. + /// A key symbol, or null if none matched. + public static string? GetKeySymbolFromVirtualKey(int virtualKey) + { + var ch = MapVirtualKey((uint)virtualKey, (uint)MapVirtualKeyMapTypes.MAPVK_VK_TO_CHAR); + if (ch == 0) + return null; + + // Bit 31 is set for dead keys — strip it to get the base character. + var c = (char)(ch & 0x7FFFFFFF); + return KeySymbolHelper.IsAllowedAsciiKeySymbol(c) ? c.ToString() : null; + } + /// /// Gets a key symbol from a Windows virtual-key and key data. /// diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 82aaac226c..2eba69960b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -1446,8 +1446,11 @@ namespace Avalonia.Win32 var physicalKey = KeyInterop.PhysicalKeyFromVirtualKey(virtualKey, keyData); // Avoid calling GetKeySymbol() for WM_SYSKEYDOWN/UP: - // it ultimately calls User32!ToUnicodeEx, which messes up the keyboard state in this case. - var keySymbol = useKeySymbol ? KeyInterop.GetKeySymbol(virtualKey, keyData) : null; + // it ultimately calls ToUnicodeEx, which corrupts keyboard state for system key events. + // Use MapVirtualKey-based fallback instead — it's layout-aware without touching keyboard state. + var keySymbol = useKeySymbol + ? KeyInterop.GetKeySymbol(virtualKey, keyData) + : KeyInterop.GetKeySymbolFromVirtualKey(virtualKey); if (key == Key.None && physicalKey == PhysicalKey.None && string.IsNullOrWhiteSpace(keySymbol)) return null; diff --git a/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs b/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs index 9443e22855..166b777ba1 100644 --- a/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs +++ b/tests/Avalonia.Base.UnitTests/Input/AccessKeyHandlerTests.cs @@ -233,6 +233,33 @@ namespace Avalonia.Base.UnitTests.Input } } + [Fact] + public void Should_Raise_AccessKey_For_System_Key_Event_With_KeySymbol() + { + // Regression test for #20961: on Windows, WM_SYSKEYDOWN (Alt+key) previously + // left KeySymbol null, breaking access keys. MapVirtualKey now provides KeySymbol. + using (UnitTestApplication.Start(TestServices.RealFocus)) + { + var button = new Button(); + var root = new TestRoot(button); + var target = new AccessKeyHandler(); + var raised = 0; + + KeyboardDevice.Instance?.SetFocusedElement(button, NavigationMethod.Unspecified, KeyModifiers.None); + + target.SetOwner(root); + target.Register("F", button); + button.AddHandler(AccessKeyHandler.AccessKeyEvent, (s, e) => ++raised); + + KeyDown(root, Key.LeftAlt); + Assert.Equal(0, raised); + + // MapVirtualKey provides lowercase KeySymbol for system key events + KeyDown(root, Key.F, "f", KeyModifiers.Alt); + Assert.Equal(1, raised); + } + } + [Fact] public void Should_Open_MainMenu_On_Alt_KeyUp() {