/ src / Ryujinx / UI / ViewModels / Input / InputViewModel.cs
InputViewModel.cs
  1  using Avalonia;
  2  using Avalonia.Collections;
  3  using Avalonia.Controls;
  4  using Avalonia.Controls.ApplicationLifetimes;
  5  using Avalonia.Svg.Skia;
  6  using Avalonia.Threading;
  7  using Ryujinx.Ava.Common.Locale;
  8  using Ryujinx.Ava.Input;
  9  using Ryujinx.Ava.UI.Helpers;
 10  using Ryujinx.Ava.UI.Models;
 11  using Ryujinx.Ava.UI.Models.Input;
 12  using Ryujinx.Ava.UI.Windows;
 13  using Ryujinx.Common;
 14  using Ryujinx.Common.Configuration;
 15  using Ryujinx.Common.Configuration.Hid;
 16  using Ryujinx.Common.Configuration.Hid.Controller;
 17  using Ryujinx.Common.Configuration.Hid.Controller.Motion;
 18  using Ryujinx.Common.Configuration.Hid.Keyboard;
 19  using Ryujinx.Common.Logging;
 20  using Ryujinx.Common.Utilities;
 21  using Ryujinx.Input;
 22  using Ryujinx.UI.Common.Configuration;
 23  using System;
 24  using System.Collections.Generic;
 25  using System.Collections.ObjectModel;
 26  using System.IO;
 27  using System.Linq;
 28  using System.Text.Json;
 29  using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
 30  using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
 31  using Key = Ryujinx.Common.Configuration.Hid.Key;
 32  
 33  namespace Ryujinx.Ava.UI.ViewModels.Input
 34  {
 35      public class InputViewModel : BaseModel, IDisposable
 36      {
 37          private const string Disabled = "disabled";
 38          private const string ProControllerResource = "Ryujinx.UI.Common/Resources/Controller_ProCon.svg";
 39          private const string JoyConPairResource = "Ryujinx.UI.Common/Resources/Controller_JoyConPair.svg";
 40          private const string JoyConLeftResource = "Ryujinx.UI.Common/Resources/Controller_JoyConLeft.svg";
 41          private const string JoyConRightResource = "Ryujinx.UI.Common/Resources/Controller_JoyConRight.svg";
 42          private const string KeyboardString = "keyboard";
 43          private const string ControllerString = "controller";
 44          private readonly MainWindow _mainWindow;
 45  
 46          private PlayerIndex _playerId;
 47          private int _controller;
 48          private int _controllerNumber;
 49          private string _controllerImage;
 50          private int _device;
 51          private object _configViewModel;
 52          private string _profileName;
 53          private bool _isLoaded;
 54  
 55          private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
 56  
 57          public IGamepadDriver AvaloniaKeyboardDriver { get; }
 58          public IGamepad SelectedGamepad { get; private set; }
 59  
 60          public ObservableCollection<PlayerModel> PlayerIndexes { get; set; }
 61          public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
 62          internal ObservableCollection<ControllerModel> Controllers { get; set; }
 63          public AvaloniaList<string> ProfilesList { get; set; }
 64          public AvaloniaList<string> DeviceList { get; set; }
 65  
 66          // XAML Flags
 67          public bool ShowSettings => _device > 0;
 68          public bool IsController => _device > 1;
 69          public bool IsKeyboard => !IsController;
 70          public bool IsRight { get; set; }
 71          public bool IsLeft { get; set; }
 72  
 73          public bool IsModified { get; set; }
 74          public event Action NotifyChangesEvent;
 75  
 76          public object ConfigViewModel
 77          {
 78              get => _configViewModel;
 79              set
 80              {
 81                  _configViewModel = value;
 82  
 83                  OnPropertyChanged();
 84              }
 85          }
 86  
 87          public PlayerIndex PlayerId
 88          {
 89              get => _playerId;
 90              set
 91              {
 92                  if (IsModified)
 93                  {
 94                      return;
 95                  }
 96  
 97                  IsModified = false;
 98                  _playerId = value;
 99  
100                  if (!Enum.IsDefined(typeof(PlayerIndex), _playerId))
101                  {
102                      _playerId = PlayerIndex.Player1;
103                  }
104  
105                  LoadConfiguration();
106                  LoadDevice();
107                  LoadProfiles();
108  
109                  _isLoaded = true;
110  
111                  OnPropertyChanged();
112              }
113          }
114  
115          public int Controller
116          {
117              get => _controller;
118              set
119              {
120                  _controller = value;
121  
122                  if (_controller == -1)
123                  {
124                      _controller = 0;
125                  }
126  
127                  if (Controllers.Count > 0 && value < Controllers.Count && _controller > -1)
128                  {
129                      ControllerType controller = Controllers[_controller].Type;
130  
131                      IsLeft = true;
132                      IsRight = true;
133  
134                      switch (controller)
135                      {
136                          case ControllerType.Handheld:
137                              ControllerImage = JoyConPairResource;
138                              break;
139                          case ControllerType.ProController:
140                              ControllerImage = ProControllerResource;
141                              break;
142                          case ControllerType.JoyconPair:
143                              ControllerImage = JoyConPairResource;
144                              break;
145                          case ControllerType.JoyconLeft:
146                              ControllerImage = JoyConLeftResource;
147                              IsRight = false;
148                              break;
149                          case ControllerType.JoyconRight:
150                              ControllerImage = JoyConRightResource;
151                              IsLeft = false;
152                              break;
153                      }
154  
155                      LoadInputDriver();
156                      LoadProfiles();
157                  }
158  
159                  OnPropertyChanged();
160                  NotifyChanges();
161              }
162          }
163  
164          public string ControllerImage
165          {
166              get => _controllerImage;
167              set
168              {
169                  _controllerImage = value;
170  
171                  OnPropertyChanged();
172                  OnPropertyChanged(nameof(Image));
173              }
174          }
175  
176          public SvgImage Image
177          {
178              get
179              {
180                  SvgImage image = new();
181  
182                  if (!string.IsNullOrWhiteSpace(_controllerImage))
183                  {
184                      SvgSource source = SvgSource.LoadFromStream(EmbeddedResources.GetStream(_controllerImage));
185  
186                      image.Source = source;
187                  }
188  
189                  return image;
190              }
191          }
192  
193          public string ProfileName
194          {
195              get => _profileName; set
196              {
197                  _profileName = value;
198  
199                  OnPropertyChanged();
200              }
201          }
202  
203          public int Device
204          {
205              get => _device;
206              set
207              {
208                  _device = value < 0 ? 0 : value;
209  
210                  if (_device >= Devices.Count)
211                  {
212                      return;
213                  }
214  
215                  var selected = Devices[_device].Type;
216  
217                  if (selected != DeviceType.None)
218                  {
219                      LoadControllers();
220  
221                      if (_isLoaded)
222                      {
223                          LoadConfiguration(LoadDefaultConfiguration());
224                      }
225                  }
226  
227                  OnPropertyChanged();
228                  NotifyChanges();
229              }
230          }
231  
232          public InputConfig Config { get; set; }
233  
234          public InputViewModel(UserControl owner) : this()
235          {
236              if (Program.PreviewerDetached)
237              {
238                  _mainWindow =
239                      (MainWindow)((IClassicDesktopStyleApplicationLifetime)Application.Current
240                          .ApplicationLifetime).MainWindow;
241  
242                  AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner);
243  
244                  _mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
245                  _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
246  
247                  _mainWindow.ViewModel.AppHost?.NpadManager.BlockInputUpdates();
248  
249                  _isLoaded = false;
250  
251                  LoadDevices();
252  
253                  PlayerId = PlayerIndex.Player1;
254              }
255          }
256  
257          public InputViewModel()
258          {
259              PlayerIndexes = new ObservableCollection<PlayerModel>();
260              Controllers = new ObservableCollection<ControllerModel>();
261              Devices = new ObservableCollection<(DeviceType Type, string Id, string Name)>();
262              ProfilesList = new AvaloniaList<string>();
263              DeviceList = new AvaloniaList<string>();
264  
265              ControllerImage = ProControllerResource;
266  
267              PlayerIndexes.Add(new(PlayerIndex.Player1, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer1]));
268              PlayerIndexes.Add(new(PlayerIndex.Player2, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer2]));
269              PlayerIndexes.Add(new(PlayerIndex.Player3, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer3]));
270              PlayerIndexes.Add(new(PlayerIndex.Player4, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer4]));
271              PlayerIndexes.Add(new(PlayerIndex.Player5, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer5]));
272              PlayerIndexes.Add(new(PlayerIndex.Player6, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer6]));
273              PlayerIndexes.Add(new(PlayerIndex.Player7, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer7]));
274              PlayerIndexes.Add(new(PlayerIndex.Player8, LocaleManager.Instance[LocaleKeys.ControllerSettingsPlayer8]));
275              PlayerIndexes.Add(new(PlayerIndex.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsHandheld]));
276          }
277  
278          private void LoadConfiguration(InputConfig inputConfig = null)
279          {
280              Config = inputConfig ?? ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerId);
281  
282              if (Config is StandardKeyboardInputConfig keyboardInputConfig)
283              {
284                  ConfigViewModel = new KeyboardInputViewModel(this, new KeyboardInputConfig(keyboardInputConfig));
285              }
286  
287              if (Config is StandardControllerInputConfig controllerInputConfig)
288              {
289                  ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig));
290              }
291          }
292  
293          public void LoadDevice()
294          {
295              if (Config == null || Config.Backend == InputBackendType.Invalid)
296              {
297                  Device = 0;
298              }
299              else
300              {
301                  var type = DeviceType.None;
302  
303                  if (Config is StandardKeyboardInputConfig)
304                  {
305                      type = DeviceType.Keyboard;
306                  }
307  
308                  if (Config is StandardControllerInputConfig)
309                  {
310                      type = DeviceType.Controller;
311                  }
312  
313                  var item = Devices.FirstOrDefault(x => x.Type == type && x.Id == Config.Id);
314                  if (item != default)
315                  {
316                      Device = Devices.ToList().FindIndex(x => x.Id == item.Id);
317                  }
318                  else
319                  {
320                      Device = 0;
321                  }
322              }
323          }
324  
325          private void LoadInputDriver()
326          {
327              if (_device < 0)
328              {
329                  return;
330              }
331  
332              string id = GetCurrentGamepadId();
333              var type = Devices[Device].Type;
334  
335              if (type == DeviceType.None)
336              {
337                  return;
338              }
339  
340              if (type == DeviceType.Keyboard)
341              {
342                  if (_mainWindow.InputManager.KeyboardDriver is AvaloniaKeyboardDriver)
343                  {
344                      // 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...
345                      SelectedGamepad = AvaloniaKeyboardDriver.GetGamepad(id);
346                  }
347                  else
348                  {
349                      SelectedGamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
350                  }
351              }
352              else
353              {
354                  SelectedGamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
355              }
356          }
357  
358          private void HandleOnGamepadDisconnected(string id)
359          {
360              Dispatcher.UIThread.Post(() =>
361              {
362                  LoadDevices();
363              });
364          }
365  
366          private void HandleOnGamepadConnected(string id)
367          {
368              Dispatcher.UIThread.Post(() =>
369              {
370                  LoadDevices();
371              });
372          }
373  
374          private string GetCurrentGamepadId()
375          {
376              if (_device < 0)
377              {
378                  return string.Empty;
379              }
380  
381              var device = Devices[Device];
382  
383              if (device.Type == DeviceType.None)
384              {
385                  return null;
386              }
387  
388              return device.Id.Split(" ")[0];
389          }
390  
391          public void LoadControllers()
392          {
393              Controllers.Clear();
394  
395              if (_playerId == PlayerIndex.Handheld)
396              {
397                  Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeHandheld]));
398  
399                  Controller = 0;
400              }
401              else
402              {
403                  Controllers.Add(new(ControllerType.ProController, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeProController]));
404                  Controllers.Add(new(ControllerType.JoyconPair, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConPair]));
405                  Controllers.Add(new(ControllerType.JoyconLeft, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConLeft]));
406                  Controllers.Add(new(ControllerType.JoyconRight, LocaleManager.Instance[LocaleKeys.ControllerSettingsControllerTypeJoyConRight]));
407  
408                  if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1)
409                  {
410                      Controller = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType);
411                  }
412                  else
413                  {
414                      Controller = 0;
415                  }
416              }
417          }
418  
419          private static string GetShortGamepadName(string str)
420          {
421              const string Ellipsis = "...";
422              const int MaxSize = 50;
423  
424              if (str.Length > MaxSize)
425              {
426                  return $"{str.AsSpan(0, MaxSize - Ellipsis.Length)}{Ellipsis}";
427              }
428  
429              return str;
430          }
431  
432          private static string GetShortGamepadId(string str)
433          {
434              const string Hyphen = "-";
435              const int Offset = 1;
436  
437              return str[(str.IndexOf(Hyphen) + Offset)..];
438          }
439  
440          public void LoadDevices()
441          {
442              lock (Devices)
443              {
444                  Devices.Clear();
445                  DeviceList.Clear();
446                  Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
447  
448                  foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds)
449                  {
450                      using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
451  
452                      if (gamepad != null)
453                      {
454                          Devices.Add((DeviceType.Keyboard, id, $"{GetShortGamepadName(gamepad.Name)}"));
455                      }
456                  }
457  
458                  foreach (string id in _mainWindow.InputManager.GamepadDriver.GamepadsIds)
459                  {
460                      using IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
461  
462                      if (gamepad != null)
463                      {
464                          if (Devices.Any(controller => GetShortGamepadId(controller.Id) == GetShortGamepadId(gamepad.Id)))
465                          {
466                              _controllerNumber++;
467                          }
468  
469                          Devices.Add((DeviceType.Controller, id, $"{GetShortGamepadName(gamepad.Name)} ({_controllerNumber})"));
470                      }
471                  }
472  
473                  _controllerNumber = 0;
474  
475                  DeviceList.AddRange(Devices.Select(x => x.Name));
476                  Device = Math.Min(Device, DeviceList.Count);
477              }
478          }
479  
480          private string GetProfileBasePath()
481          {
482              string path = AppDataManager.ProfilesDirPath;
483              var type = Devices[Device == -1 ? 0 : Device].Type;
484  
485              if (type == DeviceType.Keyboard)
486              {
487                  path = Path.Combine(path, KeyboardString);
488              }
489              else if (type == DeviceType.Controller)
490              {
491                  path = Path.Combine(path, ControllerString);
492              }
493  
494              return path;
495          }
496  
497          private void LoadProfiles()
498          {
499              ProfilesList.Clear();
500  
501              string basePath = GetProfileBasePath();
502  
503              if (!Directory.Exists(basePath))
504              {
505                  Directory.CreateDirectory(basePath);
506              }
507  
508              ProfilesList.Add((LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault]));
509  
510              foreach (string profile in Directory.GetFiles(basePath, "*.json", SearchOption.AllDirectories))
511              {
512                  ProfilesList.Add(Path.GetFileNameWithoutExtension(profile));
513              }
514  
515              if (string.IsNullOrWhiteSpace(ProfileName))
516              {
517                  ProfileName = LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault];
518              }
519          }
520  
521          public InputConfig LoadDefaultConfiguration()
522          {
523              var activeDevice = Devices.FirstOrDefault();
524  
525              if (Devices.Count > 0 && Device < Devices.Count && Device >= 0)
526              {
527                  activeDevice = Devices[Device];
528              }
529  
530              InputConfig config;
531              if (activeDevice.Type == DeviceType.Keyboard)
532              {
533                  string id = activeDevice.Id;
534  
535                  config = new StandardKeyboardInputConfig
536                  {
537                      Version = InputConfig.CurrentVersion,
538                      Backend = InputBackendType.WindowKeyboard,
539                      Id = id,
540                      ControllerType = ControllerType.ProController,
541                      LeftJoycon = new LeftJoyconCommonConfig<Key>
542                      {
543                          DpadUp = Key.Up,
544                          DpadDown = Key.Down,
545                          DpadLeft = Key.Left,
546                          DpadRight = Key.Right,
547                          ButtonMinus = Key.Minus,
548                          ButtonL = Key.E,
549                          ButtonZl = Key.Q,
550                          ButtonSl = Key.Unbound,
551                          ButtonSr = Key.Unbound,
552                      },
553                      LeftJoyconStick =
554                          new JoyconConfigKeyboardStick<Key>
555                          {
556                              StickUp = Key.W,
557                              StickDown = Key.S,
558                              StickLeft = Key.A,
559                              StickRight = Key.D,
560                              StickButton = Key.F,
561                          },
562                      RightJoycon = new RightJoyconCommonConfig<Key>
563                      {
564                          ButtonA = Key.Z,
565                          ButtonB = Key.X,
566                          ButtonX = Key.C,
567                          ButtonY = Key.V,
568                          ButtonPlus = Key.Plus,
569                          ButtonR = Key.U,
570                          ButtonZr = Key.O,
571                          ButtonSl = Key.Unbound,
572                          ButtonSr = Key.Unbound,
573                      },
574                      RightJoyconStick = new JoyconConfigKeyboardStick<Key>
575                      {
576                          StickUp = Key.I,
577                          StickDown = Key.K,
578                          StickLeft = Key.J,
579                          StickRight = Key.L,
580                          StickButton = Key.H,
581                      },
582                  };
583              }
584              else if (activeDevice.Type == DeviceType.Controller)
585              {
586                  bool isNintendoStyle = Devices.ToList().Find(x => x.Id == activeDevice.Id).Name.Contains("Nintendo");
587  
588                  string id = activeDevice.Id.Split(" ")[0];
589  
590                  config = new StandardControllerInputConfig
591                  {
592                      Version = InputConfig.CurrentVersion,
593                      Backend = InputBackendType.GamepadSDL2,
594                      Id = id,
595                      ControllerType = ControllerType.ProController,
596                      DeadzoneLeft = 0.1f,
597                      DeadzoneRight = 0.1f,
598                      RangeLeft = 1.0f,
599                      RangeRight = 1.0f,
600                      TriggerThreshold = 0.5f,
601                      LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
602                      {
603                          DpadUp = ConfigGamepadInputId.DpadUp,
604                          DpadDown = ConfigGamepadInputId.DpadDown,
605                          DpadLeft = ConfigGamepadInputId.DpadLeft,
606                          DpadRight = ConfigGamepadInputId.DpadRight,
607                          ButtonMinus = ConfigGamepadInputId.Minus,
608                          ButtonL = ConfigGamepadInputId.LeftShoulder,
609                          ButtonZl = ConfigGamepadInputId.LeftTrigger,
610                          ButtonSl = ConfigGamepadInputId.Unbound,
611                          ButtonSr = ConfigGamepadInputId.Unbound,
612                      },
613                      LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
614                      {
615                          Joystick = ConfigStickInputId.Left,
616                          StickButton = ConfigGamepadInputId.LeftStick,
617                          InvertStickX = false,
618                          InvertStickY = false,
619                      },
620                      RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
621                      {
622                          ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
623                          ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
624                          ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
625                          ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
626                          ButtonPlus = ConfigGamepadInputId.Plus,
627                          ButtonR = ConfigGamepadInputId.RightShoulder,
628                          ButtonZr = ConfigGamepadInputId.RightTrigger,
629                          ButtonSl = ConfigGamepadInputId.Unbound,
630                          ButtonSr = ConfigGamepadInputId.Unbound,
631                      },
632                      RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
633                      {
634                          Joystick = ConfigStickInputId.Right,
635                          StickButton = ConfigGamepadInputId.RightStick,
636                          InvertStickX = false,
637                          InvertStickY = false,
638                      },
639                      Motion = new StandardMotionConfigController
640                      {
641                          MotionBackend = MotionInputBackendType.GamepadDriver,
642                          EnableMotion = true,
643                          Sensitivity = 100,
644                          GyroDeadzone = 1,
645                      },
646                      Rumble = new RumbleConfigController
647                      {
648                          StrongRumble = 1f,
649                          WeakRumble = 1f,
650                          EnableRumble = false,
651                      },
652                  };
653              }
654              else
655              {
656                  config = new InputConfig();
657              }
658  
659              config.PlayerIndex = _playerId;
660  
661              return config;
662          }
663  
664          public async void LoadProfile()
665          {
666              if (Device == 0)
667              {
668                  return;
669              }
670  
671              InputConfig config = null;
672  
673              if (string.IsNullOrWhiteSpace(ProfileName))
674              {
675                  return;
676              }
677  
678              if (ProfileName == LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault])
679              {
680                  config = LoadDefaultConfiguration();
681              }
682              else
683              {
684                  string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
685  
686                  if (!File.Exists(path))
687                  {
688                      var index = ProfilesList.IndexOf(ProfileName);
689                      if (index != -1)
690                      {
691                          ProfilesList.RemoveAt(index);
692                      }
693                      return;
694                  }
695  
696                  try
697                  {
698                      config = JsonHelper.DeserializeFromFile(path, _serializerContext.InputConfig);
699                  }
700                  catch (JsonException) { }
701                  catch (InvalidOperationException)
702                  {
703                      Logger.Error?.Print(LogClass.Configuration, $"Profile {ProfileName} is incompatible with the current input configuration system.");
704  
705                      await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogProfileInvalidProfileErrorMessage, ProfileName));
706  
707                      return;
708                  }
709              }
710  
711              if (config != null)
712              {
713                  _isLoaded = false;
714  
715                  LoadConfiguration(config);
716  
717                  LoadDevice();
718  
719                  _isLoaded = true;
720  
721                  NotifyChanges();
722              }
723          }
724  
725          public async void SaveProfile()
726          {
727              if (Device == 0)
728              {
729                  return;
730              }
731  
732              if (ConfigViewModel == null)
733              {
734                  return;
735              }
736  
737              if (ProfileName == LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault])
738              {
739                  await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileDefaultProfileOverwriteErrorMessage]);
740  
741                  return;
742              }
743              else
744              {
745                  bool validFileName = ProfileName.IndexOfAny(Path.GetInvalidFileNameChars()) == -1;
746  
747                  if (validFileName)
748                  {
749                      string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
750  
751                      InputConfig config = null;
752  
753                      if (IsKeyboard)
754                      {
755                          config = (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig();
756                      }
757                      else if (IsController)
758                      {
759                          config = (ConfigViewModel as ControllerInputViewModel).Config.GetConfig();
760                      }
761  
762                      config.ControllerType = Controllers[_controller].Type;
763  
764                      string jsonString = JsonHelper.Serialize(config, _serializerContext.InputConfig);
765  
766                      await File.WriteAllTextAsync(path, jsonString);
767  
768                      LoadProfiles();
769                  }
770                  else
771                  {
772                      await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]);
773                  }
774              }
775          }
776  
777          public async void RemoveProfile()
778          {
779              if (Device == 0 || ProfileName == LocaleManager.Instance[LocaleKeys.ControllerSettingsProfileDefault] || ProfilesList.IndexOf(ProfileName) == -1)
780              {
781                  return;
782              }
783  
784              UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
785                  LocaleManager.Instance[LocaleKeys.DialogProfileDeleteProfileTitle],
786                  LocaleManager.Instance[LocaleKeys.DialogProfileDeleteProfileMessage],
787                  LocaleManager.Instance[LocaleKeys.InputDialogYes],
788                  LocaleManager.Instance[LocaleKeys.InputDialogNo],
789                  LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
790  
791              if (result == UserResult.Yes)
792              {
793                  string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json");
794  
795                  if (File.Exists(path))
796                  {
797                      File.Delete(path);
798                  }
799  
800                  LoadProfiles();
801              }
802          }
803  
804          public void Save()
805          {
806              IsModified = false;
807  
808              List<InputConfig> newConfig = new();
809  
810              newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value);
811  
812              newConfig.Remove(newConfig.Find(x => x == null));
813  
814              if (Device == 0)
815              {
816                  newConfig.Remove(newConfig.Find(x => x.PlayerIndex == this.PlayerId));
817              }
818              else
819              {
820                  var device = Devices[Device];
821  
822                  if (device.Type == DeviceType.Keyboard)
823                  {
824                      var inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config;
825                      inputConfig.Id = device.Id;
826                  }
827                  else
828                  {
829                      var inputConfig = (ConfigViewModel as ControllerInputViewModel).Config;
830                      inputConfig.Id = device.Id.Split(" ")[0];
831                  }
832  
833                  var config = !IsController
834                      ? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig()
835                      : (ConfigViewModel as ControllerInputViewModel).Config.GetConfig();
836                  config.ControllerType = Controllers[_controller].Type;
837                  config.PlayerIndex = _playerId;
838  
839                  int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId);
840                  if (i == -1)
841                  {
842                      newConfig.Add(config);
843                  }
844                  else
845                  {
846                      newConfig[i] = config;
847                  }
848              }
849  
850              _mainWindow.ViewModel.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
851  
852              // Atomically replace and signal input change.
853              // NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event.
854              ConfigurationState.Instance.Hid.InputConfig.Value = newConfig;
855  
856              ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
857          }
858  
859          public void NotifyChange(string property)
860          {
861              OnPropertyChanged(property);
862          }
863  
864          public void NotifyChanges()
865          {
866              OnPropertyChanged(nameof(ConfigViewModel));
867              OnPropertyChanged(nameof(IsController));
868              OnPropertyChanged(nameof(ShowSettings));
869              OnPropertyChanged(nameof(IsKeyboard));
870              OnPropertyChanged(nameof(IsRight));
871              OnPropertyChanged(nameof(IsLeft));
872              NotifyChangesEvent?.Invoke();
873          }
874  
875          public void Dispose()
876          {
877              GC.SuppressFinalize(this);
878  
879              _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
880              _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
881  
882              _mainWindow.ViewModel.AppHost?.NpadManager.UnblockInputUpdates();
883  
884              SelectedGamepad?.Dispose();
885  
886              AvaloniaKeyboardDriver.Dispose();
887          }
888      }
889  }