Compare commits

..

5 Commits

Author SHA1 Message Date
Max
fa72b5a298 Revert "fix/sdl3-gamepad-bugs (#19)" (#116)
This reverts commit 7f0e82fe48.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/116
2026-05-27 14:10:33 +00:00
Babib3l
e02081c09e Wire "Start games in fullscreen" option to use the new fullscreen behaviour (#113)
Final (hopefully) fix for https://github.com/Ryubing/Issues/issues/415

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/113
2026-05-27 12:25:09 +00:00
KeatonTheBot
de70f66a27 macOS: Fix black screen on Metal versions older than 3.1 (#114)
Fixes a black screen on Metal versions older than 3.1 by disabling `VK_EXT_extended_dynamic_state` (and `VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE`). Fix imported from [@sunshineinabox](https://github.com/sunshineinabox)'s unpublished extended dynamic states branch.

Co-authored-by: sunshineinabox <aqemail@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/114
2026-05-27 12:24:41 +00:00
Xam
6c1692ed60 HLE: Applets: Software keyboard: update cursor position on SetInputText (#25)
fixes cursor being at 0 position even when text is present, especially annoying for handheld devices with kde virtual keyboard, which is pretty bad, with no way to move the cursor or delete text backward

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/25
2026-05-27 12:22:05 +00:00
Babib3l
fb7c1fde11 Input: Refactor Keyboard Handling To Use Physical Keys (#13)
This PR refactors keyboard handling to use physical key mappings for all gameplay input, ensuring controls remain consistent across different OS keyboard layouts.
I'd like to give out an ENORMOUS thank you to @Neo for his very generous help on getting MacOS caps lock behaviour working, but as well for taking the time for extensive testing, planning and discussions, and finally for writing this PR message :) Keep being awsome pal 👊

### New Features :
* **New**: Gameplay input now uses physical key positions instead of OS layouts, ensuring the same physical key triggers the same action across keyboard layouts.
    * Key rebinding stores physical keys and config compatibility is preserved, with physical keys now the primary gameplay‑binding format.
    * Physical‑key model is now consistent across platforms, including updated SDL/headless behavior.
* **Added**: New Input setting "Reset keybinds to default", with a new confirmation dialog appears when changes are being overwritten.
* **Fractured**: Keyboard‑related locales to the newly created `KeyboardLayout.json`.
    * New input device settings/actions use clearer labels and tooltips.
    * UI Key Labels (such as Left Shift and Right Shift) are more accurate and standardized, with clearer symbols, consistent naming, dynamic learning of printable labels from real key events, and persistence across restarts.
### Improvements :
* **Reduced**: Incorrect key labels by using observed host symbols instead of language assumptions.
* **Reduced**: Stuck/stale keys by using binary pressed‑key tracking, fixing rebinding/gameplay paths, better held‑key recovery after focus changes, and clearing keyboard state when Ryujinx/settings windows lose focus.
* **Improved**: Device handling → refreshing no longer clears the selector, disconnect fallback is consistent, reconnect restores controllers automatically, and the UI avoids invalid/empty device states.
* **Improved**: Async input‑assignment callbacks are now guarded when switching views/devices, preventing stale callbacks from hitting detached views.
* **Adjusted**: Input visualiser to be more robust when switching sources or handling controller disconnect/reconnect, without needing to reopen settings.
* **Improved**: Modification (changes to input controls) tracking
    * Rebinding to the same value, reverting to original config, restoring defaults without differences, or reloading equivalent profiles no longer leaves Player marked as modified.
* **Reduced**: Keyboard LED noise in logs and added optional UI keyboard‑state/rebinding diagnostics.
### Fixes :
* **Special Keys**:
    * AltGr and other special keys behave correctly, including proper Ctrl+Alt → AltRight handling and more consistent normalization of special/synthetic keys.
    * Caps Lock is now reliably bindable on all platforms (Windows/Linux register every press; macOS every other).
* **Fixed**: Certain cases where keyboard input broke after pointer interactions
### Current Limitations

These are planned on being fixed/improved upon in future PRs:
* Hotkeys still use semantic (Key) mappings.
* Software keyboard / text input still uses the semantic path
* Printable key labels may fall back to defaults until observed from host input.
* Full semantic/physical split currently implemented only in the Avalonia driver.

Co-authored-by: _Neo_ <ursamajorjanus2819@gmail.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/13
2026-05-26 17:54:55 +00:00
46 changed files with 4823 additions and 3003 deletions

3
.gitignore vendored
View File

@@ -72,6 +72,9 @@ ipch/
_ReSharper*/ _ReSharper*/
*.[Rr]e[Ss]harper *.[Rr]e[Ss]harper
# .NET
.dotnet-home/
# TeamCity is a build add-in # TeamCity is a build add-in
_TeamCity* _TeamCity*

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
namespace Ryujinx.Common.Configuration.Hid.Keyboard namespace Ryujinx.Common.Configuration.Hid.Keyboard
{ {
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<Key> { } public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<PhysicalKey> { }
} }

View File

@@ -0,0 +1,142 @@
using System.Text.Json.Serialization;
namespace Ryujinx.Common.Configuration.Hid
{
[JsonConverter(typeof(JsonStringEnumConverter<PhysicalKey>))]
public enum PhysicalKey
{
Unknown,
ShiftLeft,
ShiftRight,
ControlLeft,
ControlRight,
AltLeft,
AltRight,
WinLeft,
WinRight,
Menu,
F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
F13,
F14,
F15,
F16,
F17,
F18,
F19,
F20,
F21,
F22,
F23,
F24,
F25,
F26,
F27,
F28,
F29,
F30,
F31,
F32,
F33,
F34,
F35,
Up,
Down,
Left,
Right,
Enter,
Escape,
Space,
Tab,
BackSpace,
Insert,
Delete,
PageUp,
PageDown,
Home,
End,
CapsLock,
ScrollLock,
PrintScreen,
Pause,
NumLock,
Clear,
Keypad0,
Keypad1,
Keypad2,
Keypad3,
Keypad4,
Keypad5,
Keypad6,
Keypad7,
Keypad8,
Keypad9,
KeypadDivide,
KeypadMultiply,
KeypadSubtract,
KeypadAdd,
KeypadDecimal,
KeypadEnter,
A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
Number0,
Number1,
Number2,
Number3,
Number4,
Number5,
Number6,
Number7,
Number8,
Number9,
Tilde,
Grave,
Minus,
Plus,
BracketLeft,
BracketRight,
Semicolon,
Quote,
Comma,
Period,
Slash,
BackSlash,
Unbound,
Count,
}
}

View File

@@ -588,8 +588,10 @@ namespace Ryujinx.Graphics.Vulkan
dynamicStates[5] = DynamicState.StencilReference; dynamicStates[5] = DynamicState.StencilReference;
dynamicStates[6] = DynamicState.BlendConstants; dynamicStates[6] = DynamicState.BlendConstants;
if (supportsExtDynamicState) if (supportsExtDynamicState && (gd.SupportsMTL31 || !gd.IsMoltenVk))
{ {
// Requires Metal 3.1 and new MoltenVK, however extended dynamic states extension is not
// available on older versions of MVK, so we can safely check only OS version.
dynamicStates[dynamicStatesCount++] = DynamicState.VertexInputBindingStrideExt; dynamicStates[dynamicStatesCount++] = DynamicState.VertexInputBindingStrideExt;
} }

View File

@@ -98,6 +98,7 @@ namespace Ryujinx.Graphics.Vulkan
internal bool IsIntelArc { get; private set; } internal bool IsIntelArc { get; private set; }
internal bool IsQualcommProprietary { get; private set; } internal bool IsQualcommProprietary { get; private set; }
internal bool IsMoltenVk { get; private set; } internal bool IsMoltenVk { get; private set; }
internal bool SupportsMTL31 { get; private set; }
internal bool IsTBDR { get; private set; } internal bool IsTBDR { get; private set; }
internal bool IsSharedMemory { get; private set; } internal bool IsSharedMemory { get; private set; }
@@ -123,6 +124,8 @@ namespace Ryujinx.Graphics.Vulkan
// Any device running on MacOS is using MoltenVK, even Intel and AMD vendors. // Any device running on MacOS is using MoltenVK, even Intel and AMD vendors.
if (IsMoltenVk = OperatingSystem.IsMacOS()) if (IsMoltenVk = OperatingSystem.IsMacOS())
MVKInitialization.Initialize(); MVKInitialization.Initialize();
SupportsMTL31 = OperatingSystem.IsMacOSVersionAtLeast(14);
} }
public static VulkanRenderer Create( public static VulkanRenderer Create(

View File

@@ -443,6 +443,7 @@ namespace Ryujinx.HLE.HOS.Applets
if ((newCalc.Flags & KeyboardCalcFlags.SetInputText) != 0) if ((newCalc.Flags & KeyboardCalcFlags.SetInputText) != 0)
{ {
_textValue = newCalc.InputText; _textValue = newCalc.InputText;
_cursorBegin = _textValue.Length;
updateText = true; updateText = true;
Logger.Debug?.Print(LogClass.ServiceAm, $"Input text set to {_textValue}"); Logger.Debug?.Print(LogClass.ServiceAm, $"Input text set to {_textValue}");

View File

@@ -155,9 +155,7 @@ namespace Ryujinx.Input.SDL3
result |= GamepadFeaturesFlag.Led; result |= GamepadFeaturesFlag.Led;
} }
SDL_UnlockProperties(propID); SDL_UnlockProperties(propID);
SDL_DestroyProperties(propID);
// NOTE: Do not call SDL_DestroyProperties here. These properties are owned
// internally by SDL and are freed when SDL_CloseGamepad is called (in Dispose).
return result; return result;
} }
@@ -165,6 +163,9 @@ namespace Ryujinx.Input.SDL3
public string Id { get; } public string Id { get; }
public string Name { get; } public string Name { get; }
// Expose vendor id for higher-fidelity device detection in UI
public ushort VendorId => _gamepadHandle != null ? SDL_GetGamepadVendor(_gamepadHandle) : (ushort)0;
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle); public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
protected virtual void Dispose(bool disposing) protected virtual void Dispose(bool disposing)

View File

@@ -331,18 +331,28 @@ namespace Ryujinx.Input.SDL3
public IEnumerable<IGamepad> GetGamepads() public IEnumerable<IGamepad> GetGamepads()
{ {
string[] ids; lock (_gamepadsIds)
lock (_lock)
{ {
ids = _gamepadsIds.Values foreach (var gamepad in _gamepadsIds)
.Concat(_joyConsIds.Values) {
.Concat(_linkedJoyConsIds.Values) yield return GetGamepad(gamepad.Value);
.ToArray(); }
} }
foreach (string id in ids) lock (_joyConsIds)
{ {
yield return GetGamepad(id); foreach (var gamepad in _joyConsIds)
{
yield return GetGamepad(gamepad.Value);
}
}
lock (_linkedJoyConsIds)
{
foreach (var gamepad in _linkedJoyConsIds)
{
yield return GetGamepad(gamepad.Value);
}
} }
} }
} }

View File

@@ -9,25 +9,15 @@ using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using SDL; using SDL;
using static SDL.SDL3; using static SDL.SDL3;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Input.SDL3 namespace Ryujinx.Input.SDL3
{ {
class SDL3Keyboard : IKeyboard class SDL3Keyboard : IKeyboard
{ {
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, Key From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not Key.Unbound;
}
private readonly Lock _userMappingLock = new(); private readonly Lock _userMappingLock = new();
#pragma warning disable IDE0052 // Remove unread private member
private readonly SDL3KeyboardDriver _driver;
#pragma warning restore IDE0052
private StandardKeyboardInputConfig _configuration; private StandardKeyboardInputConfig _configuration;
private readonly List<ButtonMappingEntry> _buttonsUserMapping; private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
private static readonly SDL_Keycode[] _keysDriverMapping = private static readonly SDL_Keycode[] _keysDriverMapping =
@@ -172,9 +162,8 @@ namespace Ryujinx.Input.SDL3
SDL_Keycode.SDLK_0 SDL_Keycode.SDLK_0
]; ];
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name) public SDL3Keyboard(string id, string name)
{ {
_driver = driver;
Id = id; Id = id;
Name = name; Name = name;
_buttonsUserMapping = []; _buttonsUserMapping = [];
@@ -196,9 +185,9 @@ namespace Ryujinx.Input.SDL3
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe static int ToSDL3Scancode(Key key) private unsafe static int ToSDL3Scancode(ConfigPhysicalKey key)
{ {
if (key is >= Key.Unknown and <= Key.Menu) if (key is >= ConfigPhysicalKey.Unknown and <= ConfigPhysicalKey.Menu)
{ {
return -1; return -1;
} }
@@ -206,18 +195,18 @@ namespace Ryujinx.Input.SDL3
return (int)SDL_GetScancodeFromKey(_keysDriverMapping[(int)key], null); return (int)SDL_GetScancodeFromKey(_keysDriverMapping[(int)key], null);
} }
private static SDL_Keymod GetKeyboardModifierMask(Key key) private static SDL_Keymod GetKeyboardModifierMask(ConfigPhysicalKey key)
{ {
return key switch return key switch
{ {
Key.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT, ConfigPhysicalKey.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
Key.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT, ConfigPhysicalKey.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
Key.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL, ConfigPhysicalKey.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
Key.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL, ConfigPhysicalKey.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
Key.AltLeft => SDL_Keymod.SDL_KMOD_LALT, ConfigPhysicalKey.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
Key.AltRight => SDL_Keymod.SDL_KMOD_RALT, ConfigPhysicalKey.AltRight => SDL_Keymod.SDL_KMOD_RALT,
Key.WinLeft => SDL_Keymod.SDL_KMOD_LGUI, ConfigPhysicalKey.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
Key.WinRight => SDL_Keymod.SDL_KMOD_RGUI, ConfigPhysicalKey.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
// NOTE: Menu key isn't supported by SDL3. // NOTE: Menu key isn't supported by SDL3.
_ => SDL_Keymod.SDL_KMOD_NONE _ => SDL_Keymod.SDL_KMOD_NONE
}; };
@@ -233,9 +222,9 @@ namespace Ryujinx.Input.SDL3
rawKeyboardState = SDL_GetKeyboardState(null); rawKeyboardState = SDL_GetKeyboardState(null);
} }
bool[] keysState = new bool[(int)Key.Count]; bool[] keysState = new bool[(int)ConfigPhysicalKey.Count];
for (Key key = 0; key < Key.Count; key++) for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
{ {
int index = ToSDL3Scancode(key); int index = ToSDL3Scancode(key);
if (index == -1) if (index == -1)
@@ -265,36 +254,6 @@ namespace Ryujinx.Input.SDL3
return value * ConvertRate; return value * ConvertRate;
} }
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
{
short stickX = 0;
short stickY = 0;
if (snapshot.IsPressed((Key)stickConfig.StickUp))
{
stickY += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickDown))
{
stickY -= 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickRight))
{
stickX += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
{
stickX -= 1;
}
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
public GamepadStateSnapshot GetMappedStateSnapshot() public GamepadStateSnapshot GetMappedStateSnapshot()
{ {
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot(); KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
@@ -307,9 +266,9 @@ namespace Ryujinx.Input.SDL3
return result; return result;
} }
foreach (ButtonMappingEntry entry in _buttonsUserMapping) foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
{ {
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound) if (!entry.IsValid)
{ {
continue; continue;
} }
@@ -321,8 +280,8 @@ namespace Ryujinx.Input.SDL3
} }
} }
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick); (short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick); (short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY)); result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY)); result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
@@ -358,38 +317,15 @@ namespace Ryujinx.Input.SDL3
{ {
_configuration = (StandardKeyboardInputConfig)configuration; _configuration = (StandardKeyboardInputConfig)configuration;
// First clear the buttons mapping
_buttonsUserMapping.Clear(); _buttonsUserMapping.Clear();
// Then configure left joycon _buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
// Finally configure right joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
} }
} }
public void SetLed(uint packedRgb) public void SetLed(uint packedRgb)
{ {
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard"); Logger.Debug?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard");
} }
public void SetTriggerThreshold(float triggerThreshold) public void SetTriggerThreshold(float triggerThreshold)

View File

@@ -50,7 +50,7 @@ namespace Ryujinx.Input.SDL3
return null; return null;
} }
return new SDL3Keyboard(this, _keyboardIdentifers[0], "All keyboards"); return new SDL3Keyboard(_keyboardIdentifers[0], "All keyboards");
} }
public IEnumerable<IGamepad> GetGamepads() public IEnumerable<IGamepad> GetGamepads()

View File

