mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-05-26 23:19:15 +00:00
Input: Refactor Keyboard Handling To Use Physical Keys (#13)
This PR refactors keyboard handling to use physical key mappings for all gameplay input, ensuring controls remain consistent across different OS keyboard layouts. I'd like to give out an ENORMOUS thank you to @Neo for his very generous help on getting MacOS caps lock behaviour working, but as well for taking the time for extensive testing, planning and discussions, and finally for writing this PR message :) Keep being awsome pal 👊 ### New Features : * **New**: Gameplay input now uses physical key positions instead of OS layouts, ensuring the same physical key triggers the same action across keyboard layouts. * Key rebinding stores physical keys and config compatibility is preserved, with physical keys now the primary gameplay‑binding format. * Physical‑key model is now consistent across platforms, including updated SDL/headless behavior. * **Added**: New Input setting "Reset keybinds to default", with a new confirmation dialog appears when changes are being overwritten. * **Fractured**: Keyboard‑related locales to the newly created `KeyboardLayout.json`. * New input device settings/actions use clearer labels and tooltips. * UI Key Labels (such as Left Shift and Right Shift) are more accurate and standardized, with clearer symbols, consistent naming, dynamic learning of printable labels from real key events, and persistence across restarts. ### Improvements : * **Reduced**: Incorrect key labels by using observed host symbols instead of language assumptions. * **Reduced**: Stuck/stale keys by using binary pressed‑key tracking, fixing rebinding/gameplay paths, better held‑key recovery after focus changes, and clearing keyboard state when Ryujinx/settings windows lose focus. * **Improved**: Device handling → refreshing no longer clears the selector, disconnect fallback is consistent, reconnect restores controllers automatically, and the UI avoids invalid/empty device states. * **Improved**: Async input‑assignment callbacks are now guarded when switching views/devices, preventing stale callbacks from hitting detached views. * **Adjusted**: Input visualiser to be more robust when switching sources or handling controller disconnect/reconnect, without needing to reopen settings. * **Improved**: Modification (changes to input controls) tracking * Rebinding to the same value, reverting to original config, restoring defaults without differences, or reloading equivalent profiles no longer leaves Player marked as modified. * **Reduced**: Keyboard LED noise in logs and added optional UI keyboard‑state/rebinding diagnostics. ### Fixes : * **Special Keys**: * AltGr and other special keys behave correctly, including proper Ctrl+Alt → AltRight handling and more consistent normalization of special/synthetic keys. * Caps Lock is now reliably bindable on all platforms (Windows/Linux register every press; macOS every other). * **Fixed**: Certain cases where keyboard input broke after pointer interactions ### Current Limitations These are planned on being fixed/improved upon in future PRs: * Hotkeys still use semantic (Key) mappings. * Software keyboard / text input still uses the semantic path * Printable key labels may fall back to defaults until observed from host input. * Full semantic/physical split currently implemented only in the Avalonia driver. Co-authored-by: _Neo_ <ursamajorjanus2819@gmail.com> Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/13
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -72,6 +72,9 @@ ipch/
|
|||||||
_ReSharper*/
|
_ReSharper*/
|
||||||
*.[Rr]e[Ss]harper
|
*.[Rr]e[Ss]harper
|
||||||
|
|
||||||
|
# .NET
|
||||||
|
.dotnet-home/
|
||||||
|
|
||||||
# TeamCity is a build add-in
|
# TeamCity is a build add-in
|
||||||
_TeamCity*
|
_TeamCity*
|
||||||
|
|
||||||
|
|||||||
1904
assets/Locales/KeyboardLayout.json
Normal file
1904
assets/Locales/KeyboardLayout.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
namespace Ryujinx.Common.Configuration.Hid.Keyboard
|
namespace Ryujinx.Common.Configuration.Hid.Keyboard
|
||||||
{
|
{
|
||||||
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<Key> { }
|
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<PhysicalKey> { }
|
||||||
}
|
}
|
||||||
|
|||||||
142
src/Ryujinx.Common/Configuration/Hid/PhysicalKey.cs
Normal file
142
src/Ryujinx.Common/Configuration/Hid/PhysicalKey.cs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -165,6 +165,9 @@ namespace Ryujinx.Input.SDL3
|
|||||||
public string Id { get; }
|
public string Id { get; }
|
||||||
public string Name { get; }
|
public string Name { get; }
|
||||||
|
|
||||||
|
// Expose vendor id for higher-fidelity device detection in UI
|
||||||
|
public ushort VendorId => _gamepadHandle != null ? SDL_GetGamepadVendor(_gamepadHandle) : (ushort)0;
|
||||||
|
|
||||||
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
|
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
|
|||||||
@@ -9,25 +9,15 @@ using System.Runtime.CompilerServices;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SDL;
|
using SDL;
|
||||||
using static SDL.SDL3;
|
using static SDL.SDL3;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
|
|
||||||
|
|
||||||
namespace Ryujinx.Input.SDL3
|
namespace Ryujinx.Input.SDL3
|
||||||
{
|
{
|
||||||
class SDL3Keyboard : IKeyboard
|
class SDL3Keyboard : IKeyboard
|
||||||
{
|
{
|
||||||
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, Key From)
|
|
||||||
{
|
|
||||||
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not Key.Unbound;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Lock _userMappingLock = new();
|
private readonly Lock _userMappingLock = new();
|
||||||
|
|
||||||
#pragma warning disable IDE0052 // Remove unread private member
|
|
||||||
private readonly SDL3KeyboardDriver _driver;
|
|
||||||
#pragma warning restore IDE0052
|
|
||||||
private StandardKeyboardInputConfig _configuration;
|
private StandardKeyboardInputConfig _configuration;
|
||||||
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
|
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
|
||||||
|
|
||||||
|
|
||||||
private static readonly SDL_Keycode[] _keysDriverMapping =
|
private static readonly SDL_Keycode[] _keysDriverMapping =
|
||||||
@@ -172,9 +162,8 @@ namespace Ryujinx.Input.SDL3
|
|||||||
SDL_Keycode.SDLK_0
|
SDL_Keycode.SDLK_0
|
||||||
];
|
];
|
||||||
|
|
||||||
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name)
|
public SDL3Keyboard(string id, string name)
|
||||||
{
|
{
|
||||||
_driver = driver;
|
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
_buttonsUserMapping = [];
|
_buttonsUserMapping = [];
|
||||||
@@ -196,9 +185,9 @@ namespace Ryujinx.Input.SDL3
|
|||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private unsafe static int ToSDL3Scancode(Key key)
|
private unsafe static int ToSDL3Scancode(ConfigPhysicalKey key)
|
||||||
{
|
{
|
||||||
if (key is >= Key.Unknown and <= Key.Menu)
|
if (key is >= ConfigPhysicalKey.Unknown and <= ConfigPhysicalKey.Menu)
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -206,18 +195,18 @@ namespace Ryujinx.Input.SDL3
|
|||||||
return (int)SDL_GetScancodeFromKey(_keysDriverMapping[(int)key], null);
|
return (int)SDL_GetScancodeFromKey(_keysDriverMapping[(int)key], null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SDL_Keymod GetKeyboardModifierMask(Key key)
|
private static SDL_Keymod GetKeyboardModifierMask(ConfigPhysicalKey key)
|
||||||
{
|
{
|
||||||
return key switch
|
return key switch
|
||||||
{
|
{
|
||||||
Key.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
|
ConfigPhysicalKey.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
|
||||||
Key.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
|
ConfigPhysicalKey.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
|
||||||
Key.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
|
ConfigPhysicalKey.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
|
||||||
Key.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
|
ConfigPhysicalKey.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
|
||||||
Key.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
|
ConfigPhysicalKey.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
|
||||||
Key.AltRight => SDL_Keymod.SDL_KMOD_RALT,
|
ConfigPhysicalKey.AltRight => SDL_Keymod.SDL_KMOD_RALT,
|
||||||
Key.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
|
ConfigPhysicalKey.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
|
||||||
Key.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
|
ConfigPhysicalKey.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
|
||||||
// NOTE: Menu key isn't supported by SDL3.
|
// NOTE: Menu key isn't supported by SDL3.
|
||||||
_ => SDL_Keymod.SDL_KMOD_NONE
|
_ => SDL_Keymod.SDL_KMOD_NONE
|
||||||
};
|
};
|
||||||
@@ -233,9 +222,9 @@ namespace Ryujinx.Input.SDL3
|
|||||||
rawKeyboardState = SDL_GetKeyboardState(null);
|
rawKeyboardState = SDL_GetKeyboardState(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool[] keysState = new bool[(int)Key.Count];
|
bool[] keysState = new bool[(int)ConfigPhysicalKey.Count];
|
||||||
|
|
||||||
for (Key key = 0; key < Key.Count; key++)
|
for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
|
||||||
{
|
{
|
||||||
int index = ToSDL3Scancode(key);
|
int index = ToSDL3Scancode(key);
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
@@ -265,36 +254,6 @@ namespace Ryujinx.Input.SDL3
|
|||||||
return value * ConvertRate;
|
return value * ConvertRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
|
|
||||||
{
|
|
||||||
short stickX = 0;
|
|
||||||
short stickY = 0;
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickUp))
|
|
||||||
{
|
|
||||||
stickY += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickDown))
|
|
||||||
{
|
|
||||||
stickY -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickRight))
|
|
||||||
{
|
|
||||||
stickX += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
|
|
||||||
{
|
|
||||||
stickX -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
|
|
||||||
|
|
||||||
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
public GamepadStateSnapshot GetMappedStateSnapshot()
|
public GamepadStateSnapshot GetMappedStateSnapshot()
|
||||||
{
|
{
|
||||||
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
|
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
|
||||||
@@ -307,9 +266,9 @@ namespace Ryujinx.Input.SDL3
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
|
foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
|
||||||
{
|
{
|
||||||
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
|
if (!entry.IsValid)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -321,8 +280,8 @@ namespace Ryujinx.Input.SDL3
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
||||||
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
||||||
|
|
||||||
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
||||||
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
||||||
@@ -358,38 +317,15 @@ namespace Ryujinx.Input.SDL3
|
|||||||
{
|
{
|
||||||
_configuration = (StandardKeyboardInputConfig)configuration;
|
_configuration = (StandardKeyboardInputConfig)configuration;
|
||||||
|
|
||||||
// First clear the buttons mapping
|
|
||||||
_buttonsUserMapping.Clear();
|
_buttonsUserMapping.Clear();
|
||||||
|
|
||||||
// Then configure left joycon
|
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
|
|
||||||
|
|
||||||
// Finally configure right joycon
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetLed(uint packedRgb)
|
public void SetLed(uint packedRgb)
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard");
|
Logger.Debug?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTriggerThreshold(float triggerThreshold)
|
public void SetTriggerThreshold(float triggerThreshold)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ namespace Ryujinx.Input.SDL3
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new SDL3Keyboard(this, _keyboardIdentifers[0], "All keyboards");
|
return new SDL3Keyboard(_keyboardIdentifers[0], "All keyboards");
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IGamepad> GetGamepads()
|
public IEnumerable<IGamepad> GetGamepads()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
|
||||||
namespace Ryujinx.Input.Assigner
|
namespace Ryujinx.Input.Assigner
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -8,22 +10,40 @@ namespace Ryujinx.Input.Assigner
|
|||||||
private readonly IKeyboard _keyboard;
|
private readonly IKeyboard _keyboard;
|
||||||
|
|
||||||
private KeyboardStateSnapshot _keyboardState;
|
private KeyboardStateSnapshot _keyboardState;
|
||||||
|
private Button? _pressedButton;
|
||||||
|
|
||||||
public KeyboardKeyAssigner(IKeyboard keyboard)
|
public KeyboardKeyAssigner(IKeyboard keyboard)
|
||||||
{
|
{
|
||||||
_keyboard = keyboard;
|
_keyboard = keyboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Initialize() { }
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_pressedButton = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void ReadInput()
|
public void ReadInput()
|
||||||
{
|
{
|
||||||
_keyboardState = _keyboard.GetKeyboardStateSnapshot();
|
_keyboardState = _keyboard.GetKeyboardStateSnapshot();
|
||||||
|
|
||||||
|
if (_pressedButton is null)
|
||||||
|
{
|
||||||
|
Button? buttonFromState = GetPressedButtonFromState();
|
||||||
|
Button? buttonFromBufferedPress = buttonFromState is null ? GetPressedButtonFromBufferedPress() : null;
|
||||||
|
|
||||||
|
_pressedButton = buttonFromState ?? buttonFromBufferedPress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pressedButton is not null)
|
||||||
|
{
|
||||||
|
string source = _pressedButton.HasValue && GetPressedButtonFromState() is not null ? "state" : "buffered-press";
|
||||||
|
Logger.Debug?.Print(LogClass.UI, $"Keyboard assigner registered key={_pressedButton.Value.AsHidType<Key>()}, source={source}, cancelPressed={ShouldCancel()}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsAnyButtonPressed()
|
public bool IsAnyButtonPressed()
|
||||||
{
|
{
|
||||||
return GetPressedButton() is not null;
|
return _pressedButton is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ShouldCancel()
|
public bool ShouldCancel()
|
||||||
@@ -33,18 +53,53 @@ namespace Ryujinx.Input.Assigner
|
|||||||
|
|
||||||
public Button? GetPressedButton()
|
public Button? GetPressedButton()
|
||||||
{
|
{
|
||||||
Button? keyPressed = null;
|
return !ShouldCancel() ? _pressedButton : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button? GetPressedButtonFromState()
|
||||||
|
{
|
||||||
|
Key aliasedKey = GetAliasedPressedKey();
|
||||||
|
|
||||||
|
if (aliasedKey != Key.Unknown)
|
||||||
|
{
|
||||||
|
return new Button(aliasedKey);
|
||||||
|
}
|
||||||
|
|
||||||
for (Key key = Key.Unknown; key < Key.Count; key++)
|
for (Key key = Key.Unknown; key < Key.Count; key++)
|
||||||
{
|
{
|
||||||
if (_keyboardState.IsPressed(key))
|
if (_keyboardState.IsPressed(key))
|
||||||
{
|
{
|
||||||
keyPressed = new(key);
|
return new Button(key);
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return !ShouldCancel() ? keyPressed : null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Button? GetPressedButtonFromBufferedPress()
|
||||||
|
{
|
||||||
|
return _keyboard.TryConsumePressedKey(out Key key) ? new Button(key) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Key GetAliasedPressedKey()
|
||||||
|
{
|
||||||
|
// On some layouts (for example AltGr on Windows), Right Alt is reported as Ctrl+Alt.
|
||||||
|
// Prefer AltRight in that case so the binding reflects the physical key used.
|
||||||
|
if (_keyboardState.IsPressed(Key.ControlLeft) && _keyboardState.IsPressed(Key.AltRight))
|
||||||
|
{
|
||||||
|
return Key.AltRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On some Copilot keyboards, the key in the right-control position is reported as
|
||||||
|
// ShiftLeft+Win+F23. Prefer ControlRight so the binding reflects that physical key.
|
||||||
|
if (_keyboardState.IsPressed(Key.ShiftLeft) &&
|
||||||
|
_keyboardState.IsPressed(Key.F23) &&
|
||||||
|
(_keyboardState.IsPressed(Key.WinLeft) || _keyboardState.IsPressed(Key.WinRight)))
|
||||||
|
{
|
||||||
|
return Key.ControlRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Key.Unknown;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Ryujinx.Common;
|
|||||||
using Ryujinx.Common.Configuration.Hid;
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||||
|
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.HLE.HOS.Services.Hid;
|
using Ryujinx.HLE.HOS.Services.Hid;
|
||||||
using System;
|
using System;
|
||||||
@@ -233,7 +234,9 @@ namespace Ryujinx.Input.HLE
|
|||||||
_gamepad?.Dispose();
|
_gamepad?.Dispose();
|
||||||
|
|
||||||
Id = config.Id;
|
Id = config.Id;
|
||||||
_gamepad = GamepadDriver.GetGamepad(Id);
|
_gamepad = config is StandardKeyboardInputConfig && GamepadDriver is IKeyboardModeDriver keyboardModeDriver
|
||||||
|
? keyboardModeDriver.GetKeyboard(Id, KeyboardInputMode.Physical)
|
||||||
|
: GamepadDriver.GetGamepad(Id);
|
||||||
|
|
||||||
UpdateUserConfiguration(config);
|
UpdateUserConfiguration(config);
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
|
|
||||||
private readonly Lock _lock = new();
|
private readonly Lock _lock = new();
|
||||||
|
|
||||||
private bool _blockInputUpdates;
|
private int _inputUpdateBlockCount;
|
||||||
|
|
||||||
private const int MaxControllers = 9;
|
private const int MaxControllers = 9;
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
private bool _isDisposed;
|
private bool _isDisposed;
|
||||||
|
|
||||||
private List<InputConfig> _inputConfig;
|
private List<InputConfig> _inputConfig;
|
||||||
|
private List<InputConfig> _requestedInputConfig;
|
||||||
private bool _enableKeyboard;
|
private bool _enableKeyboard;
|
||||||
private bool _enableMouse;
|
private bool _enableMouse;
|
||||||
private Switch _device;
|
private Switch _device;
|
||||||
@@ -52,6 +53,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
_gamepadDriver = gamepadDriver;
|
_gamepadDriver = gamepadDriver;
|
||||||
_mouseDriver = mouseDriver;
|
_mouseDriver = mouseDriver;
|
||||||
_inputConfig = [];
|
_inputConfig = [];
|
||||||
|
_requestedInputConfig = [];
|
||||||
|
|
||||||
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
||||||
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
||||||
@@ -89,29 +91,23 @@ namespace Ryujinx.Input.HLE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
|
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleOnGamepadConnected(string id)
|
private void HandleOnGamepadConnected(string _)
|
||||||
{
|
{
|
||||||
// Force input reload
|
// Force input reload
|
||||||
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
|
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config)
|
private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config)
|
||||||
{
|
{
|
||||||
IGamepadDriver targetDriver = _gamepadDriver;
|
IGamepadDriver targetDriver =
|
||||||
|
config is StandardKeyboardInputConfig
|
||||||
if (config is StandardControllerInputConfig)
|
? _keyboardDriver
|
||||||
{
|
: _gamepadDriver;
|
||||||
targetDriver = _gamepadDriver;
|
|
||||||
}
|
|
||||||
else if (config is StandardKeyboardInputConfig)
|
|
||||||
{
|
|
||||||
targetDriver = _keyboardDriver;
|
|
||||||
}
|
|
||||||
|
|
||||||
Debug.Assert(targetDriver != null, "Unknown input configuration!");
|
Debug.Assert(targetDriver != null, "Unknown input configuration!");
|
||||||
|
|
||||||
@@ -127,11 +123,13 @@ namespace Ryujinx.Input.HLE
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
_requestedInputConfig = inputConfig?.ToList() ?? [];
|
||||||
|
|
||||||
NpadController[] oldControllers = _controllers.ToArray();
|
NpadController[] oldControllers = _controllers.ToArray();
|
||||||
|
|
||||||
List<InputConfig> validInputs = [];
|
List<InputConfig> validInputs = [];
|
||||||
|
|
||||||
foreach (InputConfig inputConfigEntry in inputConfig)
|
foreach (InputConfig inputConfigEntry in _requestedInputConfig)
|
||||||
{
|
{
|
||||||
NpadController controller;
|
NpadController controller;
|
||||||
int index = (int)inputConfigEntry.PlayerIndex;
|
int index = (int)inputConfigEntry.PlayerIndex;
|
||||||
@@ -147,7 +145,16 @@ namespace Ryujinx.Input.HLE
|
|||||||
controller = new(_cemuHookClient);
|
controller = new(_cemuHookClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);
|
InputConfig activeConfig = inputConfigEntry;
|
||||||
|
bool isValid = DriverConfigurationUpdate(ref controller, activeConfig);
|
||||||
|
|
||||||
|
if (!isValid &&
|
||||||
|
inputConfigEntry is StandardControllerInputConfig &&
|
||||||
|
TryGetKeyboardFallback(inputConfigEntry, out StandardKeyboardInputConfig fallbackConfig))
|
||||||
|
{
|
||||||
|
activeConfig = fallbackConfig;
|
||||||
|
isValid = DriverConfigurationUpdate(ref controller, activeConfig);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isValid)
|
if (!isValid)
|
||||||
{
|
{
|
||||||
@@ -157,7 +164,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
_controllers[index] = controller;
|
_controllers[index] = controller;
|
||||||
validInputs.Add(inputConfigEntry);
|
validInputs.Add(activeConfig);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,7 +176,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
oldControllers[i] = null;
|
oldControllers[i] = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_inputConfig = inputConfig;
|
_inputConfig = validInputs;
|
||||||
_enableKeyboard = enableKeyboard;
|
_enableKeyboard = enableKeyboard;
|
||||||
_enableMouse = enableMouse;
|
_enableMouse = enableMouse;
|
||||||
|
|
||||||
@@ -177,16 +184,58 @@ namespace Ryujinx.Input.HLE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
|
||||||
|
keyboardId,
|
||||||
|
keyboard.Name,
|
||||||
|
inputConfig.ControllerType,
|
||||||
|
inputConfig.PlayerIndex);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearInputDriverStates()
|
||||||
|
{
|
||||||
|
foreach (InputConfig inputConfig in _inputConfig)
|
||||||
|
{
|
||||||
|
_controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void UnblockInputUpdates()
|
public void UnblockInputUpdates()
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
foreach (InputConfig inputConfig in _inputConfig)
|
if (_inputUpdateBlockCount == 0)
|
||||||
{
|
{
|
||||||
_controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_blockInputUpdates = false;
|
_inputUpdateBlockCount--;
|
||||||
|
|
||||||
|
if (_inputUpdateBlockCount == 0)
|
||||||
|
{
|
||||||
|
ClearInputDriverStates();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,7 +244,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
get
|
get
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
return _blockInputUpdates;
|
return _inputUpdateBlockCount > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +252,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_blockInputUpdates = true;
|
_inputUpdateBlockCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +284,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
bool isJoyconPair = false;
|
bool isJoyconPair = false;
|
||||||
|
|
||||||
// Do we allow input updates and is a controller connected?
|
// Do we allow input updates and is a controller connected?
|
||||||
if (!_blockInputUpdates && controller != null)
|
if (_inputUpdateBlockCount == 0 && controller != null)
|
||||||
{
|
{
|
||||||
DriverConfigurationUpdate(ref controller, inputConfig);
|
DriverConfigurationUpdate(ref controller, inputConfig);
|
||||||
|
|
||||||
@@ -273,7 +322,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_blockInputUpdates && _enableKeyboard)
|
if (_inputUpdateBlockCount == 0 && _enableKeyboard)
|
||||||
{
|
{
|
||||||
hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver);
|
hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver);
|
||||||
}
|
}
|
||||||
@@ -334,7 +383,7 @@ namespace Ryujinx.Input.HLE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal InputConfig GetPlayerInputConfigByIndex(int index)
|
public InputConfig GetPlayerInputConfigByIndex(int index)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Input
|
namespace Ryujinx.Input
|
||||||
{
|
{
|
||||||
@@ -33,15 +34,26 @@ namespace Ryujinx.Input
|
|||||||
{
|
{
|
||||||
if (_keyState is null)
|
if (_keyState is null)
|
||||||
{
|
{
|
||||||
_keyState = new bool[(int)Key.Count];
|
_keyState = new bool[(int)ConfigPhysicalKey.Count];
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Key key = 0; key < Key.Count; key++)
|
for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
|
||||||
{
|
{
|
||||||
_keyState[(int)key] = keyboard.IsPressed(key);
|
_keyState[(int)key] = keyboard.IsPressed((Key)(int)key);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new KeyboardStateSnapshot(_keyState);
|
return new KeyboardStateSnapshot(_keyState);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Try to consume a recently pressed key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The pressed key, if available.</param>
|
||||||
|
/// <returns>True if a key press was consumed.</returns>
|
||||||
|
bool TryConsumePressedKey(out Key key)
|
||||||
|
{
|
||||||
|
key = Key.Unknown;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/Ryujinx.Input/IKeyboardModeDriver.cs
Normal file
7
src/Ryujinx.Input/IKeyboardModeDriver.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Ryujinx.Input
|
||||||
|
{
|
||||||
|
public interface IKeyboardModeDriver : IGamepadDriver
|
||||||
|
{
|
||||||
|
IKeyboard GetKeyboard(string id, KeyboardInputMode mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
148
src/Ryujinx.Input/InputConfigDefaults.cs
Normal file
148
src/Ryujinx.Input/InputConfigDefaults.cs
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||||
|
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||||
|
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||||
|
|
||||||
|
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
||||||
|
|
||||||
|
namespace Ryujinx.Input
|
||||||
|
{
|
||||||
|
public static class InputConfigDefaults
|
||||||
|
{
|
||||||
|
public static StandardKeyboardInputConfig CreateDefaultKeyboardConfiguration(
|
||||||
|
string id,
|
||||||
|
string name,
|
||||||
|
ControllerType controllerType,
|
||||||
|
PlayerIndex playerIndex)
|
||||||
|
{
|
||||||
|
return new StandardKeyboardInputConfig
|
||||||
|
{
|
||||||
|
Version = InputConfig.CurrentVersion,
|
||||||
|
Backend = InputBackendType.WindowKeyboard,
|
||||||
|
Id = id,
|
||||||
|
Name = name,
|
||||||
|
PlayerIndex = playerIndex,
|
||||||
|
ControllerType = controllerType,
|
||||||
|
LeftJoycon = new LeftJoyconCommonConfig<ConfigPhysicalKey>
|
||||||
|
{
|
||||||
|
DpadUp = ConfigPhysicalKey.Up,
|
||||||
|
DpadDown = ConfigPhysicalKey.Down,
|
||||||
|
DpadLeft = ConfigPhysicalKey.Left,
|
||||||
|
DpadRight = ConfigPhysicalKey.Right,
|
||||||
|
ButtonMinus = ConfigPhysicalKey.Minus,
|
||||||
|
ButtonL = ConfigPhysicalKey.E,
|
||||||
|
ButtonZl = ConfigPhysicalKey.Q,
|
||||||
|
ButtonSl = ConfigPhysicalKey.Unbound,
|
||||||
|
ButtonSr = ConfigPhysicalKey.Unbound,
|
||||||
|
},
|
||||||
|
LeftJoyconStick = new JoyconConfigKeyboardStick<ConfigPhysicalKey>
|
||||||
|
{
|
||||||
|
StickUp = ConfigPhysicalKey.W,
|
||||||
|
StickDown = ConfigPhysicalKey.S,
|
||||||
|
StickLeft = ConfigPhysicalKey.A,
|
||||||
|
StickRight = ConfigPhysicalKey.D,
|
||||||
|
StickButton = ConfigPhysicalKey.F,
|
||||||
|
},
|
||||||
|
RightJoycon = new RightJoyconCommonConfig<ConfigPhysicalKey>
|
||||||
|
{
|
||||||
|
ButtonA = ConfigPhysicalKey.Z,
|
||||||
|
ButtonB = ConfigPhysicalKey.X,
|
||||||
|
ButtonX = ConfigPhysicalKey.C,
|
||||||
|
ButtonY = ConfigPhysicalKey.V,
|
||||||
|
ButtonPlus = ConfigPhysicalKey.Plus,
|
||||||
|
ButtonR = ConfigPhysicalKey.U,
|
||||||
|
ButtonZr = ConfigPhysicalKey.O,
|
||||||
|
ButtonSl = ConfigPhysicalKey.Unbound,
|
||||||
|
ButtonSr = ConfigPhysicalKey.Unbound,
|
||||||
|
},
|
||||||
|
RightJoyconStick = new JoyconConfigKeyboardStick<ConfigPhysicalKey>
|
||||||
|
{
|
||||||
|
StickUp = ConfigPhysicalKey.I,
|
||||||
|
StickDown = ConfigPhysicalKey.K,
|
||||||
|
StickLeft = ConfigPhysicalKey.J,
|
||||||
|
StickRight = ConfigPhysicalKey.L,
|
||||||
|
StickButton = ConfigPhysicalKey.H,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StandardControllerInputConfig CreateDefaultControllerConfiguration(
|
||||||
|
string id,
|
||||||
|
string name,
|
||||||
|
ControllerType controllerType,
|
||||||
|
PlayerIndex playerIndex,
|
||||||
|
bool isNintendoStyle)
|
||||||
|
{
|
||||||
|
return new StandardControllerInputConfig
|
||||||
|
{
|
||||||
|
Version = InputConfig.CurrentVersion,
|
||||||
|
Backend = InputBackendType.GamepadSDL3,
|
||||||
|
Id = id,
|
||||||
|
Name = name,
|
||||||
|
PlayerIndex = playerIndex,
|
||||||
|
ControllerType = controllerType,
|
||||||
|
DeadzoneLeft = 0.1f,
|
||||||
|
DeadzoneRight = 0.1f,
|
||||||
|
RangeLeft = 1.0f,
|
||||||
|
RangeRight = 1.0f,
|
||||||
|
TriggerThreshold = 0.5f,
|
||||||
|
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
|
||||||
|
{
|
||||||
|
DpadUp = ConfigGamepadInputId.DpadUp,
|
||||||
|
DpadDown = ConfigGamepadInputId.DpadDown,
|
||||||
|
DpadLeft = ConfigGamepadInputId.DpadLeft,
|
||||||
|
DpadRight = ConfigGamepadInputId.DpadRight,
|
||||||
|
ButtonMinus = ConfigGamepadInputId.Minus,
|
||||||
|
ButtonL = ConfigGamepadInputId.LeftShoulder,
|
||||||
|
ButtonZl = ConfigGamepadInputId.LeftTrigger,
|
||||||
|
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger0,
|
||||||
|
ButtonSr = ConfigGamepadInputId.SingleRightTrigger0,
|
||||||
|
},
|
||||||
|
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||||
|
{
|
||||||
|
Joystick = ConfigStickInputId.Left,
|
||||||
|
StickButton = ConfigGamepadInputId.LeftStick,
|
||||||
|
InvertStickX = false,
|
||||||
|
InvertStickY = false,
|
||||||
|
Rotate90CW = false,
|
||||||
|
},
|
||||||
|
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
|
||||||
|
{
|
||||||
|
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
|
||||||
|
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
|
||||||
|
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
|
||||||
|
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
|
||||||
|
ButtonPlus = ConfigGamepadInputId.Plus,
|
||||||
|
ButtonR = ConfigGamepadInputId.RightShoulder,
|
||||||
|
ButtonZr = ConfigGamepadInputId.RightTrigger,
|
||||||
|
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger1,
|
||||||
|
ButtonSr = ConfigGamepadInputId.SingleRightTrigger1,
|
||||||
|
},
|
||||||
|
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||||
|
{
|
||||||
|
Joystick = ConfigStickInputId.Right,
|
||||||
|
StickButton = ConfigGamepadInputId.RightStick,
|
||||||
|
InvertStickX = false,
|
||||||
|
InvertStickY = false,
|
||||||
|
Rotate90CW = false,
|
||||||
|
},
|
||||||
|
Motion = new StandardMotionConfigController
|
||||||
|
{
|
||||||
|
MotionBackend = MotionInputBackendType.GamepadDriver,
|
||||||
|
EnableMotion = true,
|
||||||
|
Sensitivity = 100,
|
||||||
|
GyroDeadzone = 1,
|
||||||
|
},
|
||||||
|
Rumble = new RumbleConfigController
|
||||||
|
{
|
||||||
|
StrongRumble = 1f,
|
||||||
|
WeakRumble = 1f,
|
||||||
|
EnableRumble = false,
|
||||||
|
UseHDRumble = true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/Ryujinx.Input/KeyboardInputMappingHelper.cs
Normal file
78
src/Ryujinx.Input/KeyboardInputMappingHelper.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||||
|
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
|
namespace Ryujinx.Input
|
||||||
|
{
|
||||||
|
public static class KeyboardInputMappingHelper
|
||||||
|
{
|
||||||
|
public readonly record struct KeyboardButtonMapping(GamepadButtonInputId To, ConfigPhysicalKey From)
|
||||||
|
{
|
||||||
|
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static KeyboardButtonMapping[] BuildButtonMappings(StandardKeyboardInputConfig configuration) =>
|
||||||
|
[
|
||||||
|
// Left JoyCon
|
||||||
|
new(GamepadButtonInputId.LeftStick, configuration.LeftJoyconStick.StickButton),
|
||||||
|
new(GamepadButtonInputId.DpadUp, configuration.LeftJoycon.DpadUp),
|
||||||
|
new(GamepadButtonInputId.DpadDown, configuration.LeftJoycon.DpadDown),
|
||||||
|
new(GamepadButtonInputId.DpadLeft, configuration.LeftJoycon.DpadLeft),
|
||||||
|
new(GamepadButtonInputId.DpadRight, configuration.LeftJoycon.DpadRight),
|
||||||
|
new(GamepadButtonInputId.Minus, configuration.LeftJoycon.ButtonMinus),
|
||||||
|
new(GamepadButtonInputId.LeftShoulder, configuration.LeftJoycon.ButtonL),
|
||||||
|
new(GamepadButtonInputId.LeftTrigger, configuration.LeftJoycon.ButtonZl),
|
||||||
|
new(GamepadButtonInputId.SingleRightTrigger0, configuration.LeftJoycon.ButtonSr),
|
||||||
|
new(GamepadButtonInputId.SingleLeftTrigger0, configuration.LeftJoycon.ButtonSl),
|
||||||
|
|
||||||
|
// Right JoyCon
|
||||||
|
new(GamepadButtonInputId.RightStick, configuration.RightJoyconStick.StickButton),
|
||||||
|
new(GamepadButtonInputId.A, configuration.RightJoycon.ButtonA),
|
||||||
|
new(GamepadButtonInputId.B, configuration.RightJoycon.ButtonB),
|
||||||
|
new(GamepadButtonInputId.X, configuration.RightJoycon.ButtonX),
|
||||||
|
new(GamepadButtonInputId.Y, configuration.RightJoycon.ButtonY),
|
||||||
|
new(GamepadButtonInputId.Plus, configuration.RightJoycon.ButtonPlus),
|
||||||
|
new(GamepadButtonInputId.RightShoulder, configuration.RightJoycon.ButtonR),
|
||||||
|
new(GamepadButtonInputId.RightTrigger, configuration.RightJoycon.ButtonZr),
|
||||||
|
new(GamepadButtonInputId.SingleRightTrigger1, configuration.RightJoycon.ButtonSr),
|
||||||
|
new(GamepadButtonInputId.SingleLeftTrigger1, configuration.RightJoycon.ButtonSl),
|
||||||
|
];
|
||||||
|
|
||||||
|
public static (short X, short Y) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigPhysicalKey> stickConfig)
|
||||||
|
{
|
||||||
|
short stickX = 0;
|
||||||
|
short stickY = 0;
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(stickConfig.StickUp))
|
||||||
|
{
|
||||||
|
stickY += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(stickConfig.StickDown))
|
||||||
|
{
|
||||||
|
stickY -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(stickConfig.StickRight))
|
||||||
|
{
|
||||||
|
stickX += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(stickConfig.StickLeft))
|
||||||
|
{
|
||||||
|
stickX -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stickX == 0 && stickY == 0)
|
||||||
|
{
|
||||||
|
return (0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
|
||||||
|
|
||||||
|
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/Ryujinx.Input/KeyboardInputMode.cs
Normal file
8
src/Ryujinx.Input/KeyboardInputMode.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Ryujinx.Input
|
||||||
|
{
|
||||||
|
public enum KeyboardInputMode
|
||||||
|
{
|
||||||
|
Semantic,
|
||||||
|
Physical,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Input
|
namespace Ryujinx.Input
|
||||||
{
|
{
|
||||||
@@ -25,5 +26,8 @@ namespace Ryujinx.Input
|
|||||||
/// <returns>True if the given key is pressed</returns>
|
/// <returns>True if the given key is pressed</returns>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool IsPressed(Key key) => KeysState[(int)key];
|
public bool IsPressed(Key key) => KeysState[(int)key];
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool IsPressed(ConfigPhysicalKey key) => KeysState[(int)key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ using Ryujinx.Ava.Systems.Configuration;
|
|||||||
using Ryujinx.Common.Configuration;
|
using Ryujinx.Common.Configuration;
|
||||||
using Ryujinx.Common.Configuration.Hid;
|
using Ryujinx.Common.Configuration.Hid;
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
|
||||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
|
||||||
using Ryujinx.Common.Logging;
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Common.Utilities;
|
using Ryujinx.Common.Utilities;
|
||||||
using Ryujinx.Cpu;
|
using Ryujinx.Cpu;
|
||||||
@@ -17,14 +15,12 @@ using Ryujinx.Graphics.OpenGL;
|
|||||||
using Ryujinx.Graphics.Vulkan;
|
using Ryujinx.Graphics.Vulkan;
|
||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
|
using Ryujinx.Input.SDL3;
|
||||||
using Silk.NET.Vulkan;
|
using Silk.NET.Vulkan;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
|
||||||
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
|
||||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
|
||||||
|
|
||||||
namespace Ryujinx.Headless
|
namespace Ryujinx.Headless
|
||||||
{
|
{
|
||||||
@@ -91,6 +87,19 @@ namespace Ryujinx.Headless
|
|||||||
|
|
||||||
string gamepadName = gamepad.Name;
|
string gamepadName = gamepad.Name;
|
||||||
|
|
||||||
|
bool isNintendoStyle = false;
|
||||||
|
|
||||||
|
if (gamepad is SDL3Gamepad sdlGp)
|
||||||
|
{
|
||||||
|
// Nintendo vendor ID is 0x057E
|
||||||
|
isNintendoStyle = sdlGp.VendorId == 0x057E;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback to name-based detection
|
||||||
|
isNintendoStyle = gamepadName.Contains("Nintendo", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
gamepad.Dispose();
|
gamepad.Dispose();
|
||||||
|
|
||||||
InputConfig config;
|
InputConfig config;
|
||||||
@@ -99,131 +108,21 @@ namespace Ryujinx.Headless
|
|||||||
{
|
{
|
||||||
if (isKeyboard)
|
if (isKeyboard)
|
||||||
{
|
{
|
||||||
config = new StandardKeyboardInputConfig
|
config = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
|
||||||
{
|
null,
|
||||||
Version = InputConfig.CurrentVersion,
|
null,
|
||||||
Backend = InputBackendType.WindowKeyboard,
|
ControllerType.JoyconPair,
|
||||||
Id = null,
|
index);
|
||||||
ControllerType = ControllerType.JoyconPair,
|
|
||||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
|
||||||
{
|
|
||||||
DpadUp = Key.Up,
|
|
||||||
DpadDown = Key.Down,
|
|
||||||
DpadLeft = Key.Left,
|
|
||||||
DpadRight = Key.Right,
|
|
||||||
ButtonMinus = Key.Minus,
|
|
||||||
ButtonL = Key.E,
|
|
||||||
ButtonZl = Key.Q,
|
|
||||||
ButtonSl = Key.Unbound,
|
|
||||||
ButtonSr = Key.Unbound,
|
|
||||||
},
|
|
||||||
|
|
||||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
|
||||||
{
|
|
||||||
StickUp = Key.W,
|
|
||||||
StickDown = Key.S,
|
|
||||||
StickLeft = Key.A,
|
|
||||||
StickRight = Key.D,
|
|
||||||
StickButton = Key.F,
|
|
||||||
},
|
|
||||||
|
|
||||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
|
||||||
{
|
|
||||||
ButtonA = Key.Z,
|
|
||||||
ButtonB = Key.X,
|
|
||||||
ButtonX = Key.C,
|
|
||||||
ButtonY = Key.V,
|
|
||||||
ButtonPlus = Key.Plus,
|
|
||||||
ButtonR = Key.U,
|
|
||||||
ButtonZr = Key.O,
|
|
||||||
ButtonSl = Key.Unbound,
|
|
||||||
ButtonSr = Key.Unbound,
|
|
||||||
},
|
|
||||||
|
|
||||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
|
||||||
{
|
|
||||||
StickUp = Key.I,
|
|
||||||
StickDown = Key.K,
|
|
||||||
StickLeft = Key.J,
|
|
||||||
StickRight = Key.L,
|
|
||||||
StickButton = Key.H,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bool isNintendoStyle = gamepadName.Contains("Nintendo");
|
|
||||||
|
|
||||||
config = new StandardControllerInputConfig
|
config = InputConfigDefaults.CreateDefaultControllerConfiguration(
|
||||||
{
|
null,
|
||||||
Version = InputConfig.CurrentVersion,
|
null,
|
||||||
Backend = InputBackendType.GamepadSDL3,
|
ControllerType.JoyconPair,
|
||||||
Id = null,
|
index,
|
||||||
ControllerType = ControllerType.JoyconPair,
|
isNintendoStyle);
|
||||||
DeadzoneLeft = 0.1f,
|
|
||||||
DeadzoneRight = 0.1f,
|
|
||||||
RangeLeft = 1.0f,
|
|
||||||
RangeRight = 1.0f,
|
|
||||||
TriggerThreshold = 0.5f,
|
|
||||||
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
|
|
||||||
{
|
|
||||||
DpadUp = ConfigGamepadInputId.DpadUp,
|
|
||||||
DpadDown = ConfigGamepadInputId.DpadDown,
|
|
||||||
DpadLeft = ConfigGamepadInputId.DpadLeft,
|
|
||||||
DpadRight = ConfigGamepadInputId.DpadRight,
|
|
||||||
ButtonMinus = ConfigGamepadInputId.Minus,
|
|
||||||
ButtonL = ConfigGamepadInputId.LeftShoulder,
|
|
||||||
ButtonZl = ConfigGamepadInputId.LeftTrigger,
|
|
||||||
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger0,
|
|
||||||
ButtonSr = ConfigGamepadInputId.SingleRightTrigger0,
|
|
||||||
},
|
|
||||||
|
|
||||||
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
|
||||||
{
|
|
||||||
Joystick = ConfigStickInputId.Left,
|
|
||||||
StickButton = ConfigGamepadInputId.LeftStick,
|
|
||||||
InvertStickX = false,
|
|
||||||
InvertStickY = false,
|
|
||||||
Rotate90CW = false,
|
|
||||||
},
|
|
||||||
|
|
||||||
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
|
|
||||||
{
|
|
||||||
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
|
|
||||||
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
|
|
||||||
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
|
|
||||||
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
|
|
||||||
ButtonPlus = ConfigGamepadInputId.Plus,
|
|
||||||
ButtonR = ConfigGamepadInputId.RightShoulder,
|
|
||||||
ButtonZr = ConfigGamepadInputId.RightTrigger,
|
|
||||||
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger1,
|
|
||||||
ButtonSr = ConfigGamepadInputId.SingleRightTrigger1,
|
|
||||||
},
|
|
||||||
|
|
||||||
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
|
||||||
{
|
|
||||||
Joystick = ConfigStickInputId.Right,
|
|
||||||
StickButton = ConfigGamepadInputId.RightStick,
|
|
||||||
InvertStickX = false,
|
|
||||||
InvertStickY = false,
|
|
||||||
Rotate90CW = false,
|
|
||||||
},
|
|
||||||
|
|
||||||
Motion = new StandardMotionConfigController
|
|
||||||
{
|
|
||||||
MotionBackend = MotionInputBackendType.GamepadDriver,
|
|
||||||
EnableMotion = true,
|
|
||||||
Sensitivity = 100,
|
|
||||||
GyroDeadzone = 1,
|
|
||||||
},
|
|
||||||
Rumble = new RumbleConfigController
|
|
||||||
{
|
|
||||||
StrongRumble = 1f,
|
|
||||||
WeakRumble = 1f,
|
|
||||||
EnableRumble = false,
|
|
||||||
UseHDRumble = true
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
|
|
||||||
using Key = Ryujinx.Input.Key;
|
using Key = Ryujinx.Input.Key;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Input
|
namespace Ryujinx.Ava.Input
|
||||||
{
|
{
|
||||||
internal class AvaloniaKeyboard : IKeyboard
|
internal class AvaloniaKeyboard : IKeyboard
|
||||||
{
|
{
|
||||||
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
|
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
|
||||||
private readonly AvaloniaKeyboardDriver _driver;
|
private readonly AvaloniaKeyboardDriver _driver;
|
||||||
|
private readonly KeyboardInputMode _mode;
|
||||||
private StandardKeyboardInputConfig _configuration;
|
private StandardKeyboardInputConfig _configuration;
|
||||||
|
|
||||||
private readonly Lock _userMappingLock = new();
|
private readonly Lock _userMappingLock = new();
|
||||||
@@ -25,18 +25,12 @@ namespace Ryujinx.Ava.Input
|
|||||||
|
|
||||||
public bool IsConnected => true;
|
public bool IsConnected => true;
|
||||||
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
|
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
|
||||||
|
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name, KeyboardInputMode mode)
|
||||||
private class ButtonMappingEntry(GamepadButtonInputId to, Key from)
|
|
||||||
{
|
|
||||||
public readonly GamepadButtonInputId To = to;
|
|
||||||
public readonly Key From = from;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name)
|
|
||||||
{
|
{
|
||||||
_buttonsUserMapping = [];
|
_buttonsUserMapping = [];
|
||||||
|
|
||||||
_driver = driver;
|
_driver = driver;
|
||||||
|
_mode = mode;
|
||||||
Id = id;
|
Id = id;
|
||||||
Name = name;
|
Name = name;
|
||||||
}
|
}
|
||||||
@@ -58,22 +52,18 @@ namespace Ryujinx.Ava.Input
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
|
foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
|
||||||
{
|
{
|
||||||
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
|
if (!entry.IsValid || result.IsPressed(entry.To))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: Do not touch state of the button already pressed.
|
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
|
||||||
if (!result.IsPressed(entry.To))
|
|
||||||
{
|
|
||||||
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
||||||
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
||||||
|
|
||||||
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
||||||
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
||||||
@@ -101,7 +91,7 @@ namespace Ryujinx.Ava.Input
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return _driver.IsPressed(key);
|
return _driver.IsPressed(key, _mode);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -109,6 +99,19 @@ namespace Ryujinx.Ava.Input
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryConsumePressedKey(out Key key)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _driver.TryConsumePressedKey(_mode, out key);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
key = Key.Unknown;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void SetConfiguration(InputConfig configuration)
|
public void SetConfiguration(InputConfig configuration)
|
||||||
{
|
{
|
||||||
lock (_userMappingLock)
|
lock (_userMappingLock)
|
||||||
@@ -117,53 +120,20 @@ namespace Ryujinx.Ava.Input
|
|||||||
|
|
||||||
_buttonsUserMapping.Clear();
|
_buttonsUserMapping.Clear();
|
||||||
|
|
||||||
#pragma warning disable IDE0055 // Disable formatting
|
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
|
||||||
// Left JoyCon
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
|
|
||||||
|
|
||||||
// Right JoyCon
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
|
|
||||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
|
|
||||||
#pragma warning restore IDE0055
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetLed(uint packedRgb)
|
public void SetLed(uint packedRgb)
|
||||||
{
|
{
|
||||||
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
|
Logger.Debug?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTriggerThreshold(float triggerThreshold)
|
public bool HDRumble(VibrationValue left, VibrationValue right) => false;
|
||||||
{
|
|
||||||
// No operations
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool HDRumble(VibrationValue left, VibrationValue right)
|
public void SetTriggerThreshold(float triggerThreshold) { }
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) => false;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero;
|
public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero;
|
||||||
|
|
||||||
@@ -174,41 +144,9 @@ namespace Ryujinx.Ava.Input
|
|||||||
return value * ConvertRate;
|
return value * ConvertRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
|
|
||||||
{
|
|
||||||
short stickX = 0;
|
|
||||||
short stickY = 0;
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickUp))
|
|
||||||
{
|
|
||||||
stickY += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickDown))
|
|
||||||
{
|
|
||||||
stickY -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickRight))
|
|
||||||
{
|
|
||||||
stickX += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
|
|
||||||
{
|
|
||||||
stickX -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vector2 stick = new(stickX, stickY);
|
|
||||||
|
|
||||||
stick = Vector2.Normalize(stick);
|
|
||||||
|
|
||||||
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_driver?.Clear();
|
_driver?.Clear(_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() { }
|
public void Dispose() { }
|
||||||
|
|||||||
@@ -1,19 +1,55 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Systems.Configuration;
|
||||||
|
using Ryujinx.Ava.UI.Helpers;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Runtime.Versioning;
|
||||||
using AvaKey = Avalonia.Input.Key;
|
using AvaKey = Avalonia.Input.Key;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
using Key = Ryujinx.Input.Key;
|
using Key = Ryujinx.Input.Key;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Input
|
namespace Ryujinx.Ava.Input
|
||||||
{
|
{
|
||||||
internal class AvaloniaKeyboardDriver : IGamepadDriver
|
internal class AvaloniaKeyboardDriver : IKeyboardModeDriver
|
||||||
{
|
{
|
||||||
|
private enum PhysicalKeySource
|
||||||
|
{
|
||||||
|
Direct,
|
||||||
|
ObservedFallback,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
private enum CGEventFlags : ulong
|
||||||
|
{
|
||||||
|
AlphaShift = 1UL << 16 // CapsLock
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum CGEventSourceStateID : uint
|
||||||
|
{
|
||||||
|
HIDSystemState = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
|
||||||
|
private static extern CGEventFlags CGEventSourceFlagsState(CGEventSourceStateID stateID);
|
||||||
private static readonly string[] _keyboardIdentifers = ["0"];
|
private static readonly string[] _keyboardIdentifers = ["0"];
|
||||||
private readonly Control _control;
|
private readonly Control _control;
|
||||||
private readonly HashSet<AvaKey> _pressedKeys;
|
private readonly Window _window;
|
||||||
|
private readonly HashSet<Key> _semanticPressedKeys;
|
||||||
|
private readonly HashSet<ConfigPhysicalKey> _physicalPressedKeys;
|
||||||
|
private readonly HashSet<Key> _keysToRestoreAfterActivation;
|
||||||
|
private readonly Dictionary<Key, ConfigPhysicalKey> _observedPhysicalKeysBySemanticKey;
|
||||||
|
private readonly Queue<Key> _semanticPressedKeyQueue;
|
||||||
|
private readonly Queue<Key> _physicalPressedKeyQueue;
|
||||||
|
private readonly Lock _pressedKeyQueueLock;
|
||||||
|
private readonly KeyboardInputMode _defaultMode;
|
||||||
|
|
||||||
public event EventHandler<KeyEventArgs> KeyPressed;
|
public event EventHandler<KeyEventArgs> KeyPressed;
|
||||||
public event EventHandler<KeyEventArgs> KeyRelease;
|
public event EventHandler<KeyEventArgs> KeyRelease;
|
||||||
@@ -22,14 +58,41 @@ namespace Ryujinx.Ava.Input
|
|||||||
public string DriverName => "AvaloniaKeyboardDriver";
|
public string DriverName => "AvaloniaKeyboardDriver";
|
||||||
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
|
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
|
||||||
|
|
||||||
public AvaloniaKeyboardDriver(Control control)
|
public AvaloniaKeyboardDriver(Control control, KeyboardInputMode defaultMode = KeyboardInputMode.Semantic)
|
||||||
{
|
{
|
||||||
_control = control;
|
_control = control;
|
||||||
_pressedKeys = [];
|
_window = control as Window ?? TopLevel.GetTopLevel(control) as Window;
|
||||||
|
_semanticPressedKeys = [];
|
||||||
|
_physicalPressedKeys = [];
|
||||||
|
_keysToRestoreAfterActivation = [];
|
||||||
|
_observedPhysicalKeysBySemanticKey = [];
|
||||||
|
_semanticPressedKeyQueue = [];
|
||||||
|
_physicalPressedKeyQueue = [];
|
||||||
|
_pressedKeyQueueLock = new();
|
||||||
|
_defaultMode = defaultMode;
|
||||||
|
|
||||||
_control.KeyDown += OnKeyPress;
|
_control.AddHandler(InputElement.KeyDownEvent, OnKeyPress, RoutingStrategies.Tunnel, true);
|
||||||
_control.KeyUp += OnKeyRelease;
|
_control.AddHandler(InputElement.KeyUpEvent, OnKeyRelease, RoutingStrategies.Tunnel, true);
|
||||||
_control.TextInput += Control_TextInput;
|
_control.TextInput += Control_TextInput;
|
||||||
|
_window?.Activated += Window_Activated;
|
||||||
|
_window?.Deactivated += Window_Deactivated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Window_Activated(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
RestorePressedKeysAfterActivation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Window_Deactivated(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
_keysToRestoreAfterActivation.Clear();
|
||||||
|
_keysToRestoreAfterActivation.UnionWith(_semanticPressedKeys);
|
||||||
|
_observedPhysicalKeysBySemanticKey.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Control_TextInput(object sender, TextInputEventArgs e)
|
private void Control_TextInput(object sender, TextInputEventArgs e)
|
||||||
@@ -50,13 +113,18 @@ namespace Ryujinx.Ava.Input
|
|||||||
}
|
}
|
||||||
|
|
||||||
public IGamepad GetGamepad(string id)
|
public IGamepad GetGamepad(string id)
|
||||||
|
{
|
||||||
|
return GetKeyboard(id, _defaultMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IKeyboard GetKeyboard(string id, KeyboardInputMode mode)
|
||||||
{
|
{
|
||||||
if (!_keyboardIdentifers[0].Equals(id))
|
if (!_keyboardIdentifers[0].Equals(id))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.AllKeyboards]);
|
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.KeyboardLayout_KeyboardInputMode], mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
|
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
|
||||||
@@ -65,40 +133,448 @@ namespace Ryujinx.Ava.Input
|
|||||||
{
|
{
|
||||||
if (disposing)
|
if (disposing)
|
||||||
{
|
{
|
||||||
_control.KeyUp -= OnKeyPress;
|
_control.RemoveHandler(InputElement.KeyDownEvent, OnKeyPress);
|
||||||
_control.KeyDown -= OnKeyRelease;
|
_control.RemoveHandler(InputElement.KeyUpEvent, OnKeyRelease);
|
||||||
|
_control.TextInput -= Control_TextInput;
|
||||||
|
if (_window != null)
|
||||||
|
{
|
||||||
|
_window.Activated -= Window_Activated;
|
||||||
|
_window.Deactivated -= Window_Deactivated;
|
||||||
|
}
|
||||||
|
_observedPhysicalKeysBySemanticKey.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void OnKeyPress(object sender, KeyEventArgs args)
|
protected void OnKeyPress(object sender, KeyEventArgs args)
|
||||||
{
|
{
|
||||||
_pressedKeys.Add(args.Key);
|
UpdateKeyStates(args, true);
|
||||||
|
|
||||||
KeyPressed?.Invoke(this, args);
|
KeyPressed?.Invoke(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void OnKeyRelease(object sender, KeyEventArgs args)
|
protected void OnKeyRelease(object sender, KeyEventArgs args)
|
||||||
{
|
{
|
||||||
_pressedKeys.Remove(args.Key);
|
UpdateKeyStates(args, false);
|
||||||
|
|
||||||
KeyRelease?.Invoke(this, args);
|
KeyRelease?.Invoke(this, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal bool IsPressed(Key key)
|
internal bool IsPressed(Key key, KeyboardInputMode mode)
|
||||||
{
|
{
|
||||||
if (key is Key.Unbound or Key.Unknown)
|
if (key is Key.Unbound or Key.Unknown)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AvaloniaKeyboardMappingHelper.TryGetAvaKey(key, out AvaKey nativeKey);
|
if (key == Key.CapsLock)
|
||||||
|
{
|
||||||
|
return IsCapsLockOnMacOS();
|
||||||
|
}
|
||||||
|
|
||||||
return _pressedKeys.Contains(nativeKey);
|
return mode == KeyboardInputMode.Physical
|
||||||
|
? _physicalPressedKeys.Contains((ConfigPhysicalKey)(int)key)
|
||||||
|
: _semanticPressedKeys.Contains(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsCapsLockOnMacOS()
|
||||||
|
{
|
||||||
|
bool currentState = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (OperatingSystem.IsMacOS())
|
||||||
|
{
|
||||||
|
CGEventFlags flags = CGEventSourceFlagsState(CGEventSourceStateID.HIDSystemState);
|
||||||
|
currentState = (flags & CGEventFlags.AlphaShift) != 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback: use Avalonia's tracked key state (semantic CapsLock)
|
||||||
|
if (AvaloniaKeyboardMappingHelper.TryGetAvaKey(Key.CapsLock, out AvaKey nativeKey))
|
||||||
|
{
|
||||||
|
currentState = _semanticPressedKeys.Contains(Key.CapsLock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Debug?.Print(LogClass.UI, $"Failed to query CapsLock state: {ex}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentState;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void Clear(KeyboardInputMode mode)
|
||||||
|
{
|
||||||
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
if (mode == KeyboardInputMode.Physical)
|
||||||
|
{
|
||||||
|
_physicalPressedKeys.Clear();
|
||||||
|
_physicalPressedKeyQueue.Clear();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_semanticPressedKeys.Clear();
|
||||||
|
_semanticPressedKeyQueue.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_pressedKeys.Clear();
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
_semanticPressedKeys.Clear();
|
||||||
|
_physicalPressedKeys.Clear();
|
||||||
|
_semanticPressedKeyQueue.Clear();
|
||||||
|
_physicalPressedKeyQueue.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestorePressedKeysAfterActivation()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
_keysToRestoreAfterActivation.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
if (_keysToRestoreAfterActivation.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Key key in _keysToRestoreAfterActivation)
|
||||||
|
{
|
||||||
|
if (!TryGetWindowsVirtualKey(key, out int virtualKey) ||
|
||||||
|
!IsWindowsKeyPressed(virtualKey))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_semanticPressedKeys.Add(key);
|
||||||
|
|
||||||
|
ConfigPhysicalKey physicalKey = GetPhysicalKeyForSemanticKey(key);
|
||||||
|
|
||||||
|
if (physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
|
||||||
|
{
|
||||||
|
_physicalPressedKeys.Add(physicalKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_keysToRestoreAfterActivation.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigPhysicalKey GetPhysicalKeyForSemanticKey(Key key)
|
||||||
|
{
|
||||||
|
if (_observedPhysicalKeysBySemanticKey.TryGetValue(key, out ConfigPhysicalKey physicalKey))
|
||||||
|
{
|
||||||
|
return physicalKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key is >= Key.Unknown and < Key.Count
|
||||||
|
? (ConfigPhysicalKey)(int)key
|
||||||
|
: ConfigPhysicalKey.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SupportedOSPlatform("windows")]
|
||||||
|
private static bool IsWindowsKeyPressed(int virtualKey)
|
||||||
|
{
|
||||||
|
return (Win32NativeInterop.GetAsyncKeyState(virtualKey) & 0x8000) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetWindowsVirtualKey(Key key, out int virtualKey)
|
||||||
|
{
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case >= Key.A and <= Key.Z:
|
||||||
|
virtualKey = 'A' + (int)(key - Key.A);
|
||||||
|
return true;
|
||||||
|
case >= Key.Number0 and <= Key.Number9:
|
||||||
|
virtualKey = '0' + (int)(key - Key.Number0);
|
||||||
|
return true;
|
||||||
|
case >= Key.F1 and <= Key.F24:
|
||||||
|
virtualKey = 0x70 + (int)(key - Key.F1);
|
||||||
|
return true;
|
||||||
|
case Key.ShiftLeft:
|
||||||
|
virtualKey = 0xA0;
|
||||||
|
return true;
|
||||||
|
case Key.ShiftRight:
|
||||||
|
virtualKey = 0xA1;
|
||||||
|
return true;
|
||||||
|
case Key.ControlLeft:
|
||||||
|
virtualKey = 0xA2;
|
||||||
|
return true;
|
||||||
|
case Key.ControlRight:
|
||||||
|
virtualKey = 0xA3;
|
||||||
|
return true;
|
||||||
|
case Key.AltLeft:
|
||||||
|
virtualKey = 0xA4;
|
||||||
|
return true;
|
||||||
|
case Key.AltRight:
|
||||||
|
virtualKey = 0xA5;
|
||||||
|
return true;
|
||||||
|
case Key.WinLeft:
|
||||||
|
virtualKey = 0x5B;
|
||||||
|
return true;
|
||||||
|
case Key.WinRight:
|
||||||
|
virtualKey = 0x5C;
|
||||||
|
return true;
|
||||||
|
case Key.Menu:
|
||||||
|
virtualKey = 0x5D;
|
||||||
|
return true;
|
||||||
|
case Key.Up:
|
||||||
|
virtualKey = 0x26;
|
||||||
|
return true;
|
||||||
|
case Key.Down:
|
||||||
|
virtualKey = 0x28;
|
||||||
|
return true;
|
||||||
|
case Key.Left:
|
||||||
|
virtualKey = 0x25;
|
||||||
|
return true;
|
||||||
|
case Key.Right:
|
||||||
|
virtualKey = 0x27;
|
||||||
|
return true;
|
||||||
|
case Key.Enter:
|
||||||
|
virtualKey = 0x0D;
|
||||||
|
return true;
|
||||||
|
case Key.Escape:
|
||||||
|
virtualKey = 0x1B;
|
||||||
|
return true;
|
||||||
|
case Key.Space:
|
||||||
|
virtualKey = 0x20;
|
||||||
|
return true;
|
||||||
|
case Key.Tab:
|
||||||
|
virtualKey = 0x09;
|
||||||
|
return true;
|
||||||
|
case Key.BackSpace:
|
||||||
|
virtualKey = 0x08;
|
||||||
|
return true;
|
||||||
|
case Key.Insert:
|
||||||
|
virtualKey = 0x2D;
|
||||||
|
return true;
|
||||||
|
case Key.Delete:
|
||||||
|
virtualKey = 0x2E;
|
||||||
|
return true;
|
||||||
|
case Key.PageUp:
|
||||||
|
virtualKey = 0x21;
|
||||||
|
return true;
|
||||||
|
case Key.PageDown:
|
||||||
|
virtualKey = 0x22;
|
||||||
|
return true;
|
||||||
|
case Key.Home:
|
||||||
|
virtualKey = 0x24;
|
||||||
|
return true;
|
||||||
|
case Key.End:
|
||||||
|
virtualKey = 0x23;
|
||||||
|
return true;
|
||||||
|
case Key.CapsLock:
|
||||||
|
virtualKey = 0x14;
|
||||||
|
return true;
|
||||||
|
case Key.ScrollLock:
|
||||||
|
virtualKey = 0x91;
|
||||||
|
return true;
|
||||||
|
case Key.PrintScreen:
|
||||||
|
virtualKey = 0x2C;
|
||||||
|
return true;
|
||||||
|
case Key.Pause:
|
||||||
|
virtualKey = 0x13;
|
||||||
|
return true;
|
||||||
|
case Key.NumLock:
|
||||||
|
virtualKey = 0x90;
|
||||||
|
return true;
|
||||||
|
case Key.Clear:
|
||||||
|
virtualKey = 0x0C;
|
||||||
|
return true;
|
||||||
|
case >= Key.Keypad0 and <= Key.Keypad9:
|
||||||
|
virtualKey = 0x60 + (int)(key - Key.Keypad0);
|
||||||
|
return true;
|
||||||
|
case Key.KeypadDivide:
|
||||||
|
virtualKey = 0x6F;
|
||||||
|
return true;
|
||||||
|
case Key.KeypadMultiply:
|
||||||
|
virtualKey = 0x6A;
|
||||||
|
return true;
|
||||||
|
case Key.KeypadSubtract:
|
||||||
|
virtualKey = 0x6D;
|
||||||
|
return true;
|
||||||
|
case Key.KeypadAdd:
|
||||||
|
virtualKey = 0x6B;
|
||||||
|
return true;
|
||||||
|
case Key.KeypadDecimal:
|
||||||
|
virtualKey = 0x6E;
|
||||||
|
return true;
|
||||||
|
case Key.KeypadEnter:
|
||||||
|
virtualKey = 0x0D;
|
||||||
|
return true;
|
||||||
|
case Key.Tilde:
|
||||||
|
virtualKey = 0xC0;
|
||||||
|
return true;
|
||||||
|
case Key.Grave:
|
||||||
|
virtualKey = 0xE2;
|
||||||
|
return true;
|
||||||
|
case Key.Minus:
|
||||||
|
virtualKey = 0xBD;
|
||||||
|
return true;
|
||||||
|
case Key.Plus:
|
||||||
|
virtualKey = 0xBB;
|
||||||
|
return true;
|
||||||
|
case Key.BracketLeft:
|
||||||
|
virtualKey = 0xDB;
|
||||||
|
return true;
|
||||||
|
case Key.BracketRight:
|
||||||
|
virtualKey = 0xDD;
|
||||||
|
return true;
|
||||||
|
case Key.Semicolon:
|
||||||
|
virtualKey = 0xBA;
|
||||||
|
return true;
|
||||||
|
case Key.Quote:
|
||||||
|
virtualKey = 0xDE;
|
||||||
|
return true;
|
||||||
|
case Key.Comma:
|
||||||
|
virtualKey = 0xBC;
|
||||||
|
return true;
|
||||||
|
case Key.Period:
|
||||||
|
virtualKey = 0xBE;
|
||||||
|
return true;
|
||||||
|
case Key.Slash:
|
||||||
|
virtualKey = 0xBF;
|
||||||
|
return true;
|
||||||
|
case Key.BackSlash:
|
||||||
|
virtualKey = 0xDC;
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
virtualKey = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool TryConsumePressedKey(KeyboardInputMode mode, out Key key)
|
||||||
|
{
|
||||||
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
Queue<Key> queue = mode == KeyboardInputMode.Physical ? _physicalPressedKeyQueue : _semanticPressedKeyQueue;
|
||||||
|
|
||||||
|
if (queue.TryDequeue(out key))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key = Key.Unknown;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateKeyState(HashSet<Key> pressedKeys, Key key, bool isPressed)
|
||||||
|
{
|
||||||
|
if (key is Key.Unknown or Key.Unbound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPressed)
|
||||||
|
{
|
||||||
|
pressedKeys.Add(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pressedKeys.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateKeyState(HashSet<ConfigPhysicalKey> pressedKeys, ConfigPhysicalKey key, bool isPressed)
|
||||||
|
{
|
||||||
|
if (key is ConfigPhysicalKey.Unknown or ConfigPhysicalKey.Unbound)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPressed)
|
||||||
|
{
|
||||||
|
pressedKeys.Add(key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pressedKeys.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateKeyStates(KeyEventArgs args, bool isPressed)
|
||||||
|
{
|
||||||
|
Key semanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.Key);
|
||||||
|
Key resolvedSemanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey, args.Key);
|
||||||
|
ConfigPhysicalKey physicalKey = GetPhysicalInputKey(args, semanticKey, out PhysicalKeySource physicalKeySource);
|
||||||
|
bool semanticWasPressed = _semanticPressedKeys.Contains(resolvedSemanticKey);
|
||||||
|
bool physicalWasPressed = _physicalPressedKeys.Contains(physicalKey);
|
||||||
|
bool semanticStateChanged = resolvedSemanticKey is not Key.Unknown and not Key.Unbound && semanticWasPressed != isPressed;
|
||||||
|
bool physicalStateChanged = physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound && physicalWasPressed != isPressed;
|
||||||
|
bool bufferedSemanticPress = false;
|
||||||
|
bool bufferedPhysicalPress = false;
|
||||||
|
|
||||||
|
UpdateKeyState(_semanticPressedKeys, resolvedSemanticKey, isPressed);
|
||||||
|
UpdateKeyState(_physicalPressedKeys, physicalKey, isPressed);
|
||||||
|
|
||||||
|
if (isPressed)
|
||||||
|
{
|
||||||
|
lock (_pressedKeyQueueLock)
|
||||||
|
{
|
||||||
|
if (!semanticWasPressed && resolvedSemanticKey is not Key.Unknown and not Key.Unbound)
|
||||||
|
{
|
||||||
|
_semanticPressedKeyQueue.Enqueue(resolvedSemanticKey);
|
||||||
|
bufferedSemanticPress = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!physicalWasPressed && physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
|
||||||
|
{
|
||||||
|
_physicalPressedKeyQueue.Enqueue((Key)(int)physicalKey);
|
||||||
|
bufferedPhysicalPress = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPressed &&
|
||||||
|
semanticKey is not Key.Unknown and not Key.Unbound &&
|
||||||
|
physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
|
||||||
|
{
|
||||||
|
_observedPhysicalKeysBySemanticKey[semanticKey] = physicalKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ConfigurationState.Instance.Logger.EnableAvaloniaLog &&
|
||||||
|
(semanticStateChanged || physicalStateChanged))
|
||||||
|
{
|
||||||
|
Logger.Info?.Print(
|
||||||
|
LogClass.UI,
|
||||||
|
$"Keyboard {(isPressed ? "down" : "up")}: avaloniaKey={args.Key}, avaloniaPhysical={args.PhysicalKey}, keySymbol={FormatKeySymbol(args.KeySymbol)}, modifiers={args.KeyModifiers}, semantic={semanticKey}, resolvedSemantic={resolvedSemanticKey}, physical={physicalKey}, physicalSource={physicalKeySource}, bufferedSemantic={bufferedSemanticPress}, bufferedPhysical={bufferedPhysicalPress}, semanticPressed={_semanticPressedKeys.Count}, physicalPressed={_physicalPressedKeys.Count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConfigPhysicalKey GetPhysicalInputKey(KeyEventArgs args, Key semanticKey, out PhysicalKeySource source)
|
||||||
|
{
|
||||||
|
Key key = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey);
|
||||||
|
|
||||||
|
if (key is >= Key.Unknown and < Key.Count)
|
||||||
|
{
|
||||||
|
source = PhysicalKeySource.Direct;
|
||||||
|
return (ConfigPhysicalKey)(int)key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semanticKey is not Key.Unknown and not Key.Unbound &&
|
||||||
|
_observedPhysicalKeysBySemanticKey.TryGetValue(semanticKey, out ConfigPhysicalKey observedPhysicalKey))
|
||||||
|
{
|
||||||
|
source = PhysicalKeySource.ObservedFallback;
|
||||||
|
return observedPhysicalKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
source = PhysicalKeySource.Unknown;
|
||||||
|
return ConfigPhysicalKey.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatKeySymbol(string keySymbol)
|
||||||
|
{
|
||||||
|
return string.IsNullOrEmpty(keySymbol) ? "<none>" : keySymbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using Ryujinx.Input;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using AvaKey = Avalonia.Input.Key;
|
using AvaKey = Avalonia.Input.Key;
|
||||||
|
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Input
|
namespace Ryujinx.Ava.Input
|
||||||
{
|
{
|
||||||
@@ -132,7 +133,8 @@ namespace Ryujinx.Ava.Input
|
|||||||
AvaKey.D8,
|
AvaKey.D8,
|
||||||
AvaKey.D9,
|
AvaKey.D9,
|
||||||
AvaKey.OemTilde,
|
AvaKey.OemTilde,
|
||||||
AvaKey.OemTilde,AvaKey.OemMinus,
|
AvaKey.Oem102,
|
||||||
|
AvaKey.OemMinus,
|
||||||
AvaKey.OemPlus,
|
AvaKey.OemPlus,
|
||||||
AvaKey.OemOpenBrackets,
|
AvaKey.OemOpenBrackets,
|
||||||
AvaKey.OemCloseBrackets,
|
AvaKey.OemCloseBrackets,
|
||||||
@@ -147,7 +149,149 @@ namespace Ryujinx.Ava.Input
|
|||||||
AvaKey.None
|
AvaKey.None
|
||||||
];
|
];
|
||||||
|
|
||||||
|
private static readonly AvaPhysicalKey[] _physicalKeyMapping =
|
||||||
|
[
|
||||||
|
// NOTE: Invalid
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
|
||||||
|
AvaPhysicalKey.ShiftLeft,
|
||||||
|
AvaPhysicalKey.ShiftRight,
|
||||||
|
AvaPhysicalKey.ControlLeft,
|
||||||
|
AvaPhysicalKey.ControlRight,
|
||||||
|
AvaPhysicalKey.AltLeft,
|
||||||
|
AvaPhysicalKey.AltRight,
|
||||||
|
AvaPhysicalKey.MetaLeft,
|
||||||
|
AvaPhysicalKey.MetaRight,
|
||||||
|
AvaPhysicalKey.ContextMenu,
|
||||||
|
AvaPhysicalKey.F1,
|
||||||
|
AvaPhysicalKey.F2,
|
||||||
|
AvaPhysicalKey.F3,
|
||||||
|
AvaPhysicalKey.F4,
|
||||||
|
AvaPhysicalKey.F5,
|
||||||
|
AvaPhysicalKey.F6,
|
||||||
|
AvaPhysicalKey.F7,
|
||||||
|
AvaPhysicalKey.F8,
|
||||||
|
AvaPhysicalKey.F9,
|
||||||
|
AvaPhysicalKey.F10,
|
||||||
|
AvaPhysicalKey.F11,
|
||||||
|
AvaPhysicalKey.F12,
|
||||||
|
AvaPhysicalKey.F13,
|
||||||
|
AvaPhysicalKey.F14,
|
||||||
|
AvaPhysicalKey.F15,
|
||||||
|
AvaPhysicalKey.F16,
|
||||||
|
AvaPhysicalKey.F17,
|
||||||
|
AvaPhysicalKey.F18,
|
||||||
|
AvaPhysicalKey.F19,
|
||||||
|
AvaPhysicalKey.F20,
|
||||||
|
AvaPhysicalKey.F21,
|
||||||
|
AvaPhysicalKey.F22,
|
||||||
|
AvaPhysicalKey.F23,
|
||||||
|
AvaPhysicalKey.F24,
|
||||||
|
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
AvaPhysicalKey.None,
|
||||||
|
|
||||||
|
AvaPhysicalKey.ArrowUp,
|
||||||
|
AvaPhysicalKey.ArrowDown,
|
||||||
|
AvaPhysicalKey.ArrowLeft,
|
||||||
|
AvaPhysicalKey.ArrowRight,
|
||||||
|
AvaPhysicalKey.Enter,
|
||||||
|
AvaPhysicalKey.Escape,
|
||||||
|
AvaPhysicalKey.Space,
|
||||||
|
AvaPhysicalKey.Tab,
|
||||||
|
AvaPhysicalKey.Backspace,
|
||||||
|
AvaPhysicalKey.Insert,
|
||||||
|
AvaPhysicalKey.Delete,
|
||||||
|
AvaPhysicalKey.PageUp,
|
||||||
|
AvaPhysicalKey.PageDown,
|
||||||
|
AvaPhysicalKey.Home,
|
||||||
|
AvaPhysicalKey.End,
|
||||||
|
AvaPhysicalKey.CapsLock,
|
||||||
|
AvaPhysicalKey.ScrollLock,
|
||||||
|
AvaPhysicalKey.PrintScreen,
|
||||||
|
AvaPhysicalKey.Pause,
|
||||||
|
AvaPhysicalKey.NumLock,
|
||||||
|
AvaPhysicalKey.NumPadClear,
|
||||||
|
AvaPhysicalKey.NumPad0,
|
||||||
|
AvaPhysicalKey.NumPad1,
|
||||||
|
AvaPhysicalKey.NumPad2,
|
||||||
|
AvaPhysicalKey.NumPad3,
|
||||||
|
AvaPhysicalKey.NumPad4,
|
||||||
|
AvaPhysicalKey.NumPad5,
|
||||||
|
AvaPhysicalKey.NumPad6,
|
||||||
|
AvaPhysicalKey.NumPad7,
|
||||||
|
AvaPhysicalKey.NumPad8,
|
||||||
|
AvaPhysicalKey.NumPad9,
|
||||||
|
AvaPhysicalKey.NumPadDivide,
|
||||||
|
AvaPhysicalKey.NumPadMultiply,
|
||||||
|
AvaPhysicalKey.NumPadSubtract,
|
||||||
|
AvaPhysicalKey.NumPadAdd,
|
||||||
|
AvaPhysicalKey.NumPadDecimal,
|
||||||
|
AvaPhysicalKey.NumPadEnter,
|
||||||
|
AvaPhysicalKey.A,
|
||||||
|
AvaPhysicalKey.B,
|
||||||
|
AvaPhysicalKey.C,
|
||||||
|
AvaPhysicalKey.D,
|
||||||
|
AvaPhysicalKey.E,
|
||||||
|
AvaPhysicalKey.F,
|
||||||
|
AvaPhysicalKey.G,
|
||||||
|
AvaPhysicalKey.H,
|
||||||
|
AvaPhysicalKey.I,
|
||||||
|
AvaPhysicalKey.J,
|
||||||
|
AvaPhysicalKey.K,
|
||||||
|
AvaPhysicalKey.L,
|
||||||
|
AvaPhysicalKey.M,
|
||||||
|
AvaPhysicalKey.N,
|
||||||
|
AvaPhysicalKey.O,
|
||||||
|
AvaPhysicalKey.P,
|
||||||
|
AvaPhysicalKey.Q,
|
||||||
|
AvaPhysicalKey.R,
|
||||||
|
AvaPhysicalKey.S,
|
||||||
|
AvaPhysicalKey.T,
|
||||||
|
AvaPhysicalKey.U,
|
||||||
|
AvaPhysicalKey.V,
|
||||||
|
AvaPhysicalKey.W,
|
||||||
|
AvaPhysicalKey.X,
|
||||||
|
AvaPhysicalKey.Y,
|
||||||
|
AvaPhysicalKey.Z,
|
||||||
|
AvaPhysicalKey.Digit0,
|
||||||
|
AvaPhysicalKey.Digit1,
|
||||||
|
AvaPhysicalKey.Digit2,
|
||||||
|
AvaPhysicalKey.Digit3,
|
||||||
|
AvaPhysicalKey.Digit4,
|
||||||
|
AvaPhysicalKey.Digit5,
|
||||||
|
AvaPhysicalKey.Digit6,
|
||||||
|
AvaPhysicalKey.Digit7,
|
||||||
|
AvaPhysicalKey.Digit8,
|
||||||
|
AvaPhysicalKey.Digit9,
|
||||||
|
AvaPhysicalKey.Backquote,
|
||||||
|
AvaPhysicalKey.IntlBackslash,
|
||||||
|
AvaPhysicalKey.Minus,
|
||||||
|
AvaPhysicalKey.Equal,
|
||||||
|
AvaPhysicalKey.BracketLeft,
|
||||||
|
AvaPhysicalKey.BracketRight,
|
||||||
|
AvaPhysicalKey.Semicolon,
|
||||||
|
AvaPhysicalKey.Quote,
|
||||||
|
AvaPhysicalKey.Comma,
|
||||||
|
AvaPhysicalKey.Period,
|
||||||
|
AvaPhysicalKey.Slash,
|
||||||
|
AvaPhysicalKey.Backslash,
|
||||||
|
|
||||||
|
// NOTE: invalid
|
||||||
|
AvaPhysicalKey.None
|
||||||
|
];
|
||||||
|
|
||||||
private static readonly Dictionary<AvaKey, Key> _avaKeyMapping;
|
private static readonly Dictionary<AvaKey, Key> _avaKeyMapping;
|
||||||
|
private static readonly Dictionary<AvaPhysicalKey, Key> _avaPhysicalKeyMapping;
|
||||||
|
|
||||||
static AvaloniaKeyboardMappingHelper()
|
static AvaloniaKeyboardMappingHelper()
|
||||||
{
|
{
|
||||||
@@ -155,21 +299,42 @@ namespace Ryujinx.Ava.Input
|
|||||||
|
|
||||||
// NOTE: Avalonia.Input.Key is not contiguous and quite large, so use a dictionary instead of an array.
|
// NOTE: Avalonia.Input.Key is not contiguous and quite large, so use a dictionary instead of an array.
|
||||||
_avaKeyMapping = new Dictionary<AvaKey, Key>();
|
_avaKeyMapping = new Dictionary<AvaKey, Key>();
|
||||||
|
_avaPhysicalKeyMapping = new Dictionary<AvaPhysicalKey, Key>();
|
||||||
|
|
||||||
foreach (Key key in inputKeys)
|
foreach (Key key in inputKeys)
|
||||||
{
|
{
|
||||||
if (TryGetAvaKey(key, out AvaKey index))
|
if (TryGetAvaKey(key, out AvaKey avaKey))
|
||||||
{
|
{
|
||||||
_avaKeyMapping[index] = key;
|
_avaKeyMapping[avaKey] = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetAvaPhysicalKey(key, out AvaPhysicalKey avaPhysicalKey))
|
||||||
|
{
|
||||||
|
_avaPhysicalKeyMapping[avaPhysicalKey] = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Alias additional Avalonia key values to improve non-US layout support.
|
||||||
|
_avaKeyMapping[AvaKey.Oem1] = Key.Semicolon;
|
||||||
|
_avaKeyMapping[AvaKey.Oem2] = Key.Slash;
|
||||||
|
_avaKeyMapping[AvaKey.Oem3] = Key.Tilde;
|
||||||
|
_avaKeyMapping[AvaKey.Oem4] = Key.BracketLeft;
|
||||||
|
_avaKeyMapping[AvaKey.Oem5] = Key.BackSlash;
|
||||||
|
_avaKeyMapping[AvaKey.Oem6] = Key.BracketRight;
|
||||||
|
_avaKeyMapping[AvaKey.Oem7] = Key.Quote;
|
||||||
|
_avaKeyMapping[AvaKey.OemBackslash] = Key.Grave;
|
||||||
|
_avaKeyMapping[AvaKey.Oem102] = Key.Grave;
|
||||||
|
|
||||||
|
// Common alternates for non-US/JIS physical keys.
|
||||||
|
_avaPhysicalKeyMapping[AvaPhysicalKey.IntlRo] = Key.BackSlash;
|
||||||
|
_avaPhysicalKeyMapping[AvaPhysicalKey.IntlYen] = Key.BackSlash;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetAvaKey(Key key, out AvaKey avaKey)
|
public static bool TryGetAvaKey(Key key, out AvaKey avaKey)
|
||||||
{
|
{
|
||||||
avaKey = AvaKey.None;
|
avaKey = AvaKey.None;
|
||||||
|
|
||||||
bool keyExist = (int)key < _keyMapping.Length;
|
bool keyExist = key < Key.Count && (int)key < _keyMapping.Length;
|
||||||
if (keyExist)
|
if (keyExist)
|
||||||
{
|
{
|
||||||
avaKey = _keyMapping[(int)key];
|
avaKey = _keyMapping[(int)key];
|
||||||
@@ -178,9 +343,34 @@ namespace Ryujinx.Ava.Input
|
|||||||
return keyExist;
|
return keyExist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool TryGetAvaPhysicalKey(Key key, out AvaPhysicalKey avaPhysicalKey)
|
||||||
|
{
|
||||||
|
avaPhysicalKey = AvaPhysicalKey.None;
|
||||||
|
|
||||||
|
bool keyExist = key < Key.Count && (int)key < _physicalKeyMapping.Length;
|
||||||
|
if (keyExist)
|
||||||
|
{
|
||||||
|
avaPhysicalKey = _physicalKeyMapping[(int)key];
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyExist;
|
||||||
|
}
|
||||||
|
|
||||||
public static Key ToInputKey(AvaKey key)
|
public static Key ToInputKey(AvaKey key)
|
||||||
{
|
{
|
||||||
return _avaKeyMapping.GetValueOrDefault(key, Key.Unknown);
|
return _avaKeyMapping.GetValueOrDefault(key, Key.Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Key ToInputKey(AvaPhysicalKey key)
|
||||||
|
{
|
||||||
|
return _avaPhysicalKeyMapping.GetValueOrDefault(key, Key.Unknown);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Key ToInputKey(AvaPhysicalKey physicalKey, AvaKey key)
|
||||||
|
{
|
||||||
|
Key inputKey = ToInputKey(key);
|
||||||
|
|
||||||
|
return inputKey != Key.Unknown ? inputKey : ToInputKey(physicalKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1304,6 +1304,11 @@ namespace Ryujinx.Ava.Systems
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!_viewModel.IsActive)
|
||||||
|
{
|
||||||
|
_inputManager.KeyboardDriver.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
||||||
|
|
||||||
if (_viewModel.IsActive)
|
if (_viewModel.IsActive)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
||||||
|
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Systems.Configuration
|
namespace Ryujinx.Ava.Systems.Configuration
|
||||||
@@ -271,45 +273,45 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||||||
Id = "0",
|
Id = "0",
|
||||||
PlayerIndex = PlayerIndex.Player1,
|
PlayerIndex = PlayerIndex.Player1,
|
||||||
ControllerType = ControllerType.ProController,
|
ControllerType = ControllerType.ProController,
|
||||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
DpadUp = Key.Up,
|
DpadUp = PhysicalKey.Up,
|
||||||
DpadDown = Key.Down,
|
DpadDown = PhysicalKey.Down,
|
||||||
DpadLeft = Key.Left,
|
DpadLeft = PhysicalKey.Left,
|
||||||
DpadRight = Key.Right,
|
DpadRight = PhysicalKey.Right,
|
||||||
ButtonMinus = Key.Minus,
|
ButtonMinus = PhysicalKey.Minus,
|
||||||
ButtonL = Key.E,
|
ButtonL = PhysicalKey.E,
|
||||||
ButtonZl = Key.Q,
|
ButtonZl = PhysicalKey.Q,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.W,
|
StickUp = PhysicalKey.W,
|
||||||
StickDown = Key.S,
|
StickDown = PhysicalKey.S,
|
||||||
StickLeft = Key.A,
|
StickLeft = PhysicalKey.A,
|
||||||
StickRight = Key.D,
|
StickRight = PhysicalKey.D,
|
||||||
StickButton = Key.F,
|
StickButton = PhysicalKey.F,
|
||||||
},
|
},
|
||||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
ButtonA = Key.Z,
|
ButtonA = PhysicalKey.Z,
|
||||||
ButtonB = Key.X,
|
ButtonB = PhysicalKey.X,
|
||||||
ButtonX = Key.C,
|
ButtonX = PhysicalKey.C,
|
||||||
ButtonY = Key.V,
|
ButtonY = PhysicalKey.V,
|
||||||
ButtonPlus = Key.Plus,
|
ButtonPlus = PhysicalKey.Plus,
|
||||||
ButtonR = Key.U,
|
ButtonR = PhysicalKey.U,
|
||||||
ButtonZr = Key.O,
|
ButtonZr = PhysicalKey.O,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.I,
|
StickUp = PhysicalKey.I,
|
||||||
StickDown = Key.K,
|
StickDown = PhysicalKey.K,
|
||||||
StickLeft = Key.J,
|
StickLeft = PhysicalKey.J,
|
||||||
StickRight = Key.L,
|
StickRight = PhysicalKey.L,
|
||||||
StickButton = Key.H,
|
StickButton = PhysicalKey.H,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ using Ryujinx.Graphics.Vulkan;
|
|||||||
using Ryujinx.HLE;
|
using Ryujinx.HLE;
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
||||||
|
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.Systems.Configuration
|
namespace Ryujinx.Ava.Systems.Configuration
|
||||||
{
|
{
|
||||||
@@ -288,45 +290,45 @@ namespace Ryujinx.Ava.Systems.Configuration
|
|||||||
Name = "Keyboard",
|
Name = "Keyboard",
|
||||||
PlayerIndex = PlayerIndex.Player1,
|
PlayerIndex = PlayerIndex.Player1,
|
||||||
ControllerType = ControllerType.ProController,
|
ControllerType = ControllerType.ProController,
|
||||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
DpadUp = Key.Up,
|
DpadUp = PhysicalKey.Up,
|
||||||
DpadDown = Key.Down,
|
DpadDown = PhysicalKey.Down,
|
||||||
DpadLeft = Key.Left,
|
DpadLeft = PhysicalKey.Left,
|
||||||
DpadRight = Key.Right,
|
DpadRight = PhysicalKey.Right,
|
||||||
ButtonMinus = Key.Minus,
|
ButtonMinus = PhysicalKey.Minus,
|
||||||
ButtonL = Key.E,
|
ButtonL = PhysicalKey.E,
|
||||||
ButtonZl = Key.Q,
|
ButtonZl = PhysicalKey.Q,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.W,
|
StickUp = PhysicalKey.W,
|
||||||
StickDown = Key.S,
|
StickDown = PhysicalKey.S,
|
||||||
StickLeft = Key.A,
|
StickLeft = PhysicalKey.A,
|
||||||
StickRight = Key.D,
|
StickRight = PhysicalKey.D,
|
||||||
StickButton = Key.F,
|
StickButton = PhysicalKey.F,
|
||||||
},
|
},
|
||||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
ButtonA = Key.Z,
|
ButtonA = PhysicalKey.Z,
|
||||||
ButtonB = Key.X,
|
ButtonB = PhysicalKey.X,
|
||||||
ButtonX = Key.C,
|
ButtonX = PhysicalKey.C,
|
||||||
ButtonY = Key.V,
|
ButtonY = PhysicalKey.V,
|
||||||
ButtonPlus = Key.Plus,
|
ButtonPlus = PhysicalKey.Plus,
|
||||||
ButtonR = Key.U,
|
ButtonR = PhysicalKey.U,
|
||||||
ButtonZr = Key.O,
|
ButtonZr = PhysicalKey.O,
|
||||||
ButtonSl = Key.Unbound,
|
ButtonSl = PhysicalKey.Unbound,
|
||||||
ButtonSr = Key.Unbound,
|
ButtonSr = PhysicalKey.Unbound,
|
||||||
},
|
},
|
||||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = Key.I,
|
StickUp = PhysicalKey.I,
|
||||||
StickDown = Key.K,
|
StickDown = PhysicalKey.K,
|
||||||
StickLeft = Key.J,
|
StickLeft = PhysicalKey.J,
|
||||||
StickRight = Key.L,
|
StickRight = PhysicalKey.L,
|
||||||
StickButton = Key.H,
|
StickButton = PhysicalKey.H,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler
|
class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler
|
||||||
{
|
{
|
||||||
private MainWindow _parent;
|
private MainWindow _parent;
|
||||||
|
private AvaloniaKeyboardDriver _avaloniaKeyboardDriver;
|
||||||
private readonly OffscreenTextBox _hiddenTextBox;
|
private readonly OffscreenTextBox _hiddenTextBox;
|
||||||
private bool _canProcessInput;
|
private bool _canProcessInput;
|
||||||
private IDisposable _textChangedSubscription;
|
private IDisposable _textChangedSubscription;
|
||||||
@@ -27,6 +28,7 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
|
|
||||||
if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver)
|
if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver)
|
||||||
{
|
{
|
||||||
|
_avaloniaKeyboardDriver = avaloniaKeyboardDriver;
|
||||||
avaloniaKeyboardDriver.KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed;
|
avaloniaKeyboardDriver.KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed;
|
||||||
avaloniaKeyboardDriver.KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease;
|
avaloniaKeyboardDriver.KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease;
|
||||||
avaloniaKeyboardDriver.TextInput += AvaloniaDynamicTextInputHandler_TextInput;
|
avaloniaKeyboardDriver.TextInput += AvaloniaDynamicTextInputHandler_TextInput;
|
||||||
@@ -65,7 +67,7 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
|
|
||||||
private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e)
|
private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
|
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key);
|
||||||
|
|
||||||
if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
|
if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||||
{
|
{
|
||||||
@@ -85,7 +87,7 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
|
|
||||||
private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e)
|
private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e)
|
||||||
{
|
{
|
||||||
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
|
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key);
|
||||||
|
|
||||||
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
|
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||||
{
|
{
|
||||||
@@ -115,11 +117,11 @@ namespace Ryujinx.Ava.UI.Applet
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver)
|
if (_avaloniaKeyboardDriver != null)
|
||||||
{
|
{
|
||||||
avaloniaKeyboardDriver.KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed;
|
_avaloniaKeyboardDriver.KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed;
|
||||||
avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
|
_avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
|
||||||
avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
|
_avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
_textChangedSubscription?.Dispose();
|
_textChangedSubscription?.Dispose();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Avalonia.Controls.Primitives;
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
using Ryujinx.Ava.Input;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
using Ryujinx.Input.Assigner;
|
using Ryujinx.Input.Assigner;
|
||||||
using System;
|
using System;
|
||||||
@@ -25,6 +26,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
|
|
||||||
private bool _isWaitingForInput;
|
private bool _isWaitingForInput;
|
||||||
private bool _shouldUnbind;
|
private bool _shouldUnbind;
|
||||||
|
private IKeyboard _keyboard;
|
||||||
public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
|
public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
|
||||||
|
|
||||||
public ButtonKeyAssigner(ToggleButton toggleButton)
|
public ButtonKeyAssigner(ToggleButton toggleButton)
|
||||||
@@ -34,6 +36,9 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
|
|
||||||
public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
|
public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
|
||||||
{
|
{
|
||||||
|
_keyboard = keyboard;
|
||||||
|
ClearKeyboardState(_keyboard);
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
ToggledButton.IsChecked = true;
|
ToggledButton.IsChecked = true;
|
||||||
@@ -82,6 +87,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
_isWaitingForInput = false;
|
_isWaitingForInput = false;
|
||||||
|
|
||||||
ToggledButton.IsChecked = false;
|
ToggledButton.IsChecked = false;
|
||||||
|
ClearKeyboardState(_keyboard);
|
||||||
|
|
||||||
if (pressedButton.HasValue && pressedButton.Value.AsHidType<Key>() == Key.BackSpace)
|
if (pressedButton.HasValue && pressedButton.Value.AsHidType<Key>() == Key.BackSpace)
|
||||||
{
|
{
|
||||||
@@ -98,6 +104,15 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
_isWaitingForInput = false;
|
_isWaitingForInput = false;
|
||||||
ToggledButton.IsChecked = false;
|
ToggledButton.IsChecked = false;
|
||||||
_shouldUnbind = shouldUnbind;
|
_shouldUnbind = shouldUnbind;
|
||||||
|
ClearKeyboardState(_keyboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClearKeyboardState(IKeyboard keyboard)
|
||||||
|
{
|
||||||
|
if (keyboard is AvaloniaKeyboard avaloniaKeyboard)
|
||||||
|
{
|
||||||
|
avaloniaKeyboard.Clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Ryujinx.Common.Configuration.Hid.Controller;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using Key = Ryujinx.Input.Key;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Helpers
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
{
|
{
|
||||||
@@ -12,79 +13,6 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
{
|
{
|
||||||
public static readonly KeyValueConverter Instance = new();
|
public static readonly KeyValueConverter Instance = new();
|
||||||
|
|
||||||
private static readonly Dictionary<Key, LocaleKeys> _keysMap = new()
|
|
||||||
{
|
|
||||||
{ Key.Unknown, LocaleKeys.KeyUnknown },
|
|
||||||
{ Key.ShiftLeft, LocaleKeys.KeyShiftLeft },
|
|
||||||
{ Key.ShiftRight, LocaleKeys.KeyShiftRight },
|
|
||||||
{ Key.ControlLeft, LocaleKeys.KeyControlLeft },
|
|
||||||
{ Key.ControlRight, LocaleKeys.KeyControlRight },
|
|
||||||
{ Key.AltLeft, LocaleKeys.KeyAltLeft },
|
|
||||||
{ Key.AltRight, LocaleKeys.KeyAltRight },
|
|
||||||
{ Key.WinLeft, LocaleKeys.KeyWinLeft },
|
|
||||||
{ Key.WinRight, LocaleKeys.KeyWinRight },
|
|
||||||
{ Key.Up, LocaleKeys.KeyUp },
|
|
||||||
{ Key.Down, LocaleKeys.KeyDown },
|
|
||||||
{ Key.Left, LocaleKeys.KeyLeft },
|
|
||||||
{ Key.Right, LocaleKeys.KeyRight },
|
|
||||||
{ Key.Enter, LocaleKeys.KeyEnter },
|
|
||||||
{ Key.Escape, LocaleKeys.KeyEscape },
|
|
||||||
{ Key.Space, LocaleKeys.KeySpace },
|
|
||||||
{ Key.Tab, LocaleKeys.KeyTab },
|
|
||||||
{ Key.BackSpace, LocaleKeys.KeyBackSpace },
|
|
||||||
{ Key.Insert, LocaleKeys.KeyInsert },
|
|
||||||
{ Key.Delete, LocaleKeys.KeyDelete },
|
|
||||||
{ Key.PageUp, LocaleKeys.KeyPageUp },
|
|
||||||
{ Key.PageDown, LocaleKeys.KeyPageDown },
|
|
||||||
{ Key.Home, LocaleKeys.KeyHome },
|
|
||||||
{ Key.End, LocaleKeys.KeyEnd },
|
|
||||||
{ Key.CapsLock, LocaleKeys.KeyCapsLock },
|
|
||||||
{ Key.ScrollLock, LocaleKeys.KeyScrollLock },
|
|
||||||
{ Key.PrintScreen, LocaleKeys.KeyPrintScreen },
|
|
||||||
{ Key.Pause, LocaleKeys.KeyPause },
|
|
||||||
{ Key.NumLock, LocaleKeys.KeyNumLock },
|
|
||||||
{ Key.Clear, LocaleKeys.KeyClear },
|
|
||||||
{ Key.Keypad0, LocaleKeys.KeyKeypad0 },
|
|
||||||
{ Key.Keypad1, LocaleKeys.KeyKeypad1 },
|
|
||||||
{ Key.Keypad2, LocaleKeys.KeyKeypad2 },
|
|
||||||
{ Key.Keypad3, LocaleKeys.KeyKeypad3 },
|
|
||||||
{ Key.Keypad4, LocaleKeys.KeyKeypad4 },
|
|
||||||
{ Key.Keypad5, LocaleKeys.KeyKeypad5 },
|
|
||||||
{ Key.Keypad6, LocaleKeys.KeyKeypad6 },
|
|
||||||
{ Key.Keypad7, LocaleKeys.KeyKeypad7 },
|
|
||||||
{ Key.Keypad8, LocaleKeys.KeyKeypad8 },
|
|
||||||
{ Key.Keypad9, LocaleKeys.KeyKeypad9 },
|
|
||||||
{ Key.KeypadDivide, LocaleKeys.KeyKeypadDivide },
|
|
||||||
{ Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply },
|
|
||||||
{ Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract },
|
|
||||||
{ Key.KeypadAdd, LocaleKeys.KeyKeypadAdd },
|
|
||||||
{ Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal },
|
|
||||||
{ Key.KeypadEnter, LocaleKeys.KeyKeypadEnter },
|
|
||||||
{ Key.Number0, LocaleKeys.KeyNumber0 },
|
|
||||||
{ Key.Number1, LocaleKeys.KeyNumber1 },
|
|
||||||
{ Key.Number2, LocaleKeys.KeyNumber2 },
|
|
||||||
{ Key.Number3, LocaleKeys.KeyNumber3 },
|
|
||||||
{ Key.Number4, LocaleKeys.KeyNumber4 },
|
|
||||||
{ Key.Number5, LocaleKeys.KeyNumber5 },
|
|
||||||
{ Key.Number6, LocaleKeys.KeyNumber6 },
|
|
||||||
{ Key.Number7, LocaleKeys.KeyNumber7 },
|
|
||||||
{ Key.Number8, LocaleKeys.KeyNumber8 },
|
|
||||||
{ Key.Number9, LocaleKeys.KeyNumber9 },
|
|
||||||
{ Key.Tilde, LocaleKeys.KeyTilde },
|
|
||||||
{ Key.Grave, LocaleKeys.KeyGrave },
|
|
||||||
{ Key.Minus, LocaleKeys.KeyMinus },
|
|
||||||
{ Key.Plus, LocaleKeys.KeyPlus },
|
|
||||||
{ Key.BracketLeft, LocaleKeys.KeyBracketLeft },
|
|
||||||
{ Key.BracketRight, LocaleKeys.KeyBracketRight },
|
|
||||||
{ Key.Semicolon, LocaleKeys.KeySemicolon },
|
|
||||||
{ Key.Quote, LocaleKeys.KeyQuote },
|
|
||||||
{ Key.Comma, LocaleKeys.KeyComma },
|
|
||||||
{ Key.Period, LocaleKeys.KeyPeriod },
|
|
||||||
{ Key.Slash, LocaleKeys.KeySlash },
|
|
||||||
{ Key.BackSlash, LocaleKeys.KeyBackSlash },
|
|
||||||
{ Key.Unbound, LocaleKeys.KeyUnbound },
|
|
||||||
};
|
|
||||||
|
|
||||||
private static readonly Dictionary<GamepadInputId, LocaleKeys> _gamepadInputIdMap = new()
|
private static readonly Dictionary<GamepadInputId, LocaleKeys> _gamepadInputIdMap = new()
|
||||||
{
|
{
|
||||||
{ GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick },
|
{ GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick },
|
||||||
@@ -110,78 +38,40 @@ namespace Ryujinx.Ava.UI.Helpers
|
|||||||
{ GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0},
|
{ GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0},
|
||||||
{ GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1},
|
{ GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1},
|
||||||
{ GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1},
|
{ GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1},
|
||||||
{ GamepadInputId.Unbound, LocaleKeys.KeyUnbound},
|
{ GamepadInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new()
|
private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new()
|
||||||
{
|
{
|
||||||
{ StickInputId.Left, LocaleKeys.StickLeft},
|
{ StickInputId.Left, LocaleKeys.StickLeft},
|
||||||
{ StickInputId.Right, LocaleKeys.StickRight},
|
{ StickInputId.Right, LocaleKeys.StickRight},
|
||||||
{ StickInputId.Unbound, LocaleKeys.KeyUnbound},
|
{ StickInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
|
||||||
};
|
};
|
||||||
|
|
||||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
string keyString = string.Empty;
|
return value switch
|
||||||
LocaleKeys localeKey;
|
|
||||||
|
|
||||||
switch (value)
|
|
||||||
{
|
{
|
||||||
case Key key:
|
Key key => KeyboardLayoutLocaleHelper.TryGetSemanticLabel(key, out string localizedKeyLabel)
|
||||||
if (_keysMap.TryGetValue(key, out localeKey))
|
? localizedKeyLabel
|
||||||
{
|
: key.ToString(),
|
||||||
if (OperatingSystem.IsMacOS())
|
PhysicalKey physicalKey => PhysicalKeyLabelHelper.GetDisplayString(physicalKey),
|
||||||
{
|
GamepadInputId gamepadInputId => GetLocalizedMappedValue(gamepadInputId, _gamepadInputIdMap),
|
||||||
localeKey = localeKey switch
|
StickInputId stickInputId => GetLocalizedMappedValue(stickInputId, _stickInputIdMap),
|
||||||
{
|
_ => string.Empty,
|
||||||
LocaleKeys.KeyControlLeft => LocaleKeys.KeyMacControlLeft,
|
};
|
||||||
LocaleKeys.KeyControlRight => LocaleKeys.KeyMacControlRight,
|
|
||||||
LocaleKeys.KeyAltLeft => LocaleKeys.KeyMacAltLeft,
|
|
||||||
LocaleKeys.KeyAltRight => LocaleKeys.KeyMacAltRight,
|
|
||||||
LocaleKeys.KeyWinLeft => LocaleKeys.KeyMacWinLeft,
|
|
||||||
LocaleKeys.KeyWinRight => LocaleKeys.KeyMacWinRight,
|
|
||||||
_ => localeKey
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
keyString = LocaleManager.Instance[localeKey];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
keyString = key.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case GamepadInputId gamepadInputId:
|
|
||||||
if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey))
|
|
||||||
{
|
|
||||||
keyString = LocaleManager.Instance[localeKey];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
keyString = gamepadInputId.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
case StickInputId stickInputId:
|
|
||||||
if (_stickInputIdMap.TryGetValue(stickInputId, out localeKey))
|
|
||||||
{
|
|
||||||
keyString = LocaleManager.Instance[localeKey];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
keyString = stickInputId.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyString;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException();
|
throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetLocalizedMappedValue<T>(T value, IReadOnlyDictionary<T, LocaleKeys> map) where T : notnull
|
||||||
|
{
|
||||||
|
return map.TryGetValue(value, out LocaleKeys localeKey)
|
||||||
|
? LocaleManager.Instance[localeKey]
|
||||||
|
: value.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/Ryujinx/UI/Helpers/InputDeviceNameConverter.cs
Normal file
28
src/Ryujinx/UI/Helpers/InputDeviceNameConverter.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
src/Ryujinx/UI/Helpers/KeyboardLayoutLocaleHelper.cs
Normal file
142
src/Ryujinx/UI/Helpers/KeyboardLayoutLocaleHelper.cs
Normal 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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
234
src/Ryujinx/UI/Helpers/PhysicalKeyLabelHelper.cs
Normal file
234
src/Ryujinx/UI/Helpers/PhysicalKeyLabelHelper.cs
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
using Avalonia.Input;
|
||||||
|
using Ryujinx.Ava.Common.Locale;
|
||||||
|
using Ryujinx.Ava.Input;
|
||||||
|
using Ryujinx.Common.Configuration;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
|
||||||
|
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
using InputKey = Ryujinx.Input.Key;
|
||||||
|
|
||||||
|
namespace Ryujinx.Ava.UI.Helpers
|
||||||
|
{
|
||||||
|
internal static class PhysicalKeyLabelHelper
|
||||||
|
{
|
||||||
|
private const string ObservedLabelsFileName = "keyboard_layout_labels.json";
|
||||||
|
private static readonly ConcurrentDictionary<ConfigPhysicalKey, string> _observedLayoutLabels = new();
|
||||||
|
private static readonly object _observedLayoutLabelsLock = new();
|
||||||
|
private static readonly JsonSerializerOptions _serializerOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
AllowTrailingCommas = true,
|
||||||
|
ReadCommentHandling = JsonCommentHandling.Skip
|
||||||
|
};
|
||||||
|
private static bool _observedLayoutLabelsLoaded;
|
||||||
|
public static event Action LabelsChanged;
|
||||||
|
|
||||||
|
public static string GetDisplayString(ConfigPhysicalKey key)
|
||||||
|
{
|
||||||
|
EnsureObservedLayoutLabelsLoaded();
|
||||||
|
|
||||||
|
if (KeyboardLayoutLocaleHelper.TryGetPhysicalLabel(key, out string localizedLabel))
|
||||||
|
{
|
||||||
|
return localizedLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_observedLayoutLabels.TryGetValue(key, out string observedLabel))
|
||||||
|
{
|
||||||
|
return observedLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetFallbackPrintableKeyLabel(key, out string label))
|
||||||
|
{
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ObserveKeyPress(object sender, KeyEventArgs args)
|
||||||
|
{
|
||||||
|
EnsureObservedLayoutLabelsLoaded();
|
||||||
|
|
||||||
|
if (args.KeyModifiers != KeyModifiers.None)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
InputKey inputKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey);
|
||||||
|
if (!TryConvertToConfigPhysicalKey(inputKey, out ConfigPhysicalKey physicalKey) ||
|
||||||
|
KeyboardLayoutLocaleHelper.TryGetPhysicalLocaleKey(physicalKey, out _))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryNormalizeObservedPrintableLabel(args.KeySymbol, out string label))
|
||||||
|
{
|
||||||
|
if (IsCapsLockOn() && !char.IsLetter(label[0]))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_observedLayoutLabels.TryGetValue(physicalKey, out string existingLabel) && existingLabel == label)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_observedLayoutLabels[physicalKey] = label;
|
||||||
|
SaveObservedLayoutLabels();
|
||||||
|
LabelsChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureObservedLayoutLabelsLoaded()
|
||||||
|
{
|
||||||
|
if (_observedLayoutLabelsLoaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_observedLayoutLabelsLock)
|
||||||
|
{
|
||||||
|
if (_observedLayoutLabelsLoaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string labelsPath = GetObservedLabelsPath();
|
||||||
|
if (!File.Exists(labelsPath))
|
||||||
|
{
|
||||||
|
_observedLayoutLabelsLoaded = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string labelsJson = File.ReadAllText(labelsPath);
|
||||||
|
Dictionary<string, string>? labels = JsonSerializer.Deserialize<Dictionary<string, string>>(labelsJson, _serializerOptions);
|
||||||
|
|
||||||
|
if (labels != null)
|
||||||
|
{
|
||||||
|
foreach ((string key, string value) in labels)
|
||||||
|
{
|
||||||
|
if (Enum.TryParse(key, out ConfigPhysicalKey physicalKey) &&
|
||||||
|
!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
_observedLayoutLabels[physicalKey] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.UI, $"Unable to load observed keyboard layout labels from '{labelsPath}': {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_observedLayoutLabelsLoaded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveObservedLayoutLabels()
|
||||||
|
{
|
||||||
|
lock (_observedLayoutLabelsLock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, string> labels = new();
|
||||||
|
|
||||||
|
foreach ((ConfigPhysicalKey key, string value) in _observedLayoutLabels)
|
||||||
|
{
|
||||||
|
labels[key.ToString()] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
File.WriteAllText(GetObservedLabelsPath(), JsonSerializer.Serialize(labels, _serializerOptions));
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Warning?.Print(LogClass.UI, $"Unable to save observed keyboard layout labels: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetObservedLabelsPath()
|
||||||
|
{
|
||||||
|
return Path.Combine(AppDataManager.BaseDirPath, ObservedLabelsFileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetFallbackPrintableKeyLabel(ConfigPhysicalKey key, out string label)
|
||||||
|
{
|
||||||
|
// The legacy enum name for the ISO extra key is misleading, so give it a distinct physical label.
|
||||||
|
if (key == ConfigPhysicalKey.Grave)
|
||||||
|
{
|
||||||
|
label = "<>";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AvaloniaKeyboardMappingHelper.TryGetAvaPhysicalKey((InputKey)(int)key, out AvaPhysicalKey avaPhysicalKey))
|
||||||
|
{
|
||||||
|
label = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
label = PhysicalKeyExtensions.ToQwertyKeySymbol(avaPhysicalKey, false);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(label) || label.Length != 1 || char.IsControl(label[0]))
|
||||||
|
{
|
||||||
|
label = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char.IsLetter(label[0]))
|
||||||
|
{
|
||||||
|
label = char.ToUpperInvariant(label[0]).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsCapsLockOn()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsWindows() && Console.CapsLock;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.Debug?.Print(LogClass.UI, $"CapsLock state query failed: {ex.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryNormalizeObservedPrintableLabel(string keySymbol, out string label)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(keySymbol) || keySymbol.Length != 1 || char.IsControl(keySymbol[0]))
|
||||||
|
{
|
||||||
|
label = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
label = char.IsLetter(keySymbol[0])
|
||||||
|
? char.ToUpperInvariant(keySymbol[0]).ToString()
|
||||||
|
: keySymbol;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryConvertToConfigPhysicalKey(InputKey key, out ConfigPhysicalKey physicalKey)
|
||||||
|
{
|
||||||
|
if (key is >= InputKey.Unknown and < InputKey.Count)
|
||||||
|
{
|
||||||
|
physicalKey = (ConfigPhysicalKey)(int)key;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
physicalKey = ConfigPhysicalKey.Unknown;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,88 +13,88 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
public PlayerIndex PlayerIndex { get; set; }
|
public PlayerIndex PlayerIndex { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftStickUp { get; set; }
|
public partial PhysicalKey LeftStickUp { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftStickDown { get; set; }
|
public partial PhysicalKey LeftStickDown { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftStickLeft { get; set; }
|
public partial PhysicalKey LeftStickLeft { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftStickRight { get; set; }
|
public partial PhysicalKey LeftStickRight { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftStickButton { get; set; }
|
public partial PhysicalKey LeftStickButton { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightStickUp { get; set; }
|
public partial PhysicalKey RightStickUp { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightStickDown { get; set; }
|
public partial PhysicalKey RightStickDown { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightStickLeft { get; set; }
|
public partial PhysicalKey RightStickLeft { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightStickRight { get; set; }
|
public partial PhysicalKey RightStickRight { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightStickButton { get; set; }
|
public partial PhysicalKey RightStickButton { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key DpadUp { get; set; }
|
public partial PhysicalKey DpadUp { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key DpadDown { get; set; }
|
public partial PhysicalKey DpadDown { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key DpadLeft { get; set; }
|
public partial PhysicalKey DpadLeft { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key DpadRight { get; set; }
|
public partial PhysicalKey DpadRight { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonMinus { get; set; }
|
public partial PhysicalKey ButtonMinus { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonPlus { get; set; }
|
public partial PhysicalKey ButtonPlus { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonA { get; set; }
|
public partial PhysicalKey ButtonA { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonB { get; set; }
|
public partial PhysicalKey ButtonB { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonX { get; set; }
|
public partial PhysicalKey ButtonX { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonY { get; set; }
|
public partial PhysicalKey ButtonY { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonL { get; set; }
|
public partial PhysicalKey ButtonL { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonR { get; set; }
|
public partial PhysicalKey ButtonR { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonZl { get; set; }
|
public partial PhysicalKey ButtonZl { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key ButtonZr { get; set; }
|
public partial PhysicalKey ButtonZr { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftButtonSl { get; set; }
|
public partial PhysicalKey LeftButtonSl { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key LeftButtonSr { get; set; }
|
public partial PhysicalKey LeftButtonSr { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightButtonSl { get; set; }
|
public partial PhysicalKey RightButtonSl { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial Key RightButtonSr { get; set; }
|
public partial PhysicalKey RightButtonSr { get; set; }
|
||||||
|
|
||||||
public KeyboardInputConfig(InputConfig config)
|
public KeyboardInputConfig(InputConfig config)
|
||||||
{
|
{
|
||||||
@@ -153,7 +153,7 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
Backend = InputBackendType.WindowKeyboard,
|
Backend = InputBackendType.WindowKeyboard,
|
||||||
PlayerIndex = PlayerIndex,
|
PlayerIndex = PlayerIndex,
|
||||||
ControllerType = ControllerType,
|
ControllerType = ControllerType,
|
||||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
DpadUp = DpadUp,
|
DpadUp = DpadUp,
|
||||||
DpadDown = DpadDown,
|
DpadDown = DpadDown,
|
||||||
@@ -165,7 +165,7 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
ButtonSl = LeftButtonSl,
|
ButtonSl = LeftButtonSl,
|
||||||
ButtonSr = LeftButtonSr,
|
ButtonSr = LeftButtonSr,
|
||||||
},
|
},
|
||||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||||
{
|
{
|
||||||
ButtonA = ButtonA,
|
ButtonA = ButtonA,
|
||||||
ButtonB = ButtonB,
|
ButtonB = ButtonB,
|
||||||
@@ -177,7 +177,7 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
ButtonR = ButtonR,
|
ButtonR = ButtonR,
|
||||||
ButtonZr = ButtonZr,
|
ButtonZr = ButtonZr,
|
||||||
},
|
},
|
||||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = LeftStickUp,
|
StickUp = LeftStickUp,
|
||||||
StickDown = LeftStickDown,
|
StickDown = LeftStickDown,
|
||||||
@@ -185,7 +185,7 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
StickLeft = LeftStickLeft,
|
StickLeft = LeftStickLeft,
|
||||||
StickButton = LeftStickButton,
|
StickButton = LeftStickButton,
|
||||||
},
|
},
|
||||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||||
{
|
{
|
||||||
StickUp = RightStickUp,
|
StickUp = RightStickUp,
|
||||||
StickDown = RightStickDown,
|
StickDown = RightStickDown,
|
||||||
@@ -198,5 +198,37 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void NotifyKeyLabelsChanged()
|
||||||
|
{
|
||||||
|
OnPropertiesChanged(nameof(LeftStickUp),
|
||||||
|
nameof(LeftStickDown),
|
||||||
|
nameof(LeftStickLeft),
|
||||||
|
nameof(LeftStickRight),
|
||||||
|
nameof(LeftStickButton),
|
||||||
|
nameof(RightStickUp),
|
||||||
|
nameof(RightStickDown),
|
||||||
|
nameof(RightStickLeft),
|
||||||
|
nameof(RightStickRight),
|
||||||
|
nameof(RightStickButton),
|
||||||
|
nameof(DpadUp),
|
||||||
|
nameof(DpadDown),
|
||||||
|
nameof(DpadLeft),
|
||||||
|
nameof(DpadRight),
|
||||||
|
nameof(ButtonMinus),
|
||||||
|
nameof(ButtonPlus),
|
||||||
|
nameof(ButtonA),
|
||||||
|
nameof(ButtonB),
|
||||||
|
nameof(ButtonX),
|
||||||
|
nameof(ButtonY),
|
||||||
|
nameof(ButtonL),
|
||||||
|
nameof(ButtonR),
|
||||||
|
nameof(ButtonZl),
|
||||||
|
nameof(ButtonZr),
|
||||||
|
nameof(LeftButtonSl),
|
||||||
|
nameof(LeftButtonSr),
|
||||||
|
nameof(RightButtonSl),
|
||||||
|
nameof(RightButtonSr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Ryujinx.Ava.UI.ViewModels;
|
using Ryujinx.Ava.UI.ViewModels;
|
||||||
using Ryujinx.Ava.UI.ViewModels.Input;
|
using Ryujinx.Ava.UI.ViewModels.Input;
|
||||||
|
using Ryujinx.Common.Logging;
|
||||||
using Ryujinx.Input;
|
using Ryujinx.Input;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -117,6 +118,11 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
|
|
||||||
public void UpdateConfig(object config)
|
public void UpdateConfig(object config)
|
||||||
{
|
{
|
||||||
|
KeyboardConfig = null;
|
||||||
|
GamepadConfig = null;
|
||||||
|
UiStickLeft = (0f, 0f);
|
||||||
|
UiStickRight = (0f, 0f);
|
||||||
|
|
||||||
if (config is ControllerInputViewModel padConfig)
|
if (config is ControllerInputViewModel padConfig)
|
||||||
{
|
{
|
||||||
GamepadConfig = padConfig.Config;
|
GamepadConfig = padConfig.Config;
|
||||||
@@ -145,76 +151,86 @@ namespace Ryujinx.Ava.UI.Models.Input
|
|||||||
leftBuffer = (0f, 0f);
|
leftBuffer = (0f, 0f);
|
||||||
rightBuffer = (0f, 0f);
|
rightBuffer = (0f, 0f);
|
||||||
|
|
||||||
switch (Type)
|
try
|
||||||
{
|
{
|
||||||
case DeviceType.Keyboard:
|
switch (Type)
|
||||||
IKeyboard keyboard = (IKeyboard)Parent.AvaloniaKeyboardDriver.GetGamepad("0");
|
{
|
||||||
|
case DeviceType.Keyboard:
|
||||||
|
IKeyboard keyboard = Parent?.AvaloniaKeyboardDriver?.GetGamepad("0") as IKeyboard;
|
||||||
|
|
||||||
if (keyboard != null)
|
if (keyboard != null && KeyboardConfig != null)
|
||||||
{
|
|
||||||
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickRight))
|
|
||||||
{
|
{
|
||||||
leftBuffer.Item1 += 1;
|
KeyboardStateSnapshot snapshot = keyboard.GetKeyboardStateSnapshot();
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(KeyboardConfig.LeftStickRight))
|
||||||
|
{
|
||||||
|
leftBuffer.Item1 += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(KeyboardConfig.LeftStickLeft))
|
||||||
|
{
|
||||||
|
leftBuffer.Item1 -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(KeyboardConfig.LeftStickUp))
|
||||||
|
{
|
||||||
|
leftBuffer.Item2 += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(KeyboardConfig.LeftStickDown))
|
||||||
|
{
|
||||||
|
leftBuffer.Item2 -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(KeyboardConfig.RightStickRight))
|
||||||
|
{
|
||||||
|
rightBuffer.Item1 += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(KeyboardConfig.RightStickLeft))
|
||||||
|
{
|
||||||
|
rightBuffer.Item1 -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(KeyboardConfig.RightStickUp))
|
||||||
|
{
|
||||||
|
rightBuffer.Item2 += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.IsPressed(KeyboardConfig.RightStickDown))
|
||||||
|
{
|
||||||
|
rightBuffer.Item2 -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
UiStickLeft = leftBuffer;
|
||||||
|
UiStickRight = rightBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickLeft))
|
break;
|
||||||
|
|
||||||
|
case DeviceType.Controller:
|
||||||
|
IGamepad controller = Parent?.SelectedGamepad;
|
||||||
|
|
||||||
|
if (controller is IKeyboard)
|
||||||
{
|
{
|
||||||
leftBuffer.Item1 -= 1;
|
}
|
||||||
|
else if (controller != null && GamepadConfig != null)
|
||||||
|
{
|
||||||
|
leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick);
|
||||||
|
rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickUp))
|
break;
|
||||||
{
|
|
||||||
leftBuffer.Item2 += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.LeftStickDown))
|
case DeviceType.None:
|
||||||
{
|
break;
|
||||||
leftBuffer.Item2 -= 1;
|
default:
|
||||||
}
|
throw new ArgumentException($"Unable to poll device type \"{Type}\"");
|
||||||
|
}
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickRight))
|
}
|
||||||
{
|
catch (Exception ex) when (ex is ObjectDisposedException || ex is NullReferenceException || ex is NotSupportedException)
|
||||||
rightBuffer.Item1 += 1;
|
{
|
||||||
}
|
Logger.Debug?.Print(LogClass.UI, $"StickVisualizer polling failed: {ex}");
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickLeft))
|
|
||||||
{
|
|
||||||
rightBuffer.Item1 -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickUp))
|
|
||||||
{
|
|
||||||
rightBuffer.Item2 += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.IsPressed((Key)KeyboardConfig.RightStickDown))
|
|
||||||
{
|
|
||||||
rightBuffer.Item2 -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
UiStickLeft = leftBuffer;
|
|
||||||
UiStickRight = rightBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DeviceType.Controller:
|
|
||||||
IGamepad controller = Parent.SelectedGamepad;
|
|
||||||
|
|
||||||
if (controller != null)
|
|
||||||
{
|
|
||||||
leftBuffer = controller.GetStick((StickInputId)GamepadConfig.LeftJoystick);
|
|
||||||
rightBuffer = controller.GetStick((StickInputId)GamepadConfig.RightJoystick);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case DeviceType.None:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentException($"Unable to poll device type \"{Type}\"");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UiStickLeft = leftBuffer;
|
UiStickLeft = leftBuffer;
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ namespace Ryujinx.Ava.UI.Renderer
|
|||||||
|
|
||||||
Content = EmbeddedWindow;
|
Content = EmbeddedWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (EmbeddedWindow != null)
|
if (EmbeddedWindow != null)
|
||||||
|
|||||||
@@ -88,19 +88,19 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
|||||||
public async void ShowMotionConfig()
|
public async void ShowMotionConfig()
|
||||||
{
|
{
|
||||||
await MotionInputView.Show(this);
|
await MotionInputView.Show(this);
|
||||||
ParentModel.IsModified = true;
|
ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void ShowRumbleConfig()
|
public async void ShowRumbleConfig()
|
||||||
{
|
{
|
||||||
await RumbleInputView.Show(this);
|
await RumbleInputView.Show(this);
|
||||||
ParentModel.IsModified = true;
|
ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void ShowLedConfig()
|
public async void ShowLedConfig()
|
||||||
{
|
{
|
||||||
await LedInputView.Show(this);
|
await LedInputView.Show(this);
|
||||||
ParentModel.IsModified = true;
|
ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnParentModelChanged()
|
public void OnParentModelChanged()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -104,7 +104,7 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
|
|
||||||
PointerPressed += MouseClick;
|
PointerPressed += MouseClick;
|
||||||
|
|
||||||
ControllerInputViewModel viewModel = (DataContext as ControllerInputViewModel);
|
ControllerInputViewModel viewModel = ViewModel;
|
||||||
|
|
||||||
IKeyboard keyboard =
|
IKeyboard keyboard =
|
||||||
(IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver
|
(IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver
|
||||||
@@ -113,10 +113,9 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
|
|
||||||
_currentAssigner.ButtonAssigned += (sender, e) =>
|
_currentAssigner.ButtonAssigned += (sender, e) =>
|
||||||
{
|
{
|
||||||
if (e.ButtonValue.HasValue)
|
if (e.ButtonValue.HasValue && IsActiveAssignmentContext(viewModel))
|
||||||
{
|
{
|
||||||
Button buttonValue = e.ButtonValue.Value;
|
Button buttonValue = e.ButtonValue.Value;
|
||||||
FlagInputConfigChanged();
|
|
||||||
|
|
||||||
switch (button.Name)
|
switch (button.Name)
|
||||||
{
|
{
|
||||||
@@ -187,6 +186,8 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
viewModel.Config.RightJoystick = buttonValue.AsHidType<StickInputId>();
|
viewModel.Config.RightJoystick = buttonValue.AsHidType<StickInputId>();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FlagInputConfigChanged();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,7 +213,15 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
|
|
||||||
private void FlagInputConfigChanged()
|
private void FlagInputConfigChanged()
|
||||||
{
|
{
|
||||||
(DataContext as ControllerInputViewModel)!.ParentModel.IsModified = true;
|
if (DataContext is ControllerInputViewModel viewModel && VisualRoot is not null)
|
||||||
|
{
|
||||||
|
viewModel.ParentModel.RefreshModifiedState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsActiveAssignmentContext(ControllerInputViewModel viewModel)
|
||||||
|
{
|
||||||
|
return VisualRoot is not null && ReferenceEquals(DataContext, viewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MouseClick(object sender, PointerPressedEventArgs e)
|
private void MouseClick(object sender, PointerPressedEventArgs e)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||||
|
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
ToolTip.Tip="{ext:Locale ControllerSettingsCancelCurrentChangesToolTip}"
|
ToolTip.Tip="{ext:Locale ControllerSettingsCancelCurrentChangesToolTip}"
|
||||||
Command="{Binding RevertChanges}">
|
Command="{Binding RevertChanges}">
|
||||||
<ui:SymbolIcon
|
<ui:SymbolIcon
|
||||||
Symbol="Undo"
|
Symbol="Cancel"
|
||||||
FontSize="15"
|
FontSize="15"
|
||||||
Height="20" />
|
Height="20" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -148,7 +149,7 @@
|
|||||||
<Grid
|
<Grid
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="2"
|
Margin="2"
|
||||||
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto">
|
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*,Auto,Auto">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Margin="5,0,10,0"
|
Margin="5,0,10,0"
|
||||||
@@ -161,16 +162,35 @@
|
|||||||
Name="DeviceBox"
|
Name="DeviceBox"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
ItemsSource="{Binding DeviceList}"
|
ItemsSource="{Binding Devices}"
|
||||||
SelectedIndex="{Binding Device}" />
|
SelectedItem="{Binding SelectedDeviceItem, Mode=TwoWay}">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding ., Converter={x:Static helpers:InputDeviceNameConverter.Instance}}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
<Button
|
<Button
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
MinWidth="0"
|
MinWidth="0"
|
||||||
Margin="5,0,0,0"
|
Margin="5,0,0,0"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Command="{Binding LoadDevice}">
|
ToolTip.Tip="{ext:Locale ControllerSettingsRefresh}"
|
||||||
|
Command="{Binding RefreshInputDevices}">
|
||||||
<ui:SymbolIcon
|
<ui:SymbolIcon
|
||||||
Symbol="Refresh"
|
Symbol="Sync"
|
||||||
|
FontSize="15"
|
||||||
|
Height="20"/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
Grid.Column="3"
|
||||||
|
MinWidth="0"
|
||||||
|
Margin="5,0,0,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip.Tip="{ext:Locale ControllerSettingsResetKeybindsToDefault}"
|
||||||
|
Click="ResetCurrentDeviceToDefaultsButton_OnClick">
|
||||||
|
<ui:SymbolIcon
|
||||||
|
Symbol="Undo"
|
||||||
FontSize="15"
|
FontSize="15"
|
||||||
Height="20"/>
|
Height="20"/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Media;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using Ryujinx.Ava.Common.Locale;
|
using Ryujinx.Ava.Common.Locale;
|
||||||
using Ryujinx.Ava.Systems.Configuration;
|
using Ryujinx.Ava.Systems.Configuration;
|
||||||
@@ -15,9 +19,14 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
|
|
||||||
public InputView()
|
public InputView()
|
||||||
{
|
{
|
||||||
ViewModel = new InputViewModel(this, ConfigurationState.Instance.System.UseInputGlobalConfig);
|
ReplaceViewModel(ConfigurationState.Instance.System.UseInputGlobalConfig);
|
||||||
|
}
|
||||||
|
|
||||||
InitializeComponent();
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnAttachedToVisualTree(e);
|
||||||
|
|
||||||
|
ViewModel?.RetargetKeyboardDriver(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveCurrentProfile()
|
public void SaveCurrentProfile()
|
||||||
@@ -28,8 +37,18 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
public void ToggleLocalGlobalInput(bool enableConfigGlobal)
|
public void ToggleLocalGlobalInput(bool enableConfigGlobal)
|
||||||
{
|
{
|
||||||
Dispose();
|
Dispose();
|
||||||
ViewModel = new InputViewModel(this, enableConfigGlobal); // Create new Input Page with global input configs
|
ReplaceViewModel(enableConfigGlobal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReplaceViewModel(bool useGlobalConfig)
|
||||||
|
{
|
||||||
|
ViewModel = new InputViewModel(this, useGlobalConfig); // Create new Input Page with the selected input config scope.
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|
||||||
|
if (VisualRoot is not null)
|
||||||
|
{
|
||||||
|
ViewModel.RetargetKeyboardDriver(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
@@ -83,7 +102,56 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
if (sender is FAComboBox faComboBox)
|
if (sender is FAComboBox faComboBox)
|
||||||
{
|
{
|
||||||
faComboBox.IsDropDownOpen = false;
|
faComboBox.IsDropDownOpen = false;
|
||||||
ViewModel.IsModified = true;
|
ViewModel.RefreshModifiedState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ResetCurrentDeviceToDefaultsButton_OnClick(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!ViewModel.NeedsResetCurrentDeviceToDefaultsConfirmation())
|
||||||
|
{
|
||||||
|
ViewModel.ResetCurrentDeviceToDefaults();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Window owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
|
||||||
|
StackPanel content = new()
|
||||||
|
{
|
||||||
|
Spacing = 4,
|
||||||
|
MaxWidth = 360,
|
||||||
|
};
|
||||||
|
|
||||||
|
content.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = LocaleManager.Instance[LocaleKeys.DialogControllerSettingsResetKeybindsConfirmMessage],
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
MaxWidth = 360,
|
||||||
|
});
|
||||||
|
|
||||||
|
content.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = LocaleManager.Instance[LocaleKeys.DialogControllerSettingsResetKeybindsConfirmSubMessage],
|
||||||
|
TextWrapping = TextWrapping.Wrap,
|
||||||
|
MaxWidth = 360,
|
||||||
|
});
|
||||||
|
|
||||||
|
ContentDialog contentDialog = new ContentDialog
|
||||||
|
{
|
||||||
|
Title = LocaleManager.Instance[LocaleKeys.RyujinxConfirm],
|
||||||
|
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||||
|
CloseButtonText = LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||||
|
DefaultButton = ContentDialogButton.Primary,
|
||||||
|
Content = content,
|
||||||
|
}.ApplyStyles();
|
||||||
|
|
||||||
|
ContentDialogResult result = owner is not null
|
||||||
|
? await contentDialog.ShowAsync(owner)
|
||||||
|
: await ContentDialogHelper.ShowAsync(contentDialog);
|
||||||
|
|
||||||
|
if (result == ContentDialogResult.Primary)
|
||||||
|
{
|
||||||
|
ViewModel.ResetCurrentDeviceToDefaults();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ using Ryujinx.Input.Assigner;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Button = Ryujinx.Input.Button;
|
using Button = Ryujinx.Input.Button;
|
||||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Views.Input
|
namespace Ryujinx.Ava.UI.Views.Input
|
||||||
{
|
{
|
||||||
@@ -63,105 +63,108 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
|
|
||||||
PointerPressed += MouseClick;
|
PointerPressed += MouseClick;
|
||||||
|
|
||||||
|
KeyboardInputViewModel viewModel = ViewModel;
|
||||||
|
|
||||||
IKeyboard keyboard =
|
IKeyboard keyboard =
|
||||||
(IKeyboard)ViewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
|
(IKeyboard)viewModel.ParentModel.AvaloniaKeyboardDriver.GetGamepad("0"); // Open Avalonia keyboard for cancel operations.
|
||||||
IButtonAssigner assigner =
|
IButtonAssigner assigner =
|
||||||
new KeyboardKeyAssigner((IKeyboard)ViewModel.ParentModel.SelectedGamepad);
|
new KeyboardKeyAssigner((IKeyboard)viewModel.ParentModel.SelectedGamepad);
|
||||||
|
|
||||||
_currentAssigner.ButtonAssigned += (_, be) =>
|
_currentAssigner.ButtonAssigned += (_, be) =>
|
||||||
{
|
{
|
||||||
if (be.ButtonValue.HasValue)
|
if (be.ButtonValue.HasValue && IsActiveAssignmentContext(viewModel))
|
||||||
{
|
{
|
||||||
Button buttonValue = be.ButtonValue.Value;
|
Button buttonValue = be.ButtonValue.Value;
|
||||||
ViewModel.ParentModel.IsModified = true;
|
|
||||||
|
|
||||||
switch (button.Name)
|
switch (button.Name)
|
||||||
{
|
{
|
||||||
case "ButtonZl":
|
case "ButtonZl":
|
||||||
ViewModel.Config.ButtonZl = buttonValue.AsHidType<Key>();
|
viewModel.Config.ButtonZl = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonL":
|
case "ButtonL":
|
||||||
ViewModel.Config.ButtonL = buttonValue.AsHidType<Key>();
|
viewModel.Config.ButtonL = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonMinus":
|
case "ButtonMinus":
|
||||||
ViewModel.Config.ButtonMinus = buttonValue.AsHidType<Key>();
|
viewModel.Config.ButtonMinus = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftStickButton":
|
case "LeftStickButton":
|
||||||
ViewModel.Config.LeftStickButton = buttonValue.AsHidType<Key>();
|
viewModel.Config.LeftStickButton = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftStickUp":
|
case "LeftStickUp":
|
||||||
ViewModel.Config.LeftStickUp = buttonValue.AsHidType<Key>();
|
viewModel.Config.LeftStickUp = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftStickDown":
|
case "LeftStickDown":
|
||||||
ViewModel.Config.LeftStickDown = buttonValue.AsHidType<Key>();
|
viewModel.Config.LeftStickDown = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftStickRight":
|
case "LeftStickRight":
|
||||||
ViewModel.Config.LeftStickRight = buttonValue.AsHidType<Key>();
|
viewModel.Config.LeftStickRight = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftStickLeft":
|
case "LeftStickLeft":
|
||||||
ViewModel.Config.LeftStickLeft = buttonValue.AsHidType<Key>();
|
viewModel.Config.LeftStickLeft = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "DpadUp":
|
case "DpadUp":
|
||||||
ViewModel.Config.DpadUp = buttonValue.AsHidType<Key>();
|
viewModel.Config.DpadUp = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "DpadDown":
|
case "DpadDown":
|
||||||
ViewModel.Config.DpadDown = buttonValue.AsHidType<Key>();
|
viewModel.Config.DpadDown = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "DpadLeft":
|
case "DpadLeft":
|
||||||
ViewModel.Config.DpadLeft = buttonValue.AsHidType<Key>();
|
viewModel.Config.DpadLeft = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "DpadRight":
|
case "DpadRight":
|
||||||
ViewModel.Config.DpadRight = buttonValue.AsHidType<Key>();
|
viewModel.Config.DpadRight = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftButtonSr":
|
case "LeftButtonSr":
|
||||||
ViewModel.Config.LeftButtonSr = buttonValue.AsHidType<Key>();
|
viewModel.Config.LeftButtonSr = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "LeftButtonSl":
|
case "LeftButtonSl":
|
||||||
ViewModel.Config.LeftButtonSl = buttonValue.AsHidType<Key>();
|
viewModel.Config.LeftButtonSl = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightButtonSr":
|
case "RightButtonSr":
|
||||||
ViewModel.Config.RightButtonSr = buttonValue.AsHidType<Key>();
|
viewModel.Config.RightButtonSr = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightButtonSl":
|
case "RightButtonSl":
|
||||||
ViewModel.Config.RightButtonSl = buttonValue.AsHidType<Key>();
|
viewModel.Config.RightButtonSl = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonZr":
|
case "ButtonZr":
|
||||||
ViewModel.Config.ButtonZr = buttonValue.AsHidType<Key>();
|
viewModel.Config.ButtonZr = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonR":
|
case "ButtonR":
|
||||||
ViewModel.Config.ButtonR = buttonValue.AsHidType<Key>();
|
viewModel.Config.ButtonR = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonPlus":
|
case "ButtonPlus":
|
||||||
ViewModel.Config.ButtonPlus = buttonValue.AsHidType<Key>();
|
viewModel.Config.ButtonPlus = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonA":
|
case "ButtonA":
|
||||||
ViewModel.Config.ButtonA = buttonValue.AsHidType<Key>();
|
viewModel.Config.ButtonA = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonB":
|
case "ButtonB":
|
||||||
ViewModel.Config.ButtonB = buttonValue.AsHidType<Key>();
|
viewModel.Config.ButtonB = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonX":
|
case "ButtonX":
|
||||||
ViewModel.Config.ButtonX = buttonValue.AsHidType<Key>();
|
viewModel.Config.ButtonX = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "ButtonY":
|
case "ButtonY":
|
||||||
ViewModel.Config.ButtonY = buttonValue.AsHidType<Key>();
|
viewModel.Config.ButtonY = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightStickButton":
|
case "RightStickButton":
|
||||||
ViewModel.Config.RightStickButton = buttonValue.AsHidType<Key>();
|
viewModel.Config.RightStickButton = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightStickUp":
|
case "RightStickUp":
|
||||||
ViewModel.Config.RightStickUp = buttonValue.AsHidType<Key>();
|
viewModel.Config.RightStickUp = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightStickDown":
|
case "RightStickDown":
|
||||||
ViewModel.Config.RightStickDown = buttonValue.AsHidType<Key>();
|
viewModel.Config.RightStickDown = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightStickRight":
|
case "RightStickRight":
|
||||||
ViewModel.Config.RightStickRight = buttonValue.AsHidType<Key>();
|
viewModel.Config.RightStickRight = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
case "RightStickLeft":
|
case "RightStickLeft":
|
||||||
ViewModel.Config.RightStickLeft = buttonValue.AsHidType<Key>();
|
viewModel.Config.RightStickLeft = buttonValue.AsHidType<PhysicalKey>();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModel.ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -207,40 +210,40 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
{
|
{
|
||||||
Dictionary<string, Action> buttonActions = new()
|
Dictionary<string, Action> buttonActions = new()
|
||||||
{
|
{
|
||||||
{ "ButtonZl", () => ViewModel.Config.ButtonZl = Key.Unbound },
|
{ "ButtonZl", () => ViewModel.Config.ButtonZl = PhysicalKey.Unbound },
|
||||||
{ "ButtonL", () => ViewModel.Config.ButtonL = Key.Unbound },
|
{ "ButtonL", () => ViewModel.Config.ButtonL = PhysicalKey.Unbound },
|
||||||
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = Key.Unbound },
|
{ "ButtonMinus", () => ViewModel.Config.ButtonMinus = PhysicalKey.Unbound },
|
||||||
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = Key.Unbound },
|
{ "LeftStickButton", () => ViewModel.Config.LeftStickButton = PhysicalKey.Unbound },
|
||||||
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = Key.Unbound },
|
{ "LeftStickUp", () => ViewModel.Config.LeftStickUp = PhysicalKey.Unbound },
|
||||||
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = Key.Unbound },
|
{ "LeftStickDown", () => ViewModel.Config.LeftStickDown = PhysicalKey.Unbound },
|
||||||
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = Key.Unbound },
|
{ "LeftStickRight", () => ViewModel.Config.LeftStickRight = PhysicalKey.Unbound },
|
||||||
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = Key.Unbound },
|
{ "LeftStickLeft", () => ViewModel.Config.LeftStickLeft = PhysicalKey.Unbound },
|
||||||
{ "DpadUp", () => ViewModel.Config.DpadUp = Key.Unbound },
|
{ "DpadUp", () => ViewModel.Config.DpadUp = PhysicalKey.Unbound },
|
||||||
{ "DpadDown", () => ViewModel.Config.DpadDown = Key.Unbound },
|
{ "DpadDown", () => ViewModel.Config.DpadDown = PhysicalKey.Unbound },
|
||||||
{ "DpadLeft", () => ViewModel.Config.DpadLeft = Key.Unbound },
|
{ "DpadLeft", () => ViewModel.Config.DpadLeft = PhysicalKey.Unbound },
|
||||||
{ "DpadRight", () => ViewModel.Config.DpadRight = Key.Unbound },
|
{ "DpadRight", () => ViewModel.Config.DpadRight = PhysicalKey.Unbound },
|
||||||
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = Key.Unbound },
|
{ "LeftButtonSr", () => ViewModel.Config.LeftButtonSr = PhysicalKey.Unbound },
|
||||||
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = Key.Unbound },
|
{ "LeftButtonSl", () => ViewModel.Config.LeftButtonSl = PhysicalKey.Unbound },
|
||||||
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = Key.Unbound },
|
{ "RightButtonSr", () => ViewModel.Config.RightButtonSr = PhysicalKey.Unbound },
|
||||||
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = Key.Unbound },
|
{ "RightButtonSl", () => ViewModel.Config.RightButtonSl = PhysicalKey.Unbound },
|
||||||
{ "ButtonZr", () => ViewModel.Config.ButtonZr = Key.Unbound },
|
{ "ButtonZr", () => ViewModel.Config.ButtonZr = PhysicalKey.Unbound },
|
||||||
{ "ButtonR", () => ViewModel.Config.ButtonR = Key.Unbound },
|
{ "ButtonR", () => ViewModel.Config.ButtonR = PhysicalKey.Unbound },
|
||||||
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = Key.Unbound },
|
{ "ButtonPlus", () => ViewModel.Config.ButtonPlus = PhysicalKey.Unbound },
|
||||||
{ "ButtonA", () => ViewModel.Config.ButtonA = Key.Unbound },
|
{ "ButtonA", () => ViewModel.Config.ButtonA = PhysicalKey.Unbound },
|
||||||
{ "ButtonB", () => ViewModel.Config.ButtonB = Key.Unbound },
|
{ "ButtonB", () => ViewModel.Config.ButtonB = PhysicalKey.Unbound },
|
||||||
{ "ButtonX", () => ViewModel.Config.ButtonX = Key.Unbound },
|
{ "ButtonX", () => ViewModel.Config.ButtonX = PhysicalKey.Unbound },
|
||||||
{ "ButtonY", () => ViewModel.Config.ButtonY = Key.Unbound },
|
{ "ButtonY", () => ViewModel.Config.ButtonY = PhysicalKey.Unbound },
|
||||||
{ "RightStickButton", () => ViewModel.Config.RightStickButton = Key.Unbound },
|
{ "RightStickButton", () => ViewModel.Config.RightStickButton = PhysicalKey.Unbound },
|
||||||
{ "RightStickUp", () => ViewModel.Config.RightStickUp = Key.Unbound },
|
{ "RightStickUp", () => ViewModel.Config.RightStickUp = PhysicalKey.Unbound },
|
||||||
{ "RightStickDown", () => ViewModel.Config.RightStickDown = Key.Unbound },
|
{ "RightStickDown", () => ViewModel.Config.RightStickDown = PhysicalKey.Unbound },
|
||||||
{ "RightStickRight", () => ViewModel.Config.RightStickRight = Key.Unbound },
|
{ "RightStickRight", () => ViewModel.Config.RightStickRight = PhysicalKey.Unbound },
|
||||||
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = Key.Unbound }
|
{ "RightStickLeft", () => ViewModel.Config.RightStickLeft = PhysicalKey.Unbound }
|
||||||
};
|
};
|
||||||
|
|
||||||
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
|
if (buttonActions.TryGetValue(_currentAssigner.ToggledButton.Name, out Action action))
|
||||||
{
|
{
|
||||||
action();
|
action();
|
||||||
ViewModel.ParentModel.IsModified = true;
|
ViewModel.ParentModel.RefreshModifiedState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -251,5 +254,10 @@ namespace Ryujinx.Ava.UI.Views.Input
|
|||||||
_currentAssigner?.Cancel();
|
_currentAssigner?.Cancel();
|
||||||
_currentAssigner = null;
|
_currentAssigner = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool IsActiveAssignmentContext(KeyboardInputViewModel viewModel)
|
||||||
|
{
|
||||||
|
return VisualRoot is not null && ReferenceEquals(DataContext, viewModel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ namespace Ryujinx.Ava.UI.Views.Settings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this);
|
_avaloniaKeyboardDriver = new AvaloniaKeyboardDriver(this, KeyboardInputMode.Semantic);
|
||||||
|
_avaloniaKeyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
protected override void OnPointerReleased(PointerReleasedEventArgs e)
|
||||||
|
|||||||
@@ -1,17 +1,65 @@
|
|||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Ryujinx.Ava.UI.Windows;
|
||||||
|
|
||||||
namespace Ryujinx.Ava.UI.Views.Settings
|
namespace Ryujinx.Ava.UI.Views.Settings
|
||||||
{
|
{
|
||||||
public partial class SettingsInputView : UserControl
|
public partial class SettingsInputView : UserControl
|
||||||
{
|
{
|
||||||
|
private bool _inputUpdatesBlocked;
|
||||||
|
|
||||||
public SettingsInputView()
|
public SettingsInputView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnAttachedToVisualTree(e);
|
||||||
|
SetInputUpdatesBlocked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
SetInputUpdatesBlocked(false);
|
||||||
|
base.OnDetachedFromVisualTree(e);
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
InputView.Dispose();
|
try
|
||||||
|
{
|
||||||
|
InputView.Dispose();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetInputUpdatesBlocked(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetInputUpdatesBlocked(bool blocked)
|
||||||
|
{
|
||||||
|
if (_inputUpdatesBlocked == blocked)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MainWindow? mainWindow = RyujinxApp.MainWindow;
|
||||||
|
if (mainWindow?.ViewModel?.AppHost?.NpadManager is not { } npadManager)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blocked)
|
||||||
|
{
|
||||||
|
npadManager.BlockInputUpdates();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
npadManager.UnblockInputUpdates();
|
||||||
|
}
|
||||||
|
|
||||||
|
_inputUpdatesBlocked = blocked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ using Ryujinx.HLE.HOS;
|
|||||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||||
using Ryujinx.Input.HLE;
|
using Ryujinx.Input.HLE;
|
||||||
using Ryujinx.Input.SDL3;
|
using Ryujinx.Input.SDL3;
|
||||||
|
using Ryujinx.Input;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -107,7 +108,9 @@ namespace Ryujinx.Ava.UI.Windows
|
|||||||
|
|
||||||
if (Program.PreviewerDetached)
|
if (Program.PreviewerDetached)
|
||||||
{
|
{
|
||||||
InputManager = new InputManager(new AvaloniaKeyboardDriver(this), new SDL3GamepadDriver());
|
AvaloniaKeyboardDriver keyboardDriver = new(this, KeyboardInputMode.Semantic);
|
||||||
|
keyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
|
||||||
|
InputManager = new InputManager(keyboardDriver, new SDL3GamepadDriver());
|
||||||
|
|
||||||
_ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it);
|
_ = this.GetObservable(IsActiveProperty).Subscribe(it => ViewModel.IsActive = it);
|
||||||
this.ScalingChanged += OnScalingChanged;
|
this.ScalingChanged += OnScalingChanged;
|
||||||
|
|||||||
Reference in New Issue
Block a user