mirror of
https://git.naxdy.org/Mirror/Ryujinx.git
synced 2026-07-26 19:12:09 +00:00
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
1356 lines
46 KiB
C#
1356 lines
46 KiB
C#
using Avalonia.Collections;
|
|
using Avalonia.Controls;
|
|
using Avalonia.Svg.Skia;
|
|
using Avalonia.Threading;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using Gommon;
|
|
using Ryujinx.Ava.Common.Locale;
|
|
using Ryujinx.Ava.Input;
|
|
using Ryujinx.Ava.Systems.Configuration;
|
|
using Ryujinx.Ava.UI.Helpers;
|
|
using Ryujinx.Ava.UI.Models;
|
|
using Ryujinx.Ava.UI.Models.Input;
|
|
using Ryujinx.Ava.UI.Windows;
|
|
using Ryujinx.Common;
|
|
using Ryujinx.Common.Configuration;
|
|
using Ryujinx.Common.Configuration.Hid;
|
|
using Ryujinx.Common.Configuration.Hid.Controller;
|
|
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
|
using Ryujinx.Common.Logging;
|
|
using Ryujinx.Common.Utilities;
|
|
using Ryujinx.Input;
|
|
using Ryujinx.Input.SDL3;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace Ryujinx.Ava.UI.ViewModels.Input
|
|
{
|
|
public partial class InputViewModel : BaseModel, IDisposable
|
|
{
|
|
private const string Disabled = "disabled";
|
|
private const string ProControllerResource = "Ryujinx/Assets/Icons/Controller_ProCon.svg";
|
|
private const string JoyConPairResource = "Ryujinx/Assets/Icons/Controller_JoyConPair.svg";
|
|
private const string JoyConLeftResource = "Ryujinx/Assets/Icons/Controller_JoyConLeft.svg";
|
|
private const string JoyConRightResource = "Ryujinx/Assets/Icons/Controller_JoyConRight.svg";
|
|
private const string KeyboardString = "keyboard";
|
|
private const string ControllerString = "controller";
|
|
private readonly MainWindow _mainWindow;
|
|
private Control _keyboardDriverControl;
|
|
|
|
private PlayerIndex _playerId;
|
|
private PlayerIndex _playerIdChoose;
|
|
private int _controller;
|
|
private string _controllerImage;
|
|
private int _device;
|
|
private bool _isChangeTrackingActive;
|
|
[ObservableProperty]
|
|
public partial bool IsModified { get; set; }
|
|
|
|
[ObservableProperty]
|
|
public partial string ProfileName { get; set; }
|
|
|
|
[ObservableProperty]
|
|
public partial bool NotificationIsVisible { get; set; } // Automatically call the NotificationView property with OnPropertyChanged()
|
|
|
|
[ObservableProperty]
|
|
public partial string NotificationText { get; set; } // Automatically call the NotificationText property with OnPropertyChanged()
|
|
|
|
private bool _isLoaded;
|
|
|
|
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
|
|
|
public IGamepadDriver AvaloniaKeyboardDriver { get; private set; }
|
|
|
|
public IGamepad SelectedGamepad
|
|
{
|
|
get;
|
|
private set
|
|
{
|
|
if (!ReferenceEquals(field, value))
|
|
{
|
|
field?.Dispose();
|
|
}
|
|
|
|
Rainbow.Reset();
|
|
|
|
field = value;
|
|
|
|
if ((field?.Features & GamepadFeaturesFlag.Led) != 0 &&
|
|
ConfigViewModel is ControllerInputViewModel { Config.UseRainbowLed: true })
|
|
Rainbow.Updated += (ref Color color) => field.SetLed((uint)color.ToArgb());
|
|
|
|
OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed));
|
|
}
|
|
}
|
|
|
|
public StickVisualizer VisualStick { get; private set; }
|
|
|
|
public ObservableCollection<PlayerModel> PlayerIndexes { get; set; }
|
|
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
|
|
internal ObservableCollection<ControllerModel> Controllers { get; set; }
|
|
public AvaloniaList<string> ProfilesList { get; set; }
|
|
|
|
public bool UseGlobalConfig;
|
|
|
|
// XAML Flags
|
|
public bool ShowSettings => _device > 0;
|
|
public bool IsController => _device > 1;
|
|
public bool IsKeyboard => !IsController;
|
|
public bool IsRight { get; set; }
|
|
public bool IsLeft { get; set; }
|
|
public bool HasLed => (SelectedGamepad.Features & GamepadFeaturesFlag.Led) != 0;
|
|
public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense");
|
|
|
|
public event Action NotifyChangesEvent;
|
|
|
|
public string ChosenProfile
|
|
{
|
|
get;
|
|
set
|
|
{
|
|
// When you select a profile, the settings from the profile will be applied.
|
|
// To save the settings, you still need to click the apply button
|
|
field = value;
|
|
LoadProfile();
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public object ConfigViewModel
|
|
{
|
|
get;
|
|
set
|
|
{
|
|
field = value;
|
|
|
|
VisualStick.UpdateConfig(value);
|
|
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public PlayerIndex PlayerIdChoose
|
|
{
|
|
get => _playerIdChoose;
|
|
set { }
|
|
}
|
|
|
|
public PlayerIndex PlayerId
|
|
{
|
|
get => _playerId;
|
|
set
|
|
{
|
|
if (IsModified)
|
|
{
|
|
_playerIdChoose = value;
|
|
return;
|
|
}
|
|
|
|
IsModified = false;
|
|
_playerId = value;
|
|
_isChangeTrackingActive = false;
|
|
|
|
if (!Enum.IsDefined<PlayerIndex>(_playerId))
|
|
{
|
|
_playerId = PlayerIndex.Player1;
|
|
|
|
}
|
|
|
|
_isLoaded = false;
|
|
LoadConfiguration();
|
|
LoadDevice();
|
|
LoadProfiles();
|
|
|
|
_isLoaded = true;
|
|
_isChangeTrackingActive = true;
|
|
OnPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public int Controller
|
|
{
|
|
get => _controller;
|
|
set
|
|
{
|
|
int controllerIndex = value < 0 ? 0 : value;
|
|
|
|
if (controllerIndex == _controller)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ApplyControllerSelection(controllerIndex);
|
|
RefreshModifiedState();
|
|
}
|
|
}
|
|
|
|
private void ApplyControllerSelection(int controllerIndex)
|
|
{
|
|
_controller = controllerIndex;
|
|
|
|
if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1)
|
|
{
|
|
ControllerType controller = Controllers[_controller].Type;
|
|
|
|
IsLeft = true;
|
|
IsRight = true;
|
|
|
|
switch (controller)
|
|
{
|
|
case ControllerType.Handheld:
|
|
ControllerImage = JoyConPairResource;
|
|
break;
|
|
case ControllerType.ProController:
|
|
ControllerImage = ProControllerResource;
|
|
break;
|
|
case ControllerType.JoyconPair:
|
|
ControllerImage = JoyConPairResource;
|
|
break;
|
|
case ControllerType.JoyconLeft:
|
|
ControllerImage = JoyConLeftResource;
|
|
IsRight = false;
|
|
break;
|
|
case ControllerType.JoyconRight:
|
|
ControllerImage = JoyConRightResource;
|
|
IsLeft = false;
|
|
break;
|
|
}
|
|
|
|
LoadInputDriver();
|
|
LoadProfiles();
|
|
}
|
|
|
|
OnPropertyChanged(nameof(Controller));
|
|
NotifyChanges();
|
|
}
|
|
|
|
public string ControllerImage
|
|
{
|
|
get => _controllerImage;
|
|
set
|
|
{
|
|
_controllerImage = value;
|
|
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(Image));
|
|
}
|
|
}
|
|
|
|
public SvgImage Image
|
|
{
|
|
get
|
|
{
|
|
SvgImage image = new();
|
|
|
|
if (!string.IsNullOrWhiteSpace(_controllerImage))
|
|
{
|
|
SvgSource source = SvgSource.LoadFromStream(EmbeddedResources.GetStream(_controllerImage));
|
|
|
|
image.Source = source;
|
|
}
|
|
|
|
return image;
|
|
}
|
|
}
|
|
|
|
public int Device
|
|
{
|
|
get => _device;
|
|
set
|
|
{
|
|
if (value < 0 || value >= Devices.Count)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_device = value;
|
|
|
|
DeviceType selected = Devices[_device].Type;
|
|
|
|
if (selected != DeviceType.None)
|
|
{
|
|
if (_isLoaded)
|
|
{
|
|
LoadSelectedDeviceDefaults();
|
|
}
|
|
else if (_device < Devices.Count)
|
|
{
|
|
LoadControllers();
|
|
}
|
|
}
|
|
|
|
RefreshModifiedState();
|
|
FindPairedDeviceInConfigFile();
|
|
OnPropertyChanged();
|
|
OnPropertyChanged(nameof(SelectedDeviceItem));
|
|
NotifyChanges();
|
|
}
|
|
}
|
|
|
|
public bool NeedsResetCurrentDeviceToDefaultsConfirmation()
|
|
{
|
|
if (_device <= 0 || _device >= Devices.Count || Devices[_device].Type == DeviceType.None)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
InputConfig defaultConfig = LoadDefaultConfiguration();
|
|
InputConfig currentConfig = GetSelectedDeviceConfig();
|
|
|
|
return !ConfigsMatch(currentConfig, defaultConfig);
|
|
}
|
|
|
|
public void ResetCurrentDeviceToDefaults()
|
|
{
|
|
RefreshAvailableDevices();
|
|
|
|
if (_device <= 0 || _device >= Devices.Count || Devices[_device].Type == DeviceType.None)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LoadSelectedDeviceDefaults();
|
|
RefreshModifiedState();
|
|
FindPairedDeviceInConfigFile();
|
|
NotifyChanges();
|
|
}
|
|
|
|
public void RefreshInputDevices()
|
|
{
|
|
RefreshAvailableDevices();
|
|
}
|
|
|
|
public object SelectedDeviceItem
|
|
{
|
|
get => _device >= 0 && _device < Devices.Count ? Devices[_device] : null;
|
|
set
|
|
{
|
|
if (value is not ValueTuple<DeviceType, string, string> selectedDevice)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int deviceIndex = -1;
|
|
for (int i = 0; i < Devices.Count; i++)
|
|
{
|
|
(DeviceType Type, string Id, string Name) device = Devices[i];
|
|
if (device.Type == selectedDevice.Item1 && device.Id == selectedDevice.Item2)
|
|
{
|
|
deviceIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (deviceIndex < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (deviceIndex == _device)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Device = deviceIndex;
|
|
}
|
|
}
|
|
|
|
public InputConfig Config { get; set; }
|
|
|
|
public InputViewModel(UserControl owner, bool useGlobal = false) : this()
|
|
{
|
|
if (Program.PreviewerDetached)
|
|
{
|
|
_mainWindow = RyujinxApp.MainWindow;
|
|
|
|
ReplaceKeyboardDriver(owner);
|
|
PhysicalKeyLabelHelper.LabelsChanged += OnPhysicalKeyLabelsChanged;
|
|
|
|
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
|
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
|
|
|
UseGlobalConfig = useGlobal;
|
|
|
|
_isLoaded = false;
|
|
|
|
RefreshAvailableDevices();
|
|
|
|
PlayerId = PlayerIndex.Player1;
|
|
}
|
|
|
|
_isChangeTrackingActive = true;
|
|
}
|
|
|
|
public void RetargetKeyboardDriver(Control owner)
|
|
{
|
|
if (!Program.PreviewerDetached)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ReplaceKeyboardDriver(owner);
|
|
}
|
|
|
|
public InputViewModel()
|
|
{
|
|
PlayerIndexes = [];
|
|
Controllers = [];
|
|
Devices = [];
|
|
ProfilesList = [];
|
|
VisualStick = new StickVisualizer(this);
|
|
|
|
ControllerImage = ProControllerResource;
|
|
|
|
PlayerIndexes.Add(new(PlayerIndex.Player1, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer1]));
|
|
PlayerIndexes.Add(new(PlayerIndex.Player2, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer2]));
|
|
PlayerIndexes.Add(new(PlayerIndex.Player3, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer3]));
|
|
PlayerIndexes.Add(new(PlayerIndex.Player4, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer4]));
|
|
PlayerIndexes.Add(new(PlayerIndex.Player5, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer5]));
|
|
PlayerIndexes.Add(new(PlayerIndex.Player6, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer6]));
|
|
PlayerIndexes.Add(new(PlayerIndex.Player7, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer7]));
|
|
PlayerIndexes.Add(new(PlayerIndex.Player8, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer8]));
|
|
PlayerIndexes.Add(new(PlayerIndex.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsHandheld]));
|
|
}
|
|
|
|
|
|
|
|
private InputConfig GetPersistedInputConfig()
|
|
{
|
|
if (UseGlobalConfig && Program.UseExtraConfig)
|
|
{
|
|
return ConfigurationState.InstanceExtra.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
|
|
}
|
|
|
|
return ConfigurationState.Instance.Hid.InputConfig.Value.FirstOrDefault(inputConfig => inputConfig.PlayerIndex == _playerId);
|
|
}
|
|
|
|
private void LoadConfiguration(InputConfig inputConfig = null)
|
|
{
|
|
Config = inputConfig ?? GetDisplayedInputConfig(GetPersistedInputConfig());
|
|
ConfigViewModel = null;
|
|
|
|
if (Config is StandardKeyboardInputConfig keyboardInputConfig)
|
|
{
|
|
ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig), VisualStick);
|
|
}
|
|
|
|
if (Config is StandardControllerInputConfig controllerInputConfig)
|
|
{
|
|
ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig), VisualStick);
|
|
}
|
|
}
|
|
|
|
private InputConfig GetDisplayedInputConfig(InputConfig persistedConfig)
|
|
{
|
|
if (persistedConfig is not StandardControllerInputConfig controllerConfig)
|
|
{
|
|
return persistedConfig;
|
|
}
|
|
|
|
// If runtime has already fallen back to keyboard, reflect that active config in settings
|
|
// instead of showing the stale persisted controller config.
|
|
InputConfig activeConfig = _mainWindow?.ViewModel.AppHost?.NpadManager?.GetPlayerInputConfigByIndex((int)_playerId);
|
|
|
|
if (activeConfig is StandardKeyboardInputConfig)
|
|
{
|
|
return activeConfig;
|
|
}
|
|
|
|
// When no game is running (NpadManager unavailable) and the persisted controller
|
|
// device isn't currently connected, fall back to keyboard so the user isn't
|
|
// stuck on "Disabled".
|
|
if (activeConfig == null &&
|
|
!Devices.Any(device => device.Type == DeviceType.Controller && device.Id == controllerConfig.Id) &&
|
|
TryCreateKeyboardFallbackConfig(persistedConfig, out StandardKeyboardInputConfig fallbackConfig))
|
|
{
|
|
return fallbackConfig;
|
|
}
|
|
|
|
return persistedConfig;
|
|
}
|
|
|
|
private void FindPairedDeviceInConfigFile()
|
|
{
|
|
// This function allows you to output a message about the device configuration found in the file
|
|
// NOTE: if the configuration is found, we display the message "Waiting for controller connection",
|
|
// but only if the id gamepad belongs to the selected player
|
|
|
|
NotificationIsVisible =
|
|
Config != null &&
|
|
!Devices.Any(device => device.Id == Config.Id) &&
|
|
Config.PlayerIndex == PlayerId;
|
|
if (NotificationIsVisible)
|
|
{
|
|
if (string.IsNullOrEmpty(Config.Name))
|
|
{
|
|
NotificationText = $"{LocaleManager.Instance[LocaleKeys.ControllerSettingsWaitingConnectDevice].Format("No information", Config.Id)}";
|
|
}
|
|
else
|
|
{
|
|
NotificationText = $"{LocaleManager.Instance[LocaleKeys.ControllerSettingsWaitingConnectDevice].Format(Config.Name, Config.Id)}";
|
|
}
|
|
}
|
|
}
|
|
|
|
public void UnlinkDevice()
|
|
{
|
|
// "Disabled" mode is available after unbinding the device
|
|
// NOTE: the IsModified flag to be able to apply the settings.
|
|
NotificationIsVisible = false;
|
|
IsModified = true;
|
|
}
|
|
|
|
public void LoadDevice()
|
|
{
|
|
int deviceIndex = 0;
|
|
|
|
if (Config == null || Config.Backend == InputBackendType.Invalid)
|
|
{
|
|
ApplyLoadedDevice(deviceIndex);
|
|
return;
|
|
}
|
|
|
|
DeviceType type = DeviceType.None;
|
|
|
|
if (Config is StandardKeyboardInputConfig)
|
|
{
|
|
type = DeviceType.Keyboard;
|
|
}
|
|
|
|
if (Config is StandardControllerInputConfig)
|
|
{
|
|
type = DeviceType.Controller;
|
|
}
|
|
|
|
for (int i = 0; i < Devices.Count; i++)
|
|
{
|
|
if (Devices[i].Type == type && Devices[i].Id == Config.Id)
|
|
{
|
|
deviceIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ApplyLoadedDevice(deviceIndex);
|
|
}
|
|
|
|
private void ApplyLoadedDevice(int deviceIndex)
|
|
{
|
|
_device = deviceIndex is >= 0 and < int.MaxValue ? deviceIndex : 0;
|
|
|
|
if (_device >= Devices.Count)
|
|
{
|
|
_device = 0;
|
|
}
|
|
|
|
if (_device > 0 && Devices[_device].Type != DeviceType.None)
|
|
{
|
|
LoadControllers();
|
|
}
|
|
|
|
FindPairedDeviceInConfigFile();
|
|
OnPropertyChanged(nameof(Device));
|
|
OnPropertyChanged(nameof(SelectedDeviceItem));
|
|
NotifyChanges();
|
|
}
|
|
|
|
private void LoadSelectedDeviceDefaults()
|
|
{
|
|
if (_device > 0 && _device < Devices.Count && Devices[_device].Type != DeviceType.None)
|
|
{
|
|
LoadControllers();
|
|
}
|
|
|
|
LoadConfiguration(LoadDefaultConfiguration());
|
|
}
|
|
|
|
public void RefreshModifiedState()
|
|
{
|
|
if (!_isChangeTrackingActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
IsModified = !ConfigsMatch(GetSelectedDeviceConfig(), GetDisplayedInputConfig(GetPersistedInputConfig()));
|
|
}
|
|
|
|
private static bool ConfigsMatch(InputConfig currentConfig, InputConfig otherConfig)
|
|
{
|
|
if (currentConfig == null || otherConfig == null)
|
|
{
|
|
return currentConfig == otherConfig;
|
|
}
|
|
|
|
return SerializeComparableConfig(currentConfig) ==
|
|
SerializeComparableConfig(otherConfig);
|
|
}
|
|
|
|
private static string SerializeComparableConfig(InputConfig config)
|
|
{
|
|
InputConfig comparableConfig =
|
|
JsonHelper.Deserialize(
|
|
JsonHelper.Serialize(config, _serializerContext.InputConfig),
|
|
_serializerContext.InputConfig);
|
|
|
|
comparableConfig.Name = string.Empty;
|
|
|
|
if (comparableConfig is StandardControllerInputConfig controllerConfig &&
|
|
controllerConfig.Led is { EnableLed: false, TurnOffLed: false, UseRainbow: false, LedColor: 0 })
|
|
{
|
|
controllerConfig.Led = null;
|
|
}
|
|
|
|
return JsonHelper.Serialize(comparableConfig, _serializerContext.InputConfig);
|
|
}
|
|
|
|
private InputConfig GetSelectedDeviceConfig()
|
|
{
|
|
if (!TryGetCurrentDevice(out (DeviceType Type, string Id, string Name) device) || device.Type == DeviceType.None)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
InputConfig config = device.Type switch
|
|
{
|
|
DeviceType.Keyboard => (ConfigViewModel as KeyboardInputViewModel)?.Config.GetConfig(),
|
|
DeviceType.Controller => (ConfigViewModel as ControllerInputViewModel)?.Config.GetConfig(),
|
|
_ => null,
|
|
};
|
|
|
|
if (config == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
config.Id = GetConfigDeviceId(device);
|
|
config.Name = device.Name;
|
|
config.PlayerIndex = _playerId;
|
|
config.ControllerType = Controllers[_controller].Type;
|
|
|
|
return config;
|
|
}
|
|
|
|
private void LoadInputDriver()
|
|
{
|
|
if (!TryGetCurrentDevice(out (DeviceType Type, string Id, string Name) device))
|
|
{
|
|
SelectedGamepad = null;
|
|
return;
|
|
}
|
|
|
|
string id = GetGamepadId(device);
|
|
DeviceType type = device.Type;
|
|
|
|
if (type == DeviceType.None)
|
|
{
|
|
SelectedGamepad = null;
|
|
return;
|
|
}
|
|
|
|
if (type == DeviceType.Keyboard)
|
|
{
|
|
if (_mainWindow.InputManager.KeyboardDriver is AvaloniaKeyboardDriver)
|
|
{
|
|
// NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused...
|
|
SelectedGamepad = AvaloniaKeyboardDriver.GetGamepad(id);
|
|
}
|
|
else
|
|
{
|
|
SelectedGamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SelectedGamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
|
|
}
|
|
}
|
|
|
|
private void HandleOnGamepadDisconnected(string id)
|
|
{
|
|
_isChangeTrackingActive = false; // Disable configuration change tracking
|
|
|
|
bool selectedControllerDisconnected =
|
|
TryGetCurrentDevice(out (DeviceType Type, string Id, string Name) currentDevice) &&
|
|
currentDevice.Type == DeviceType.Controller &&
|
|
string.Equals(GetGamepadId(currentDevice), id, StringComparison.Ordinal);
|
|
|
|
RefreshAvailableDevices();
|
|
|
|
InputConfig persistedConfig = GetPersistedInputConfig();
|
|
InputConfig displayedConfig = GetDisplayedInputConfig(persistedConfig);
|
|
bool shouldApplyKeyboardFallback =
|
|
selectedControllerDisconnected ||
|
|
displayedConfig is StandardKeyboardInputConfig;
|
|
|
|
if (shouldApplyKeyboardFallback)
|
|
{
|
|
if (selectedControllerDisconnected &&
|
|
displayedConfig is not StandardKeyboardInputConfig &&
|
|
TryCreateKeyboardFallbackConfig(persistedConfig, out StandardKeyboardInputConfig fallbackConfig))
|
|
{
|
|
displayedConfig = fallbackConfig;
|
|
}
|
|
|
|
LoadConfiguration(displayedConfig);
|
|
LoadDevice();
|
|
LoadProfiles();
|
|
FindPairedDeviceInConfigFile();
|
|
IsModified = false;
|
|
NotifyChanges();
|
|
}
|
|
else
|
|
{
|
|
IsModified = true;
|
|
RevertChanges();
|
|
FindPairedDeviceInConfigFile();
|
|
}
|
|
|
|
_isChangeTrackingActive = true; // Enable configuration change tracking
|
|
|
|
}
|
|
|
|
private async void HandleOnGamepadConnected(string id)
|
|
{
|
|
_isChangeTrackingActive = false; // Disable configuration change tracking
|
|
|
|
try
|
|
{
|
|
InputConfig persistedConfig = GetPersistedInputConfig();
|
|
bool shouldRestoreControllerAfterFallback =
|
|
Config is StandardKeyboardInputConfig &&
|
|
persistedConfig is StandardControllerInputConfig;
|
|
|
|
if (shouldRestoreControllerAfterFallback)
|
|
{
|
|
const int reconnectRestoreAttempts = 20;
|
|
const int reconnectRestoreDelayMs = 250;
|
|
|
|
string controllerId = persistedConfig.Id;
|
|
|
|
for (int attempt = 0; attempt < reconnectRestoreAttempts; attempt++)
|
|
{
|
|
RefreshAvailableDevices();
|
|
|
|
if (Devices.Any(device => device.Type == DeviceType.Controller && device.Id == controllerId))
|
|
{
|
|
IsModified = true;
|
|
RevertChanges();
|
|
return;
|
|
}
|
|
|
|
await Task.Delay(reconnectRestoreDelayMs);
|
|
}
|
|
}
|
|
|
|
RefreshAvailableDevices();
|
|
|
|
IsModified = true;
|
|
RevertChanges();
|
|
}
|
|
finally
|
|
{
|
|
_isChangeTrackingActive = true;// Enable configuration change tracking
|
|
}
|
|
}
|
|
|
|
private bool TryGetCurrentDevice(out (DeviceType Type, string Id, string Name) device)
|
|
{
|
|
if (_device < 0 || _device >= Devices.Count)
|
|
{
|
|
device = default;
|
|
return false;
|
|
}
|
|
|
|
device = Devices[_device];
|
|
return true;
|
|
}
|
|
|
|
private static string GetGamepadId((DeviceType Type, string Id, string Name) device)
|
|
{
|
|
return device.Type == DeviceType.None ? null : device.Id.Split(" ")[0];
|
|
}
|
|
|
|
// Keyboard configs keep the full ID, while displayed controller entries include
|
|
// the user-facing numbered suffix and need normalization before persistence/lookup.
|
|
private static string GetConfigDeviceId((DeviceType Type, string Id, string Name) device)
|
|
{
|
|
return device.Type switch
|
|
{
|
|
DeviceType.Keyboard => device.Id,
|
|
DeviceType.Controller => GetGamepadId(device),
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
public void LoadControllers()
|
|
{
|
|
Controllers.Clear();
|
|
|
|
if (_playerId == PlayerIndex.Handheld)
|
|
{
|
|
Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeHandheld]));
|
|
|
|
ApplyControllerSelection(0);
|
|
}
|
|
else
|
|
{
|
|
Controllers.Add(new(ControllerType.ProController, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeProController]));
|
|
Controllers.Add(new(ControllerType.JoyconPair, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConPair]));
|
|
Controllers.Add(new(ControllerType.JoyconLeft, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConLeft]));
|
|
Controllers.Add(new(ControllerType.JoyconRight, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConRight]));
|
|
|
|
if (Config != null)
|
|
{
|
|
int controllerIndex = -1;
|
|
for (int i = 0; i < Controllers.Count; i++)
|
|
{
|
|
if (Controllers[i].Type == Config.ControllerType)
|
|
{
|
|
controllerIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (controllerIndex != -1)
|
|
{
|
|
// Avalonia bug: setting a newly instanced ComboBox to 0
|
|
// causes the selected item to show up blank.
|
|
// Workaround: set the box to 1 and then 0.
|
|
// See: https://github.com/AvaloniaUI/Avalonia/issues/4610
|
|
// https://github.com/AvaloniaUI/Avalonia/discussions/18834
|
|
if (controllerIndex == 0)
|
|
{
|
|
ApplyControllerSelection(1);
|
|
}
|
|
|
|
ApplyControllerSelection(controllerIndex);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ApplyControllerSelection(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string GetShortGamepadName(string str)
|
|
{
|
|
const string Ellipsis = "...";
|
|
const int MaxSize = 50;
|
|
|
|
if (str.Length > MaxSize)
|
|
{
|
|
return $"{str.AsSpan(0, MaxSize - Ellipsis.Length)}{Ellipsis}";
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
private void RefreshAvailableDevices()
|
|
{
|
|
int selectedDeviceIndex = 0;
|
|
(DeviceType Type, string Id, string Name) selectedDevice = default;
|
|
|
|
if (_device >= 0 && _device < Devices.Count)
|
|
{
|
|
selectedDevice = Devices[_device];
|
|
}
|
|
|
|
string GetGamepadName(IGamepad gamepad, int controllerNumber)
|
|
{
|
|
return $"{GetShortGamepadName(gamepad.Name)} ({controllerNumber})";
|
|
}
|
|
|
|
string GetUniqueGamepadName(IGamepad gamepad, ref int controllerNumber)
|
|
{
|
|
string name = GetGamepadName(gamepad, controllerNumber);
|
|
if (Devices.Any(controller => controller.Name == name))
|
|
{
|
|
controllerNumber++;
|
|
name = GetUniqueGamepadName(gamepad, ref controllerNumber);
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
lock (Devices)
|
|
{
|
|
Devices.Clear();
|
|
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
|
|
|
|
|
|
foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds)
|
|
{
|
|
using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
|
|
|
|
if (gamepad != null)
|
|
{
|
|
Devices.Add((DeviceType.Keyboard, id, $"{GetShortGamepadName(gamepad.Name)}"));
|
|
}
|
|
}
|
|
|
|
foreach (string id in _mainWindow.InputManager.GamepadDriver.GamepadsIds)
|
|
{
|
|
using IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
|
|
|
|
if (gamepad != null)
|
|
{
|
|
int controllerNumber = 0;
|
|
string name = GetUniqueGamepadName(gamepad, ref controllerNumber);
|
|
Devices.Add((DeviceType.Controller, id, name));
|
|
}
|
|
}
|
|
|
|
if (selectedDevice != default)
|
|
{
|
|
selectedDeviceIndex = -1;
|
|
for (int i = 0; i < Devices.Count; i++)
|
|
{
|
|
(DeviceType Type, string Id, string Name) device = Devices[i];
|
|
if (device.Type == selectedDevice.Type && device.Id == selectedDevice.Id)
|
|
{
|
|
selectedDeviceIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selectedDeviceIndex < 0)
|
|
{
|
|
selectedDeviceIndex = Math.Clamp(_device, 0, Devices.Count - 1);
|
|
}
|
|
}
|
|
|
|
ApplyLoadedDevice(selectedDeviceIndex);
|
|
}
|
|
|
|
private string GetProfileBasePath()
|
|
{
|
|
string path = AppDataManager.ProfilesDirPath;
|
|
DeviceType type = Devices[Device == -1 ? 0 : Device].Type;
|
|
|
|
if (type == DeviceType.Keyboard)
|
|
{
|
|
path = Path.Combine(path, KeyboardString);
|
|
}
|
|
else if (type == DeviceType.Controller)
|
|
{
|
|
path = Path.Combine(path, ControllerString);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
private void LoadProfiles()
|
|
{
|
|
ProfilesList.Clear();
|
|
|
|
string basePath = GetProfileBasePath();
|
|
|
|
if (!Directory.Exists(basePath))
|
|
{
|
|
Directory.CreateDirectory(basePath);
|
|
}
|
|
|
|
ProfilesList.Add((LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault]));
|
|
|
|
foreach (string profile in Directory.GetFiles(basePath, "*.json", SearchOption.AllDirectories))
|
|
{
|
|
ProfilesList.Add(Path.GetFileNameWithoutExtension(profile));
|
|
}
|
|
|
|
if (string.IsNullOrWhiteSpace(ProfileName))
|
|
{
|
|
ProfileName = LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault];
|
|
}
|
|
}
|
|
|
|
public InputConfig LoadDefaultConfiguration()
|
|
{
|
|
(DeviceType Type, string Id, string Name) activeDevice = Devices.FirstOrDefault();
|
|
|
|
if (Devices.Count > 0 && Device < Devices.Count && Device >= 0)
|
|
{
|
|
activeDevice = Devices[Device];
|
|
}
|
|
|
|
InputConfig config;
|
|
if (activeDevice.Type == DeviceType.Keyboard)
|
|
{
|
|
config = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
|
|
activeDevice.Id,
|
|
activeDevice.Name,
|
|
ControllerType.ProController,
|
|
_playerId);
|
|
}
|
|
else if (activeDevice.Type == DeviceType.Controller)
|
|
{
|
|
string id = activeDevice.Id.Split(" ")[0];
|
|
string name = activeDevice.Name;
|
|
|
|
bool isNintendoStyle = false;
|
|
|
|
try
|
|
{
|
|
IGamepad gp = _mainWindow?.InputManager?.GamepadDriver?.GetGamepad(id);
|
|
|
|
if (gp is SDL3Gamepad sdlGp)
|
|
{
|
|
// Nintendo vendor ID is 0x057E
|
|
isNintendoStyle = sdlGp.VendorId == 0x057E;
|
|
}
|
|
else
|
|
{
|
|
// Fallback to name-based detection
|
|
isNintendoStyle = name.Contains("Nintendo", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.Debug?.Print(LogClass.UI, $"Controller vendor detection failed for '{name}': {ex.Message}");
|
|
isNintendoStyle = name.Contains("Nintendo", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
config = InputConfigDefaults.CreateDefaultControllerConfiguration(
|
|
id,
|
|
name,
|
|
ControllerType.ProController,
|
|
_playerId,
|
|
isNintendoStyle);
|
|
}
|
|
else
|
|
{
|
|
config = new InputConfig();
|
|
}
|
|
|
|
config.PlayerIndex = _playerId;
|
|
|
|
return config;
|
|
}
|
|
|
|
private bool TryCreateKeyboardFallbackConfig(InputConfig sourceConfig, out StandardKeyboardInputConfig fallbackConfig)
|
|
{
|
|
fallbackConfig = null;
|
|
|
|
(DeviceType Type, string Id, string Name) keyboardDevice =
|
|
Devices.FirstOrDefault(device => device.Type == DeviceType.Keyboard);
|
|
|
|
if (keyboardDevice == default)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ControllerType controllerType = sourceConfig?.ControllerType ?? ControllerType.ProController;
|
|
PlayerIndex playerIndex = sourceConfig?.PlayerIndex ?? _playerId;
|
|
|
|
fallbackConfig = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
|
|
keyboardDevice.Id,
|
|
keyboardDevice.Name,
|
|
controllerType,
|
|
playerIndex);
|
|
return true;
|
|
}
|
|
|
|
public void LoadProfileButton()
|
|
{
|
|
LoadProfile();
|
|
}
|
|
|
|
public async void LoadProfile()
|
|
{
|
|
if (Device == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
InputConfig config = null;
|
|
|
|
if (string.IsNullOrWhiteSpace(ProfileName))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ProfileName == LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault])
|
|
{
|
|
config = LoadDefaultConfiguration();
|
|
}
|
|
else
|
|
{
|
|
string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
|
|
|
|
if (!File.Exists(path))
|
|
{
|
|
int index = ProfilesList.IndexOf(ProfileName);
|
|
if (index != -1)
|
|
{
|
|
ProfilesList.RemoveAt(index);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
config = JsonHelper.DeserializeFromFile(path, _serializerContext.InputConfig);
|
|
}
|
|
catch (JsonException) { }
|
|
catch (InvalidOperationException)
|
|
{
|
|
Logger.Error?.Print(LogClass.Configuration, $"Profile {ProfileName} is incompatible with the current input configuration system.");
|
|
|
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogProfileInvalidProfileErrorMessage, ProfileName));
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (config != null)
|
|
{
|
|
_isLoaded = false;
|
|
|
|
string currentDeviceId = Config?.Id ??
|
|
(TryGetCurrentDevice(out (DeviceType Type, string Id, string Name) currentDevice)
|
|
? GetConfigDeviceId(currentDevice)
|
|
: null);
|
|
if (string.IsNullOrEmpty(currentDeviceId))
|
|
{
|
|
Logger.Warning?.Print(LogClass.Configuration, $"Ignoring profile load for {ProfileName} because no active input device is selected.");
|
|
return;
|
|
}
|
|
|
|
config.Id = currentDeviceId; // Set current device id instead of changing device(independent profiles)
|
|
|
|
LoadConfiguration(config);
|
|
|
|
//LoadDevice(); This line of code hard-links profiles to controllers, the commented line allows profiles to be applied to all controllers
|
|
|
|
_isLoaded = true;
|
|
|
|
RefreshModifiedState();
|
|
NotifyChanges();
|
|
}
|
|
}
|
|
|
|
public async void SaveProfile()
|
|
{
|
|
|
|
if (Device == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ConfigViewModel == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ProfileName == LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault])
|
|
{
|
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileDefaultProfileOverwriteErrorMessage]);
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1;
|
|
|
|
if (validFileName)
|
|
{
|
|
string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
|
|
|
|
InputConfig config = null;
|
|
|
|
if (IsKeyboard)
|
|
{
|
|
config = (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig();
|
|
}
|
|
else if (IsController)
|
|
{
|
|
config = (ConfigViewModel as ControllerInputViewModel).Config.GetConfig();
|
|
}
|
|
|
|
config.ControllerType = Controllers[_controller].Type;
|
|
|
|
string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig);
|
|
|
|
await File.WriteAllTextAsync(path, jsonString);
|
|
|
|
LoadProfiles();
|
|
|
|
ChosenProfile = ProfileName; // Show new profile
|
|
}
|
|
else
|
|
{
|
|
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]);
|
|
}
|
|
}
|
|
}
|
|
|
|
public async void RemoveProfile()
|
|
{
|
|
if (Device == 0 || ProfileName == LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault] || ProfilesList.IndexOf(ProfileName) == -1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
|
LocaleManager.Instance[LocaleKeys.DialogProfileDeleteProfileTitle],
|
|
LocaleManager.Instance[LocaleKeys.DialogProfileDeleteProfileMessage],
|
|
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
|
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
|
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
|
|
|
|
if (result == UserResult.Yes)
|
|
{
|
|
string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
|
|
LoadProfiles();
|
|
|
|
ChosenProfile = ProfilesList[0].ToString(); // Show default profile
|
|
}
|
|
}
|
|
|
|
public void RevertChanges()
|
|
{
|
|
_isLoaded = false;
|
|
LoadConfiguration();
|
|
LoadDevice();
|
|
_isLoaded = true;
|
|
|
|
OnPropertyChanged();
|
|
IsModified = false;
|
|
}
|
|
|
|
public void Save()
|
|
{
|
|
|
|
if (!IsModified)
|
|
{
|
|
return; //If the input settings were not touched, then do nothing
|
|
}
|
|
|
|
IsModified = false;
|
|
|
|
List<InputConfig> newConfig = [];
|
|
|
|
if (UseGlobalConfig && Program.UseExtraConfig)
|
|
{
|
|
newConfig.AddRange(ConfigurationState.InstanceExtra.Hid.InputConfig.Value);
|
|
}
|
|
else
|
|
{
|
|
newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value);
|
|
}
|
|
|
|
newConfig.RemoveAll(static inputConfig => inputConfig == null);
|
|
|
|
if (Device == 0)
|
|
{
|
|
newConfig.RemoveAll(inputConfig => inputConfig.PlayerIndex == PlayerId);
|
|
}
|
|
else
|
|
{
|
|
InputConfig config = GetSelectedDeviceConfig();
|
|
|
|
int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId);
|
|
if (i == -1)
|
|
{
|
|
newConfig.Add(config);
|
|
}
|
|
else
|
|
{
|
|
newConfig[i] = config;
|
|
}
|
|
}
|
|
|
|
// Atomically replace and signal input change.
|
|
// NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event.
|
|
_mainWindow.ViewModel.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
|
|
|
|
if (UseGlobalConfig && Program.UseExtraConfig)
|
|
{
|
|
// In User Settings when "Use Global Input" is enabled, it saves global input to global setting
|
|
ConfigurationState.InstanceExtra.Hid.InputConfig.Value = newConfig;
|
|
ConfigurationState.InstanceExtra.ToFileFormat().SaveConfig(Program.GlobalConfigurationPath);
|
|
}
|
|
else
|
|
{
|
|
ConfigurationState.Instance.Hid.InputConfig.Value = newConfig;
|
|
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
|
}
|
|
}
|
|
|
|
public void NotifyChanges()
|
|
{
|
|
OnPropertyChanged(nameof(ConfigViewModel));
|
|
OnPropertyChanged(nameof(IsController));
|
|
OnPropertyChanged(nameof(ShowSettings));
|
|
OnPropertyChanged(nameof(IsKeyboard));
|
|
OnPropertyChanged(nameof(IsRight));
|
|
OnPropertyChanged(nameof(IsLeft));
|
|
NotifyChangesEvent?.Invoke();
|
|
}
|
|
|
|
private void OnPhysicalKeyLabelsChanged()
|
|
{
|
|
if (ConfigViewModel is KeyboardInputViewModel keyboardInputViewModel)
|
|
{
|
|
Dispatcher.UIThread.Post(keyboardInputViewModel.Config.NotifyKeyLabelsChanged);
|
|
}
|
|
}
|
|
|
|
private void ReplaceKeyboardDriver(Control owner)
|
|
{
|
|
Control target = TopLevel.GetTopLevel(owner) as Control ?? owner;
|
|
|
|
if (ReferenceEquals(_keyboardDriverControl, target))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (AvaloniaKeyboardDriver is AvaloniaKeyboardDriver oldKeyboardDriver)
|
|
{
|
|
oldKeyboardDriver.KeyPressed -= PhysicalKeyLabelHelper.ObserveKeyPress;
|
|
oldKeyboardDriver.Dispose();
|
|
}
|
|
|
|
_keyboardDriverControl = target;
|
|
|
|
AvaloniaKeyboardDriver keyboardDriver = new(target, KeyboardInputMode.Physical);
|
|
keyboardDriver.KeyPressed += PhysicalKeyLabelHelper.ObserveKeyPress;
|
|
AvaloniaKeyboardDriver = keyboardDriver;
|
|
|
|
if (_isLoaded && Device > 0 && Device < Devices.Count && Devices[Device].Type == DeviceType.Keyboard)
|
|
{
|
|
SelectedGamepad?.Dispose();
|
|
LoadInputDriver();
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
GC.SuppressFinalize(this);
|
|
PhysicalKeyLabelHelper.LabelsChanged -= OnPhysicalKeyLabelsChanged;
|
|
|
|
_mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
|
|
_mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
|
|
|
|
VisualStick.Dispose();
|
|
|
|
SelectedGamepad?.Dispose();
|
|
|
|
AvaloniaKeyboardDriver?.Dispose();
|
|
}
|
|
}
|
|
}
|