@@ -1,3 +1,5 @@
using Ryujinx.Common.Logging;
namespace Ryujinx.Input.Assigner namespace Ryujinx.Input.Assigner
{ {
/// <summary> /// <summary>
@@ -8,22 +10,40 @@ 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();
if (_pressedButton is null)
{
Button? buttonFromState = GetPressedButtonFromState();
Button? buttonFromBufferedPress = buttonFromState is null ? GetPressedButtonFromBufferedPress() : null;
_pressedButton = buttonFromState ?? buttonFromBufferedPress;
}
if (_pressedButton is not null)
{
string source = _pressedButton.HasValue && GetPressedButtonFromState() is not null ? "state" : "buffered-press";
Logger.Debug?.Print(LogClass.UI, $"Keyboard assigner registered key={_pressedButton.Value.AsHidType<Key>()}, source={source}, cancelPressed={ShouldCancel()}");
}
} }
public bool IsAnyButtonPressed() public bool IsAnyButtonPressed()
{ {
return GetPressedButton() is not null; return _pressedButton is not null;
} }
public bool ShouldCancel() public bool ShouldCancel()
@@ -33,18 +53,53 @@ namespace Ryujinx.Input.Assigner
public Button? GetPressedButton() public Button? GetPressedButton()
{ {
Button? keyPressed = null; return !ShouldCancel() ? _pressedButton : null;
}
private Button? GetPressedButtonFromState()
{
Key aliasedKey = GetAliasedPressedKey();
if (aliasedKey != Key.Unknown)
{
return new Button(aliasedKey);
}
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()
{
// On some layouts (for example AltGr on Windows), Right Alt is reported as Ctrl+Alt.
// Prefer AltRight in that case so the binding reflects the physical key used.
if (_keyboardState.IsPressed(Key.ControlLeft) && _keyboardState.IsPressed(Key.AltRight))
{
return Key.AltRight;
}
// On some Copilot keyboards, the key in the right-control position is reported as
// ShiftLeft+Win+F23. Prefer ControlRight so the binding reflects that physical key.
if (_keyboardState.IsPressed(Key.ShiftLeft) &&
_keyboardState.IsPressed(Key.F23) &&
(_keyboardState.IsPressed(Key.WinLeft) || _keyboardState.IsPressed(Key.WinRight)))
{
return Key.ControlRight;
}
return Key.Unknown;
} }
} }
} }

View File

@@ -2,6 +2,7 @@ using Ryujinx.Common;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion; using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid; using Ryujinx.HLE.HOS.Services.Hid;
using System; using System;
@@ -233,7 +234,9 @@ namespace Ryujinx.Input.HLE
_gamepad?.Dispose(); _gamepad?.Dispose();
Id = config.Id; Id = config.Id;
_gamepad = GamepadDriver.GetGamepad(Id); _gamepad = config is StandardKeyboardInputConfig && GamepadDriver is IKeyboardModeDriver keyboardModeDriver
? keyboardModeDriver.GetKeyboard(Id, KeyboardInputMode.Physical)
: GamepadDriver.GetGamepad(Id);
UpdateUserConfiguration(config); UpdateUserConfiguration(config);
@@ -558,6 +561,7 @@ namespace Ryujinx.Input.HLE
!controllerConfig.Rumble.EnableRumble) !controllerConfig.Rumble.EnableRumble)
{ {
return; return;
} }
VibrationValue leftVibrationValue = dualVibrationValue.Item1; VibrationValue leftVibrationValue = dualVibrationValue.Item1;

View File

