Merge branch 'keyboard-localisation-fracture-share' into 'master'

Draft: Input: Refactor Keyboard Handling To Use Physical Keys

See merge request [ryubing/ryujinx!292](https://legacy.git.ryujinx.app/ryubing/ryujinx/-/merge_requests/292)
This commit is contained in:
Babib3l
2026-04-08 23:32:28 +02:00
39 changed files with 3928 additions and 2625 deletions

3
.gitignore vendored
View File

@@ -72,6 +72,9 @@ ipch/
_ReSharper*/
*.[Rr]e[Ss]harper
#.NET
.dotnet-home/
# TeamCity is a build add-in
_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
{
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

@@ -8,25 +8,15 @@ using System.Runtime.CompilerServices;
using System.Threading;
using SDL;
using static SDL.SDL3;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input.SDL3
{
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();
#pragma warning disable IDE0052 // Remove unread private member
private readonly SDL3KeyboardDriver _driver;
#pragma warning restore IDE0052
private StandardKeyboardInputConfig _configuration;
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
private static readonly SDL_Keycode[] _keysDriverMapping =
@@ -171,9 +161,8 @@ namespace Ryujinx.Input.SDL3
SDL_Keycode.SDLK_0
];
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name)
public SDL3Keyboard(string id, string name)
{
_driver = driver;
Id = id;
Name = name;
_buttonsUserMapping = [];
@@ -195,9 +184,9 @@ namespace Ryujinx.Input.SDL3
}
[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;
}
@@ -205,18 +194,18 @@ namespace Ryujinx.Input.SDL3
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
{
Key.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
Key.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
Key.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
Key.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
Key.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
Key.AltRight => SDL_Keymod.SDL_KMOD_RALT,
Key.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
Key.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
ConfigPhysicalKey.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
ConfigPhysicalKey.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
ConfigPhysicalKey.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
ConfigPhysicalKey.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
ConfigPhysicalKey.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
ConfigPhysicalKey.AltRight => SDL_Keymod.SDL_KMOD_RALT,
ConfigPhysicalKey.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
ConfigPhysicalKey.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
// NOTE: Menu key isn't supported by SDL3.
_ => SDL_Keymod.SDL_KMOD_NONE
};
@@ -232,9 +221,9 @@ namespace Ryujinx.Input.SDL3
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);
if (index == -1)
@@ -264,36 +253,6 @@ namespace Ryujinx.Input.SDL3
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()
{
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
@@ -306,9 +265,9 @@ namespace Ryujinx.Input.SDL3
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;
}
@@ -320,8 +279,8 @@ namespace Ryujinx.Input.SDL3
}
}
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
@@ -357,38 +316,15 @@ namespace Ryujinx.Input.SDL3
{
_configuration = (StandardKeyboardInputConfig)configuration;
// First clear the buttons mapping
_buttonsUserMapping.Clear();
// Then configure 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));
// 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));
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
}
}
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)

View File

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

View File

@@ -1,3 +1,5 @@
using Ryujinx.Common.Logging;
namespace Ryujinx.Input.Assigner
{
/// <summary>
@@ -8,22 +10,42 @@ namespace Ryujinx.Input.Assigner
private readonly IKeyboard _keyboard;
private KeyboardStateSnapshot _keyboardState;
private Button? _pressedButton;
public KeyboardKeyAssigner(IKeyboard keyboard)
{
_keyboard = keyboard;
}
public void Initialize() { }
public void Initialize()
{
_pressedButton = null;
}
public void ReadInput()
{
_keyboardState = _keyboard.GetKeyboardStateSnapshot();
if (_pressedButton is not null)
{
return;
}
Button? buttonFromState = GetPressedButtonFromState();
Button? buttonFromBufferedPress = buttonFromState is null ? GetPressedButtonFromBufferedPress() : null;
_pressedButton = buttonFromState ?? buttonFromBufferedPress;
if (_pressedButton is not null)
{
string source = buttonFromState 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()
{
return GetPressedButton() is not null;
return _pressedButton is not null;
}
public bool ShouldCancel()
@@ -33,18 +55,53 @@ namespace Ryujinx.Input.Assigner
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++)
{
if (_keyboardState.IsPressed(key))
{
keyPressed = new(key);
break;
return new Button(key);
}
}
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.Controller;
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Services.Hid;
using System;
@@ -234,7 +235,9 @@ namespace Ryujinx.Input.HLE
_gamepad?.Dispose();
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);

View File

@@ -36,6 +36,7 @@ namespace Ryujinx.Input.HLE
private bool _isDisposed;
private List<InputConfig> _inputConfig;
private List<InputConfig> _requestedInputConfig;
private bool _enableKeyboard;
private bool _enableMouse;
private Switch _device;
@@ -52,6 +53,7 @@ namespace Ryujinx.Input.HLE
_gamepadDriver = gamepadDriver;
_mouseDriver = mouseDriver;
_inputConfig = [];
_requestedInputConfig = [];
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
@@ -89,14 +91,14 @@ namespace Ryujinx.Input.HLE
}
}
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
}
}
private void HandleOnGamepadConnected(string id)
{
// Force input reload
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -127,11 +129,13 @@ namespace Ryujinx.Input.HLE
{
lock (_lock)
{
_requestedInputConfig = inputConfig?.ToList() ?? [];
NpadController[] oldControllers = _controllers.ToArray();
List<InputConfig> validInputs = [];
foreach (InputConfig inputConfigEntry in inputConfig)
foreach (InputConfig inputConfigEntry in _requestedInputConfig)
{
NpadController controller;
int index = (int)inputConfigEntry.PlayerIndex;
@@ -147,7 +151,17 @@ namespace Ryujinx.Input.HLE
controller = new(_cemuHookClient);
}
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);
InputConfig activeConfig = inputConfigEntry;
bool isValid = DriverConfigurationUpdate(ref controller, activeConfig);
if (!isValid &&
enableKeyboard &&
inputConfigEntry is StandardControllerInputConfig &&
TryGetKeyboardFallback(inputConfigEntry, out StandardKeyboardInputConfig fallbackConfig))
{
activeConfig = fallbackConfig;
isValid = DriverConfigurationUpdate(ref controller, activeConfig);
}
if (!isValid)
{
@@ -157,7 +171,7 @@ namespace Ryujinx.Input.HLE
else
{
_controllers[index] = controller;
validInputs.Add(inputConfigEntry);
validInputs.Add(activeConfig);
}
}
@@ -169,7 +183,7 @@ namespace Ryujinx.Input.HLE
oldControllers[i] = null;
}
_inputConfig = inputConfig;
_inputConfig = validInputs;
_enableKeyboard = enableKeyboard;
_enableMouse = enableMouse;
@@ -177,6 +191,79 @@ namespace Ryujinx.Input.HLE
}
}
private bool TryGetKeyboardFallback(InputConfig inputConfig, out StandardKeyboardInputConfig fallbackConfig)
{
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 = new StandardKeyboardInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.WindowKeyboard,
Id = keyboardId,
Name = keyboard.Name,
PlayerIndex = inputConfig.PlayerIndex,
ControllerType = inputConfig.ControllerType,
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{
DpadUp = PhysicalKey.Up,
DpadDown = PhysicalKey.Down,
DpadLeft = PhysicalKey.Left,
DpadRight = PhysicalKey.Right,
ButtonMinus = PhysicalKey.Minus,
ButtonL = PhysicalKey.E,
ButtonZl = PhysicalKey.Q,
ButtonSl = PhysicalKey.Unbound,
ButtonSr = PhysicalKey.Unbound,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
},
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{
ButtonA = PhysicalKey.Z,
ButtonB = PhysicalKey.X,
ButtonX = PhysicalKey.C,
ButtonY = PhysicalKey.V,
ButtonPlus = PhysicalKey.Plus,
ButtonR = PhysicalKey.U,
ButtonZr = PhysicalKey.O,
ButtonSl = PhysicalKey.Unbound,
ButtonSr = PhysicalKey.Unbound,
},
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
},
};
return true;
}
public void UnblockInputUpdates()
{
lock (_lock)
@@ -334,7 +421,7 @@ namespace Ryujinx.Input.HLE
}
}
internal InputConfig GetPlayerInputConfigByIndex(int index)
public InputConfig GetPlayerInputConfigByIndex(int index)
{
lock (_lock)
{

View File

@@ -1,5 +1,6 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input
{
@@ -33,15 +34,26 @@ namespace Ryujinx.Input
{
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);
}
/// <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,78 @@
using Ryujinx.Common.Configuration.Hid.Keyboard;
using System.Collections.Generic;
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 ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Input
{
@@ -25,5 +26,8 @@ namespace Ryujinx.Input
/// <returns>True if the given key is pressed</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsPressed(Key key) => KeysState[(int)key];
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsPressed(ConfigPhysicalKey key) => KeysState[(int)key];
}
}

View File

@@ -24,7 +24,7 @@ using System.Text.Json;
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;
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Headless
{
@@ -105,48 +105,48 @@ namespace Ryujinx.Headless
Backend = InputBackendType.WindowKeyboard,
Id = null,
ControllerType = ControllerType.JoyconPair,
LeftJoycon = new LeftJoyconCommonConfig<Key>
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{
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,
DpadUp = PhysicalKey.Up,
DpadDown = PhysicalKey.Down,
DpadLeft = PhysicalKey.Left,
DpadRight = PhysicalKey.Right,
ButtonMinus = PhysicalKey.Minus,
ButtonL = PhysicalKey.E,
ButtonZl = PhysicalKey.Q,
ButtonSl = PhysicalKey.Unbound,
ButtonSr = PhysicalKey.Unbound,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{
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,
ButtonA = PhysicalKey.Z,
ButtonB = PhysicalKey.X,
ButtonX = PhysicalKey.C,
ButtonY = PhysicalKey.V,
ButtonPlus = PhysicalKey.Plus,
ButtonR = PhysicalKey.U,
ButtonZr = PhysicalKey.O,
ButtonSl = PhysicalKey.Unbound,
ButtonSr = PhysicalKey.Unbound,
},
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
},
};
}

