diff options
| author | Emmanuel Hansen <emmausssss@gmail.com> | 2022-07-05 18:06:31 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-07-05 20:06:31 +0200 |
| commit | 594246ea4727c9377b1c916934d9b257a1b5d0d0 (patch) | |
| tree | d3a8538f729575262543bcdb16e5c2c01522796b /Ryujinx.Ava/Ui/ViewModels | |
| parent | d21b403886fdc00dac2493532b3e8ff53d5882c4 (diff) | |
UI - Avalonia Part 2 (#3351)
* add settings windows and children views
* Expose hotkeys configuration on the UI
* Remove double spacing from locale JSON
* simplify button assigner
* add cemuhook buttons and title to locale
* move common button assigner to own class
* cancel button assigner when window is closed
* remove unused setting
* address review. fix controller profile not loading default when switching devices
* fix updater file name
* Input cleanup (#37)
* addressed review
* add device type to controller device checks
* change accessibility modifier of public classes to internal
* Update Ryujinx.Ava/Ui/ViewModels/ControllerSettingsViewModel.cs
Co-authored-by: gdkchan <gab.dark.100@gmail.com>
* Update de_DE.json
* Update de_DE.json
* Update tr_TR.json
Translated newly added lines
* Update it_IT.json
* fix rebase
* update avalonia
* fix wrong key used for button text
* Align settings window elements
* Tabs to spaces
* Update brazilian portuguese translation
* Minor improvement on brazilian portuguese translation
* fix turkish translation
* remove unused text
* change view related classes to public
* unsubscribe from deferred event if dialog is closed
* Load the default language before loading any other when switching languages
* Make controller settings more compact
* increase default width of settings window, reduce profile buttons width
Co-authored-by: gdk <gab.dark.100@gmail.com>
Co-authored-by: MutantAura <44103205+MutantAura@users.noreply.github.com>
Co-authored-by: Niwu34 <67392333+Niwu34@users.noreply.github.com>
Co-authored-by: aegiff <99728970+aegiff@users.noreply.github.com>
Co-authored-by: Antonio Brugnolo <36473846+AntoSkate@users.noreply.github.com>
Diffstat (limited to 'Ryujinx.Ava/Ui/ViewModels')
| -rw-r--r-- | Ryujinx.Ava/Ui/ViewModels/ControllerSettingsViewModel.cs | 887 | ||||
| -rw-r--r-- | Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs | 40 | ||||
| -rw-r--r-- | Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs | 403 |
3 files changed, 1322 insertions, 8 deletions
diff --git a/Ryujinx.Ava/Ui/ViewModels/ControllerSettingsViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/ControllerSettingsViewModel.cs new file mode 100644 index 00000000..8811a3c5 --- /dev/null +++ b/Ryujinx.Ava/Ui/ViewModels/ControllerSettingsViewModel.cs @@ -0,0 +1,887 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Svg.Skia; +using Avalonia.Threading; +using Avalonia.VisualTree; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Input; +using Ryujinx.Ava.Ui.Controls; +using Ryujinx.Ava.Ui.Models; +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.Controller.Motion; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.Input; +using Ryujinx.Ui.Common.Configuration; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; +using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; +using Key = Ryujinx.Common.Configuration.Hid.Key; + +namespace Ryujinx.Ava.Ui.ViewModels +{ + public class ControllerSettingsViewModel : BaseModel, IDisposable + { + private const string Disabled = "disabled"; + private const string ProControllerResource = "Ryujinx.Ui.Common/Resources/Controller_ProCon.svg"; + private const string JoyConPairResource = "Ryujinx.Ui.Common/Resources/Controller_JoyConPair.svg"; + private const string JoyConLeftResource = "Ryujinx.Ui.Common/Resources/Controller_JoyConLeft.svg"; + private const string JoyConRightResource = "Ryujinx.Ui.Common/Resources/Controller_JoyConRight.svg"; + private const string KeyboardString = "keyboard"; + private const string ControllerString = "controller"; + private readonly MainWindow _mainWindow; + + private PlayerIndex _playerId; + private int _controller; + private string _controllerImage; + private int _device; + private object _configuration; + private string _profileName; + private bool _isLoaded; + private readonly UserControl _owner; + + public IGamepadDriver AvaloniaKeyboardDriver { get; } + public IGamepad SelectedGamepad { 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 AvaloniaList<string> DeviceList { get; set; } + + // 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 IsModified { get; set; } + + public object Configuration + { + get => _configuration; + set + { + _configuration = value; + + OnPropertyChanged(); + } + } + + public PlayerIndex PlayerId + { + get => _playerId; + set + { + if (IsModified) + { + return; + } + + IsModified = false; + _playerId = value; + + if (!Enum.IsDefined(typeof(PlayerIndex), _playerId)) + { + _playerId = PlayerIndex.Player1; + } + + LoadConfiguration(); + LoadDevice(); + LoadProfiles(); + + _isLoaded = true; + + OnPropertyChanged(); + } + } + + public int Controller + { + get => _controller; + set + { + _controller = value; + + if (_controller == -1) + { + _controller = 0; + } + + if (Controllers.Count > 0 && value < Controllers.Count && _controller > -1) + { + ControllerType controller = Controllers[_controller].Type; + + IsLeft = true; + IsRight = true; + + switch (controller) + { + case ControllerType.Handheld: + ControllerImage = JoyConPairResource; + break; + case ControllerType.ProController: + ControllerImage = ProControllerResource; + break; + case ControllerType.JoyconPair: + ControllerImage = JoyConPairResource; + break; + case ControllerType.JoyconLeft: + ControllerImage = JoyConLeftResource; + IsRight = false; + break; + case ControllerType.JoyconRight: + ControllerImage = JoyConRightResource; + IsLeft = false; + break; + } + + LoadInputDriver(); + LoadProfiles(); + } + + OnPropertyChanged(); + NotifyChanges(); + } + } + + public string ControllerImage + { + get => _controllerImage; + set + { + _controllerImage = value; + + OnPropertyChanged(); + OnPropertyChanged(nameof(Image)); + } + } + + public SvgImage Image + { + get + { + SvgImage image = new SvgImage(); + + if (!string.IsNullOrWhiteSpace(_controllerImage)) + { + SvgSource source = new SvgSource(); + + source.Load(EmbeddedResources.GetStream(_controllerImage)); + + image.Source = source; + } + + return image; + } + } + + public string ProfileName + { + get => _profileName; set + { + _profileName = value; + + OnPropertyChanged(); + } + } + + public int Device + { + get => _device; + set + { + _device = value < 0 ? 0 : value; + + if (_device >= Devices.Count) + { + return; + } + + var selected = Devices[_device].Type; + + if (selected != DeviceType.None) + { + LoadControllers(); + + if (_isLoaded) + { + LoadConfiguration(LoadDefaultConfiguration()); + } + } + + OnPropertyChanged(); + NotifyChanges(); + } + } + + public InputConfig Config { get; set; } + + public ControllerSettingsViewModel(UserControl owner) : this() + { + _owner = owner; + + if (Program.PreviewerDetached) + { + _mainWindow = + (MainWindow)((IClassicDesktopStyleApplicationLifetime)Avalonia.Application.Current + .ApplicationLifetime).MainWindow; + + AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner); + + _mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; + _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; + if (_mainWindow.AppHost != null) + { + _mainWindow.AppHost.NpadManager.BlockInputUpdates(); + } + + _isLoaded = false; + + LoadDevices(); + + PlayerId = PlayerIndex.Player1; + } + } + + public ControllerSettingsViewModel() + { + PlayerIndexes = new ObservableCollection<PlayerModel>(); + Controllers = new ObservableCollection<ControllerModel>(); + Devices = new ObservableCollection<(DeviceType Type, string Id, string Name)>(); + ProfilesList = new AvaloniaList<string>(); + DeviceList = new AvaloniaList<string>(); + + ControllerImage = ProControllerResource; + + PlayerIndexes.Add(new(PlayerIndex.Player1, LocaleManager.Instance["ControllerSettingsPlayer1"])); + PlayerIndexes.Add(new(PlayerIndex.Player2, LocaleManager.Instance["ControllerSettingsPlayer2"])); + PlayerIndexes.Add(new(PlayerIndex.Player3, LocaleManager.Instance["ControllerSettingsPlayer3"])); + PlayerIndexes.Add(new(PlayerIndex.Player4, LocaleManager.Instance["ControllerSettingsPlayer4"])); + PlayerIndexes.Add(new(PlayerIndex.Player5, LocaleManager.Instance["ControllerSettingsPlayer5"])); + PlayerIndexes.Add(new(PlayerIndex.Player6, LocaleManager.Instance["ControllerSettingsPlayer6"])); + PlayerIndexes.Add(new(PlayerIndex.Player7, LocaleManager.Instance["ControllerSettingsPlayer7"])); + PlayerIndexes.Add(new(PlayerIndex.Player8, LocaleManager.Instance["ControllerSettingsPlayer8"])); + PlayerIndexes.Add(new(PlayerIndex.Handheld, LocaleManager.Instance["ControllerSettingsHandheld"])); + } + + private void LoadConfiguration(InputConfig inputConfig = null) + { + Config = inputConfig ?? ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerId); + + if (Config is StandardKeyboardInputConfig keyboardInputConfig) + { + Configuration = new InputConfiguration<Key, ConfigStickInputId>(keyboardInputConfig); + } + + if (Config is StandardControllerInputConfig controllerInputConfig) + { + Configuration = new InputConfiguration<ConfigGamepadInputId, ConfigStickInputId>(controllerInputConfig); + } + } + + public void LoadDevice() + { + if (Config == null || Config.Backend == InputBackendType.Invalid) + { + Device = 0; + } + else + { + var type = DeviceType.None; + + if (Config is StandardKeyboardInputConfig) + { + type = DeviceType.Keyboard; + } + + if (Config is StandardControllerInputConfig) + { + type = DeviceType.Controller; + } + + var item = Devices.FirstOrDefault(x => x.Type == type && x.Id == Config.Id); + if (item != default) + { + Device = Devices.ToList().FindIndex(x => x.Id == item.Id); + } + else + { + Device = 0; + } + } + } + + public async void ShowMotionConfig() + { + await MotionSettingsWindow.Show(this, _owner.GetVisualRoot() as StyleableWindow); + } + + public async void ShowRumbleConfig() + { + await RumbleSettingsWindow.Show(this, _owner.GetVisualRoot() as StyleableWindow); + } + + private void LoadInputDriver() + { + if (_device < 0) + { + return; + } + + string id = GetCurrentGamepadId(); + var type = Devices[Device].Type; + + if (type == DeviceType.None) + { + return; + } + else 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) + { + Dispatcher.UIThread.Post(() => + { + LoadDevices(); + }); + } + + private void HandleOnGamepadConnected(string id) + { + Dispatcher.UIThread.Post(() => + { + LoadDevices(); + }); + } + + private string GetCurrentGamepadId() + { + if (_device < 0) + { + return string.Empty; + } + + var device = Devices[Device]; + + if (device.Type == DeviceType.None) + { + return null; + } + + return device.Id.Split(" ")[0]; + } + + public void LoadControllers() + { + Controllers.Clear(); + + if (_playerId == PlayerIndex.Handheld) + { + Controllers.Add(new(ControllerType.Handheld, LocaleManager.Instance["ControllerSettingsControllerTypeHandheld"])); + + Controller = 0; + } + else + { + Controllers.Add(new(ControllerType.ProController, LocaleManager.Instance["ControllerSettingsControllerTypeProController"])); + Controllers.Add(new(ControllerType.JoyconPair, LocaleManager.Instance["ControllerSettingsControllerTypeJoyConPair"])); + Controllers.Add(new(ControllerType.JoyconLeft, LocaleManager.Instance["ControllerSettingsControllerTypeJoyConLeft"])); + Controllers.Add(new(ControllerType.JoyconRight, LocaleManager.Instance["ControllerSettingsControllerTypeJoyConRight"])); + + if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1) + { + Controller = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType); + } + else + { + Controller = 0; + } + } + } + + private static string GetShortGamepadName(string str) + { + const string Ellipsis = "..."; + const int MaxSize = 50; + + if (str.Length > MaxSize) + { + return str.Substring(0, MaxSize - Ellipsis.Length) + Ellipsis; + } + + return str; + } + + public void LoadDevices() + { + lock (Devices) + { + Devices.Clear(); + DeviceList.Clear(); + Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance["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)} ({id})")); + } + } + + foreach (string id in _mainWindow.InputManager.GamepadDriver.GamepadsIds) + { + using IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id); + + if (gamepad != null) + { + Devices.Add((DeviceType.Controller, id, $"{GetShortGamepadName(gamepad.Name)} ({id})")); + } + } + + DeviceList.AddRange(Devices.Select(x => x.Name)); + Device = Math.Min(Device, DeviceList.Count); + } + } + + private string GetProfileBasePath() + { + string path = AppDataManager.ProfilesDirPath; + var 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["ControllerSettingsProfileDefault"])); + + foreach (string profile in Directory.GetFiles(basePath, "*.json", SearchOption.AllDirectories)) + { + ProfilesList.Add(Path.GetFileNameWithoutExtension(profile)); + } + + if (string.IsNullOrWhiteSpace(ProfileName)) + { + ProfileName = LocaleManager.Instance["ControllerSettingsProfileDefault"]; + } + } + + public InputConfig LoadDefaultConfiguration() + { + var activeDevice = Devices.FirstOrDefault(); + + if (Devices.Count > 0 && Device < Devices.Count && Device >= 0) + { + activeDevice = Devices[Device]; + } + + InputConfig config; + if (activeDevice.Type == DeviceType.Keyboard) + { + string id = activeDevice.Id; + + config = new StandardKeyboardInputConfig + { + Version = Ryujinx.Common.Configuration.Hid.InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = id, + ControllerType = ControllerType.ProController, + LeftJoycon = new LeftJoyconCommonConfig<Key> + { + DpadUp = Key.Up, + DpadDown = Key.Down, + DpadLeft = Key.Left, + DpadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound + }, + LeftJoyconStick = + new JoyconConfigKeyboardStick<Key> + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F + }, + RightJoycon = new RightJoyconCommonConfig<Key> + { + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound + }, + RightJoyconStick = new JoyconConfigKeyboardStick<Key> + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H + } + }; + } + else if (activeDevice.Type == DeviceType.Controller) + { + bool isNintendoStyle = Devices.ToList().Find(x => x.Id == activeDevice.Id).Name.Contains("Nintendo"); + + string id = activeDevice.Id.Split(" ")[0]; + + config = new StandardControllerInputConfig + { + Version = Ryujinx.Common.Configuration.Hid.InputConfig.CurrentVersion, + Backend = InputBackendType.GamepadSDL2, + Id = id, + ControllerType = ControllerType.ProController, + DeadzoneLeft = 0.1f, + DeadzoneRight = 0.1f, + RangeLeft = 1.0f, + RangeRight = 1.0f, + TriggerThreshold = 0.5f, + LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId> + { + DpadUp = ConfigGamepadInputId.DpadUp, + DpadDown = ConfigGamepadInputId.DpadDown, + DpadLeft = ConfigGamepadInputId.DpadLeft, + DpadRight = ConfigGamepadInputId.DpadRight, + ButtonMinus = ConfigGamepadInputId.Minus, + ButtonL = ConfigGamepadInputId.LeftShoulder, + ButtonZl = ConfigGamepadInputId.LeftTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound + }, + LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId> + { + Joystick = ConfigStickInputId.Left, + StickButton = ConfigGamepadInputId.LeftStick, + InvertStickX = false, + InvertStickY = false + }, + RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId> + { + ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B, + ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A, + ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y, + ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X, + ButtonPlus = ConfigGamepadInputId.Plus, + ButtonR = ConfigGamepadInputId.RightShoulder, + ButtonZr = ConfigGamepadInputId.RightTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound + }, + RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId> + { + Joystick = ConfigStickInputId.Right, + StickButton = ConfigGamepadInputId.RightStick, + InvertStickX = false, + InvertStickY = false + }, + Motion = new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + EnableMotion = true, + Sensitivity = 100, + GyroDeadzone = 1 + }, + Rumble = new RumbleConfigController + { + StrongRumble = 1f, + WeakRumble = 1f, + EnableRumble = false + } + }; + } + else + { + config = new InputConfig(); + } + + config.PlayerIndex = _playerId; + + return config; + } + + public void LoadProfile() + { + if (Device == 0) + { + return; + } + + InputConfig config = null; + + if (string.IsNullOrWhiteSpace(ProfileName)) + { + return; + } + + if (ProfileName == LocaleManager.Instance["ControllerSettingsProfileDefault"]) + { + config = LoadDefaultConfiguration(); + } + else + { + string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); + + if (!File.Exists(path)) + { + var index = ProfilesList.IndexOf(ProfileName); + if (index != -1) + { + ProfilesList.RemoveAt(index); + } + return; + } + + try + { + using (Stream stream = File.OpenRead(path)) + { + config = JsonHelper.Deserialize<InputConfig>(stream); + } + } + catch (JsonException) { } + catch (InvalidOperationException) + { + ContentDialogHelper.CreateErrorDialog(_owner.GetVisualRoot() as StyleableWindow, + String.Format(LocaleManager.Instance["DialogProfileInvalidProfileErrorMessage"], ProfileName)); + Logger.Error?.Print(LogClass.Configuration, $"Profile {ProfileName} is incompatible with the current input configuration system."); + + return; + } + } + + if (config != null) + { + _isLoaded = false; + + LoadConfiguration(config); + + LoadDevice(); + + _isLoaded = true; + + NotifyChanges(); + } + } + + public async void SaveProfile() + { + if (Device == 0) + { + return; + } + + if (Configuration == null) + { + return; + } + + if (ProfileName == LocaleManager.Instance["ControllerSettingsProfileDefault"]) + { + ContentDialogHelper.CreateErrorDialog(_owner.GetVisualRoot() as StyleableWindow, LocaleManager.Instance["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 = (Configuration as InputConfiguration<Key, ConfigStickInputId>).GetConfig(); + } + else if (IsController) + { + config = (Configuration as InputConfiguration<GamepadInputId, ConfigStickInputId>).GetConfig(); + } + + config.ControllerType = Controllers[_controller].Type; + + string jsonString = JsonHelper.Serialize(config, true); + + await File.WriteAllTextAsync(path, jsonString); + + LoadProfiles(); + } + else + { + ContentDialogHelper.CreateErrorDialog(_owner.GetVisualRoot() as StyleableWindow, LocaleManager.Instance["DialogProfileInvalidProfileNameErrorMessage"]); + } + } + } + + public async void RemoveProfile() + { + if (Device == 0 || ProfileName == LocaleManager.Instance["ControllerSettingsProfileDefault"] || ProfilesList.IndexOf(ProfileName) == -1) + { + return; + } + + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + _owner.GetVisualRoot() as StyleableWindow, + LocaleManager.Instance["DialogProfileDeleteProfileTitle"], + LocaleManager.Instance["DialogProfileDeleteProfileMessage"], + LocaleManager.Instance["InputDialogYes"], + LocaleManager.Instance["InputDialogNo"], + LocaleManager.Instance["RyujinxConfirm"]); + + if (result == UserResult.Yes) + { + string path = Path.Combine(GetProfileBasePath(), ProfileName + ".json"); + + if (File.Exists(path)) + { + File.Delete(path); + } + + LoadProfiles(); + } + } + + public void Save() + { + IsModified = false; + + List<InputConfig> newConfig = new(); + + newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); + + newConfig.Remove(newConfig.Find(x => x == null)); + + if (Device == 0) + { + newConfig.Remove(newConfig.Find(x => x.PlayerIndex == this.PlayerId)); + } + else + { + var device = Devices[Device]; + + if (device.Type == DeviceType.Keyboard) + { + var inputConfig = Configuration as InputConfiguration<Key, ConfigStickInputId>; + inputConfig.Id = device.Id; + } + else + { + var inputConfig = Configuration as InputConfiguration<GamepadInputId, ConfigStickInputId>; + inputConfig.Id = device.Id.Split(" ")[0]; + } + + var config = !IsController + ? (Configuration as InputConfiguration<Key, ConfigStickInputId>).GetConfig() + : (Configuration as InputConfiguration<GamepadInputId, ConfigStickInputId>).GetConfig(); + config.ControllerType = Controllers[_controller].Type; + config.PlayerIndex = _playerId; + + int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId); + if (i == -1) + { + newConfig.Add(config); + } + else + { + newConfig[i] = config; + } + } + + _mainWindow.AppHost?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); + + // Atomically replace and signal input change. + // NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event. + ConfigurationState.Instance.Hid.InputConfig.Value = newConfig; + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + } + + public void NotifyChange(string property) + { + OnPropertyChanged(property); + } + + public void NotifyChanges() + { + OnPropertyChanged(nameof(Configuration)); + OnPropertyChanged(nameof(IsController)); + OnPropertyChanged(nameof(ShowSettings)); + OnPropertyChanged(nameof(IsKeyboard)); + OnPropertyChanged(nameof(IsRight)); + OnPropertyChanged(nameof(IsLeft)); + } + + public void Dispose() + { + _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected; + _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected; + + _mainWindow.AppHost?.NpadManager.UnblockInputUpdates(); + + SelectedGamepad?.Dispose(); + + AvaloniaKeyboardDriver.Dispose(); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs index dcb015bb..dbc19f75 100644 --- a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs +++ b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs @@ -11,6 +11,7 @@ using LibHac.FsSystem; using LibHac.Ncm; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Input; using Ryujinx.Ava.Ui.Controls; using Ryujinx.Ava.Ui.Windows; using Ryujinx.Common; @@ -35,7 +36,7 @@ using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState; namespace Ryujinx.Ava.Ui.ViewModels { - public class MainWindowViewModel : BaseModel + internal class MainWindowViewModel : BaseModel { private readonly MainWindow _owner; private ObservableCollection<ApplicationData> _applications; @@ -86,9 +87,7 @@ namespace Ryujinx.Ava.Ui.ViewModels if (Program.PreviewerDetached) { - ShowUiKey = KeyGesture.Parse(ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUi.ToString()); - ScreenshotKey = KeyGesture.Parse(ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot.ToString()); - PauseKey = KeyGesture.Parse(ConfigurationState.Instance.Hid.Hotkeys.Value.Pause.ToString()); + LoadConfigurableHotKeys(); Volume = ConfigurationState.Instance.System.AudioVolume; } @@ -836,6 +835,22 @@ namespace Ryujinx.Ava.Ui.ViewModels } } + public void LoadConfigurableHotKeys() + { + if (AvaloniaMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUi, out var showUiKey)) + { + ShowUiKey = new KeyGesture(showUiKey, KeyModifiers.None); + } + if (AvaloniaMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot, out var screenshotKey)) + { + ScreenshotKey = new KeyGesture(screenshotKey, KeyModifiers.None); + } + if (AvaloniaMappingHelper.TryGetAvaKey((Ryujinx.Input.Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause, out var pauseKey)) + { + PauseKey = new KeyGesture(pauseKey, KeyModifiers.None); + } + } + public void TakeScreenshot() { _owner.AppHost.ScreenshotRequested = true; @@ -930,10 +945,12 @@ namespace Ryujinx.Ava.Ui.ViewModels } } - public void OpenSettings() + public async void OpenSettings() { - // TODO : Implement Settings window - ContentDialogHelper.ShowNotAvailableMessage(_owner); + _owner.SettingsWindow = new(_owner.VirtualFileSystem, _owner.ContentManager); + + await _owner.SettingsWindow.ShowDialog(_owner); + LoadConfigurableHotKeys(); } public void ManageProfiles() @@ -951,6 +968,7 @@ namespace Ryujinx.Ava.Ui.ViewModels public void ChangeLanguage(object obj) { + LocaleManager.Instance.LoadDefaultLanguage(); LocaleManager.Instance.LoadLanguage((string)obj); } @@ -1350,7 +1368,13 @@ namespace Ryujinx.Ava.Ui.ViewModels dialogMessage += LocaleManager.Instance["DialogFirmwareInstallerFirmwareInstallConfirmMessage"]; - UserResult result = await ContentDialogHelper.CreateConfirmationDialog(_owner, dialogTitle, dialogMessage, LocaleManager.Instance["InputDialogYes"], LocaleManager.Instance["InputDialogNo"], LocaleManager.Instance["RyujinxConfirm"]); + UserResult result = await ContentDialogHelper.CreateConfirmationDialog( + _owner, + dialogTitle, + dialogMessage, + LocaleManager.Instance["InputDialogYes"], + LocaleManager.Instance["InputDialogNo"], + LocaleManager.Instance["RyujinxConfirm"]); UpdateWaitWindow waitingDialog = ContentDialogHelper.CreateWaitingDialog(dialogTitle, LocaleManager.Instance["DialogFirmwareInstallerFirmwareInstallWaitMessage"]); diff --git a/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs new file mode 100644 index 00000000..ef7a53e6 --- /dev/null +++ b/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs @@ -0,0 +1,403 @@ +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Threading; +using LibHac.Tools.FsSystem; +using Ryujinx.Audio.Backends.OpenAL; +using Ryujinx.Audio.Backends.SDL2; +using Ryujinx.Audio.Backends.SoundIo; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Input; +using Ryujinx.Ava.Ui.Controls; +using Ryujinx.Ava.Ui.Windows; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.GraphicsDriver; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; +using Ryujinx.Input; +using Ryujinx.Ui.Common.Configuration; +using Ryujinx.Ui.Common.Configuration.System; +using System; +using System.Collections.Generic; +using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone; + +namespace Ryujinx.Ava.Ui.ViewModels +{ + internal class SettingsViewModel : BaseModel + { + private readonly VirtualFileSystem _virtualFileSystem; + private readonly ContentManager _contentManager; + private readonly StyleableWindow _owner; + private TimeZoneContentManager _timeZoneContentManager; + + private readonly List<string> _validTzRegions; + + private float _customResolutionScale; + private int _resolutionScale; + private int _graphicsBackendMultithreadingIndex; + private float _previousVolumeLevel; + private float _volume; + + public int ResolutionScale + { + get => _resolutionScale; + set + { + _resolutionScale = value; + + OnPropertyChanged(nameof(CustomResolutionScale)); + OnPropertyChanged(nameof(IsCustomResolutionScaleActive)); + } + } + public int GraphicsBackendMultithreadingIndex + { + get => _graphicsBackendMultithreadingIndex; + set + { + _graphicsBackendMultithreadingIndex = value; + + if (_owner != null) + { + if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value) + { + Dispatcher.UIThread.Post(async () => + { + await ContentDialogHelper.CreateInfoDialog(_owner, + LocaleManager.Instance["DialogSettingsBackendThreadingWarningMessage"], + "", + "", + LocaleManager.Instance["InputDialogOk"], + LocaleManager.Instance["DialogSettingsBackendThreadingWarningTitle"]); + }); + } + } + + OnPropertyChanged(); + } + } + + public float CustomResolutionScale + { + get => _customResolutionScale; + set + { + _customResolutionScale = MathF.Round(value, 1); + + OnPropertyChanged(); + } + } + + public bool EnableDiscordIntegration { get; set; } + public bool CheckUpdatesOnStart { get; set; } + public bool ShowConfirmExit { get; set; } + public bool HideCursorOnIdle { get; set; } + public bool EnableDockedMode { get; set; } + public bool EnableKeyboard { get; set; } + public bool EnableMouse { get; set; } + public bool EnableVsync { get; set; } + public bool EnablePptc { get; set; } + public bool EnableInternetAccess { get; set; } + public bool EnableFsIntegrityChecks { get; set; } + public bool IgnoreMissingServices { get; set; } + public bool ExpandDramSize { get; set; } + public bool EnableShaderCache { get; set; } + public bool EnableFileLog { get; set; } + public bool EnableStub { get; set; } + public bool EnableInfo { get; set; } + public bool EnableWarn { get; set; } + public bool EnableError { get; set; } + public bool EnableTrace { get; set; } + public bool EnableGuest { get; set; } + public bool EnableFsAccessLog { get; set; } + public bool EnableDebug { get; set; } + public bool IsOpenAlEnabled { get; set; } + public bool IsSoundIoEnabled { get; set; } + public bool IsSDL2Enabled { get; set; } + public bool EnableCustomTheme { get; set; } + public bool IsCustomResolutionScaleActive => _resolutionScale == 0; + + public string TimeZone { get; set; } + public string ShaderDumpPath { get; set; } + public string CustomThemePath { get; set; } + + public int Language { get; set; } + public int Region { get; set; } + public int FsGlobalAccessLogMode { get; set; } + public int AudioBackend { get; set; } + public int MaxAnisotropy { get; set; } + public int AspectRatio { get; set; } + public int OpenglDebugLevel { get; set; } + public int MemoryMode { get; set; } + public int BaseStyleIndex { get; set; } + + public float Volume + { + get => _volume; + set + { + _volume = value; + + ConfigurationState.Instance.System.AudioVolume.Value = (float)(_volume / 100); + + OnPropertyChanged(); + } + } + + public DateTimeOffset DateOffset { get; set; } + public TimeSpan TimeOffset { get; set; } + public AvaloniaList<TimeZone> TimeZones { get; set; } + + public AvaloniaList<string> GameDirectories { get; set; } + + private KeyboardHotkeys _keyboardHotkeys; + + public KeyboardHotkeys KeyboardHotkeys + { + get => _keyboardHotkeys; + set + { + _keyboardHotkeys = value; + + OnPropertyChanged(); + } + } + + public IGamepadDriver AvaloniaKeyboardDriver { get; } + + public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager, StyleableWindow owner) : this() + { + _virtualFileSystem = virtualFileSystem; + _contentManager = contentManager; + _owner = owner; + if (Program.PreviewerDetached) + { + LoadTimeZones(); + AvaloniaKeyboardDriver = new AvaloniaKeyboardDriver(owner); + } + } + + public SettingsViewModel() + { + GameDirectories = new AvaloniaList<string>(); + TimeZones = new AvaloniaList<TimeZone>(); + _validTzRegions = new List<string>(); + + CheckSoundBackends(); + + if (Program.PreviewerDetached) + { + LoadCurrentConfiguration(); + } + } + + public void CheckSoundBackends() + { + IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported; + IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported; + IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported; + } + + public void LoadTimeZones() + { + _timeZoneContentManager = new TimeZoneContentManager(); + + _timeZoneContentManager.InitializeInstance(_virtualFileSystem, _contentManager, IntegrityCheckLevel.None); + + foreach ((int offset, string location, string abbr) in _timeZoneContentManager.ParseTzOffsets()) + { + int hours = Math.DivRem(offset, 3600, out int seconds); + int minutes = Math.Abs(seconds) / 60; + + string abbr2 = abbr.StartsWith('+') || abbr.StartsWith('-') ? string.Empty : abbr; + + TimeZones.Add(new TimeZone($"UTC{hours:+0#;-0#;+00}:{minutes:D2}", location, abbr2)); + + _validTzRegions.Add(location); + } + } + + public void ValidateAndSetTimeZone(string location) + { + if (_validTzRegions.Contains(location)) + { + TimeZone = location; + + OnPropertyChanged(nameof(TimeZone)); + } + } + + public async void BrowseTheme() + { + var dialog = new OpenFileDialog() + { + Title = LocaleManager.Instance["SettingsSelectThemeFileDialogTitle"], + AllowMultiple = false + }; + + dialog.Filters.Add(new FileDialogFilter() { Extensions = { "xaml" }, Name = LocaleManager.Instance["SettingsXamlThemeFile"] }); + + var file = await dialog.ShowAsync(_owner); + + if (file != null && file.Length > 0) + { + CustomThemePath = file[0]; + OnPropertyChanged(nameof(CustomThemePath)); + } + } + + public void LoadCurrentConfiguration() + { + ConfigurationState config = ConfigurationState.Instance; + + GameDirectories.Clear(); + GameDirectories.AddRange(config.Ui.GameDirs.Value); + + EnableDiscordIntegration = config.EnableDiscordIntegration; + CheckUpdatesOnStart = config.CheckUpdatesOnStart; + ShowConfirmExit = config.ShowConfirmExit; + HideCursorOnIdle = config.HideCursorOnIdle; + EnableDockedMode = config.System.EnableDockedMode; + EnableKeyboard = config.Hid.EnableKeyboard; + EnableMouse = config.Hid.EnableMouse; + EnableVsync = config.Graphics.EnableVsync; + EnablePptc = config.System.EnablePtc; + EnableInternetAccess = config.System.EnableInternetAccess; + EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; + IgnoreMissingServices = config.System.IgnoreMissingServices; + ExpandDramSize = config.System.ExpandRam; + EnableShaderCache = config.Graphics.EnableShaderCache; + EnableFileLog = config.Logger.EnableFileLog; + EnableStub = config.Logger.EnableStub; + EnableInfo = config.Logger.EnableInfo; + EnableWarn = config.Logger.EnableWarn; + EnableError = config.Logger.EnableError; + EnableTrace = config.Logger.EnableTrace; + EnableGuest = config.Logger.EnableGuest; + EnableDebug = config.Logger.EnableDebug; + EnableFsAccessLog = config.Logger.EnableFsAccessLog; + EnableCustomTheme = config.Ui.EnableCustomTheme; + Volume = config.System.AudioVolume * 100; + + GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value; + + OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; + + TimeZone = config.System.TimeZone; + ShaderDumpPath = config.Graphics.ShadersDumpPath; + CustomThemePath = config.Ui.CustomThemePath; + BaseStyleIndex = config.Ui.BaseStyle == "Light" ? 0 : 1; + + Language = (int)config.System.Language.Value; + Region = (int)config.System.Region.Value; + FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; + AudioBackend = (int)config.System.AudioBackend.Value; + MemoryMode = (int)config.System.MemoryManagerMode.Value; + + float anisotropy = config.Graphics.MaxAnisotropy; + + MaxAnisotropy = anisotropy == -1 ? 0 : (int)(MathF.Log2(anisotropy)); + AspectRatio = (int)config.Graphics.AspectRatio.Value; + + int resolution = config.Graphics.ResScale; + + ResolutionScale = resolution == -1 ? 0 : resolution; + CustomResolutionScale = config.Graphics.ResScaleCustom; + + DateTime dateTimeOffset = DateTime.Now.AddSeconds(config.System.SystemTimeOffset); + + DateOffset = dateTimeOffset.Date; + TimeOffset = dateTimeOffset.TimeOfDay; + + KeyboardHotkeys = config.Hid.Hotkeys.Value; + + _previousVolumeLevel = Volume; + } + + public void SaveSettings() + { + List<string> gameDirs = new List<string>(GameDirectories); + + ConfigurationState config = ConfigurationState.Instance; + + if (_validTzRegions.Contains(TimeZone)) + { + config.System.TimeZone.Value = TimeZone; + } + + config.Logger.EnableError.Value = EnableError; + config.Logger.EnableTrace.Value = EnableTrace; + config.Logger.EnableWarn.Value = EnableWarn; + config.Logger.EnableInfo.Value = EnableInfo; + config.Logger.EnableStub.Value = EnableStub; + config.Logger.EnableDebug.Value = EnableDebug; + config.Logger.EnableGuest.Value = EnableGuest; + config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog; + config.Logger.EnableFileLog.Value = EnableFileLog; + config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; + config.System.EnableDockedMode.Value = EnableDockedMode; + config.EnableDiscordIntegration.Value = EnableDiscordIntegration; + config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart; + config.ShowConfirmExit.Value = ShowConfirmExit; + config.HideCursorOnIdle.Value = HideCursorOnIdle; + config.Graphics.EnableVsync.Value = EnableVsync; + config.Graphics.EnableShaderCache.Value = EnableShaderCache; + config.System.EnablePtc.Value = EnablePptc; + config.System.EnableInternetAccess.Value = EnableInternetAccess; + config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; + config.System.IgnoreMissingServices.Value = IgnoreMissingServices; + config.System.ExpandRam.Value = ExpandDramSize; + config.Hid.EnableKeyboard.Value = EnableKeyboard; + config.Hid.EnableMouse.Value = EnableMouse; + config.Ui.CustomThemePath.Value = CustomThemePath; + config.Ui.EnableCustomTheme.Value = EnableCustomTheme; + config.Ui.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark"; + config.System.Language.Value = (Language)Language; + config.System.Region.Value = (Region)Region; + + if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex) + { + DriverUtilities.ToggleOGLThreading(GraphicsBackendMultithreadingIndex == (int)BackendThreading.Off); + } + + config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex; + + TimeSpan systemTimeOffset = DateOffset - DateTime.Now; + + config.System.SystemTimeOffset.Value = systemTimeOffset.Seconds; + config.Graphics.ShadersDumpPath.Value = ShaderDumpPath; + config.Ui.GameDirs.Value = gameDirs; + config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; + config.System.MemoryManagerMode.Value = (MemoryManagerMode)MemoryMode; + + float anisotropy = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy); + + config.Graphics.MaxAnisotropy.Value = anisotropy; + config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio; + config.Graphics.ResScale.Value = ResolutionScale == 0 ? -1 : ResolutionScale; + config.Graphics.ResScaleCustom.Value = CustomResolutionScale; + config.System.AudioVolume.Value = Volume / 100; + + AudioBackend audioBackend = (AudioBackend)AudioBackend; + if (audioBackend != config.System.AudioBackend.Value) + { + config.System.AudioBackend.Value = audioBackend; + + Logger.Info?.Print(LogClass.Application, $"AudioBackend toggled to: {audioBackend}"); + } + + config.Hid.Hotkeys.Value = KeyboardHotkeys; + + config.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + MainWindow.UpdateGraphicsConfig(); + + _previousVolumeLevel = Volume; + } + + public void RevertIfNotSaved() + { + Program.ReloadConfig(); + } + } +}
\ No newline at end of file |