@@ -24,7 +24,7 @@ namespace Ryujinx.Input.HLE
private readonly Lock _lock = new(); private readonly Lock _lock = new();
private bool _blockInputUpdates; private int _inputUpdateBlockCount;
private const int MaxControllers = 9; private const int MaxControllers = 9;
@@ -36,6 +36,7 @@ namespace Ryujinx.Input.HLE
private bool _isDisposed; private bool _isDisposed;
private List<InputConfig> _inputConfig; private List<InputConfig> _inputConfig;
private List<InputConfig> _requestedInputConfig;
private bool _enableKeyboard; private bool _enableKeyboard;
private bool _enableMouse; private bool _enableMouse;
private Switch _device; private Switch _device;
@@ -52,6 +53,7 @@ namespace Ryujinx.Input.HLE
_gamepadDriver = gamepadDriver; _gamepadDriver = gamepadDriver;
_mouseDriver = mouseDriver; _mouseDriver = mouseDriver;
_inputConfig = []; _inputConfig = [];
_requestedInputConfig = [];
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; _gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; _gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
@@ -89,29 +91,23 @@ namespace Ryujinx.Input.HLE
} }
} }
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse); ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
} }
} }
private void HandleOnGamepadConnected(string id) private void HandleOnGamepadConnected(string _)
{ {
// Force input reload // Force input reload
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse); ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config) private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config)
{ {
IGamepadDriver targetDriver = _gamepadDriver; IGamepadDriver targetDriver =
config is StandardKeyboardInputConfig
if (config is StandardControllerInputConfig) ? _keyboardDriver
{ : _gamepadDriver;
targetDriver = _gamepadDriver;
}
else if (config is StandardKeyboardInputConfig)
{
targetDriver = _keyboardDriver;
}
Debug.Assert(targetDriver != null, "Unknown input configuration!"); Debug.Assert(targetDriver != null, "Unknown input configuration!");
@@ -127,11 +123,13 @@ namespace Ryujinx.Input.HLE
{ {
lock (_lock) lock (_lock)
{ {
_requestedInputConfig = inputConfig?.ToList() ?? [];
NpadController[] oldControllers = _controllers.ToArray(); NpadController[] oldControllers = _controllers.ToArray();
List<InputConfig> validInputs = []; List<InputConfig> validInputs = [];
foreach (InputConfig inputConfigEntry in inputConfig) foreach (InputConfig inputConfigEntry in _requestedInputConfig)
{ {
NpadController controller; NpadController controller;
int index = (int)inputConfigEntry.PlayerIndex; int index = (int)inputConfigEntry.PlayerIndex;
@@ -147,7 +145,16 @@ namespace Ryujinx.Input.HLE
controller = new(_cemuHookClient); controller = new(_cemuHookClient);
} }
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry); InputConfig activeConfig = inputConfigEntry;
bool isValid = DriverConfigurationUpdate(ref controller, activeConfig);
if (!isValid &&
inputConfigEntry is StandardControllerInputConfig &&
TryGetKeyboardFallback(inputConfigEntry, out StandardKeyboardInputConfig fallbackConfig))
{
activeConfig = fallbackConfig;
isValid = DriverConfigurationUpdate(ref controller, activeConfig);
}
if (!isValid) if (!isValid)
{ {
@@ -157,7 +164,7 @@ namespace Ryujinx.Input.HLE
else else
{ {
_controllers[index] = controller; _controllers[index] = controller;
validInputs.Add(inputConfigEntry); validInputs.Add(activeConfig);
} }
} }
@@ -169,7 +176,7 @@ namespace Ryujinx.Input.HLE
oldControllers[i] = null; oldControllers[i] = null;
} }
_inputConfig = inputConfig; _inputConfig = validInputs;
_enableKeyboard = enableKeyboard; _enableKeyboard = enableKeyboard;
_enableMouse = enableMouse; _enableMouse = enableMouse;
@@ -177,16 +184,58 @@ namespace Ryujinx.Input.HLE
} }
} }
public void UnblockInputUpdates() private bool TryGetKeyboardFallback(InputConfig inputConfig, out StandardKeyboardInputConfig fallbackConfig)
{ {
lock (_lock) fallbackConfig = null;
ReadOnlySpan<string> keyboardIds = _keyboardDriver.GamepadsIds;
if (keyboardIds.IsEmpty)
{
return false;
}
string keyboardId = keyboardIds[0];
using IGamepad keyboard = _keyboardDriver.GetGamepad(keyboardId);
if (keyboard == null)
{
return false;
}
fallbackConfig = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
keyboardId,
keyboard.Name,
inputConfig.ControllerType,
inputConfig.PlayerIndex);
return true;
}
private void ClearInputDriverStates()
{ {
foreach (InputConfig inputConfig in _inputConfig) foreach (InputConfig inputConfig in _inputConfig)
{ {
_controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear(); _controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear();
} }
}
_blockInputUpdates = false; public void UnblockInputUpdates()
{
lock (_lock)
{
if (_inputUpdateBlockCount == 0)
{
return;
}
_inputUpdateBlockCount--;
if (_inputUpdateBlockCount == 0)
{
ClearInputDriverStates();
}
} }
} }
@@ -195,7 +244,7 @@ namespace Ryujinx.Input.HLE
get get
{ {
lock (_lock) lock (_lock)
return _blockInputUpdates; return _inputUpdateBlockCount > 0;
} }
} }
@@ -203,7 +252,7 @@ namespace Ryujinx.Input.HLE
{ {
lock (_lock) lock (_lock)
{ {
_blockInputUpdates = true; _inputUpdateBlockCount++;
} }
} }
@@ -235,7 +284,7 @@ namespace Ryujinx.Input.HLE
bool isJoyconPair = false; bool isJoyconPair = false;
// Do we allow input updates and is a controller connected? // Do we allow input updates and is a controller connected?
if (!_blockInputUpdates && controller != null) if (_inputUpdateBlockCount == 0 && controller != null)
{ {
DriverConfigurationUpdate(ref controller, inputConfig); DriverConfigurationUpdate(ref controller, inputConfig);
@@ -273,7 +322,7 @@ namespace Ryujinx.Input.HLE
} }
} }
if (!_blockInputUpdates && _enableKeyboard) if (_inputUpdateBlockCount == 0 && _enableKeyboard)
{ {
hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver); hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver);
} }
@@ -334,7 +383,7 @@ namespace Ryujinx.Input.HLE
} }
} }
internal InputConfig GetPlayerInputConfigByIndex(int index) public InputConfig GetPlayerInputConfigByIndex(int index)
{ {
lock (_lock) lock (_lock)
{ {

View File

@@ -1,5 +1,6 @@
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input namespace Ryujinx.Input
{ {
@@ -33,15 +34,26 @@ namespace Ryujinx.Input
{ {
if (_keyState is null) if (_keyState is null)
{ {
_keyState = new bool[(int)Key.Count]; _keyState = new bool[(int)ConfigPhysicalKey.Count];
} }
for (Key key = 0; key < Key.Count; key++) for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
{ {
_keyState[(int)key] = keyboard.IsPressed(key); _keyState[(int)key] = keyboard.IsPressed((Key)(int)key);
} }
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

@@ -0,0 +1,7 @@
namespace Ryujinx.Input
{
public interface IKeyboardModeDriver : IGamepadDriver
{
IKeyboard GetKeyboard(string id, KeyboardInputMode mode);
}
}

View File

@@ -0,0 +1,148 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
namespace Ryujinx.Input
{
public static class InputConfigDefaults
{
public static StandardKeyboardInputConfig CreateDefaultKeyboardConfiguration(
string id,
string name,
ControllerType controllerType,
PlayerIndex playerIndex)
{
return new StandardKeyboardInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.WindowKeyboard,
Id = id,
Name = name,
PlayerIndex = playerIndex,
ControllerType = controllerType,
LeftJoycon = new LeftJoyconCommonConfig<ConfigPhysicalKey>
{
DpadUp = ConfigPhysicalKey.Up,
DpadDown = ConfigPhysicalKey.Down,
DpadLeft = ConfigPhysicalKey.Left,
DpadRight = ConfigPhysicalKey.Right,
ButtonMinus = ConfigPhysicalKey.Minus,
ButtonL = ConfigPhysicalKey.E,
ButtonZl = ConfigPhysicalKey.Q,
ButtonSl = ConfigPhysicalKey.Unbound,
ButtonSr = ConfigPhysicalKey.Unbound,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<ConfigPhysicalKey>
{
StickUp = ConfigPhysicalKey.W,
StickDown = ConfigPhysicalKey.S,
StickLeft = ConfigPhysicalKey.A,
StickRight = ConfigPhysicalKey.D,
StickButton = ConfigPhysicalKey.F,
},
RightJoycon = new RightJoyconCommonConfig<ConfigPhysicalKey>
{
ButtonA = ConfigPhysicalKey.Z,
ButtonB = ConfigPhysicalKey.X,
ButtonX = ConfigPhysicalKey.C,
ButtonY = ConfigPhysicalKey.V,
ButtonPlus = ConfigPhysicalKey.Plus,
ButtonR = ConfigPhysicalKey.U,
ButtonZr = ConfigPhysicalKey.O,
ButtonSl = ConfigPhysicalKey.Unbound,
ButtonSr = ConfigPhysicalKey.Unbound,
},
RightJoyconStick = new JoyconConfigKeyboardStick<ConfigPhysicalKey>
{
StickUp = ConfigPhysicalKey.I,
StickDown = ConfigPhysicalKey.K,
StickLeft = ConfigPhysicalKey.J,
StickRight = ConfigPhysicalKey.L,
StickButton = ConfigPhysicalKey.H,
},
};
}
public static StandardControllerInputConfig CreateDefaultControllerConfiguration(
string id,
string name,
ControllerType controllerType,
PlayerIndex playerIndex,
bool isNintendoStyle)
{
return new StandardControllerInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL3,
Id = id,
Name = name,
PlayerIndex = playerIndex,
ControllerType = controllerType,
DeadzoneLeft = 0.1f,
DeadzoneRight = 0.1f,
RangeLeft = 1.0f,
RangeRight = 1.0f,
TriggerThreshold = 0.5f,
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
{
DpadUp = ConfigGamepadInputId.DpadUp,
DpadDown = ConfigGamepadInputId.DpadDown,
DpadLeft = ConfigGamepadInputId.DpadLeft,
DpadRight = ConfigGamepadInputId.DpadRight,
ButtonMinus = ConfigGamepadInputId.Minus,
ButtonL = ConfigGamepadInputId.LeftShoulder,
ButtonZl = ConfigGamepadInputId.LeftTrigger,
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger0,
ButtonSr = ConfigGamepadInputId.SingleRightTrigger0,
},
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Left,
StickButton = ConfigGamepadInputId.LeftStick,
InvertStickX = false,
InvertStickY = false,
Rotate90CW = false,
},
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
{
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
ButtonPlus = ConfigGamepadInputId.Plus,
ButtonR = ConfigGamepadInputId.RightShoulder,
ButtonZr = ConfigGamepadInputId.RightTrigger,
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger1,
ButtonSr = ConfigGamepadInputId.SingleRightTrigger1,
},
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Right,
StickButton = ConfigGamepadInputId.RightStick,
InvertStickX = false,
InvertStickY = false,
Rotate90CW = false,
},
Motion = new StandardMotionConfigController
{
MotionBackend = MotionInputBackendType.GamepadDriver,
EnableMotion = true,
Sensitivity = 100,
GyroDeadzone = 1,
},
Rumble = new RumbleConfigController
{
StrongRumble = 1f,
WeakRumble = 1f,
EnableRumble = false,
UseHDRumble = true,
},
};
}
}
}

View File

@@ -0,0 +1,78 @@
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using System.Numerics;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input
{
public static class KeyboardInputMappingHelper
{
public readonly record struct KeyboardButtonMapping(GamepadButtonInputId To, ConfigPhysicalKey From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound;
}
public static KeyboardButtonMapping[] BuildButtonMappings(StandardKeyboardInputConfig configuration) =>
[
// Left JoyCon
new(GamepadButtonInputId.LeftStick, configuration.LeftJoyconStick.StickButton),
new(GamepadButtonInputId.DpadUp, configuration.LeftJoycon.DpadUp),
new(GamepadButtonInputId.DpadDown, configuration.LeftJoycon.DpadDown),
new(GamepadButtonInputId.DpadLeft, configuration.LeftJoycon.DpadLeft),
new(GamepadButtonInputId.DpadRight, configuration.LeftJoycon.DpadRight),
new(GamepadButtonInputId.Minus, configuration.LeftJoycon.ButtonMinus),
new(GamepadButtonInputId.LeftShoulder, configuration.LeftJoycon.ButtonL),
new(GamepadButtonInputId.LeftTrigger, configuration.LeftJoycon.ButtonZl),
new(GamepadButtonInputId.SingleRightTrigger0, configuration.LeftJoycon.ButtonSr),
new(GamepadButtonInputId.SingleLeftTrigger0, configuration.LeftJoycon.ButtonSl),
// Right JoyCon
new(GamepadButtonInputId.RightStick, configuration.RightJoyconStick.StickButton),
new(GamepadButtonInputId.A, configuration.RightJoycon.ButtonA),
new(GamepadButtonInputId.B, configuration.RightJoycon.ButtonB),
new(GamepadButtonInputId.X, configuration.RightJoycon.ButtonX),
new(GamepadButtonInputId.Y, configuration.RightJoycon.ButtonY),
new(GamepadButtonInputId.Plus, configuration.RightJoycon.ButtonPlus),
new(GamepadButtonInputId.RightShoulder, configuration.RightJoycon.ButtonR),
new(GamepadButtonInputId.RightTrigger, configuration.RightJoycon.ButtonZr),
new(GamepadButtonInputId.SingleRightTrigger1, configuration.RightJoycon.ButtonSr),
new(GamepadButtonInputId.SingleLeftTrigger1, configuration.RightJoycon.ButtonSl),
];
public static (short X, short Y) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigPhysicalKey> stickConfig)
{
short stickX = 0;
short stickY = 0;
if (snapshot.IsPressed(stickConfig.StickUp))
{
stickY += 1;
}
if (snapshot.IsPressed(stickConfig.StickDown))
{
stickY -= 1;
}
if (snapshot.IsPressed(stickConfig.StickRight))
{
stickX += 1;
}
if (snapshot.IsPressed(stickConfig.StickLeft))
{
stickX -= 1;
}
if (stickX == 0 && stickY == 0)
{
return (0, 0);
}
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Ryujinx.Input
{
public enum KeyboardInputMode
{
Semantic,
Physical,
}
}

View File

@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input namespace Ryujinx.Input
{ {
@@ -25,5 +26,8 @@ namespace Ryujinx.Input
/// <returns>True if the given key is pressed</returns> /// <returns>True if the given key is pressed</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsPressed(Key key) => KeysState[(int)key]; public bool IsPressed(Key key) => KeysState[(int)key];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsPressed(ConfigPhysicalKey key) => KeysState[(int)key];
} }
} }

View File

@@ -7,8 +7,6 @@ using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging; using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities; using Ryujinx.Common.Utilities;
using Ryujinx.Cpu; using Ryujinx.Cpu;
@@ -17,14 +15,12 @@ using Ryujinx.Graphics.OpenGL;
using Ryujinx.Graphics.Vulkan; using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE; using Ryujinx.HLE;
using Ryujinx.Input; using Ryujinx.Input;
using Ryujinx.Input.SDL3;
using Silk.NET.Vulkan; using Silk.NET.Vulkan;
using System; using System;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Headless namespace Ryujinx.Headless
{ {
@@ -91,6 +87,19 @@ namespace Ryujinx.Headless
string gamepadName = gamepad.Name; string gamepadName = gamepad.Name;
bool isNintendoStyle = false;
if (gamepad is SDL3Gamepad sdlGp)
{
// Nintendo vendor ID is 0x057E
isNintendoStyle = sdlGp.VendorId == 0x057E;
}
else
{
// Fallback to name-based detection
isNintendoStyle = gamepadName.Contains("Nintendo", StringComparison.OrdinalIgnoreCase);
}
gamepad.Dispose(); gamepad.Dispose();
InputConfig config; InputConfig config;
@@ -99,131 +108,21 @@ namespace Ryujinx.Headless
{ {
if (isKeyboard) if (isKeyboard)
{ {
config = new StandardKeyboardInputConfig config = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
{ null,
Version = InputConfig.CurrentVersion, null,
Backend = InputBackendType.WindowKeyboard, ControllerType.JoyconPair,
Id = null, index);
ControllerType = ControllerType.JoyconPair,
LeftJoycon = new LeftJoyconCommonConfig<Key>
{
DpadUp = Key.Up,
DpadDown = Key.Down,
DpadLeft = Key.Left,
DpadRight = Key.Right,
ButtonMinus = Key.Minus,
ButtonL = Key.E,
ButtonZl = Key.Q,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
{
ButtonA = Key.Z,
ButtonB = Key.X,
ButtonX = Key.C,
ButtonY = Key.V,
ButtonPlus = Key.Plus,
ButtonR = Key.U,
ButtonZr = Key.O,
ButtonSl = Key.Unbound,
ButtonSr = Key.Unbound,
},
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
},
};
} }
else else
{ {
bool isNintendoStyle = gamepadName.Contains("Nintendo");
config = new StandardControllerInputConfig config = InputConfigDefaults.CreateDefaultControllerConfiguration(
{ null,
Version = InputConfig.CurrentVersion, null,
Backend = InputBackendType.GamepadSDL3, ControllerType.JoyconPair,
Id = null, index,
ControllerType = ControllerType.JoyconPair, isNintendoStyle);
DeadzoneLeft = 0.1f,
DeadzoneRight = 0.1f,
RangeLeft = 1.0f,
RangeRight = 1.0f,
TriggerThreshold = 0.5f,
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
{
DpadUp = ConfigGamepadInputId.DpadUp,
DpadDown = ConfigGamepadInputId.DpadDown,
DpadLeft = ConfigGamepadInputId.DpadLeft,
DpadRight = ConfigGamepadInputId.DpadRight,
ButtonMinus = ConfigGamepadInputId.Minus,
ButtonL = ConfigGamepadInputId.LeftShoulder,
ButtonZl = ConfigGamepadInputId.LeftTrigger,
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger0,
ButtonSr = ConfigGamepadInputId.SingleRightTrigger0,
},
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Left,
StickButton = ConfigGamepadInputId.LeftStick,
InvertStickX = false,
InvertStickY = false,
Rotate90CW = false,
},
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
{
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
ButtonPlus = ConfigGamepadInputId.Plus,
ButtonR = ConfigGamepadInputId.RightShoulder,
ButtonZr = ConfigGamepadInputId.RightTrigger,
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger1,
ButtonSr = ConfigGamepadInputId.SingleRightTrigger1,
},
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
{
Joystick = ConfigStickInputId.Right,
StickButton = ConfigGamepadInputId.RightStick,
InvertStickX = false,
InvertStickY = false,
Rotate90CW = false,
},
Motion = new StandardMotionConfigController
{
MotionBackend = MotionInputBackendType.GamepadDriver,
EnableMotion = true,
Sensitivity = 100,
GyroDeadzone = 1,
},
Rumble = new RumbleConfigController
{
StrongRumble = 1f,
WeakRumble = 1f,
EnableRumble = false,
UseHDRumble = true
},
};
} }
} }
else else

View File

@@ -7,15 +7,15 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Threading; using System.Threading;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
using Key = Ryujinx.Input.Key; using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.Input namespace Ryujinx.Ava.Input
{ {
internal class AvaloniaKeyboard : IKeyboard internal class AvaloniaKeyboard : IKeyboard
{ {
private readonly List<ButtonMappingEntry> _buttonsUserMapping; private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
private readonly AvaloniaKeyboardDriver _driver; private readonly AvaloniaKeyboardDriver _driver;
private readonly KeyboardInputMode _mode;
private StandardKeyboardInputConfig _configuration; private StandardKeyboardInputConfig _configuration;
private readonly Lock _userMappingLock = new(); private readonly Lock _userMappingLock = new();
@@ -25,18 +25,12 @@ namespace Ryujinx.Ava.Input
public bool IsConnected => true; public bool IsConnected => true;
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None; public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name, KeyboardInputMode mode)
private class ButtonMappingEntry(GamepadButtonInputId to, Key from)
{
public readonly GamepadButtonInputId To = to;
public readonly Key From = from;
}
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name)
{ {
_buttonsUserMapping = []; _buttonsUserMapping = [];
_driver = driver; _driver = driver;
_mode = mode;
Id = id; Id = id;
Name = name; Name = name;
} }
@@ -58,22 +52,18 @@ namespace Ryujinx.Ava.Input
return result; return result;
} }
foreach (ButtonMappingEntry entry in _buttonsUserMapping) foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
{ {
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound) if (!entry.IsValid || result.IsPressed(entry.To))
{ {
continue; continue;
} }
// NOTE: Do not touch state of the button already pressed.
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From)); result.SetPressed(entry.To, rawState.IsPressed(entry.From));
} }
}
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick); (short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick); (short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY)); result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY)); result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
@@ -101,7 +91,7 @@ namespace Ryujinx.Ava.Input
{ {
try try
{ {
return _driver.IsPressed(key); return _driver.IsPressed(key, _mode);
} }
catch catch
{ {
@@ -109,6 +99,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)
@@ -117,53 +120,20 @@ namespace Ryujinx.Ava.Input
_buttonsUserMapping.Clear(); _buttonsUserMapping.Clear();
#pragma warning disable IDE0055 // Disable formatting _buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
// Left JoyCon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
// Right JoyCon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
#pragma warning restore IDE0055
} }
} }
public void SetLed(uint packedRgb) public void SetLed(uint packedRgb)
{ {
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard"); Logger.Debug?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
} }
public void SetTriggerThreshold(float triggerThreshold) public bool HDRumble(VibrationValue left, VibrationValue right) => false;
{
// No operations
}
public bool HDRumble(VibrationValue left, VibrationValue right) public void SetTriggerThreshold(float triggerThreshold) { }
{
return false;
}
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) => false;
{
return false;
}
public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero; public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero;
@@ -174,41 +144,9 @@ namespace Ryujinx.Ava.Input
return value * ConvertRate; return value * ConvertRate;
} }
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
{
short stickX = 0;
short stickY = 0;
if (snapshot.IsPressed((Key)stickConfig.StickUp))
{
stickY += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickDown))
{
stickY -= 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickRight))
{
stickX += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
{
stickX -= 1;
}
Vector2 stick = new(stickX, stickY);
stick = Vector2.Normalize(stick);
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
public void Clear() public void Clear()
{ {
_driver?.Clear(); _driver?.Clear(_mode);
} }
public void Dispose() { } public void Dispose() { }

View File

@@ -1,19 +1,55 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common.Logging;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using AvaKey = Avalonia.Input.Key; using AvaKey = Avalonia.Input.Key;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using Key = Ryujinx.Input.Key; using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.Input namespace Ryujinx.Ava.Input
{ {
internal class AvaloniaKeyboardDriver : IGamepadDriver internal class AvaloniaKeyboardDriver : IKeyboardModeDriver
{ {
private enum PhysicalKeySource
{
Direct,
ObservedFallback,
Unknown,
}
[Flags]
private enum CGEventFlags : ulong
{
AlphaShift = 1UL << 16 // CapsLock
}
private enum CGEventSourceStateID : uint
{
HIDSystemState = 1
}
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
private static extern CGEventFlags CGEventSourceFlagsState(CGEventSourceStateID stateID);
private static readonly string[] _keyboardIdentifers = ["0"]; private static readonly string[] _keyboardIdentifers = ["0"];
private readonly Control _control; private readonly Control _control;
private readonly HashSet<AvaKey> _pressedKeys; private readonly Window _window;
private readonly HashSet<Key> _semanticPressedKeys;
private readonly HashSet<ConfigPhysicalKey> _physicalPressedKeys;
private readonly HashSet<Key> _keysToRestoreAfterActivation;
private readonly Dictionary<Key, ConfigPhysicalKey> _observedPhysicalKeysBySemanticKey;
private readonly Queue<Key> _semanticPressedKeyQueue;
private readonly Queue<Key> _physicalPressedKeyQueue;
private readonly Lock _pressedKeyQueueLock;
private readonly KeyboardInputMode _defaultMode;
public event EventHandler<KeyEventArgs> KeyPressed; public event EventHandler<KeyEventArgs> KeyPressed;
public event EventHandler<KeyEventArgs> KeyRelease; public event EventHandler<KeyEventArgs> KeyRelease;
@@ -22,14 +58,41 @@ namespace Ryujinx.Ava.Input
public string DriverName => "AvaloniaKeyboardDriver"; public string DriverName => "AvaloniaKeyboardDriver";
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers; public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
public AvaloniaKeyboardDriver(Control control) public AvaloniaKeyboardDriver(Control control, KeyboardInputMode defaultMode = KeyboardInputMode.Semantic)
{ {
_control = control; _control = control;
_pressedKeys = []; _window = control as Window ?? TopLevel.GetTopLevel(control) as Window;
_semanticPressedKeys = [];
_physicalPressedKeys = [];
_keysToRestoreAfterActivation = [];
_observedPhysicalKeysBySemanticKey = [];
_semanticPressedKeyQueue = [];
_physicalPressedKeyQueue = [];
_pressedKeyQueueLock = new();
_defaultMode = defaultMode;
_control.KeyDown += OnKeyPress; _control.AddHandler(InputElement.KeyDownEvent, OnKeyPress, RoutingStrategies.Tunnel, true);
_control.KeyUp += OnKeyRelease; _control.AddHandler(InputElement.KeyUpEvent, OnKeyRelease, RoutingStrategies.Tunnel, true);
_control.TextInput += Control_TextInput; _control.TextInput += Control_TextInput;
_window?.Activated += Window_Activated;
_window?.Deactivated += Window_Deactivated;
}
private void Window_Activated(object sender, EventArgs e)
{
RestorePressedKeysAfterActivation();
}
private void Window_Deactivated(object sender, EventArgs e)
{
lock (_pressedKeyQueueLock)
{
_keysToRestoreAfterActivation.Clear();
_keysToRestoreAfterActivation.UnionWith(_semanticPressedKeys);
_observedPhysicalKeysBySemanticKey.Clear();
}
Clear();
} }
private void Control_TextInput(object sender, TextInputEventArgs e) private void Control_TextInput(object sender, TextInputEventArgs e)
@@ -50,13 +113,18 @@ namespace Ryujinx.Ava.Input
} }
public IGamepad GetGamepad(string id) public IGamepad GetGamepad(string id)
{
return GetKeyboard(id, _defaultMode);
}
public IKeyboard GetKeyboard(string id, KeyboardInputMode mode)
{ {
if (!_keyboardIdentifers[0].Equals(id)) if (!_keyboardIdentifers[0].Equals(id))
{ {
return null; return null;
} }
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.AllKeyboards]); return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.KeyboardLayout_KeyboardInputMode], mode);
} }
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")]; public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
@@ -65,40 +133,448 @@ namespace Ryujinx.Ava.Input
{ {
if (disposing) if (disposing)
{ {
_control.KeyUp -= OnKeyPress; _control.RemoveHandler(InputElement.KeyDownEvent, OnKeyPress);
_control.KeyDown -= OnKeyRelease; _control.RemoveHandler(InputElement.KeyUpEvent, OnKeyRelease);
_control.TextInput -= Control_TextInput;
if (_window != null)
{
_window.Activated -= Window_Activated;
_window.Deactivated -= Window_Deactivated;
}
_observedPhysicalKeysBySemanticKey.Clear();
} }
} }
protected void OnKeyPress(object sender, KeyEventArgs args) protected void OnKeyPress(object sender, KeyEventArgs args)
{ {
_pressedKeys.Add(args.Key); UpdateKeyStates(args, true);
KeyPressed?.Invoke(this, args); KeyPressed?.Invoke(this, args);
} }
protected void OnKeyRelease(object sender, KeyEventArgs args) protected void OnKeyRelease(object sender, KeyEventArgs args)
{ {
_pressedKeys.Remove(args.Key); UpdateKeyStates(args, false);
KeyRelease?.Invoke(this, args); KeyRelease?.Invoke(this, args);
} }
internal bool IsPressed(Key key) internal bool IsPressed(Key key, KeyboardInputMode mode)
{ {
if (key is Key.Unbound or Key.Unknown) if (key is Key.Unbound or Key.Unknown)
{ {
return false; return false;
} }
AvaloniaKeyboardMappingHelper.TryGetAvaKey(key, out AvaKey nativeKey); if (key == Key.CapsLock)
{
return IsCapsLockOnMacOS();
}
return _pressedKeys.Contains(nativeKey); return mode == KeyboardInputMode.Physical
? _physicalPressedKeys.Contains((ConfigPhysicalKey)(int)key)
: _semanticPressedKeys.Contains(key);
}
private bool IsCapsLockOnMacOS()
{
bool currentState = false;
try
{
if (OperatingSystem.IsMacOS())
{
CGEventFlags flags = CGEventSourceFlagsState(CGEventSourceStateID.HIDSystemState);
currentState = (flags & CGEventFlags.AlphaShift) != 0;
}
else
{
// Fallback: use Avalonia's tracked key state (semantic CapsLock)
if (AvaloniaKeyboardMappingHelper.TryGetAvaKey(Key.CapsLock, out AvaKey nativeKey))
{
currentState = _semanticPressedKeys.Contains(Key.CapsLock);
}
}
}
catch (Exception ex)
{
Logger.Debug?.Print(LogClass.UI, $"Failed to query CapsLock state: {ex}");
}
return currentState;
}
internal void Clear(KeyboardInputMode mode)
{
lock (_pressedKeyQueueLock)
{
if (mode == KeyboardInputMode.Physical)
{
_physicalPressedKeys.Clear();
_physicalPressedKeyQueue.Clear();
}
else
{
_semanticPressedKeys.Clear();
_semanticPressedKeyQueue.Clear();
}
}
} }
public void Clear() public void Clear()
{ {
_pressedKeys.Clear(); lock (_pressedKeyQueueLock)
{
_semanticPressedKeys.Clear();
_physicalPressedKeys.Clear();
_semanticPressedKeyQueue.Clear();
_physicalPressedKeyQueue.Clear();
}
}
private void RestorePressedKeysAfterActivation()
{
if (!OperatingSystem.IsWindows())
{
lock (_pressedKeyQueueLock)
{
_keysToRestoreAfterActivation.Clear();
}
return;
}
lock (_pressedKeyQueueLock)
{
if (_keysToRestoreAfterActivation.Count == 0)
{
return;
}
foreach (Key key in _keysToRestoreAfterActivation)
{
if (!TryGetWindowsVirtualKey(key, out int virtualKey) ||
!IsWindowsKeyPressed(virtualKey))
{
continue;
}
_semanticPressedKeys.Add(key);
ConfigPhysicalKey physicalKey = GetPhysicalKeyForSemanticKey(key);
if (physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
{
_physicalPressedKeys.Add(physicalKey);
}
}
_keysToRestoreAfterActivation.Clear();
}
}
private ConfigPhysicalKey GetPhysicalKeyForSemanticKey(Key key)
{
if (_observedPhysicalKeysBySemanticKey.TryGetValue(key, out ConfigPhysicalKey physicalKey))
{
return physicalKey;
}
return key is >= Key.Unknown and < Key.Count
? (ConfigPhysicalKey)(int)key
: ConfigPhysicalKey.Unknown;
}
[SupportedOSPlatform("windows")]
private static bool IsWindowsKeyPressed(int virtualKey)
{
return (Win32NativeInterop.GetAsyncKeyState(virtualKey) & 0x8000) != 0;
}
private static bool TryGetWindowsVirtualKey(Key key, out int virtualKey)
{
switch (key)
{
case >= Key.A and <= Key.Z:
virtualKey = 'A' + (int)(key - Key.A);
return true;
case >= Key.Number0 and <= Key.Number9:
virtualKey = '0' + (int)(key - Key.Number0);
return true;
case >= Key.F1 and <= Key.F24:
virtualKey = 0x70 + (int)(key - Key.F1);
return true;
case Key.ShiftLeft:
virtualKey = 0xA0;
return true;
case Key.ShiftRight:
virtualKey = 0xA1;
return true;
case Key.ControlLeft:
virtualKey = 0xA2;
return true;
case Key.ControlRight:
virtualKey = 0xA3;
return true;
case Key.AltLeft:
virtualKey = 0xA4;
return true;
case Key.AltRight:
virtualKey = 0xA5;
return true;
case Key.WinLeft:
virtualKey = 0x5B;
return true;
case Key.WinRight:
virtualKey = 0x5C;
return true;
case Key.Menu:
virtualKey = 0x5D;
return true;
case Key.Up:
virtualKey = 0x26;
return true;
case Key.Down:
virtualKey = 0x28;
return true;
case Key.Left:
virtualKey = 0x25;
return true;
case Key.Right:
virtualKey = 0x27;
return true;
case Key.Enter:
virtualKey = 0x0D;
return true;
case Key.Escape:
virtualKey = 0x1B;
return true;
case Key.Space:
virtualKey = 0x20;
return true;
case Key.Tab:
virtualKey = 0x09;
return true;
case Key.BackSpace:
virtualKey = 0x08;
return true;
case Key.Insert:
virtualKey = 0x2D;
return true;
case Key.Delete:
virtualKey = 0x2E;
return true;
case Key.PageUp:
virtualKey = 0x21;
return true;
case Key.PageDown:
virtualKey = 0x22;
return true;
case Key.Home:
virtualKey = 0x24;
return true;
case Key.End:
virtualKey = 0x23;
return true;
case Key.CapsLock:
virtualKey = 0x14;
return true;
case Key.ScrollLock:
virtualKey = 0x91;
return true;
case Key.PrintScreen:
virtualKey = 0x2C;
return true;
case Key.Pause:
virtualKey = 0x13;
return true;
case Key.NumLock:
virtualKey = 0x90;
return true;
case Key.Clear:
virtualKey = 0x0C;
return true;
case >= Key.Keypad0 and <= Key.Keypad9:
virtualKey = 0x60 + (int)(key - Key.Keypad0);
return true;
case Key.KeypadDivide:
virtualKey = 0x6F;
return true;
case Key.KeypadMultiply:
virtualKey = 0x6A;
return true;
case Key.KeypadSubtract:
virtualKey = 0x6D;
return true;
case Key.KeypadAdd:
virtualKey = 0x6B;
return true;
case Key.KeypadDecimal:
virtualKey = 0x6E;
return true;
case Key.KeypadEnter:
virtualKey = 0x0D;
return true;
case Key.Tilde:
virtualKey = 0xC0;
return true;
case Key.Grave:
virtualKey = 0xE2;
return true;
case Key.Minus:
virtualKey = 0xBD;
return true;
case Key.Plus:
virtualKey = 0xBB;
return true;
case Key.BracketLeft:
virtualKey = 0xDB;
return true;
case Key.BracketRight:
virtualKey = 0xDD;
return true;
case Key.Semicolon:
virtualKey = 0xBA;
return true;
case Key.Quote:
virtualKey = 0xDE;
return true;
case Key.Comma:
virtualKey = 0xBC;
return true;
case Key.Period:
virtualKey = 0xBE;
return true;
case Key.Slash:
virtualKey = 0xBF;
return true;
case Key.BackSlash:
virtualKey = 0xDC;
return true;
default:
virtualKey = 0;
return false;
}
}
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)
{
if (key is Key.Unknown or Key.Unbound)
{
return;
}
if (isPressed)
{
pressedKeys.Add(key);
return;
}
pressedKeys.Remove(key);
}
private static void UpdateKeyState(HashSet<ConfigPhysicalKey> pressedKeys, ConfigPhysicalKey key, bool isPressed)
{
if (key is ConfigPhysicalKey.Unknown or ConfigPhysicalKey.Unbound)
{
return;
}
if (isPressed)
{
pressedKeys.Add(key);
return;
}
pressedKeys.Remove(key);
}
private void UpdateKeyStates(KeyEventArgs args, bool isPressed)
{
Key semanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.Key);
Key resolvedSemanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey, args.Key);
ConfigPhysicalKey physicalKey = GetPhysicalInputKey(args, semanticKey, out PhysicalKeySource physicalKeySource);
bool semanticWasPressed = _semanticPressedKeys.Contains(resolvedSemanticKey);
bool physicalWasPressed = _physicalPressedKeys.Contains(physicalKey);
bool semanticStateChanged = resolvedSemanticKey is not Key.Unknown and not Key.Unbound && semanticWasPressed != isPressed;
bool physicalStateChanged = physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound && physicalWasPressed != isPressed;
bool bufferedSemanticPress = false;
bool bufferedPhysicalPress = false;
UpdateKeyState(_semanticPressedKeys, resolvedSemanticKey, isPressed);
UpdateKeyState(_physicalPressedKeys, physicalKey, isPressed);
if (isPressed)
{
lock (_pressedKeyQueueLock)
{
if (!semanticWasPressed && resolvedSemanticKey is not Key.Unknown and not Key.Unbound)
{
_semanticPressedKeyQueue.Enqueue(resolvedSemanticKey);
bufferedSemanticPress = true;
}
if (!physicalWasPressed && physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
{
_physicalPressedKeyQueue.Enqueue((Key)(int)physicalKey);
bufferedPhysicalPress = true;
}
}
}
if (isPressed &&
semanticKey is not Key.Unknown and not Key.Unbound &&
physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
{
_observedPhysicalKeysBySemanticKey[semanticKey] = physicalKey;
}
if (ConfigurationState.Instance.Logger.EnableAvaloniaLog &&
(semanticStateChanged || physicalStateChanged))
{
Logger.Info?.Print(
LogClass.UI,
$"Keyboard {(isPressed ? "down" : "up")}: avaloniaKey={args.Key}, avaloniaPhysical={args.PhysicalKey}, keySymbol={FormatKeySymbol(args.KeySymbol)}, modifiers={args.KeyModifiers}, semantic={semanticKey}, resolvedSemantic={resolvedSemanticKey}, physical={physicalKey}, physicalSource={physicalKeySource}, bufferedSemantic={bufferedSemanticPress}, bufferedPhysical={bufferedPhysicalPress}, semanticPressed={_semanticPressedKeys.Count}, physicalPressed={_physicalPressedKeys.Count}");
}
}
private ConfigPhysicalKey GetPhysicalInputKey(KeyEventArgs args, Key semanticKey, out PhysicalKeySource source)
{
Key key = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey);
if (key is >= Key.Unknown and < Key.Count)
{
source = PhysicalKeySource.Direct;
return (ConfigPhysicalKey)(int)key;
}
if (semanticKey is not Key.Unknown and not Key.Unbound &&
_observedPhysicalKeysBySemanticKey.TryGetValue(semanticKey, out ConfigPhysicalKey observedPhysicalKey))
{
source = PhysicalKeySource.ObservedFallback;
return observedPhysicalKey;
}
source = PhysicalKeySource.Unknown;
return ConfigPhysicalKey.Unknown;
}
private static string FormatKeySymbol(string keySymbol)
{
return string.IsNullOrEmpty(keySymbol) ? "<none>" : keySymbol;
} }
public void Dispose() public void Dispose()