View File

@@ -6,15 +6,15 @@ using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.Input
{
internal class AvaloniaKeyboard : IKeyboard
{
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
private readonly AvaloniaKeyboardDriver _driver;
private readonly KeyboardInputMode _mode;
private StandardKeyboardInputConfig _configuration;
private readonly Lock _userMappingLock = new();
@@ -24,18 +24,12 @@ namespace Ryujinx.Ava.Input
public bool IsConnected => true;
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
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)
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name, KeyboardInputMode mode)
{
_buttonsUserMapping = [];
_driver = driver;
_mode = mode;
Id = id;
Name = name;
}
@@ -57,22 +51,18 @@ namespace Ryujinx.Ava.Input
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;
}
// 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 rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
@@ -100,7 +90,7 @@ namespace Ryujinx.Ava.Input
{
try
{
return _driver.IsPressed(key);
return _driver.IsPressed(key, _mode);
}
catch
{
@@ -108,6 +98,19 @@ namespace Ryujinx.Ava.Input
}
}
public bool TryConsumePressedKey(out Key key)
{
try
{
return _driver.TryConsumePressedKey(_mode, out key);
}
catch
{
key = Key.Unknown;
return false;
}
}
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
@@ -116,37 +119,13 @@ namespace Ryujinx.Ava.Input
_buttonsUserMapping.Clear();
#pragma warning disable IDE0055 // Disable formatting
// 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
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
}
}
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) { }
@@ -162,41 +141,9 @@ namespace Ryujinx.Ava.Input
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()
{
_driver?.Clear();
_driver?.Clear(_mode);
}
public void Dispose() { }

View File

