Fix macOS Caps Lock rebinding by buffering one-shot key-down events during keyboard assignment

This commit is contained in:
Babib3l
2026-03-24 22:45:37 +01:00
parent c1845aac4d
commit d3f6460fdf
4 changed files with 104 additions and 16 deletions

View File

@@ -8,22 +8,28 @@ namespace Ryujinx.Input.Assigner
private readonly IKeyboard _keyboard; private readonly IKeyboard _keyboard;
private KeyboardStateSnapshot _keyboardState; private KeyboardStateSnapshot _keyboardState;
private Button? _pressedButton;
public KeyboardKeyAssigner(IKeyboard keyboard) public KeyboardKeyAssigner(IKeyboard keyboard)
{ {
_keyboard = keyboard; _keyboard = keyboard;
} }
public void Initialize() { } public void Initialize()
{
_pressedButton = null;
}
public void ReadInput() public void ReadInput()
{ {
_keyboardState = _keyboard.GetKeyboardStateSnapshot(); _keyboardState = _keyboard.GetKeyboardStateSnapshot();
_pressedButton ??= GetPressedButtonFromState() ?? GetPressedButtonFromBufferedPress();
} }
public bool IsAnyButtonPressed() public bool IsAnyButtonPressed()
{ {
return GetPressedButton() is not null; return _pressedButton is not null;
} }
public bool ShouldCancel() public bool ShouldCancel()
@@ -32,26 +38,33 @@ namespace Ryujinx.Input.Assigner
} }
public Button? GetPressedButton() public Button? GetPressedButton()
{
return !ShouldCancel() ? _pressedButton : null;
}
private Button? GetPressedButtonFromState()
{ {
Key aliasedKey = GetAliasedPressedKey(); Key aliasedKey = GetAliasedPressedKey();
if (aliasedKey != Key.Unknown) if (aliasedKey != Key.Unknown)
{ {
return !ShouldCancel() ? new Button(aliasedKey) : null; return new Button(aliasedKey);
} }
Button? keyPressed = null;
for (Key key = Key.Unknown; key < Key.Count; key++) for (Key key = Key.Unknown; key < Key.Count; key++)
{ {
if (_keyboardState.IsPressed(key)) if (_keyboardState.IsPressed(key))
{ {
keyPressed = new(key); return new Button(key);
break;
} }
} }
return !ShouldCancel() ? keyPressed : null; return null;
}
private Button? GetPressedButtonFromBufferedPress()
{
return _keyboard.TryConsumePressedKey(out Key key) ? new Button(key) : null;
} }
private Key GetAliasedPressedKey() private Key GetAliasedPressedKey()

View File

@@ -44,5 +44,16 @@ namespace Ryujinx.Input
return new KeyboardStateSnapshot(_keyState); return new KeyboardStateSnapshot(_keyState);
} }
/// <summary>
/// Try to consume a recently pressed key.
/// </summary>
/// <param name="key">The pressed key, if available.</param>
/// <returns>True if a key press was consumed.</returns>
bool TryConsumePressedKey(out Key key)
{
key = Key.Unknown;
return false;
}
} }
} }

View File