View File

@@ -2,6 +2,7 @@ using Ryujinx.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using AvaKey = Avalonia.Input.Key; using AvaKey = Avalonia.Input.Key;
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
namespace Ryujinx.Ava.Input namespace Ryujinx.Ava.Input
{ {
@@ -132,7 +133,8 @@ namespace Ryujinx.Ava.Input
AvaKey.D8, AvaKey.D8,
AvaKey.D9, AvaKey.D9,
AvaKey.OemTilde, AvaKey.OemTilde,
AvaKey.OemTilde,AvaKey.OemMinus, AvaKey.Oem102,
AvaKey.OemMinus,
AvaKey.OemPlus, AvaKey.OemPlus,
AvaKey.OemOpenBrackets, AvaKey.OemOpenBrackets,
AvaKey.OemCloseBrackets, AvaKey.OemCloseBrackets,
@@ -147,7 +149,149 @@ namespace Ryujinx.Ava.Input
AvaKey.None AvaKey.None
]; ];
private static readonly AvaPhysicalKey[] _physicalKeyMapping =
[
// NOTE: Invalid
AvaPhysicalKey.None,
AvaPhysicalKey.ShiftLeft,
AvaPhysicalKey.ShiftRight,
AvaPhysicalKey.ControlLeft,
AvaPhysicalKey.ControlRight,
AvaPhysicalKey.AltLeft,
AvaPhysicalKey.AltRight,
AvaPhysicalKey.MetaLeft,
AvaPhysicalKey.MetaRight,
AvaPhysicalKey.ContextMenu,
AvaPhysicalKey.F1,
AvaPhysicalKey.F2,
AvaPhysicalKey.F3,
AvaPhysicalKey.F4,
AvaPhysicalKey.F5,
AvaPhysicalKey.F6,
AvaPhysicalKey.F7,
AvaPhysicalKey.F8,
AvaPhysicalKey.F9,
AvaPhysicalKey.F10,
AvaPhysicalKey.F11,
AvaPhysicalKey.F12,
AvaPhysicalKey.F13,
AvaPhysicalKey.F14,
AvaPhysicalKey.F15,
AvaPhysicalKey.F16,
AvaPhysicalKey.F17,
AvaPhysicalKey.F18,
AvaPhysicalKey.F19,
AvaPhysicalKey.F20,
AvaPhysicalKey.F21,
AvaPhysicalKey.F22,
AvaPhysicalKey.F23,
AvaPhysicalKey.F24,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.None,
AvaPhysicalKey.ArrowUp,
AvaPhysicalKey.ArrowDown,
AvaPhysicalKey.ArrowLeft,
AvaPhysicalKey.ArrowRight,
AvaPhysicalKey.Enter,
AvaPhysicalKey.Escape,
AvaPhysicalKey.Space,
AvaPhysicalKey.Tab,
AvaPhysicalKey.Backspace,
AvaPhysicalKey.Insert,
AvaPhysicalKey.Delete,
AvaPhysicalKey.PageUp,
AvaPhysicalKey.PageDown,
AvaPhysicalKey.Home,
AvaPhysicalKey.End,
AvaPhysicalKey.CapsLock,
AvaPhysicalKey.ScrollLock,
AvaPhysicalKey.PrintScreen,
AvaPhysicalKey.Pause,
AvaPhysicalKey.NumLock,
AvaPhysicalKey.NumPadClear,
AvaPhysicalKey.NumPad0,
AvaPhysicalKey.NumPad1,
AvaPhysicalKey.NumPad2,
AvaPhysicalKey.NumPad3,
AvaPhysicalKey.NumPad4,
AvaPhysicalKey.NumPad5,
AvaPhysicalKey.NumPad6,
AvaPhysicalKey.NumPad7,
AvaPhysicalKey.NumPad8,
AvaPhysicalKey.NumPad9,
AvaPhysicalKey.NumPadDivide,
AvaPhysicalKey.NumPadMultiply,
AvaPhysicalKey.NumPadSubtract,
AvaPhysicalKey.NumPadAdd,
AvaPhysicalKey.NumPadDecimal,
AvaPhysicalKey.NumPadEnter,
AvaPhysicalKey.A,
AvaPhysicalKey.B,
AvaPhysicalKey.C,
AvaPhysicalKey.D,
AvaPhysicalKey.E,
AvaPhysicalKey.F,
AvaPhysicalKey.G,
AvaPhysicalKey.H,
AvaPhysicalKey.I,
AvaPhysicalKey.J,
AvaPhysicalKey.K,
AvaPhysicalKey.L,
AvaPhysicalKey.M,
AvaPhysicalKey.N,
AvaPhysicalKey.O,
AvaPhysicalKey.P,
AvaPhysicalKey.Q,
AvaPhysicalKey.R,
AvaPhysicalKey.S,
AvaPhysicalKey.T,
AvaPhysicalKey.U,
AvaPhysicalKey.V,
AvaPhysicalKey.W,
AvaPhysicalKey.X,
AvaPhysicalKey.Y,
AvaPhysicalKey.Z,
AvaPhysicalKey.Digit0,
AvaPhysicalKey.Digit1,
AvaPhysicalKey.Digit2,
AvaPhysicalKey.Digit3,
AvaPhysicalKey.Digit4,
AvaPhysicalKey.Digit5,
AvaPhysicalKey.Digit6,
AvaPhysicalKey.Digit7,
AvaPhysicalKey.Digit8,
AvaPhysicalKey.Digit9,
AvaPhysicalKey.Backquote,
AvaPhysicalKey.IntlBackslash,
AvaPhysicalKey.Minus,
AvaPhysicalKey.Equal,
AvaPhysicalKey.BracketLeft,
AvaPhysicalKey.BracketRight,
AvaPhysicalKey.Semicolon,
AvaPhysicalKey.Quote,
AvaPhysicalKey.Comma,
AvaPhysicalKey.Period,
AvaPhysicalKey.Slash,
AvaPhysicalKey.Backslash,
// NOTE: invalid
AvaPhysicalKey.None
];
private static readonly Dictionary<AvaKey, Key> _avaKeyMapping; private static readonly Dictionary<AvaKey, Key> _avaKeyMapping;
private static readonly Dictionary<AvaPhysicalKey, Key> _avaPhysicalKeyMapping;
static AvaloniaKeyboardMappingHelper() static AvaloniaKeyboardMappingHelper()
{ {
@@ -155,21 +299,42 @@ namespace Ryujinx.Ava.Input
// NOTE: Avalonia.Input.Key is not contiguous and quite large, so use a dictionary instead of an array. // NOTE: Avalonia.Input.Key is not contiguous and quite large, so use a dictionary instead of an array.
_avaKeyMapping = new Dictionary<AvaKey, Key>(); _avaKeyMapping = new Dictionary<AvaKey, Key>();
_avaPhysicalKeyMapping = new Dictionary<AvaPhysicalKey, Key>();
foreach (Key key in inputKeys) foreach (Key key in inputKeys)
{ {
if (TryGetAvaKey(key, out AvaKey index)) if (TryGetAvaKey(key, out AvaKey avaKey))
{ {
_avaKeyMapping[index] = key; _avaKeyMapping[avaKey] = key;
}
if (TryGetAvaPhysicalKey(key, out AvaPhysicalKey avaPhysicalKey))
{
_avaPhysicalKeyMapping[avaPhysicalKey] = key;
} }
} }
// Alias additional Avalonia key values to improve non-US layout support.
_avaKeyMapping[AvaKey.Oem1] = Key.Semicolon;
_avaKeyMapping[AvaKey.Oem2] = Key.Slash;
_avaKeyMapping[AvaKey.Oem3] = Key.Tilde;
_avaKeyMapping[AvaKey.Oem4] = Key.BracketLeft;
_avaKeyMapping[AvaKey.Oem5] = Key.BackSlash;
_avaKeyMapping[AvaKey.Oem6] = Key.BracketRight;
_avaKeyMapping[AvaKey.Oem7] = Key.Quote;
_avaKeyMapping[AvaKey.OemBackslash] = Key.Grave;
_avaKeyMapping[AvaKey.Oem102] = Key.Grave;
// Common alternates for non-US/JIS physical keys.
_avaPhysicalKeyMapping[AvaPhysicalKey.IntlRo] = Key.BackSlash;
_avaPhysicalKeyMapping[AvaPhysicalKey.IntlYen] = Key.BackSlash;
} }
public static bool TryGetAvaKey(Key key, out AvaKey avaKey) public static bool TryGetAvaKey(Key key, out AvaKey avaKey)
{ {
avaKey = AvaKey.None; avaKey = AvaKey.None;
bool keyExist = (int)key < _keyMapping.Length; bool keyExist = key < Key.Count && (int)key < _keyMapping.Length;
if (keyExist) if (keyExist)
{ {
avaKey = _keyMapping[(int)key]; avaKey = _keyMapping[(int)key];
@@ -178,9 +343,34 @@ namespace Ryujinx.Ava.Input
return keyExist; return keyExist;
} }
public static bool TryGetAvaPhysicalKey(Key key, out AvaPhysicalKey avaPhysicalKey)
{
avaPhysicalKey = AvaPhysicalKey.None;
bool keyExist = key < Key.Count && (int)key < _physicalKeyMapping.Length;
if (keyExist)
{
avaPhysicalKey = _physicalKeyMapping[(int)key];
}
return keyExist;
}
public static Key ToInputKey(AvaKey key) public static Key ToInputKey(AvaKey key)
{ {
return _avaKeyMapping.GetValueOrDefault(key, Key.Unknown); return _avaKeyMapping.GetValueOrDefault(key, Key.Unknown);
} }
public static Key ToInputKey(AvaPhysicalKey key)
{
return _avaPhysicalKeyMapping.GetValueOrDefault(key, Key.Unknown);
}
public static Key ToInputKey(AvaPhysicalKey physicalKey, AvaKey key)
{
Key inputKey = ToInputKey(key);
return inputKey != Key.Unknown ? inputKey : ToInputKey(physicalKey);
}
} }
} }