@@ -1,19 +1,37 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
using AvaKey = Avalonia.Input.Key;
using System.Threading;
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.Input
{
internal class AvaloniaKeyboardDriver : IGamepadDriver
internal class AvaloniaKeyboardDriver : IKeyboardModeDriver
{
private enum PhysicalKeySource
{
Direct,
ObservedFallback,
Unknown,
}
private static readonly string[] _keyboardIdentifers = ["0"];
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 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> KeyRelease;
@@ -22,14 +40,30 @@ namespace Ryujinx.Ava.Input
public string DriverName => "AvaloniaKeyboardDriver";
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
public AvaloniaKeyboardDriver(Control control)
public AvaloniaKeyboardDriver(Control control, KeyboardInputMode defaultMode = KeyboardInputMode.Semantic)
{
_control = control;
_pressedKeys = [];
_window = control as Window ?? TopLevel.GetTopLevel(control) as Window;
_semanticPressedKeys = [];
_physicalPressedKeys = [];
_observedPhysicalKeysBySemanticKey = [];
_semanticPressedKeyQueue = [];
_physicalPressedKeyQueue = [];
_pressedKeyQueueLock = new();
_defaultMode = defaultMode;
_control.KeyDown += OnKeyPress;
_control.KeyUp += OnKeyRelease;
// Use routed handlers so keys consumed earlier in the Avalonia pipeline
// can still be observed by the input driver. This is needed for keys like
// Caps Lock on macOS, which may not reach the plain CLR event path.
_control.AddHandler(InputElement.KeyDownEvent, OnKeyPress, RoutingStrategies.Tunnel, true);
_control.AddHandler(InputElement.KeyUpEvent, OnKeyRelease, RoutingStrategies.Tunnel, true);
_control.TextInput += Control_TextInput;
_window?.Deactivated += Window_Deactivated;
}
private void Window_Deactivated(object sender, EventArgs e)
{
Clear();
}
private void Control_TextInput(object sender, TextInputEventArgs e)
@@ -50,13 +84,18 @@ namespace Ryujinx.Ava.Input
}
public IGamepad GetGamepad(string id)
{
return GetKeyboard(id, _defaultMode);
}
public IKeyboard GetKeyboard(string id, KeyboardInputMode mode)
{
if (!_keyboardIdentifers[0].Equals(id))
{
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")];
@@ -65,40 +104,189 @@ namespace Ryujinx.Ava.Input
{
if (disposing)
{
_control.KeyUp -= OnKeyPress;
_control.KeyDown -= OnKeyRelease;
_control.RemoveHandler(InputElement.KeyDownEvent, OnKeyPress);
_control.RemoveHandler(InputElement.KeyUpEvent, OnKeyRelease);
_control.TextInput -= Control_TextInput;
if (_window != null)
{
_window.Deactivated -= Window_Deactivated;
}
}
}
protected void OnKeyPress(object sender, KeyEventArgs args)
{
_pressedKeys.Add(args.Key);
UpdateKeyStates(args, isPressed: true);
KeyPressed?.Invoke(this, args);
}
protected void OnKeyRelease(object sender, KeyEventArgs args)
{
_pressedKeys.Remove(args.Key);
UpdateKeyStates(args, isPressed: false);
KeyRelease?.Invoke(this, args);
}
internal bool IsPressed(Key key)
internal bool IsPressed(Key key, KeyboardInputMode mode)
{
if (key is Key.Unbound or Key.Unknown)
{
return false;
}
AvaloniaKeyboardMappingHelper.TryGetAvaKey(key, out AvaKey nativeKey);
return mode == KeyboardInputMode.Physical
? _physicalPressedKeys.Contains((ConfigPhysicalKey)(int)key)
: _semanticPressedKeys.Contains(key);
}
return _pressedKeys.Contains(nativeKey);
internal void Clear(KeyboardInputMode mode)
{
lock (_pressedKeyQueueLock)
{
if (mode == KeyboardInputMode.Physical)
{
_physicalPressedKeys.Clear();
_physicalPressedKeyQueue.Clear();
}
else
{
_semanticPressedKeys.Clear();
_semanticPressedKeyQueue.Clear();
}
}
}
public void Clear()
{
_pressedKeys.Clear();
lock (_pressedKeyQueueLock)
{
_semanticPressedKeys.Clear();
_physicalPressedKeys.Clear();
_semanticPressedKeyQueue.Clear();
_physicalPressedKeyQueue.Clear();
}
}
internal bool TryConsumePressedKey(KeyboardInputMode mode, out Key key)
{
lock (_pressedKeyQueueLock)
{
Queue<Key> queue = mode == KeyboardInputMode.Physical ? _physicalPressedKeyQueue : _semanticPressedKeyQueue;
if (queue.TryDequeue(out key))
{
return true;
}
}
key = Key.Unknown;
return false;
}
private static void UpdateKeyState(HashSet<Key> pressedKeys, Key key, bool isPressed)
{
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()

View File

@@ -2,6 +2,7 @@ using Ryujinx.Input;
using System;
using System.Collections.Generic;
using AvaKey = Avalonia.Input.Key;
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
namespace Ryujinx.Ava.Input
{
@@ -132,7 +133,8 @@ namespace Ryujinx.Ava.Input
AvaKey.D8,
AvaKey.D9,
AvaKey.OemTilde,
AvaKey.OemTilde,AvaKey.OemMinus,
AvaKey.Oem102,
AvaKey.OemMinus,
AvaKey.OemPlus,
AvaKey.OemOpenBrackets,
AvaKey.OemCloseBrackets,
@@ -147,7 +149,149 @@ namespace Ryujinx.Ava.Input
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<AvaPhysicalKey, Key> _avaPhysicalKeyMapping;
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.
_avaKeyMapping = new Dictionary<AvaKey, Key>();
_avaPhysicalKeyMapping = new Dictionary<AvaPhysicalKey, Key>();
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)
{
avaKey = AvaKey.None;
bool keyExist = (int)key < _keyMapping.Length;
bool keyExist = key < Key.Count && (int)key < _keyMapping.Length;
if (keyExist)
{
avaKey = _keyMapping[(int)key];
@@ -178,9 +343,34 @@ namespace Ryujinx.Ava.Input
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)
{
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

@@ -1234,9 +1234,17 @@ namespace Ryujinx.Ava.Systems
return false;
}
bool hasModalFocusLoss = _viewModel.Window is MainWindow mainWindow &&
mainWindow.SettingsWindow?.IsActive == true;
if (!_viewModel.IsActive || hasModalFocusLoss)
{
_inputManager.KeyboardDriver.Clear();
}
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
if (_viewModel.IsActive)
if (_viewModel.IsActive && !hasModalFocusLoss)
{
bool isCursorVisible = true;
@@ -1369,7 +1377,7 @@ namespace Ryujinx.Ava.Systems
// Touchscreen.
bool hasTouch = false;
if (_viewModel.IsActive && !ConfigurationState.Instance.Hid.EnableMouse.Value)
if (_viewModel.IsActive && !hasModalFocusLoss && !ConfigurationState.Instance.Hid.EnableMouse.Value)
{
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as AvaloniaMouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
}

View File

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

View File

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

View File

@@ -65,7 +65,7 @@ namespace Ryujinx.Ava.UI.Applet
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))
{
@@ -85,7 +85,7 @@ namespace Ryujinx.Ava.UI.Applet
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))
{

View File

@@ -1,5 +1,6 @@
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
using Ryujinx.Ava.Input;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System;
@@ -25,6 +26,7 @@ namespace Ryujinx.Ava.UI.Helpers
private bool _isWaitingForInput;
private bool _shouldUnbind;
private IKeyboard _keyboard;
public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
public ButtonKeyAssigner(ToggleButton toggleButton)
@@ -34,6 +36,9 @@ namespace Ryujinx.Ava.UI.Helpers
public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
{
_keyboard = keyboard;
ClearKeyboardState(_keyboard);
Dispatcher.UIThread.Post(() =>
{
ToggledButton.IsChecked = true;
@@ -82,6 +87,7 @@ namespace Ryujinx.Ava.UI.Helpers
_isWaitingForInput = false;
ToggledButton.IsChecked = false;
ClearKeyboardState(_keyboard);
if (pressedButton.HasValue && pressedButton.Value.AsHidType<Key>() == Key.BackSpace)
{
@@ -98,6 +104,15 @@ namespace Ryujinx.Ava.UI.Helpers
_isWaitingForInput = false;
ToggledButton.IsChecked = false;
_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.Collections.Generic;
using System.Globalization;
using Key = Ryujinx.Input.Key;
namespace Ryujinx.Ava.UI.Helpers
{
@@ -12,79 +13,6 @@ namespace Ryujinx.Ava.UI.Helpers
{
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()
{
{ GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick },
@@ -110,49 +38,38 @@ namespace Ryujinx.Ava.UI.Helpers
{ GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0},
{ GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1},
{ GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1},
{ GamepadInputId.Unbound, LocaleKeys.KeyUnbound},
{ GamepadInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
};
private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new()
{
{ StickInputId.Left, LocaleKeys.StickLeft},
{ StickInputId.Right, LocaleKeys.StickRight},
{ StickInputId.Unbound, LocaleKeys.KeyUnbound},
{ StickInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
};
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string keyString = string.Empty;
LocaleKeys localeKey;
switch (value)
{
case Key key:
if (_keysMap.TryGetValue(key, out localeKey))
if (KeyboardLayoutLocaleHelper.TryGetSemanticLabel(key, out string localizedKeyLabel))
{
if (OperatingSystem.IsMacOS())
{
localeKey = localeKey switch
{
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];
keyString = localizedKeyLabel;
}
else
{
keyString = key.ToString();
}
break;
case PhysicalKey physicalKey:
keyString = PhysicalKeyLabelHelper.GetDisplayString(physicalKey);
break;
case GamepadInputId gamepadInputId:
LocaleKeys localeKey;
if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey))
{
keyString = LocaleManager.Instance[localeKey];

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,226 @@
using Avalonia.Input;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
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;
}
try
{
string labelsPath = GetObservedLabelsPath();
if (File.Exists(labelsPath))
{
Dictionary<string, string> labels = JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(labelsPath), _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
{
}
_observedLayoutLabelsLoaded = true;
}
}
private static void SaveObservedLayoutLabels()
{
lock (_observedLayoutLabelsLock)
{
try
{
Dictionary<string, string> labels = [];
foreach ((ConfigPhysicalKey key, string value) in _observedLayoutLabels)
{
labels[key.ToString()] = value;
}
File.WriteAllText(GetObservedLabelsPath(), JsonSerializer.Serialize(labels, _serializerOptions));
}
catch
{
}
}
}
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
{
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; }
[ObservableProperty]
public partial Key LeftStickUp { get; set; }
public partial PhysicalKey LeftStickUp { get; set; }
[ObservableProperty]
public partial Key LeftStickDown { get; set; }
public partial PhysicalKey LeftStickDown { get; set; }
[ObservableProperty]
public partial Key LeftStickLeft { get; set; }
public partial PhysicalKey LeftStickLeft { get; set; }
[ObservableProperty]
public partial Key LeftStickRight { get; set; }
public partial PhysicalKey LeftStickRight { get; set; }
[ObservableProperty]
public partial Key LeftStickButton { get; set; }
public partial PhysicalKey LeftStickButton { get; set; }
[ObservableProperty]
public partial Key RightStickUp { get; set; }
public partial PhysicalKey RightStickUp { get; set; }
[ObservableProperty]
public partial Key RightStickDown { get; set; }
public partial PhysicalKey RightStickDown { get; set; }
[ObservableProperty]
public partial Key RightStickLeft { get; set; }
public partial PhysicalKey RightStickLeft { get; set; }
[ObservableProperty]
public partial Key RightStickRight { get; set; }
public partial PhysicalKey RightStickRight { get; set; }
[ObservableProperty]
public partial Key RightStickButton { get; set; }
public partial PhysicalKey RightStickButton { get; set; }
[ObservableProperty]
public partial Key DpadUp { get; set; }
public partial PhysicalKey DpadUp { get; set; }
[ObservableProperty]
public partial Key DpadDown { get; set; }
public partial PhysicalKey DpadDown { get; set; }
[ObservableProperty]
public partial Key DpadLeft { get; set; }
public partial PhysicalKey DpadLeft { get; set; }
[ObservableProperty]
public partial Key DpadRight { get; set; }
public partial PhysicalKey DpadRight { get; set; }
[ObservableProperty]
public partial Key ButtonMinus { get; set; }
public partial PhysicalKey ButtonMinus { get; set; }
[ObservableProperty]
public partial Key ButtonPlus { get; set; }
public partial PhysicalKey ButtonPlus { get; set; }
[ObservableProperty]
public partial Key ButtonA { get; set; }
public partial PhysicalKey ButtonA { get; set; }
[ObservableProperty]
public partial Key ButtonB { get; set; }
public partial PhysicalKey ButtonB { get; set; }
[ObservableProperty]
public partial Key ButtonX { get; set; }
public partial PhysicalKey ButtonX { get; set; }
[ObservableProperty]
public partial Key ButtonY { get; set; }
public partial PhysicalKey ButtonY { get; set; }
[ObservableProperty]
public partial Key ButtonL { get; set; }
public partial PhysicalKey ButtonL { get; set; }
[ObservableProperty]
public partial Key ButtonR { get; set; }
public partial PhysicalKey ButtonR { get; set; }
[ObservableProperty]
public partial Key ButtonZl { get; set; }
public partial PhysicalKey ButtonZl { get; set; }
[ObservableProperty]
public partial Key ButtonZr { get; set; }
public partial PhysicalKey ButtonZr { get; set; }
[ObservableProperty]
public partial Key LeftButtonSl { get; set; }
public partial PhysicalKey LeftButtonSl { get; set; }
[ObservableProperty]
public partial Key LeftButtonSr { get; set; }
public partial PhysicalKey LeftButtonSr { get; set; }
[ObservableProperty]
public partial Key RightButtonSl { get; set; }
public partial PhysicalKey RightButtonSl { get; set; }
[ObservableProperty]
public partial Key RightButtonSr { get; set; }
public partial PhysicalKey RightButtonSr { get; set; }
public KeyboardInputConfig(InputConfig config)
{
@@ -153,7 +153,7 @@ namespace Ryujinx.Ava.UI.Models.Input
Backend = InputBackendType.WindowKeyboard,
PlayerIndex = PlayerIndex,
ControllerType = ControllerType,
LeftJoycon = new LeftJoyconCommonConfig<Key>
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{
DpadUp = DpadUp,
DpadDown = DpadDown,
@@ -165,7 +165,7 @@ namespace Ryujinx.Ava.UI.Models.Input
ButtonSl = LeftButtonSl,
ButtonSr = LeftButtonSr,
},
RightJoycon = new RightJoyconCommonConfig<Key>
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{
ButtonA = ButtonA,
ButtonB = ButtonB,
@@ -177,7 +177,7 @@ namespace Ryujinx.Ava.UI.Models.Input
ButtonR = ButtonR,
ButtonZr = ButtonZr,
},
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = LeftStickUp,
StickDown = LeftStickDown,
@@ -185,7 +185,7 @@ namespace Ryujinx.Ava.UI.Models.Input
StickLeft = LeftStickLeft,
StickButton = LeftStickButton,
},
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = RightStickUp,
StickDown = RightStickDown,
@@ -198,5 +198,37 @@ namespace Ryujinx.Ava.UI.Models.Input
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

@@ -154,42 +154,42 @@ namespace Ryujinx.Ava.UI.Models.Input
{
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight))
if (snapshot.IsPressed(KeyboardConfig.LeftStickRight))
{
leftBuffer.Item1 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft))
if (snapshot.IsPressed(KeyboardConfig.LeftStickLeft))
{
leftBuffer.Item1 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp))
if (snapshot.IsPressed(KeyboardConfig.LeftStickUp))
{
leftBuffer.Item2 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown))
if (snapshot.IsPressed(KeyboardConfig.LeftStickDown))
{
leftBuffer.Item2 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight))
if (snapshot.IsPressed(KeyboardConfig.RightStickRight))
{
rightBuffer.Item1 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft))
if (snapshot.IsPressed(KeyboardConfig.RightStickLeft))
{
rightBuffer.Item1 -= 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp))
if (snapshot.IsPressed(KeyboardConfig.RightStickUp))
{
rightBuffer.Item2 += 1;
}
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown))
if (snapshot.IsPressed(KeyboardConfig.RightStickDown))
{
rightBuffer.Item2 -= 1;
}

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Svg.Skia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using Ryujinx.Ava.Common.Locale;
@@ -28,7 +29,7 @@ using System.Linq;
using System.Text.Json;
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
using Key = Ryujinx.Common.Configuration.Hid.Key;
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
@@ -42,6 +43,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private const string KeyboardString = "keyboard";
private const string ControllerString = "controller";
private readonly MainWindow _mainWindow;
private Control _keyboardDriverControl;
private PlayerIndex _playerId;
private PlayerIndex _playerIdChoose;
@@ -65,7 +67,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public IGamepadDriver AvaloniaKeyboardDriver { get; }
public IGamepadDriver AvaloniaKeyboardDriver { get; private set; }
public IGamepad SelectedGamepad
{
@@ -89,7 +91,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
internal ObservableCollection<ControllerModel> Controllers { get; set; }
public AvaloniaList<string> ProfilesList { get; set; }
public AvaloniaList<string> DeviceList { get; set; }
public bool UseGlobalConfig;
@@ -99,7 +100,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool IsKeyboard => !IsController;
public bool IsRight { get; set; }
public bool IsLeft { get; set; }
public string RevertDeviceId { get; set; }
public bool HasLed => (SelectedGamepad.Features & GamepadFeaturesFlag.Led) != 0;
public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense");
@@ -163,7 +163,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
LoadDevice();
LoadProfiles();
RevertDeviceId = Devices[Device].Id;
_isLoaded = true;
_isChangeTrackingActive = true;
OnPropertyChanged();
@@ -175,52 +174,58 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
get => _controller;
set
{
MarkAsChanged();
int controllerIndex = value < 0 ? 0 : value;
_controller = value;
if (_controller == -1)
if (controllerIndex == _controller)
{
_controller = 0;
return;
}
if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1)
{
ControllerType controller = Controllers[_controller].Type;
IsLeft = true;
IsRight = true;
switch (controller)
{
case ControllerType.Handheld:
ControllerImage = JoyConPairResource;
break;
case ControllerType.ProController:
ControllerImage = ProControllerResource;
break;
case ControllerType.JoyconPair:
ControllerImage = JoyConPairResource;
break;
case ControllerType.JoyconLeft:
ControllerImage = JoyConLeftResource;
IsRight = false;
break;
case ControllerType.JoyconRight:
ControllerImage = JoyConRightResource;
IsLeft = false;
break;
}
LoadInputDriver();
LoadProfiles();
}
OnPropertyChanged();
NotifyChanges();
ApplyControllerSelection(controllerIndex);
RefreshModifiedState();
}
}
private void ApplyControllerSelection(int controllerIndex)
{
_controller = controllerIndex;
if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1)
{
ControllerType controller = Controllers[_controller].Type;
IsLeft = true;
IsRight = true;
switch (controller)
{
case ControllerType.Handheld:
ControllerImage = JoyConPairResource;
break;
case ControllerType.ProController:
ControllerImage = ProControllerResource;
break;
case ControllerType.JoyconPair:
ControllerImage = JoyConPairResource;
break;
case ControllerType.JoyconLeft:
ControllerImage = JoyConLeftResource;
IsRight = false;
break;
case ControllerType.JoyconRight:
ControllerImage = JoyConRightResource;
IsLeft = false;
break;
}
LoadInputDriver();
LoadProfiles();
}
OnPropertyChanged(nameof(Controller));
NotifyChanges();
}
public string ControllerImage
{
get => _controllerImage;
@@ -255,33 +260,83 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
get => _device;
set
{
MarkAsChanged();
_device = value < 0 ? 0 : value;
if (_device >= Devices.Count)
if (value < 0 || value >= Devices.Count)
{
return;
}
_device = value;
DeviceType selected = Devices[_device].Type;
if (selected != DeviceType.None)
{
LoadControllers();
if (_isLoaded)
{
LoadConfiguration(LoadDefaultConfiguration());
LoadSelectedDeviceDefaults();
}
else
{
LoadSelectedDeviceControllers();
}
}
RefreshModifiedState();
FindPairedDeviceInConfigFile();
OnPropertyChanged();
OnPropertyChanged(nameof(SelectedDeviceItem));
NotifyChanges();
}
}
public void ResetCurrentDeviceToDefaults()
{
RefreshAvailableDevices();
if (_device <= 0 || _device >= Devices.Count || Devices[_device].Type == DeviceType.None)
{
return;
}
LoadSelectedDeviceDefaults();
RefreshModifiedState();
FindPairedDeviceInConfigFile();
NotifyChanges();
}
public void RefreshInputDevices()
{
RefreshAvailableDevices();
}
public object SelectedDeviceItem
{
get => _device >= 0 && _device < Devices.Count ? Devices[_device] : null;
set
{
if (value is not ValueTuple<DeviceType, string, string> selectedDevice)
{
return;
}
int deviceIndex = Devices.ToList().FindIndex(device =>
device.Type == selectedDevice.Item1 &&
device.Id == selectedDevice.Item2);
if (deviceIndex < 0)
{
return;
}
if (deviceIndex == _device)
{
return;
}
Device = deviceIndex;
}
}
public InputConfig Config { get; set; }
public InputViewModel(UserControl owner, bool useGlobal = false) : this()
@@ -290,7 +345,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_mainWindow = RyujinxApp.MainWindow;
AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner);
ReplaceKeyboardDriver(owner);
PhysicalKeyLabelHelper.LabelsChanged += OnPhysicalKeyLabelsChanged;
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
@@ -301,7 +357,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_isLoaded = false;
LoadDevices();
RefreshAvailableDevices();
PlayerId = PlayerIndex.Player1;
}
@@ -309,13 +365,22 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
_isChangeTrackingActive = true;
}
public void RetargetKeyboardDriver(Control owner)
{
if (!Program.PreviewerDetached)
{
return;
}
ReplaceKeyboardDriver(owner);
}
public InputViewModel()
{
PlayerIndexes = [];
Controllers = [];
Devices = [];
ProfilesList = [];
DeviceList = [];
VisualStick = new StickVisualizer(this);
ControllerImage = ProControllerResource;
@@ -333,17 +398,20 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private void LoadConfiguration(InputConfig inputConfig = null)
private InputConfig GetPersistedInputConfig()
{
if (UseGlobalConfig && Program.UseExtraConfig)
{
Config = inputConfig ?? ConfigurationState.InstanceExtra.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
}
else
{
Config = inputConfig ?? ConfigurationState.Instance.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
return ConfigurationState.InstanceExtra.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
}
return ConfigurationState.Instance.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
}
private void LoadConfiguration(InputConfig inputConfig = null)
{
Config = inputConfig ?? GetDisplayedInputConfig(GetPersistedInputConfig());
if (Config is StandardKeyboardInputConfig keyboardInputConfig)
{
ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig), VisualStick);
@@ -355,6 +423,18 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
}
private InputConfig GetDisplayedInputConfig(InputConfig persistedConfig)
{
if (persistedConfig is not StandardControllerInputConfig)
{
return persistedConfig;
}
InputConfig activeConfig = _mainWindow?.ViewModel.AppHost?.NpadManager?.GetPlayerInputConfigByIndex((int)_playerId);
return activeConfig is StandardKeyboardInputConfig ? activeConfig : persistedConfig;
}
private void FindPairedDeviceInConfigFile()
{
// This function allows you to output a message about the device configuration found in the file
@@ -375,16 +455,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
}
private void MarkAsChanged()
{
//If tracking is active, then allow changing the modifier
if (!IsModified && _isChangeTrackingActive)
{
RevertDeviceId = Devices[Device].Id; // Remember the device to undo changes
IsModified = true;
}
}
public void UnlinkDevice()
{
// "Disabled" mode is available after unbinding the device
@@ -395,34 +465,117 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public void LoadDevice()
{
int deviceIndex = 0;
if (Config == null || Config.Backend == InputBackendType.Invalid)
{
Device = 0;
ApplyLoadedDevice(deviceIndex);
return;
}
else
DeviceType type = DeviceType.None;
if (Config is StandardKeyboardInputConfig)
{
DeviceType type = DeviceType.None;
if (Config is StandardKeyboardInputConfig)
{
type = DeviceType.Keyboard;
}
if (Config is StandardControllerInputConfig)
{
type = DeviceType.Controller;
}
(DeviceType Type, string Id, string Name) item = Devices.FirstOrDefault(x => x.Type == type && x.Id == Config.Id);
if (item != default)
{
Device = Devices.ToList().FindIndex(x => x.Id == item.Id);
}
else
{
Device = 0;
}
type = DeviceType.Keyboard;
}
if (Config is StandardControllerInputConfig)
{
type = DeviceType.Controller;
}
(DeviceType Type, string Id, string Name) item = Devices.FirstOrDefault(x => x.Type == type && x.Id == Config.Id);
if (item != default)
{
deviceIndex = Devices.ToList().FindIndex(x => x.Id == item.Id);
}
ApplyLoadedDevice(deviceIndex);
}
private void ApplyLoadedDevice(int deviceIndex)
{
_device = deviceIndex is >= 0 and < int.MaxValue ? deviceIndex : 0;
if (_device >= Devices.Count)
{
_device = 0;
}
if (_device > 0 && Devices[_device].Type != DeviceType.None)
{
LoadControllers();
}
FindPairedDeviceInConfigFile();
OnPropertyChanged(nameof(Device));
OnPropertyChanged(nameof(SelectedDeviceItem));
NotifyChanges();
}
private void LoadSelectedDeviceControllers()
{
if (_device > 0 && _device < Devices.Count && Devices[_device].Type != DeviceType.None)
{
LoadControllers();
}
}
private void LoadSelectedDeviceDefaults()
{
LoadSelectedDeviceControllers();
LoadConfiguration(LoadDefaultConfiguration());
}
public void RefreshModifiedState()
{
if (!_isChangeTrackingActive)
{
return;
}
IsModified = !ConfigsMatch(GetSelectedDeviceConfig(), GetDisplayedInputConfig(GetPersistedInputConfig()));
}
private static bool ConfigsMatch(InputConfig currentConfig, InputConfig otherConfig)
{
if (currentConfig == null || otherConfig == null)
{
return currentConfig == otherConfig;
}
return JsonHelper.Serialize(currentConfig, _serializerContext.InputConfig) ==
JsonHelper.Serialize(otherConfig, _serializerContext.InputConfig);
}
private InputConfig GetSelectedDeviceConfig()
{
if (_device <= 0 || _device >= Devices.Count)
{
return null;
}
(DeviceType Type, string Id, string Name) device = Devices[_device];
InputConfig config = device.Type switch
{
DeviceType.Keyboard => (ConfigViewModel as KeyboardInputViewModel)?.Config.GetConfig(),
DeviceType.Controller => (ConfigViewModel as ControllerInputViewModel)?.Config.GetConfig(),
_ => null,
};
if (config == null)
{
return null;
}
config.Id = device.Type == DeviceType.Keyboard ? device.Id : device.Id.Split(" ")[0];
config.Name = device.Name;
config.PlayerIndex = _playerId;
config.ControllerType = Controllers[_controller].Type;
return config;
}
private void LoadInputDriver()
@@ -462,11 +615,24 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_isChangeTrackingActive = false; // Disable configuration change tracking
LoadDevices();
bool shouldApplyKeyboardFallback = Config is StandardControllerInputConfig controllerConfig && controllerConfig.Id == id;
IsModified = true;
RevertChanges();
FindPairedDeviceInConfigFile();
RefreshAvailableDevices();
if (shouldApplyKeyboardFallback)
{
LoadConfiguration();
LoadDevice();
NotificationIsVisible = false;
IsModified = false;
NotifyChanges();
}
else
{
IsModified = true;
RevertChanges();
FindPairedDeviceInConfigFile();
}
_isChangeTrackingActive = true; // Enable configuration change tracking
@@ -476,7 +642,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_isChangeTrackingActive = false; // Disable configuration change tracking
LoadDevices();
RefreshAvailableDevices();
IsModified = true;
RevertChanges();
@@ -502,6 +668,23 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
return device.Id.Split(" ")[0];
}
private string GetCurrentConfigDeviceId()
{
if (_device < 0 || _device >= Devices.Count)
{
return null;
}
(DeviceType Type, string Id, string Name) device = Devices[_device];
return device.Type switch
{
DeviceType.Keyboard => device.Id,
DeviceType.Controller => device.Id.Split(" ")[0],
_ => null,
};
}
public void LoadControllers()
{
Controllers.Clear();
@@ -510,7 +693,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeHandheld]));
Controller = 0;
ApplyControllerSelection(0);
}
else
{
@@ -528,14 +711,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
// Workaround: set the box to 1 and then 0
if (controllerIndex == 0)
{
Controller = 1;
ApplyControllerSelection(1);
}
Controller = controllerIndex;
ApplyControllerSelection(controllerIndex);
}
else
{
Controller = 0;
ApplyControllerSelection(0);
}
}
}
@@ -561,8 +744,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
return str[(str.IndexOf(Hyphen) + Offset)..];
}
public void LoadDevices()
private void RefreshAvailableDevices()
{
int selectedDeviceIndex = 0;
(DeviceType Type, string Id, string Name) selectedDevice = default;
if (_device >= 0 && _device < Devices.Count)
{
selectedDevice = Devices[_device];
}
string GetGamepadName(IGamepad gamepad, int controllerNumber)
{
return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
@@ -583,7 +774,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
lock (Devices)
{
Devices.Clear();
DeviceList.Clear();
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
@@ -609,9 +799,20 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
}
DeviceList.AddRange(Devices.Select(x => x.Name));
Device = Math.Min(Device, DeviceList.Count);
if (selectedDevice != default)
{
selectedDeviceIndex = Devices.ToList().FindIndex(device =>
device.Type == selectedDevice.Type &&
device.Id == selectedDevice.Id);
}
if (selectedDeviceIndex < 0)
{
selectedDeviceIndex = Math.Clamp(_device, 0, Devices.Count - 1);
}
}
ApplyLoadedDevice(selectedDeviceIndex);
}
private string GetProfileBasePath()
@@ -677,46 +878,46 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
Id = id,
Name = name,
ControllerType = ControllerType.ProController,
LeftJoycon = new LeftJoyconCommonConfig<Key>
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
{
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,
DpadUp = PhysicalKey.Up,
DpadDown = PhysicalKey.Down,
DpadLeft = PhysicalKey.Left,
DpadRight = PhysicalKey.Right,
ButtonMinus = PhysicalKey.Minus,
ButtonL = PhysicalKey.E,
ButtonZl = PhysicalKey.Q,
ButtonSl = PhysicalKey.Unbound,
ButtonSr = PhysicalKey.Unbound,
},
LeftJoyconStick =
new JoyconConfigKeyboardStick<Key>
new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.W,
StickDown = Key.S,
StickLeft = Key.A,
StickRight = Key.D,
StickButton = Key.F,
StickUp = PhysicalKey.W,
StickDown = PhysicalKey.S,
StickLeft = PhysicalKey.A,
StickRight = PhysicalKey.D,
StickButton = PhysicalKey.F,
},
RightJoycon = new RightJoyconCommonConfig<Key>
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
{
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,
ButtonA = PhysicalKey.Z,
ButtonB = PhysicalKey.X,
ButtonX = PhysicalKey.C,
ButtonY = PhysicalKey.V,
ButtonPlus = PhysicalKey.Plus,
ButtonR = PhysicalKey.U,
ButtonZr = PhysicalKey.O,
ButtonSl = PhysicalKey.Unbound,
ButtonSr = PhysicalKey.Unbound,
},
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
{
StickUp = Key.I,
StickDown = Key.K,
StickLeft = Key.J,
StickRight = Key.L,
StickButton = Key.H,
StickUp = PhysicalKey.I,
StickDown = PhysicalKey.K,
StickLeft = PhysicalKey.J,
StickRight = PhysicalKey.L,
StickButton = PhysicalKey.H,
},
};
}
@@ -860,7 +1061,14 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
{
_isLoaded = false;
config.Id = Config.Id; // Set current device id instead of changing device(independent profiles)
string currentDeviceId = Config?.Id ?? GetCurrentConfigDeviceId();
if (string.IsNullOrEmpty(currentDeviceId))
{
Logger.Warning?.Print(LogClass.Configuration, $"Ignoring profile load for {ProfileName} because no active input device is selected.");
return;
}
config.Id = currentDeviceId; // Set current device id instead of changing device(independent profiles)
LoadConfiguration(config);
@@ -958,9 +1166,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public void RevertChanges()
{
LoadConfiguration(); // configuration preload is required if the paired gamepad was disconnected but was changed to another gamepad
Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId);
_isLoaded = false;
LoadConfiguration();
LoadDevice();
@@ -980,8 +1185,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
IsModified = false;
RevertDeviceId = Devices[Device].Id; // Remember selected device after saving
List<InputConfig> newConfig = [];
if (UseGlobalConfig && Program.UseExtraConfig)
@@ -1001,25 +1204,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
else
{
(DeviceType Type, string Id, string Name) device = Devices[Device];
if (device.Type == DeviceType.Keyboard)
{
KeyboardInputConfig inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config;
inputConfig.Id = device.Id;
}
else
{
GamepadInputConfig inputConfig = (ConfigViewModel as ControllerInputViewModel).Config;
inputConfig.Id = device.Id.Split(" ")[0];
}
InputConfig config = !IsController
? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig()
: (ConfigViewModel as ControllerInputViewModel).Config.GetConfig();
config.ControllerType = Controllers[_controller].Type;
config.PlayerIndex = _playerId;
config.Name = device.Name;
InputConfig config = GetSelectedDeviceConfig();
int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId);
if (i == -1)
@@ -1060,9 +1245,46 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
NotifyChangesEvent?.Invoke();
}
private void OnPhysicalKeyLabelsChanged()
{
if (ConfigViewModel is KeyboardInputViewModel keyboardInputViewModel)
{
Dispatcher.UIThread.Post(keyboardInputViewModel.Config.NotifyKeyLabelsChanged);
}
}
private void ReplaceKeyboardDriver(Control owner)
{
Control target = TopLevel.GetTopLevel(owner) as Control ?? owner;
if (ReferenceEquals(_keyboardDriverControl, target))
{
return;
}
if (AvaloniaKeyboardDriver is AvaloniaKeyboardDriver oldKeyboardDriver)
{
oldKeyboardDriver.KeyPressed -= PhysicalKeyLabelHelper.ObserveKeyPress;
oldKeyboardDriver.Dispose();
}
_keyboardDriverControl = target;
AvaloniaKeyboardDriver keyboardDriver = new(target, KeyboardInputMode.Physical);
keyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
AvaloniaKeyboardDriver = keyboardDriver;
if (_isLoaded && Device > 0 && Device < Devices.Count && Devices[Device].Type == DeviceType.Keyboard)
{
SelectedGamepad?.Dispose();
LoadInputDriver();
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
PhysicalKeyLabelHelper.LabelsChanged -= OnPhysicalKeyLabelsChanged;
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
@@ -1073,7 +1295,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
SelectedGamepad?.Dispose();
AvaloniaKeyboardDriver.Dispose();
AvaloniaKeyboardDriver?.Dispose();
}
}
}