@@ -98,6 +98,19 @@ namespace Ryujinx.Ava.Input
} }
} }
public bool TryConsumePressedKey(out Key key)
{
try
{
return _driver.TryConsumePressedKey(_mode, out key);
}
catch
{
key = Key.Unknown;
return false;
}
}
public void SetConfiguration(InputConfig configuration) public void SetConfiguration(InputConfig configuration)
{ {
lock (_userMappingLock) lock (_userMappingLock)

View File

@@ -4,6 +4,7 @@ using Ryujinx.Ava.Common.Locale;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey; using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using Key = Ryujinx.Input.Key; using Key = Ryujinx.Input.Key;
@@ -16,6 +17,9 @@ namespace Ryujinx.Ava.Input
private readonly HashSet<Key> _semanticPressedKeys; private readonly HashSet<Key> _semanticPressedKeys;
private readonly HashSet<ConfigPhysicalKey> _physicalPressedKeys; private readonly HashSet<ConfigPhysicalKey> _physicalPressedKeys;
private readonly Dictionary<Key, ConfigPhysicalKey> _observedPhysicalKeysBySemanticKey; private readonly Dictionary<Key, ConfigPhysicalKey> _observedPhysicalKeysBySemanticKey;
private readonly Queue<Key> _semanticPressedKeyQueue;
private readonly Queue<Key> _physicalPressedKeyQueue;
private readonly Lock _pressedKeyQueueLock;
private readonly KeyboardInputMode _defaultMode; private readonly KeyboardInputMode _defaultMode;
public event EventHandler<KeyEventArgs> KeyPressed; public event EventHandler<KeyEventArgs> KeyPressed;
@@ -31,6 +35,9 @@ namespace Ryujinx.Ava.Input
_semanticPressedKeys = []; _semanticPressedKeys = [];
_physicalPressedKeys = []; _physicalPressedKeys = [];
_observedPhysicalKeysBySemanticKey = []; _observedPhysicalKeysBySemanticKey = [];
_semanticPressedKeyQueue = [];
_physicalPressedKeyQueue = [];
_pressedKeyQueueLock = new();
_defaultMode = defaultMode; _defaultMode = defaultMode;
_control.KeyDown += OnKeyPress; _control.KeyDown += OnKeyPress;
@@ -107,21 +114,47 @@ namespace Ryujinx.Ava.Input
} }
internal void Clear(KeyboardInputMode mode) internal void Clear(KeyboardInputMode mode)
{
lock (_pressedKeyQueueLock)
{ {
if (mode == KeyboardInputMode.Physical) if (mode == KeyboardInputMode.Physical)
{ {
_physicalPressedKeys.Clear(); _physicalPressedKeys.Clear();
_physicalPressedKeyQueue.Clear();
} }
else else
{ {
_semanticPressedKeys.Clear(); _semanticPressedKeys.Clear();
_semanticPressedKeyQueue.Clear();
}
} }
} }
public void Clear() public void Clear()
{
lock (_pressedKeyQueueLock)
{ {
_semanticPressedKeys.Clear(); _semanticPressedKeys.Clear();
_physicalPressedKeys.Clear(); _physicalPressedKeys.Clear();
_semanticPressedKeyQueue.Clear();
_physicalPressedKeyQueue.Clear();
}
}
internal bool TryConsumePressedKey(KeyboardInputMode mode, out Key key)
{
lock (_pressedKeyQueueLock)
{
Queue<Key> queue = mode == KeyboardInputMode.Physical ? _physicalPressedKeyQueue : _semanticPressedKeyQueue;
if (queue.TryDequeue(out key))
{
return true;
}
}
key = Key.Unknown;
return false;
} }
private static void UpdateKeyState(HashSet<Key> pressedKeys, Key key, bool isPressed) private static void UpdateKeyState(HashSet<Key> pressedKeys, Key key, bool isPressed)
@@ -161,10 +194,28 @@ namespace Ryujinx.Ava.Input
Key semanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.Key); Key semanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.Key);
Key resolvedSemanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey, args.Key); Key resolvedSemanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey, args.Key);
ConfigPhysicalKey physicalKey = GetPhysicalInputKey(args, semanticKey); ConfigPhysicalKey physicalKey = GetPhysicalInputKey(args, semanticKey);
bool semanticWasPressed = _semanticPressedKeys.Contains(resolvedSemanticKey);
bool physicalWasPressed = _physicalPressedKeys.Contains(physicalKey);
UpdateKeyState(_semanticPressedKeys, resolvedSemanticKey, isPressed); UpdateKeyState(_semanticPressedKeys, resolvedSemanticKey, isPressed);
UpdateKeyState(_physicalPressedKeys, physicalKey, isPressed); UpdateKeyState(_physicalPressedKeys, physicalKey, isPressed);
if (isPressed)
{
lock (_pressedKeyQueueLock)
{
if (!semanticWasPressed && resolvedSemanticKey is not Key.Unknown and not Key.Unbound)
{
_semanticPressedKeyQueue.Enqueue(resolvedSemanticKey);
}
if (!physicalWasPressed && physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
{
_physicalPressedKeyQueue.Enqueue((Key)(int)physicalKey);
}
}
}
if (isPressed && if (isPressed &&
semanticKey is not Key.Unknown and not Key.Unbound && semanticKey is not Key.Unknown and not Key.Unbound &&
physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound) physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)