View File

@@ -1094,10 +1094,10 @@ namespace Ryujinx.Ava.Systems
{ {
Dispatcher.UIThread.InvokeAsync(() => Dispatcher.UIThread.InvokeAsync(() =>
{ {
if (_viewModel.StartGamesInFullscreen) if (_viewModel.StartGamesInFullscreen && _viewModel.WindowState is not WindowState.FullScreen)
{ {
_viewModel.WindowState = WindowState.FullScreen; // Use the view model toggle so decoration ordering matches user toggles.
_viewModel.Window.TitleBar.ExtendsContentIntoTitleBar = true; _viewModel.ToggleFullscreen();
} }
if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUi) if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUi)
@@ -1304,6 +1304,11 @@ namespace Ryujinx.Ava.Systems
return false; return false;
} }
if (!_viewModel.IsActive)
{
_inputManager.KeyboardDriver.Clear();
}
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat()); NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
if (_viewModel.IsActive) if (_viewModel.IsActive)

View File

@@ -13,6 +13,8 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Key = Ryujinx.Common.Configuration.Hid.Key;
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using RyuLogger = Ryujinx.Common.Logging.Logger; using RyuLogger = Ryujinx.Common.Logging.Logger;
namespace Ryujinx.Ava.Systems.Configuration namespace Ryujinx.Ava.Systems.Configuration
@@ -271,45 +273,45 @@ namespace Ryujinx.Ava.Systems.Configuration
Id = "0", Id = "0",
PlayerIndex = PlayerIndex.Player1, PlayerIndex = PlayerIndex.Player1,
ControllerType = ControllerType.ProController, ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<Key> LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{ {
DpadUp = Key.Up, DpadUp = PhysicalKey.Up,
DpadDown = Key.Down, DpadDown = PhysicalKey.Down,
DpadLeft = Key.Left, DpadLeft = PhysicalKey.Left,
DpadRight = Key.Right, DpadRight = PhysicalKey.Right,
ButtonMinus = Key.Minus, ButtonMinus = PhysicalKey.Minus,
ButtonL = Key.E, ButtonL = PhysicalKey.E,
ButtonZl = Key.Q, ButtonZl = PhysicalKey.Q,
ButtonSl = Key.Unbound, ButtonSl = PhysicalKey.Unbound,
ButtonSr = Key.Unbound, ButtonSr = PhysicalKey.Unbound,
}, },
LeftJoyconStick = new JoyconConfigKeyboardStick<Key> LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{ {
StickUp = Key.W, StickUp = PhysicalKey.W,
StickDown = Key.S, StickDown = PhysicalKey.S,
StickLeft = Key.A, StickLeft = PhysicalKey.A,
StickRight = Key.D, StickRight = PhysicalKey.D,
StickButton = Key.F, StickButton = PhysicalKey.F,
}, },
RightJoycon = new RightJoyconCommonConfig<Key> RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{ {
ButtonA = Key.Z, ButtonA = PhysicalKey.Z,
ButtonB = Key.X, ButtonB = PhysicalKey.X,
ButtonX = Key.C, ButtonX = PhysicalKey.C,
ButtonY = Key.V, ButtonY = PhysicalKey.V,
ButtonPlus = Key.Plus, ButtonPlus = PhysicalKey.Plus,
ButtonR = Key.U, ButtonR = PhysicalKey.U,
ButtonZr = Key.O, ButtonZr = PhysicalKey.O,
ButtonSl = Key.Unbound, ButtonSl = PhysicalKey.Unbound,
ButtonSr = Key.Unbound, ButtonSr = PhysicalKey.Unbound,
}, },
RightJoyconStick = new JoyconConfigKeyboardStick<Key> RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{ {
StickUp = Key.I, StickUp = PhysicalKey.I,
StickDown = Key.K, StickDown = PhysicalKey.K,
StickLeft = Key.J, StickLeft = PhysicalKey.J,
StickRight = Key.L, StickRight = PhysicalKey.L,
StickButton = Key.H, StickButton = PhysicalKey.H,
}, },
} }
]; ];

View File

@@ -8,6 +8,8 @@ using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE; using Ryujinx.HLE;
using System; using System;
using System.Linq; using System.Linq;
using Key = Ryujinx.Common.Configuration.Hid.Key;
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Ava.Systems.Configuration namespace Ryujinx.Ava.Systems.Configuration
{ {
@@ -288,45 +290,45 @@ namespace Ryujinx.Ava.Systems.Configuration
Name = "Keyboard", Name = "Keyboard",
PlayerIndex = PlayerIndex.Player1, PlayerIndex = PlayerIndex.Player1,
ControllerType = ControllerType.ProController, ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<Key> LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{ {
DpadUp = Key.Up, DpadUp = PhysicalKey.Up,
DpadDown = Key.Down, DpadDown = PhysicalKey.Down,
DpadLeft = Key.Left, DpadLeft = PhysicalKey.Left,
DpadRight = Key.Right, DpadRight = PhysicalKey.Right,
ButtonMinus = Key.Minus, ButtonMinus = PhysicalKey.Minus,
ButtonL = Key.E, ButtonL = PhysicalKey.E,
ButtonZl = Key.Q, ButtonZl = PhysicalKey.Q,
ButtonSl = Key.Unbound, ButtonSl = PhysicalKey.Unbound,
ButtonSr = Key.Unbound, ButtonSr = PhysicalKey.Unbound,
}, },
LeftJoyconStick = new JoyconConfigKeyboardStick<Key> LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{ {
StickUp = Key.W, StickUp = PhysicalKey.W,
StickDown = Key.S, StickDown = PhysicalKey.S,
StickLeft = Key.A, StickLeft = PhysicalKey.A,
StickRight = Key.D, StickRight = PhysicalKey.D,
StickButton = Key.F, StickButton = PhysicalKey.F,
}, },
RightJoycon = new RightJoyconCommonConfig<Key> RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{ {
ButtonA = Key.Z, ButtonA = PhysicalKey.Z,
ButtonB = Key.X, ButtonB = PhysicalKey.X,
ButtonX = Key.C, ButtonX = PhysicalKey.C,
ButtonY = Key.V, ButtonY = PhysicalKey.V,
ButtonPlus = Key.Plus, ButtonPlus = PhysicalKey.Plus,
ButtonR = Key.U, ButtonR = PhysicalKey.U,
ButtonZr = Key.O, ButtonZr = PhysicalKey.O,
ButtonSl = Key.Unbound, ButtonSl = PhysicalKey.Unbound,
ButtonSr = Key.Unbound, ButtonSr = PhysicalKey.Unbound,
}, },
RightJoyconStick = new JoyconConfigKeyboardStick<Key> RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{ {
StickUp = Key.I, StickUp = PhysicalKey.I,
StickDown = Key.K, StickDown = PhysicalKey.K,
StickLeft = Key.J, StickLeft = PhysicalKey.J,
StickRight = Key.L, StickRight = PhysicalKey.L,
StickButton = Key.H, StickButton = PhysicalKey.H,
}, },
} }
]; ];

View File

@@ -15,6 +15,7 @@ namespace Ryujinx.Ava.UI.Applet
class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler
{ {
private MainWindow _parent; private MainWindow _parent;
private AvaloniaKeyboardDriver _avaloniaKeyboardDriver;
private readonly OffscreenTextBox _hiddenTextBox; private readonly OffscreenTextBox _hiddenTextBox;
private bool _canProcessInput; private bool _canProcessInput;
private IDisposable _textChangedSubscription; private IDisposable _textChangedSubscription;
@@ -27,6 +28,7 @@ namespace Ryujinx.Ava.UI.Applet
if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver) if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver)
{ {
_avaloniaKeyboardDriver = avaloniaKeyboardDriver;
avaloniaKeyboardDriver.KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed; avaloniaKeyboardDriver.KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed;
avaloniaKeyboardDriver.KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease; avaloniaKeyboardDriver.KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease;
avaloniaKeyboardDriver.TextInput += AvaloniaDynamicTextInputHandler_TextInput; avaloniaKeyboardDriver.TextInput += AvaloniaDynamicTextInputHandler_TextInput;
@@ -65,7 +67,7 @@ namespace Ryujinx.Ava.UI.Applet
private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e) private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e)
{ {
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key); HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key);
if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true)) if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
{ {
@@ -85,7 +87,7 @@ namespace Ryujinx.Ava.UI.Applet
private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e) private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e)
{ {
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key); HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key);
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true)) if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
{ {
@@ -115,11 +117,11 @@ namespace Ryujinx.Ava.UI.Applet
public void Dispose() public void Dispose()
{ {
if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver) if (_avaloniaKeyboardDriver != null)
{ {
avaloniaKeyboardDriver.KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed; _avaloniaKeyboardDriver.KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed;
avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease; _avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput; _avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
} }
_textChangedSubscription?.Dispose(); _textChangedSubscription?.Dispose();

View File

@@ -1,5 +1,6 @@
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Threading; using Avalonia.Threading;
using Ryujinx.Ava.Input;
using Ryujinx.Input; using Ryujinx.Input;
using Ryujinx.Input.Assigner; using Ryujinx.Input.Assigner;
using System; using System;
@@ -25,6 +26,7 @@ namespace Ryujinx.Ava.UI.Helpers
private bool _isWaitingForInput; private bool _isWaitingForInput;
private bool _shouldUnbind; private bool _shouldUnbind;
private IKeyboard _keyboard;
public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned; public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
public ButtonKeyAssigner(ToggleButton toggleButton) public ButtonKeyAssigner(ToggleButton toggleButton)
@@ -34,6 +36,9 @@ namespace Ryujinx.Ava.UI.Helpers
public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null) public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
{ {
_keyboard = keyboard;
ClearKeyboardState(_keyboard);
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
ToggledButton.IsChecked = true; ToggledButton.IsChecked = true;
@@ -82,6 +87,7 @@ namespace Ryujinx.Ava.UI.Helpers
_isWaitingForInput = false; _isWaitingForInput = false;
ToggledButton.IsChecked = false; ToggledButton.IsChecked = false;
ClearKeyboardState(_keyboard);
if (pressedButton.HasValue && pressedButton.Value.AsHidType<Key>() == Key.BackSpace) if (pressedButton.HasValue && pressedButton.Value.AsHidType<Key>() == Key.BackSpace)
{ {
@@ -98,6 +104,15 @@ namespace Ryujinx.Ava.UI.Helpers
_isWaitingForInput = false; _isWaitingForInput = false;
ToggledButton.IsChecked = false; ToggledButton.IsChecked = false;
_shouldUnbind = shouldUnbind; _shouldUnbind = shouldUnbind;
ClearKeyboardState(_keyboard);
}
private static void ClearKeyboardState(IKeyboard keyboard)
{
if (keyboard is AvaloniaKeyboard avaloniaKeyboard)
{
avaloniaKeyboard.Clear();
}
} }
} }
} }

View File