View File

@@ -116,7 +116,6 @@ namespace Ryujinx.Ava.UI.Views.Input
if (e.ButtonValue.HasValue)
{
Button buttonValue = e.ButtonValue.Value;
FlagInputConfigChanged();
switch (button.Name)
{
@@ -187,6 +186,8 @@ namespace Ryujinx.Ava.UI.Views.Input
viewModel.Config.RightJoystick = buttonValue.AsHidType<StickInputId>();
break;
}
FlagInputConfigChanged();
}
};
@@ -212,7 +213,7 @@ namespace Ryujinx.Ava.UI.Views.Input
private void FlagInputConfigChanged()
{
(DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true;
(DataContext as ControllerInputViewModel)!.ParentModel.RefreshModifiedState();
}
private void MouseClick(object sender, PointerPressedEventArgs e)

View File

@@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
@@ -148,7 +149,7 @@
<Grid
Grid.Column="0"
Margin="2"
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto">
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto,Auto">
<TextBlock
Grid.Column="0"
Margin="5,0,10,0"
@@ -161,19 +162,38 @@
Name="DeviceBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
ItemsSource="{Binding DeviceList}"
SelectedIndex="{Binding Device}" />
ItemsSource="{Binding Devices}"
SelectedItem="{Binding SelectedDeviceItem, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding ., Converter={x:Static helpers:InputDeviceNameConverter.Instance}}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button
Grid.Column="2"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
Command="{Binding LoadDevice}">
ToolTip.Tip="{ext:Locale ControllerSettingsRefresh}"
Command="{Binding RefreshInputDevices}">
<ui:SymbolIcon
Symbol="Refresh"
FontSize="15"
Height="20"/>
</Button>
<Button
Grid.Column="3"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
ToolTip.Tip="{ext:Locale ControllerSettingsResetKeybindsToDefault}"
Command="{Binding ResetCurrentDeviceToDefaults}">
<ui:SymbolIcon
Symbol="Undo"
FontSize="15"
Height="20"/>
</Button>
</Grid>
<!-- Controller Type -->
<Grid

View File

@@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Systems.Configuration;
@@ -15,9 +16,14 @@ namespace Ryujinx.Ava.UI.Views.Input
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()
@@ -28,8 +34,18 @@ namespace Ryujinx.Ava.UI.Views.Input
public void ToggleLocalGlobalInput(bool enableConfigGlobal)
{
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();
if (VisualRoot is not null)
{
ViewModel.RetargetKeyboardDriver(this);
}
}
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)

