From 1060839683d2f8bb1b752d5bc021a53f90a0129d Mon Sep 17 00:00:00 2001 From: Nathan Nguyen <146415969+NathanDrake2406@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:31:28 +1000 Subject: [PATCH] Fix access keys not working when KeySymbol is null (#21077) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: add regression test for access key with system key events Regression test for #20961: verifies that access keys fire correctly when triggered via Alt+key (system key events). * fix: provide KeySymbol for system key events via MapVirtualKey On Windows, WM_SYSKEYDOWN (Alt+key) intentionally skips ToUnicodeEx to avoid corrupting keyboard state. This left KeySymbol null, which broke access keys after #20662 switched from Key to KeySymbol. Use MapVirtualKey(VK, MAPVK_VK_TO_CHAR) as a layout-aware fallback for system key events — it resolves the character without touching keyboard state. Fixes #20961 * chore: retrigger CI --- .../Avalonia.Win32/Input/KeyInterop.cs | 18 +++++++++++++ .../Avalonia.Win32/WindowImpl.AppWndProc.cs | 7 +++-- .../Input/AccessKeyHandlerTests.cs | 27 +++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) 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() {