@@ -5,6 +5,7 @@ using Ryujinx.Common.Configuration.Hid.Controller;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.UI.Helpers namespace Ryujinx.Ava.UI.Helpers
{ {
@@ -12,79 +13,6 @@ namespace Ryujinx.Ava.UI.Helpers
{ {
public static readonly KeyValueConverter Instance = new(); public static readonly KeyValueConverter Instance = new();
private static readonly Dictionary<Key, LocaleKeys> _keysMap = new()
{
{ Key.Unknown, LocaleKeys.KeyUnknown },
{ Key.ShiftLeft, LocaleKeys.KeyShiftLeft },
{ Key.ShiftRight, LocaleKeys.KeyShiftRight },
{ Key.ControlLeft, LocaleKeys.KeyControlLeft },
{ Key.ControlRight, LocaleKeys.KeyControlRight },
{ Key.AltLeft, LocaleKeys.KeyAltLeft },
{ Key.AltRight, LocaleKeys.KeyAltRight },
{ Key.WinLeft, LocaleKeys.KeyWinLeft },
{ Key.WinRight, LocaleKeys.KeyWinRight },
{ Key.Up, LocaleKeys.KeyUp },
{ Key.Down, LocaleKeys.KeyDown },
{ Key.Left, LocaleKeys.KeyLeft },
{ Key.Right, LocaleKeys.KeyRight },
{ Key.Enter, LocaleKeys.KeyEnter },
{ Key.Escape, LocaleKeys.KeyEscape },
{ Key.Space, LocaleKeys.KeySpace },
{ Key.Tab, LocaleKeys.KeyTab },
{ Key.BackSpace, LocaleKeys.KeyBackSpace },
{ Key.Insert, LocaleKeys.KeyInsert },
{ Key.Delete, LocaleKeys.KeyDelete },
{ Key.PageUp, LocaleKeys.KeyPageUp },
{ Key.PageDown, LocaleKeys.KeyPageDown },
{ Key.Home, LocaleKeys.KeyHome },
{ Key.End, LocaleKeys.KeyEnd },
{ Key.CapsLock, LocaleKeys.KeyCapsLock },
{ Key.ScrollLock, LocaleKeys.KeyScrollLock },
{ Key.PrintScreen, LocaleKeys.KeyPrintScreen },
{ Key.Pause, LocaleKeys.KeyPause },
{ Key.NumLock, LocaleKeys.KeyNumLock },
{ Key.Clear, LocaleKeys.KeyClear },
{ Key.Keypad0, LocaleKeys.KeyKeypad0 },
{ Key.Keypad1, LocaleKeys.KeyKeypad1 },
{ Key.Keypad2, LocaleKeys.KeyKeypad2 },
{ Key.Keypad3, LocaleKeys.KeyKeypad3 },
{ Key.Keypad4, LocaleKeys.KeyKeypad4 },
{ Key.Keypad5, LocaleKeys.KeyKeypad5 },
{ Key.Keypad6, LocaleKeys.KeyKeypad6 },
{ Key.Keypad7, LocaleKeys.KeyKeypad7 },
{ Key.Keypad8, LocaleKeys.KeyKeypad8 },
{ Key.Keypad9, LocaleKeys.KeyKeypad9 },
{ Key.KeypadDivide, LocaleKeys.KeyKeypadDivide },
{ Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply },
{ Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract },
{ Key.KeypadAdd, LocaleKeys.KeyKeypadAdd },
{ Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal },
{ Key.KeypadEnter, LocaleKeys.KeyKeypadEnter },
{ Key.Number0, LocaleKeys.KeyNumber0 },
{ Key.Number1, LocaleKeys.KeyNumber1 },
{ Key.Number2, LocaleKeys.KeyNumber2 },
{ Key.Number3, LocaleKeys.KeyNumber3 },
{ Key.Number4, LocaleKeys.KeyNumber4 },
{ Key.Number5, LocaleKeys.KeyNumber5 },
{ Key.Number6, LocaleKeys.KeyNumber6 },
{ Key.Number7, LocaleKeys.KeyNumber7 },
{ Key.Number8, LocaleKeys.KeyNumber8 },
{ Key.Number9, LocaleKeys.KeyNumber9 },
{ Key.Tilde, LocaleKeys.KeyTilde },
{ Key.Grave, LocaleKeys.KeyGrave },
{ Key.Minus, LocaleKeys.KeyMinus },
{ Key.Plus, LocaleKeys.KeyPlus },
{ Key.BracketLeft, LocaleKeys.KeyBracketLeft },
{ Key.BracketRight, LocaleKeys.KeyBracketRight },
{ Key.Semicolon, LocaleKeys.KeySemicolon },
{ Key.Quote, LocaleKeys.KeyQuote },
{ Key.Comma, LocaleKeys.KeyComma },
{ Key.Period, LocaleKeys.KeyPeriod },
{ Key.Slash, LocaleKeys.KeySlash },
{ Key.BackSlash, LocaleKeys.KeyBackSlash },
{ Key.Unbound, LocaleKeys.KeyUnbound },
};
private static readonly Dictionary<GamepadInputId, LocaleKeys> _gamepadInputIdMap = new() private static readonly Dictionary<GamepadInputId, LocaleKeys> _gamepadInputIdMap = new()
{ {
{ GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick }, { GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick },
@@ -110,78 +38,40 @@ namespace Ryujinx.Ava.UI.Helpers
{ GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0}, { GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0},
{ GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1}, { GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1},
{ GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1}, { GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1},
{ GamepadInputId.Unbound, LocaleKeys.KeyUnbound}, { GamepadInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
}; };
private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new() private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new()
{ {
{ StickInputId.Left, LocaleKeys.StickLeft}, { StickInputId.Left, LocaleKeys.StickLeft},
{ StickInputId.Right, LocaleKeys.StickRight}, { StickInputId.Right, LocaleKeys.StickRight},
{ StickInputId.Unbound, LocaleKeys.KeyUnbound}, { StickInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
}; };
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{ {
string keyString = string.Empty; return value switch
LocaleKeys localeKey;
switch (value)
{ {
case Key key: Key key => KeyboardLayoutLocaleHelper.TryGetSemanticLabel(key, out string localizedKeyLabel)
if (_keysMap.TryGetValue(key, out localeKey)) ? localizedKeyLabel
{ : key.ToString(),
if (OperatingSystem.IsMacOS()) PhysicalKey physicalKey => PhysicalKeyLabelHelper.GetDisplayString(physicalKey),
{ GamepadInputId gamepadInputId => GetLocalizedMappedValue(gamepadInputId, _gamepadInputIdMap),
localeKey = localeKey switch StickInputId stickInputId => GetLocalizedMappedValue(stickInputId, _stickInputIdMap),
{ _ => string.Empty,
LocaleKeys.KeyControlLeft => LocaleKeys.KeyMacControlLeft,
LocaleKeys.KeyControlRight => LocaleKeys.KeyMacControlRight,
LocaleKeys.KeyAltLeft => LocaleKeys.KeyMacAltLeft,
LocaleKeys.KeyAltRight => LocaleKeys.KeyMacAltRight,
LocaleKeys.KeyWinLeft => LocaleKeys.KeyMacWinLeft,
LocaleKeys.KeyWinRight => LocaleKeys.KeyMacWinRight,
_ => localeKey
}; };
} }
keyString = LocaleManager.Instance[localeKey];
}
else
{
keyString = key.ToString();
}
break;
case GamepadInputId gamepadInputId:
if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey))
{
keyString = LocaleManager.Instance[localeKey];
}
else
{
keyString = gamepadInputId.ToString();
}
break;
case StickInputId stickInputId:
if (_stickInputIdMap.TryGetValue(stickInputId, out localeKey))
{
keyString = LocaleManager.Instance[localeKey];
}
else
{
keyString = stickInputId.ToString();
}
break;
}
return keyString;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{ {
throw new NotSupportedException(); throw new NotSupportedException();
} }
private static string GetLocalizedMappedValue<T>(T value, IReadOnlyDictionary<T, LocaleKeys> map) where T : notnull
{
return map.TryGetValue(value, out LocaleKeys localeKey)
? LocaleManager.Instance[localeKey]
: value.ToString();
}
} }
} }

View File

@@ -0,0 +1,28 @@
using Avalonia.Data.Converters;
using Avalonia.Markup.Xaml;
using Ryujinx.Ava.UI.Models;
using System;
using System.Globalization;
namespace Ryujinx.Ava.UI.Helpers
{
internal class InputDeviceNameConverter : MarkupExtension, IValueConverter
{
public static readonly InputDeviceNameConverter Instance = new();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is ValueTuple<DeviceType, string, string> device ? device.Item3 : string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return Instance;
}
}
}

View File

@@ -0,0 +1,142 @@
using Ryujinx.Ava.Common.Locale;
using System;
using System.Collections.Generic;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using InputKey = Ryujinx.Input.Key;
namespace Ryujinx.Ava.UI.Helpers
{
internal static class KeyboardLayoutLocaleHelper
{
private static readonly Dictionary<InputKey, LocaleKeys> _sharedLocalizedKeysMap = new()
{
[InputKey.Unknown] = LocaleKeys.KeyboardLayout_KeyUnknown,
[InputKey.ShiftLeft] = LocaleKeys.KeyboardLayout_KeyShiftLeft,
[InputKey.ShiftRight] = LocaleKeys.KeyboardLayout_KeyShiftRight,
[InputKey.ControlLeft] = LocaleKeys.KeyboardLayout_KeyControlLeft,
[InputKey.ControlRight] = LocaleKeys.KeyboardLayout_KeyControlRight,
[InputKey.AltLeft] = LocaleKeys.KeyboardLayout_KeyAltLeft,
[InputKey.AltRight] = LocaleKeys.KeyboardLayout_KeyAltRight,
[InputKey.WinLeft] = LocaleKeys.KeyboardLayout_KeyWinLeft,
[InputKey.WinRight] = LocaleKeys.KeyboardLayout_KeyWinRight,
[InputKey.Up] = LocaleKeys.KeyboardLayout_KeyUp,
[InputKey.Down] = LocaleKeys.KeyboardLayout_KeyDown,
[InputKey.Left] = LocaleKeys.KeyboardLayout_KeyLeft,
[InputKey.Right] = LocaleKeys.KeyboardLayout_KeyRight,
[InputKey.Enter] = LocaleKeys.KeyboardLayout_KeyEnter,
[InputKey.Escape] = LocaleKeys.KeyboardLayout_KeyEscape,
[InputKey.Space] = LocaleKeys.KeyboardLayout_KeySpace,
[InputKey.Tab] = LocaleKeys.KeyboardLayout_KeyTab,
[InputKey.BackSpace] = LocaleKeys.KeyboardLayout_KeyBackSpace,
[InputKey.Insert] = LocaleKeys.KeyboardLayout_KeyInsert,
[InputKey.Delete] = LocaleKeys.KeyboardLayout_KeyDelete,
[InputKey.PageUp] = LocaleKeys.KeyboardLayout_KeyPageUp,
[InputKey.PageDown] = LocaleKeys.KeyboardLayout_KeyPageDown,
[InputKey.Home] = LocaleKeys.KeyboardLayout_KeyHome,
[InputKey.End] = LocaleKeys.KeyboardLayout_KeyEnd,
[InputKey.CapsLock] = LocaleKeys.KeyboardLayout_KeyCapsLock,
[InputKey.ScrollLock] = LocaleKeys.KeyboardLayout_KeyScrollLock,
[InputKey.PrintScreen] = LocaleKeys.KeyboardLayout_KeyPrintScreen,
[InputKey.Pause] = LocaleKeys.KeyboardLayout_KeyPause,
[InputKey.NumLock] = LocaleKeys.KeyboardLayout_KeyNumLock,
[InputKey.Clear] = LocaleKeys.KeyboardLayout_KeyClear,
[InputKey.Keypad0] = LocaleKeys.KeyboardLayout_KeyKeypad0,
[InputKey.Keypad1] = LocaleKeys.KeyboardLayout_KeyKeypad1,
[InputKey.Keypad2] = LocaleKeys.KeyboardLayout_KeyKeypad2,
[InputKey.Keypad3] = LocaleKeys.KeyboardLayout_KeyKeypad3,
[InputKey.Keypad4] = LocaleKeys.KeyboardLayout_KeyKeypad4,
[InputKey.Keypad5] = LocaleKeys.KeyboardLayout_KeyKeypad5,
[InputKey.Keypad6] = LocaleKeys.KeyboardLayout_KeyKeypad6,
[InputKey.Keypad7] = LocaleKeys.KeyboardLayout_KeyKeypad7,
[InputKey.Keypad8] = LocaleKeys.KeyboardLayout_KeyKeypad8,
[InputKey.Keypad9] = LocaleKeys.KeyboardLayout_KeyKeypad9,
[InputKey.KeypadDivide] = LocaleKeys.KeyboardLayout_KeyKeypadDivide,
[InputKey.KeypadMultiply] = LocaleKeys.KeyboardLayout_KeyKeypadMultiply,
[InputKey.KeypadSubtract] = LocaleKeys.KeyboardLayout_KeyKeypadSubtract,
[InputKey.KeypadAdd] = LocaleKeys.KeyboardLayout_KeyKeypadAdd,
[InputKey.KeypadDecimal] = LocaleKeys.KeyboardLayout_KeyKeypadDecimal,
[InputKey.KeypadEnter] = LocaleKeys.KeyboardLayout_KeyKeypadEnter,
[InputKey.Unbound] = LocaleKeys.KeyboardLayout_KeyUnbound,
};
private static readonly Dictionary<InputKey, LocaleKeys> _semanticPrintableKeysMap = new()
{
[InputKey.Number0] = LocaleKeys.KeyboardLayout_KeyNumber0,
[InputKey.Number1] = LocaleKeys.KeyboardLayout_KeyNumber1,
[InputKey.Number2] = LocaleKeys.KeyboardLayout_KeyNumber2,
[InputKey.Number3] = LocaleKeys.KeyboardLayout_KeyNumber3,
[InputKey.Number4] = LocaleKeys.KeyboardLayout_KeyNumber4,
[InputKey.Number5] = LocaleKeys.KeyboardLayout_KeyNumber5,
[InputKey.Number6] = LocaleKeys.KeyboardLayout_KeyNumber6,
[InputKey.Number7] = LocaleKeys.KeyboardLayout_KeyNumber7,
[InputKey.Number8] = LocaleKeys.KeyboardLayout_KeyNumber8,
[InputKey.Number9] = LocaleKeys.KeyboardLayout_KeyNumber9,
[InputKey.Tilde] = LocaleKeys.KeyboardLayout_KeyTilde,
[InputKey.Grave] = LocaleKeys.KeyboardLayout_KeyGrave,
[InputKey.Minus] = LocaleKeys.KeyboardLayout_KeyMinus,
[InputKey.Plus] = LocaleKeys.KeyboardLayout_KeyPlus,
[InputKey.BracketLeft] = LocaleKeys.KeyboardLayout_KeyBracketLeft,
[InputKey.BracketRight] = LocaleKeys.KeyboardLayout_KeyBracketRight,
[InputKey.Semicolon] = LocaleKeys.KeyboardLayout_KeySemicolon,
[InputKey.Quote] = LocaleKeys.KeyboardLayout_KeyQuote,
[InputKey.Comma] = LocaleKeys.KeyboardLayout_KeyComma,
[InputKey.Period] = LocaleKeys.KeyboardLayout_KeyPeriod,
[InputKey.Slash] = LocaleKeys.KeyboardLayout_KeySlash,
[InputKey.BackSlash] = LocaleKeys.KeyboardLayout_KeyBackSlash,
};
public static bool TryGetSemanticLabel(InputKey key, out string label)
{
if (TryGetSemanticLocaleKey(key, out LocaleKeys localeKey))
{
label = GetLocalizedString(localeKey);
return true;
}
label = string.Empty;
return false;
}
public static bool TryGetPhysicalLabel(ConfigPhysicalKey key, out string label)
{
if (TryGetPhysicalLocaleKey(key, out LocaleKeys localeKey))
{
label = GetLocalizedString(localeKey);
return true;
}
label = string.Empty;
return false;
}
public static bool TryGetPhysicalLocaleKey(ConfigPhysicalKey key, out LocaleKeys localeKey)
{
return _sharedLocalizedKeysMap.TryGetValue((InputKey)(int)key, out localeKey);
}
private static bool TryGetSemanticLocaleKey(InputKey key, out LocaleKeys localeKey)
{
return _sharedLocalizedKeysMap.TryGetValue(key, out localeKey) ||
_semanticPrintableKeysMap.TryGetValue(key, out localeKey);
}
private static string GetLocalizedString(LocaleKeys localeKey)
{
if (OperatingSystem.IsMacOS())
{
localeKey = localeKey switch
{
LocaleKeys.KeyboardLayout_KeyControlLeft => LocaleKeys.KeyboardLayout_KeyMacControlLeft,
LocaleKeys.KeyboardLayout_KeyControlRight => LocaleKeys.KeyboardLayout_KeyMacControlRight,
LocaleKeys.KeyboardLayout_KeyAltLeft => LocaleKeys.KeyboardLayout_KeyMacAltLeft,
LocaleKeys.KeyboardLayout_KeyAltRight => LocaleKeys.KeyboardLayout_KeyMacAltRight,
LocaleKeys.KeyboardLayout_KeyWinLeft => LocaleKeys.KeyboardLayout_KeyMacWinLeft,
LocaleKeys.KeyboardLayout_KeyWinRight => LocaleKeys.KeyboardLayout_KeyMacWinRight,
_ => localeKey
};
}
return LocaleManager.Instance[localeKey];
}
}
}

View File