View File

@@ -12,7 +12,7 @@ using Ryujinx.Input.Assigner;
using System;
using System.Collections.Generic;
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
{
@@ -73,95 +73,96 @@ namespace Ryujinx.Ava.UI.Views.Input
if (be.ButtonValue.HasValue)
{
Button buttonValue = be.ButtonValue.Value;
ViewModel.ParentModel.IsModified = true;
switch (button.Name)
{
case "ButtonZl":
ViewModel.Config.ButtonZl = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonZl = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonL":
ViewModel.Config.ButtonL = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonL = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonMinus":
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickButton":
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickUp":
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickDown":
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickRight":
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftStickLeft":
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadUp":
ViewModel.Config.DpadUp = buttonValue.AsHidType<Key>();
ViewModel.Config.DpadUp = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadDown":
ViewModel.Config.DpadDown = buttonValue.AsHidType<Key>();
ViewModel.Config.DpadDown = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadLeft":
ViewModel.Config.DpadLeft = buttonValue.AsHidType<Key>();
ViewModel.Config.DpadLeft = buttonValue.AsHidType<PhysicalKey>();
break;
case "DpadRight":
ViewModel.Config.DpadRight = buttonValue.AsHidType<Key>();
ViewModel.Config.DpadRight = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftButtonSr":
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<PhysicalKey>();
break;
case "LeftButtonSl":
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<Key>();
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightButtonSr":
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<Key>();
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightButtonSl":
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<Key>();
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonZr":
ViewModel.Config.ButtonZr = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonZr = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonR":
ViewModel.Config.ButtonR = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonR = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonPlus":
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonA":
ViewModel.Config.ButtonA = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonA = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonB":
ViewModel.Config.ButtonB = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonB = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonX":
ViewModel.Config.ButtonX = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonX = buttonValue.AsHidType<PhysicalKey>();
break;
case "ButtonY":
ViewModel.Config.ButtonY = buttonValue.AsHidType<Key>();
ViewModel.Config.ButtonY = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickButton":
ViewModel.Config.RightStickButton = buttonValue.AsHidType<Key>();
ViewModel.Config.RightStickButton = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickUp":
ViewModel.Config.RightStickUp = buttonValue.AsHidType<Key>();
ViewModel.Config.RightStickUp = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickDown":
ViewModel.Config.RightStickDown = buttonValue.AsHidType<Key>();
ViewModel.Config.RightStickDown = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickRight":
ViewModel.Config.RightStickRight = buttonValue.AsHidType<Key>();
ViewModel.Config.RightStickRight = buttonValue.AsHidType<PhysicalKey>();
break;
case "RightStickLeft":
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<Key>();
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<PhysicalKey>();
break;
}
ViewModel.ParentModel.RefreshModifiedState();
}
};
@@ -207,40 +208,40 @@ namespace Ryujinx.Ava.UI.Views.Input
{
Dictionary<string, Action> buttonActions = new()
{
{ "ButtonZl", () => ViewModel.Config.ButtonZl = Key.Unbound },
{ "ButtonL", () => ViewModel.Config.ButtonL = Key.Unbound },
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = Key.Unbound },
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = Key.Unbound },
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = Key.Unbound },
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = Key.Unbound },
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = Key.Unbound },
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = Key.Unbound },
{ "DpadUp", () => ViewModel.Config.DpadUp = Key.Unbound },
{ "DpadDown", () => ViewModel.Config.DpadDown = Key.Unbound },
{ "DpadLeft", () => ViewModel.Config.DpadLeft = Key.Unbound },
{ "DpadRight", () => ViewModel.Config.DpadRight = Key.Unbound },
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = Key.Unbound },
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = Key.Unbound },
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = Key.Unbound },
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = Key.Unbound },
{ "ButtonZr", () => ViewModel.Config.ButtonZr = Key.Unbound },
{ "ButtonR", () => ViewModel.Config.ButtonR = Key.Unbound },
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = Key.Unbound },
{ "ButtonA", () => ViewModel.Config.ButtonA = Key.Unbound },
{ "ButtonB", () => ViewModel.Config.ButtonB = Key.Unbound },
{ "ButtonX", () => ViewModel.Config.ButtonX = Key.Unbound },
{ "ButtonY", () => ViewModel.Config.ButtonY = Key.Unbound },
{ "RightStickButton", () => ViewModel.Config.RightStickButton = Key.Unbound },
{ "RightStickUp", () => ViewModel.Config.RightStickUp = Key.Unbound },
{ "RightStickDown", () => ViewModel.Config.RightStickDown = Key.Unbound },
{ "RightStickRight", () => ViewModel.Config.RightStickRight = Key.Unbound },
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = Key.Unbound }
{ "ButtonZl", () => ViewModel.Config.ButtonZl = PhysicalKey.Unbound },
{ "ButtonL", () => ViewModel.Config.ButtonL = PhysicalKey.Unbound },
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = PhysicalKey.Unbound },
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = PhysicalKey.Unbound },
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = PhysicalKey.Unbound },
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = PhysicalKey.Unbound },
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = PhysicalKey.Unbound },
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = PhysicalKey.Unbound },
{ "DpadUp", () => ViewModel.Config.DpadUp = PhysicalKey.Unbound },
{ "DpadDown", () => ViewModel.Config.DpadDown = PhysicalKey.Unbound },
{ "DpadLeft", () => ViewModel.Config.DpadLeft = PhysicalKey.Unbound },
{ "DpadRight", () => ViewModel.Config.DpadRight = PhysicalKey.Unbound },
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = PhysicalKey.Unbound },
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = PhysicalKey.Unbound },
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = PhysicalKey.Unbound },
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = PhysicalKey.Unbound },
{ "ButtonZr", () => ViewModel.Config.ButtonZr = PhysicalKey.Unbound },
{ "ButtonR", () => ViewModel.Config.ButtonR = PhysicalKey.Unbound },
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = PhysicalKey.Unbound },
{ "ButtonA", () => ViewModel.Config.ButtonA = PhysicalKey.Unbound },
{ "ButtonB", () => ViewModel.Config.ButtonB = PhysicalKey.Unbound },
{ "ButtonX", () => ViewModel.Config.ButtonX = PhysicalKey.Unbound },
{ "ButtonY", () => ViewModel.Config.ButtonY = PhysicalKey.Unbound },
{ "RightStickButton", () => ViewModel.Config.RightStickButton = PhysicalKey.Unbound },
{ "RightStickUp", () => ViewModel.Config.RightStickUp = PhysicalKey.Unbound },
{ "RightStickDown", () => ViewModel.Config.RightStickDown = PhysicalKey.Unbound },
{ "RightStickRight", () => ViewModel.Config.RightStickRight = PhysicalKey.Unbound },
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = PhysicalKey.Unbound }
};
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
{
action();
ViewModel.ParentModel.IsModified = true;
ViewModel.ParentModel.RefreshModifiedState();
}
}
}

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)

View File

@@ -30,6 +30,7 @@ using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL3;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -105,7 +106,9 @@ namespace Ryujinx.Ava.UI.Windows
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.ScalingChanged += OnScalingChanged;