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()
{