@@ -0,0 +1,234 @@
using Avalonia.Input;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using InputKey = Ryujinx.Input.Key;
namespace Ryujinx.Ava.UI.Helpers
{
internal static class PhysicalKeyLabelHelper
{
private const string ObservedLabelsFileName = "keyboard_layout_labels.json";
private static readonly ConcurrentDictionary<ConfigPhysicalKey, string> _observedLayoutLabels = new();
private static readonly object _observedLayoutLabelsLock = new();
private static readonly JsonSerializerOptions _serializerOptions = new()
{
WriteIndented = true,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip
};
private static bool _observedLayoutLabelsLoaded;
public static event Action LabelsChanged;
public static string GetDisplayString(ConfigPhysicalKey key)
{
EnsureObservedLayoutLabelsLoaded();
if (KeyboardLayoutLocaleHelper.TryGetPhysicalLabel(key, out string localizedLabel))
{
return localizedLabel;
}
if (_observedLayoutLabels.TryGetValue(key, out string observedLabel))
{
return observedLabel;
}
if (TryGetFallbackPrintableKeyLabel(key, out string label))
{
return label;
}
return key.ToString();
}
public static void ObserveKeyPress(object sender, KeyEventArgs args)
{
EnsureObservedLayoutLabelsLoaded();
if (args.KeyModifiers != KeyModifiers.None)
{
return;
}
InputKey inputKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey);
if (!TryConvertToConfigPhysicalKey(inputKey, out ConfigPhysicalKey physicalKey) ||
KeyboardLayoutLocaleHelper.TryGetPhysicalLocaleKey(physicalKey, out _))
{
return;
}
if (TryNormalizeObservedPrintableLabel(args.KeySymbol, out string label))
{
if (IsCapsLockOn() && !char.IsLetter(label[0]))
{
return;
}
if (_observedLayoutLabels.TryGetValue(physicalKey, out string existingLabel) && existingLabel == label)
{
return;
}
_observedLayoutLabels[physicalKey] = label;
SaveObservedLayoutLabels();
LabelsChanged?.Invoke();
}
}
private static void EnsureObservedLayoutLabelsLoaded()
{
if (_observedLayoutLabelsLoaded)
{
return;
}
lock (_observedLayoutLabelsLock)
{
if (_observedLayoutLabelsLoaded)
{
return;
}
string labelsPath = GetObservedLabelsPath();
if (!File.Exists(labelsPath))
{
_observedLayoutLabelsLoaded = true;
return;
}
try
{
string labelsJson = File.ReadAllText(labelsPath);
Dictionary<string, string>? labels = JsonSerializer.Deserialize<Dictionary<string, string>>(labelsJson, _serializerOptions);
if (labels != null)
{
foreach ((string key, string value) in labels)
{
if (Enum.TryParse(key, out ConfigPhysicalKey physicalKey) &&
!string.IsNullOrEmpty(value))
{
_observedLayoutLabels[physicalKey] = value;
}
}
}
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.UI, $"Unable to load observed keyboard layout labels from '{labelsPath}': {ex.Message}");
}
finally
{
_observedLayoutLabelsLoaded = true;
}
}
}
private static void SaveObservedLayoutLabels()
{
lock (_observedLayoutLabelsLock)
{
try
{
Dictionary<string, string> labels = new();
foreach ((ConfigPhysicalKey key, string value) in _observedLayoutLabels)
{
labels[key.ToString()] = value;
}
File.WriteAllText(GetObservedLabelsPath(), JsonSerializer.Serialize(labels, _serializerOptions));
}
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.UI, $"Unable to save observed keyboard layout labels: {ex.Message}");
}
}
}
private static string GetObservedLabelsPath()
{
return Path.Combine(AppDataManager.BaseDirPath, ObservedLabelsFileName);
}
private static bool TryGetFallbackPrintableKeyLabel(ConfigPhysicalKey key, out string label)
{
// The legacy enum name for the ISO extra key is misleading, so give it a distinct physical label.
if (key == ConfigPhysicalKey.Grave)
{
label = "<>";
return true;
}
if (!AvaloniaKeyboardMappingHelper.TryGetAvaPhysicalKey((InputKey)(int)key, out AvaPhysicalKey avaPhysicalKey))
{
label = string.Empty;
return false;
}
label = PhysicalKeyExtensions.ToQwertyKeySymbol(avaPhysicalKey, false);
if (string.IsNullOrEmpty(label) || label.Length != 1 || char.IsControl(label[0]))
{
label = string.Empty;
return false;
}
if (char.IsLetter(label[0]))
{
label = char.ToUpperInvariant(label[0]).ToString();
}
return true;
}
private static bool IsCapsLockOn()
{
try
{
return OperatingSystem.IsWindows() && Console.CapsLock;
}
catch (Exception ex)
{
Logger.Debug?.Print(LogClass.UI, $"CapsLock state query failed: {ex.Message}");
return false;
}
}
private static bool TryNormalizeObservedPrintableLabel(string keySymbol, out string label)
{
if (string.IsNullOrEmpty(keySymbol) || keySymbol.Length != 1 || char.IsControl(keySymbol[0]))
{
label = string.Empty;
return false;
}
label = char.IsLetter(keySymbol[0])
? char.ToUpperInvariant(keySymbol[0]).ToString()
: keySymbol;
return true;
}
private static bool TryConvertToConfigPhysicalKey(InputKey key, out ConfigPhysicalKey physicalKey)
{
if (key is >= InputKey.Unknown and < InputKey.Count)
{
physicalKey = (ConfigPhysicalKey)(int)key;
return true;
}
physicalKey = ConfigPhysicalKey.Unknown;
return false;
}
}
}

View File

