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.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; OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed)); } } public StickVisualizer VisualStick { get; private set; } public ObservableCollection PlayerIndexes { get; set; } public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; } internal ObservableCollection Controllers { get; set; } public AvaloniaList 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(_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 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 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(); } } }