@@ -13,88 +13,88 @@ namespace Ryujinx.Ava.UI.Models.Input
public PlayerIndex PlayerIndex { get; set; } public PlayerIndex PlayerIndex { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key LeftStickUp { get; set; } public partial PhysicalKey LeftStickUp { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key LeftStickDown { get; set; } public partial PhysicalKey LeftStickDown { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key LeftStickLeft { get; set; } public partial PhysicalKey LeftStickLeft { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key LeftStickRight { get; set; } public partial PhysicalKey LeftStickRight { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key LeftStickButton { get; set; } public partial PhysicalKey LeftStickButton { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key RightStickUp { get; set; } public partial PhysicalKey RightStickUp { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key RightStickDown { get; set; } public partial PhysicalKey RightStickDown { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key RightStickLeft { get; set; } public partial PhysicalKey RightStickLeft { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key RightStickRight { get; set; } public partial PhysicalKey RightStickRight { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key RightStickButton { get; set; } public partial PhysicalKey RightStickButton { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key DpadUp { get; set; } public partial PhysicalKey DpadUp { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key DpadDown { get; set; } public partial PhysicalKey DpadDown { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key DpadLeft { get; set; } public partial PhysicalKey DpadLeft { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key DpadRight { get; set; } public partial PhysicalKey DpadRight { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key ButtonMinus { get; set; } public partial PhysicalKey ButtonMinus { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key ButtonPlus { get; set; } public partial PhysicalKey ButtonPlus { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key ButtonA { get; set; } public partial PhysicalKey ButtonA { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key ButtonB { get; set; } public partial PhysicalKey ButtonB { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key ButtonX { get; set; } public partial PhysicalKey ButtonX { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key ButtonY { get; set; } public partial PhysicalKey ButtonY { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key ButtonL { get; set; } public partial PhysicalKey ButtonL { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key ButtonR { get; set; } public partial PhysicalKey ButtonR { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key ButtonZl { get; set; } public partial PhysicalKey ButtonZl { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key ButtonZr { get; set; } public partial PhysicalKey ButtonZr { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key LeftButtonSl { get; set; } public partial PhysicalKey LeftButtonSl { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key LeftButtonSr { get; set; } public partial PhysicalKey LeftButtonSr { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key RightButtonSl { get; set; } public partial PhysicalKey RightButtonSl { get; set; }
[ObservableProperty] [ObservableProperty]
public partial Key RightButtonSr { get; set; } public partial PhysicalKey RightButtonSr { get; set; }
public KeyboardInputConfig(InputConfig config) public KeyboardInputConfig(InputConfig config)
{ {
@@ -153,7 +153,7 @@ namespace Ryujinx.Ava.UI.Models.Input
Backend = InputBackendType.WindowKeyboard, Backend = InputBackendType.WindowKeyboard,
PlayerIndex = PlayerIndex, PlayerIndex = PlayerIndex,
ControllerType = ControllerType, ControllerType = ControllerType,
LeftJoycon = new LeftJoyconCommonConfig<Key> LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{ {
DpadUp = DpadUp, DpadUp = DpadUp,
DpadDown = DpadDown, DpadDown = DpadDown,
@@ -165,7 +165,7 @@ namespace Ryujinx.Ava.UI.Models.Input
ButtonSl = LeftButtonSl, ButtonSl = LeftButtonSl,
ButtonSr = LeftButtonSr, ButtonSr = LeftButtonSr,
}, },
RightJoycon = new RightJoyconCommonConfig<Key> RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{ {
ButtonA = ButtonA, ButtonA = ButtonA,
ButtonB = ButtonB, ButtonB = ButtonB,
@@ -177,7 +177,7 @@ namespace Ryujinx.Ava.UI.Models.Input
ButtonR = ButtonR, ButtonR = ButtonR,
ButtonZr = ButtonZr, ButtonZr = ButtonZr,
}, },
LeftJoyconStick = new JoyconConfigKeyboardStick<Key> LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{ {
StickUp = LeftStickUp, StickUp = LeftStickUp,
StickDown = LeftStickDown, StickDown = LeftStickDown,
@@ -185,7 +185,7 @@ namespace Ryujinx.Ava.UI.Models.Input
StickLeft = LeftStickLeft, StickLeft = LeftStickLeft,
StickButton = LeftStickButton, StickButton = LeftStickButton,
}, },
RightJoyconStick = new JoyconConfigKeyboardStick<Key> RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{ {
StickUp = RightStickUp, StickUp = RightStickUp,
StickDown = RightStickDown, StickDown = RightStickDown,
@@ -198,5 +198,37 @@ namespace Ryujinx.Ava.UI.Models.Input
return config; return config;
} }
public void NotifyKeyLabelsChanged()
{
OnPropertiesChanged(nameof(LeftStickUp),
nameof(LeftStickDown),
nameof(LeftStickLeft),
nameof(LeftStickRight),
nameof(LeftStickButton),
nameof(RightStickUp),
nameof(RightStickDown),
nameof(RightStickLeft),
nameof(RightStickRight),
nameof(RightStickButton),
nameof(DpadUp),
nameof(DpadDown),
nameof(DpadLeft),
nameof(DpadRight),
nameof(ButtonMinus),
nameof(ButtonPlus),
nameof(ButtonA),
nameof(ButtonB),
nameof(ButtonX),
nameof(ButtonY),
nameof(ButtonL),
nameof(ButtonR),
nameof(ButtonZl),
nameof(ButtonZr),
nameof(LeftButtonSl),
nameof(LeftButtonSr),
nameof(RightButtonSl),
nameof(RightButtonSr));
}
} }
} }

View File

@@ -1,5 +1,6 @@
using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input; using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Common.Logging;
using Ryujinx.Input; using Ryujinx.Input;
using System; using System;
using System.Threading; using System.Threading;
@@ -117,6 +118,11 @@ namespace Ryujinx.Ava.UI.Models.Input
public void UpdateConfig(object config) public void UpdateConfig(object config)
{ {
KeyboardConfig = null;
GamepadConfig = null;
UiStickLeft = (0f, 0f);
UiStickRight = (0f, 0f);
if (config is ControllerInputViewModel padConfig) if (config is ControllerInputViewModel padConfig)
{ {
GamepadConfig = padConfig.Config; GamepadConfig = padConfig.Config;
@@ -145,51 +151,53 @@ namespace Ryujinx.Ava.UI.Models.Input
leftBuffer = (0f, 0f); leftBuffer = (0f, 0f);
rightBuffer = (0f, 0f); rightBuffer = (0f, 0f);
try
{
switch (Type) switch (Type)
{ {
case DeviceType.Keyboard: case DeviceType.Keyboard:
IKeyboard keyboard = (IKeyboard)Parent.AvaloniaKeyboardDriver.GetGamepad("0"); IKeyboard keyboard = Parent?.AvaloniaKeyboardDriver?.GetGamepad("0") as IKeyboard;
if (keyboard != null) if (keyboard != null && KeyboardConfig != null)
{ {
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot(); KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight)) if (snapshot.IsPressed(KeyboardConfig.LeftStickRight))
{ {
leftBuffer.Item1 += 1; leftBuffer.Item1 += 1;
} }
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft)) if (snapshot.IsPressed(KeyboardConfig.LeftStickLeft))
{ {
leftBuffer.Item1 -= 1; leftBuffer.Item1 -= 1;
} }
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp)) if (snapshot.IsPressed(KeyboardConfig.LeftStickUp))
{ {
leftBuffer.Item2 += 1; leftBuffer.Item2 += 1;
} }
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown)) if (snapshot.IsPressed(KeyboardConfig.LeftStickDown))
{ {
leftBuffer.Item2 -= 1; leftBuffer.Item2 -= 1;
} }
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight)) if (snapshot.IsPressed(KeyboardConfig.RightStickRight))
{ {
rightBuffer.Item1 += 1; rightBuffer.Item1 += 1;
} }
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft)) if (snapshot.IsPressed(KeyboardConfig.RightStickLeft))
{ {
rightBuffer.Item1 -= 1; rightBuffer.Item1 -= 1;
} }
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp)) if (snapshot.IsPressed(KeyboardConfig.RightStickUp))
{ {
rightBuffer.Item2 += 1; rightBuffer.Item2 += 1;
} }
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown)) if (snapshot.IsPressed(KeyboardConfig.RightStickDown))
{ {
rightBuffer.Item2 -= 1; rightBuffer.Item2 -= 1;
} }
@@ -201,9 +209,12 @@ namespace Ryujinx.Ava.UI.Models.Input
break; break;
case DeviceType.Controller: case DeviceType.Controller:
IGamepad controller = Parent.SelectedGamepad; IGamepad controller = Parent?.SelectedGamepad;
if (controller != null) if (controller is IKeyboard)
{
}
else if (controller != null && GamepadConfig != null)
{ {
leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick); leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick);
rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick); rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick);
@@ -216,6 +227,11 @@ namespace Ryujinx.Ava.UI.Models.Input
default: default:
throw new ArgumentException($"Unable to poll device type \"{Type}\""); throw new ArgumentException($"Unable to poll device type \"{Type}\"");
} }
}
catch (Exception ex) when (ex is ObjectDisposedException || ex is NullReferenceException || ex is NotSupportedException)
{
Logger.Debug?.Print(LogClass.UI, $"StickVisualizer polling failed: {ex}");
}
UiStickLeft = leftBuffer; UiStickLeft = leftBuffer;
UiStickRight = rightBuffer; UiStickRight = rightBuffer;

View File

@@ -45,7 +45,6 @@ namespace Ryujinx.Ava.UI.Renderer
Content = EmbeddedWindow; Content = EmbeddedWindow;
} }
public void Dispose() public void Dispose()
{ {
if (EmbeddedWindow != null) if (EmbeddedWindow != null)

View File

@@ -88,19 +88,19 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public async void ShowMotionConfig() public async void ShowMotionConfig()
{ {
await MotionInputView.Show(this); await MotionInputView.Show(this);
ParentModel.IsModified = true; ParentModel.RefreshModifiedState();
} }
public async void ShowRumbleConfig() public async void ShowRumbleConfig()
{ {
await RumbleInputView.Show(this); await RumbleInputView.Show(this);
ParentModel.IsModified = true; ParentModel.RefreshModifiedState();
} }
public async void ShowLedConfig() public async void ShowLedConfig()
{ {
await LedInputView.Show(this); await LedInputView.Show(this);
ParentModel.IsModified = true; ParentModel.RefreshModifiedState();
} }
public void OnParentModelChanged() public void OnParentModelChanged()

File diff suppressed because it is too large Load Diff

View File

@@ -104,7 +104,7 @@ namespace Ryujinx.Ava.UI.Views.Input
PointerPressed += MouseClick; PointerPressed += MouseClick;
ControllerInputViewModel viewModel = (DataContext as ControllerInputViewModel); ControllerInputViewModel viewModel = ViewModel;
IKeyboard keyboard = IKeyboard keyboard =
(IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver (IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver
@@ -113,10 +113,9 @@ namespace Ryujinx.Ava.UI.Views.Input
_currentAssigner.ButtonAssigned += (sender, e) => _currentAssigner.ButtonAssigned += (sender, e) =>
{ {
if (e.ButtonValue.HasValue) if (e.ButtonValue.HasValue && IsActiveAssignmentContext(viewModel))
{ {
Button buttonValue = e.ButtonValue.Value; Button buttonValue = e.ButtonValue.Value;
FlagInputConfigChanged();
switch (button.Name) switch (button.Name)
{ {
@@ -187,6 +186,8 @@ namespace Ryujinx.Ava.UI.Views.Input
viewModel.Config.RightJoystick = buttonValue.AsHidType<StickInputId>(); viewModel.Config.RightJoystick = buttonValue.AsHidType<StickInputId>();
break; break;
} }
FlagInputConfigChanged();
} }
}; };
@@ -212,7 +213,15 @@ namespace Ryujinx.Ava.UI.Views.Input
private void FlagInputConfigChanged() private void FlagInputConfigChanged()
{ {
(DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true; if (DataContext is ControllerInputViewModel viewModel && VisualRoot is not null)
{
viewModel.ParentModel.RefreshModifiedState();
}
}
private bool IsActiveAssignmentContext(ControllerInputViewModel viewModel)
{
return VisualRoot is not null && ReferenceEquals(DataContext, viewModel);
} }
private void MouseClick(object sender, PointerPressedEventArgs e) private void MouseClick(object sender, PointerPressedEventArgs e)

View File

@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
@@ -77,7 +78,7 @@
ToolTip.Tip="{ext:Locale ControllerSettingsCancelCurrentChangesToolTip}" ToolTip.Tip="{ext:Locale ControllerSettingsCancelCurrentChangesToolTip}"
Command="{Binding RevertChanges}"> Command="{Binding RevertChanges}">
<ui:SymbolIcon <ui:SymbolIcon
Symbol="Undo" Symbol="Cancel"
FontSize="15" FontSize="15"
Height="20" /> Height="20" />
</Button> </Button>
@@ -148,7 +149,7 @@
<Grid <Grid
Grid.Column="0" Grid.Column="0"
Margin="2" Margin="2"
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto"> HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock <TextBlock
Grid.Column="0" Grid.Column="0"
Margin="5,0,10,0" Margin="5,0,10,0"
@@ -161,16 +162,35 @@
Name="DeviceBox" Name="DeviceBox"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Center" VerticalAlignment="Center"
ItemsSource="{Binding DeviceList}" ItemsSource="{Binding Devices}"
SelectedIndex="{Binding Device}" /> SelectedItem="{Binding SelectedDeviceItem, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ., Converter={x:Static helpers:InputDeviceNameConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button <Button
Grid.Column="2" Grid.Column="2"
MinWidth="0" MinWidth="0"
Margin="5,0,0,0" Margin="5,0,0,0"
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding LoadDevice}"> ToolTip.Tip="{ext:Locale ControllerSettingsRefresh}"
Command="{Binding RefreshInputDevices}">
<ui:SymbolIcon <ui:SymbolIcon
Symbol="Refresh" Symbol="Sync"
FontSize="15"
Height="20"/>
</Button>
<Button
Grid.Column="3"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
ToolTip.Tip="{ext:Locale ControllerSettingsResetKeybindsToDefault}"
Click="ResetCurrentDeviceToDefaultsButton_OnClick">
<ui:SymbolIcon
Symbol="Undo"
FontSize="15" FontSize="15"
Height="20"/> Height="20"/>
</Button> </Button>

View File

@@ -1,4 +1,8 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia;
using Avalonia.Layout;
using Avalonia.Media;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration; using Ryujinx.Ava.Systems.Configuration;
@@ -15,9 +19,14 @@ namespace Ryujinx.Ava.UI.Views.Input
public InputView() public InputView()
{ {
ViewModel = new InputViewModel(this, ConfigurationState.Instance.System.UseInputGlobalConfig); ReplaceViewModel(ConfigurationState.Instance.System.UseInputGlobalConfig);
}
InitializeComponent(); protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
ViewModel?.RetargetKeyboardDriver(this);
} }
public void SaveCurrentProfile() public void SaveCurrentProfile()
@@ -28,8 +37,18 @@ namespace Ryujinx.Ava.UI.Views.Input
public void ToggleLocalGlobalInput(bool enableConfigGlobal) public void ToggleLocalGlobalInput(bool enableConfigGlobal)
{ {
Dispose(); Dispose();
ViewModel = new InputViewModel(this, enableConfigGlobal); // Create new Input Page with global input configs ReplaceViewModel(enableConfigGlobal);
}
private void ReplaceViewModel(bool useGlobalConfig)
{
ViewModel = new InputViewModel(this, useGlobalConfig); // Create new Input Page with the selected input config scope.
InitializeComponent(); InitializeComponent();
if (VisualRoot is not null)
{
ViewModel.RetargetKeyboardDriver(this);
}
} }
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -83,7 +102,56 @@ namespace Ryujinx.Ava.UI.Views.Input
if (sender is FAComboBox faComboBox) if (sender is FAComboBox faComboBox)
{ {
faComboBox.IsDropDownOpen = false; faComboBox.IsDropDownOpen = false;
ViewModel.IsModified = true; ViewModel.RefreshModifiedState();
}
}
private async void ResetCurrentDeviceToDefaultsButton_OnClick(object sender, RoutedEventArgs e)
{
if (!ViewModel.NeedsResetCurrentDeviceToDefaultsConfirmation())
{
ViewModel.ResetCurrentDeviceToDefaults();
return;
}
Window owner = TopLevel.GetTopLevel(this) as Window;
StackPanel content = new()
{
Spacing = 4,
MaxWidth = 360,
};
content.Children.Add(new TextBlock
{
Text = LocaleManager.Instance[LocaleKeys.DialogControllerSettingsResetKeybindsConfirmMessage],
TextWrapping = TextWrapping.Wrap,
MaxWidth = 360,
});
content.Children.Add(new TextBlock
{
Text = LocaleManager.Instance[LocaleKeys.DialogControllerSettingsResetKeybindsConfirmSubMessage],
TextWrapping = TextWrapping.Wrap,
MaxWidth = 360,
});
ContentDialog contentDialog = new ContentDialog
{
Title = LocaleManager.Instance[LocaleKeys.RyujinxConfirm],
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.InputDialogYes],
CloseButtonText = LocaleManager.Instance[LocaleKeys.InputDialogNo],
DefaultButton = ContentDialogButton.Primary,
Content = content,
}.ApplyStyles();
ContentDialogResult result = owner is not null
? await contentDialog.ShowAsync(owner)
: await ContentDialogHelper.ShowAsync(contentDialog);
if (result == ContentDialogResult.Primary)
{
ViewModel.ResetCurrentDeviceToDefaults();
} }
} }

View File

@@ -12,7 +12,7 @@ using Ryujinx.Input.Assigner;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Button = Ryujinx.Input.Button; using Button = Ryujinx.Input.Button;
using Key = Ryujinx.Common.Configuration.Hid.Key; using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Ava.UI.Views.Input namespace Ryujinx.Ava.UI.Views.Input
{ {
@@ -63,105 +63,108 @@ namespace Ryujinx.Ava.UI.Views.Input
PointerPressed += MouseClick; PointerPressed += MouseClick;
KeyboardInputViewModel viewModel = ViewModel;
IKeyboard keyboard = IKeyboard keyboard =
(IKeyboard)ViewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations. (IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
IButtonAssigner assigner = IButtonAssigner assigner =
new KeyboardKeyAssigner((IKeyboard)ViewModel.ParentModel.SelectedGamepad); new KeyboardKeyAssigner((IKeyboard)viewModel.ParentModel.SelectedGamepad);
_currentAssigner.ButtonAssigned += (_, be) => _currentAssigner.ButtonAssigned += (_, be) =>
{ {
if (be.ButtonValue.HasValue) if (be.ButtonValue.HasValue && IsActiveAssignmentContext(viewModel))
{ {
Button buttonValue = be.ButtonValue.Value; Button buttonValue = be.ButtonValue.Value;
ViewModel.ParentModel.IsModified = true;
switch (button.Name) switch (button.Name)
{ {
case "ButtonZl": case "ButtonZl":
ViewModel.Config.ButtonZl = buttonValue.AsHidType<Key>(); viewModel.Config.ButtonZl = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "ButtonL": case "ButtonL":
ViewModel.Config.ButtonL = buttonValue.AsHidType<Key>(); viewModel.Config.ButtonL = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "ButtonMinus": case "ButtonMinus":
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<Key>(); viewModel.Config.ButtonMinus = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "LeftStickButton": case "LeftStickButton":
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<Key>(); viewModel.Config.LeftStickButton = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "LeftStickUp": case "LeftStickUp":
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<Key>(); viewModel.Config.LeftStickUp = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "LeftStickDown": case "LeftStickDown":
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<Key>(); viewModel.Config.LeftStickDown = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "LeftStickRight": case "LeftStickRight":
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<Key>(); viewModel.Config.LeftStickRight = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "LeftStickLeft": case "LeftStickLeft":
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<Key>(); viewModel.Config.LeftStickLeft = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "DpadUp": case "DpadUp":
ViewModel.Config.DpadUp = buttonValue.AsHidType<Key>(); viewModel.Config.DpadUp = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "DpadDown": case "DpadDown":
ViewModel.Config.DpadDown = buttonValue.AsHidType<Key>(); viewModel.Config.DpadDown = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "DpadLeft": case "DpadLeft":
ViewModel.Config.DpadLeft = buttonValue.AsHidType<Key>(); viewModel.Config.DpadLeft = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "DpadRight": case "DpadRight":
ViewModel.Config.DpadRight = buttonValue.AsHidType<Key>(); viewModel.Config.DpadRight = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "LeftButtonSr": case "LeftButtonSr":
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<Key>(); viewModel.Config.LeftButtonSr = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "LeftButtonSl": case "LeftButtonSl":
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<Key>(); viewModel.Config.LeftButtonSl = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "RightButtonSr": case "RightButtonSr":
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<Key>(); viewModel.Config.RightButtonSr = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "RightButtonSl": case "RightButtonSl":
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<Key>(); viewModel.Config.RightButtonSl = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "ButtonZr": case "ButtonZr":
ViewModel.Config.ButtonZr = buttonValue.AsHidType<Key>(); viewModel.Config.ButtonZr = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "ButtonR": case "ButtonR":
ViewModel.Config.ButtonR = buttonValue.AsHidType<Key>(); viewModel.Config.ButtonR = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "ButtonPlus": case "ButtonPlus":
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<Key>(); viewModel.Config.ButtonPlus = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "ButtonA": case "ButtonA":
ViewModel.Config.ButtonA = buttonValue.AsHidType<Key>(); viewModel.Config.ButtonA = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "ButtonB": case "ButtonB":
ViewModel.Config.ButtonB = buttonValue.AsHidType<Key>(); viewModel.Config.ButtonB = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "ButtonX": case "ButtonX":
ViewModel.Config.ButtonX = buttonValue.AsHidType<Key>(); viewModel.Config.ButtonX = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "ButtonY": case "ButtonY":
ViewModel.Config.ButtonY = buttonValue.AsHidType<Key>(); viewModel.Config.ButtonY = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "RightStickButton": case "RightStickButton":
ViewModel.Config.RightStickButton = buttonValue.AsHidType<Key>(); viewModel.Config.RightStickButton = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "RightStickUp": case "RightStickUp":
ViewModel.Config.RightStickUp = buttonValue.AsHidType<Key>(); viewModel.Config.RightStickUp = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "RightStickDown": case "RightStickDown":
ViewModel.Config.RightStickDown = buttonValue.AsHidType<Key>(); viewModel.Config.RightStickDown = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "RightStickRight": case "RightStickRight":
ViewModel.Config.RightStickRight = buttonValue.AsHidType<Key>(); viewModel.Config.RightStickRight = buttonValue.AsHidType<PhysicalKey>();
break; break;
case "RightStickLeft": case "RightStickLeft":
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<Key>(); viewModel.Config.RightStickLeft = buttonValue.AsHidType<PhysicalKey>();
break; break;
} }
viewModel.ParentModel.RefreshModifiedState();
} }
}; };
@@ -207,40 +210,40 @@ namespace Ryujinx.Ava.UI.Views.Input
{ {
Dictionary<string, Action> buttonActions = new() Dictionary<string, Action> buttonActions = new()
{ {
{ "ButtonZl", () => ViewModel.Config.ButtonZl = Key.Unbound }, { "ButtonZl", () => ViewModel.Config.ButtonZl = PhysicalKey.Unbound },
{ "ButtonL", () => ViewModel.Config.ButtonL = Key.Unbound }, { "ButtonL", () => ViewModel.Config.ButtonL = PhysicalKey.Unbound },
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = Key.Unbound }, { "ButtonMinus", () => ViewModel.Config.ButtonMinus = PhysicalKey.Unbound },
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = Key.Unbound }, { "LeftStickButton", () => ViewModel.Config.LeftStickButton = PhysicalKey.Unbound },
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = Key.Unbound }, { "LeftStickUp", () => ViewModel.Config.LeftStickUp = PhysicalKey.Unbound },
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = Key.Unbound }, { "LeftStickDown", () => ViewModel.Config.LeftStickDown = PhysicalKey.Unbound },
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = Key.Unbound }, { "LeftStickRight", () => ViewModel.Config.LeftStickRight = PhysicalKey.Unbound },
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = Key.Unbound }, { "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = PhysicalKey.Unbound },
{ "DpadUp", () => ViewModel.Config.DpadUp = Key.Unbound }, { "DpadUp", () => ViewModel.Config.DpadUp = PhysicalKey.Unbound },
{ "DpadDown", () => ViewModel.Config.DpadDown = Key.Unbound }, { "DpadDown", () => ViewModel.Config.DpadDown = PhysicalKey.Unbound },
{ "DpadLeft", () => ViewModel.Config.DpadLeft = Key.Unbound }, { "DpadLeft", () => ViewModel.Config.DpadLeft = PhysicalKey.Unbound },
{ "DpadRight", () => ViewModel.Config.DpadRight = Key.Unbound }, { "DpadRight", () => ViewModel.Config.DpadRight = PhysicalKey.Unbound },
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = Key.Unbound }, { "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = PhysicalKey.Unbound },
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = Key.Unbound }, { "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = PhysicalKey.Unbound },
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = Key.Unbound }, { "RightButtonSr", () => ViewModel.Config.RightButtonSr = PhysicalKey.Unbound },
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = Key.Unbound }, { "RightButtonSl", () => ViewModel.Config.RightButtonSl = PhysicalKey.Unbound },
{ "ButtonZr", () => ViewModel.Config.ButtonZr = Key.Unbound }, { "ButtonZr", () => ViewModel.Config.ButtonZr = PhysicalKey.Unbound },
{ "ButtonR", () => ViewModel.Config.ButtonR = Key.Unbound }, { "ButtonR", () => ViewModel.Config.ButtonR = PhysicalKey.Unbound },
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = Key.Unbound }, { "ButtonPlus", () => ViewModel.Config.ButtonPlus = PhysicalKey.Unbound },
{ "ButtonA", () => ViewModel.Config.ButtonA = Key.Unbound }, { "ButtonA", () => ViewModel.Config.ButtonA = PhysicalKey.Unbound },
{ "ButtonB", () => ViewModel.Config.ButtonB = Key.Unbound }, { "ButtonB", () => ViewModel.Config.ButtonB = PhysicalKey.Unbound },
{ "ButtonX", () => ViewModel.Config.ButtonX = Key.Unbound }, { "ButtonX", () => ViewModel.Config.ButtonX = PhysicalKey.Unbound },
{ "ButtonY", () => ViewModel.Config.ButtonY = Key.Unbound }, { "ButtonY", () => ViewModel.Config.ButtonY = PhysicalKey.Unbound },
{ "RightStickButton", () => ViewModel.Config.RightStickButton = Key.Unbound }, { "RightStickButton", () => ViewModel.Config.RightStickButton = PhysicalKey.Unbound },
{ "RightStickUp", () => ViewModel.Config.RightStickUp = Key.Unbound }, { "RightStickUp", () => ViewModel.Config.RightStickUp = PhysicalKey.Unbound },
{ "RightStickDown", () => ViewModel.Config.RightStickDown = Key.Unbound }, { "RightStickDown", () => ViewModel.Config.RightStickDown = PhysicalKey.Unbound },
{ "RightStickRight", () => ViewModel.Config.RightStickRight = Key.Unbound }, { "RightStickRight", () => ViewModel.Config.RightStickRight = PhysicalKey.Unbound },
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = Key.Unbound } { "RightStickLeft", () => ViewModel.Config.RightStickLeft = PhysicalKey.Unbound }
}; };
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action)) if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
{ {
action(); action();
ViewModel.ParentModel.IsModified = true; ViewModel.ParentModel.RefreshModifiedState();
} }
} }
} }
@@ -251,5 +254,10 @@ namespace Ryujinx.Ava.UI.Views.Input
_currentAssigner?.Cancel(); _currentAssigner?.Cancel();
_currentAssigner = null; _currentAssigner = null;
} }
private bool IsActiveAssignmentContext(KeyboardInputViewModel viewModel)
{
return VisualRoot is not null && ReferenceEquals(DataContext, viewModel);
}
} }
} }

View File

@@ -34,7 +34,8 @@ namespace Ryujinx.Ava.UI.Views.Settings
} }
} }
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this); _avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this, KeyboardInputMode.Semantic);
_avaloniaKeyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
} }
protected override void OnPointerReleased(PointerReleasedEventArgs e) protected override void OnPointerReleased(PointerReleasedEventArgs e)

View File

@@ -1,17 +1,65 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Ryujinx.Ava.UI.Windows;
namespace Ryujinx.Ava.UI.Views.Settings namespace Ryujinx.Ava.UI.Views.Settings
{ {
public partial class SettingsInputView : UserControl public partial class SettingsInputView : UserControl
{ {
private bool _inputUpdatesBlocked;
public SettingsInputView() public SettingsInputView()
{ {
InitializeComponent(); InitializeComponent();
} }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
SetInputUpdatesBlocked(true);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
SetInputUpdatesBlocked(false);
base.OnDetachedFromVisualTree(e);
}
public void Dispose() public void Dispose()
{
try
{ {
InputView.Dispose(); InputView.Dispose();
} }
finally
{
SetInputUpdatesBlocked(false);
}
}
private void SetInputUpdatesBlocked(bool blocked)
{
if (_inputUpdatesBlocked == blocked)
{
return;
}
MainWindow? mainWindow = RyujinxApp.MainWindow;
if (mainWindow?.ViewModel?.AppHost?.NpadManager is not { } npadManager)
{
return;
}
if (blocked)
{
npadManager.BlockInputUpdates();
}
else
{
npadManager.UnblockInputUpdates();
}
_inputUpdatesBlocked = blocked;
}
} }
} }

View File

@@ -30,6 +30,7 @@ using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input.HLE; using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL3; using Ryujinx.Input.SDL3;
using Ryujinx.Input;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -107,7 +108,9 @@ namespace Ryujinx.Ava.UI.Windows
if (Program.PreviewerDetached) if (Program.PreviewerDetached)
{ {
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL3GamepadDriver()); AvaloniaKeyboardDriver keyboardDriver = new(this, KeyboardInputMode.Semantic);
keyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
InputManager = new InputManager(keyboardDriver, new SDL3GamepadDriver());
_ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it); _ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it);
this.ScalingChanged += OnScalingChanged; this.ScalingChanged += OnScalingChanged;