diff --git a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs index 0cb49ca8c..a05070ed6 100644 --- a/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs +++ b/src/Ryujinx.Common/Configuration/Hid/KeyboardHotkeys.cs @@ -1,6 +1,8 @@ +using System; + namespace Ryujinx.Common.Configuration.Hid { - public class KeyboardHotkeys + public class KeyboardHotkeys : IEquatable { public Key ToggleVsync { get; set; } public Key Screenshot { get; set; } @@ -11,5 +13,65 @@ namespace Ryujinx.Common.Configuration.Hid public Key ResScaleDown { get; set; } public Key VolumeUp { get; set; } public Key VolumeDown { get; set; } + + public bool Equals(KeyboardHotkeys other) + { + if (other == null) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return ToggleVsync == other.ToggleVsync && + Screenshot == other.Screenshot && + ShowUI == other.ShowUI && + Pause == other.Pause && + ToggleMute == other.ToggleMute && + ResScaleUp == other.ResScaleUp && + ResScaleDown == other.ResScaleDown && + VolumeUp == other.VolumeUp && + VolumeDown == other.VolumeDown; + } + + public override bool Equals(object obj) + { + if (obj == null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return Equals((KeyboardHotkeys)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (int)ToggleVsync; + hashCode = (hashCode * 397) ^ (int)Screenshot; + hashCode = (hashCode * 397) ^ (int)ShowUI; + hashCode = (hashCode * 397) ^ (int)Pause; + hashCode = (hashCode * 397) ^ (int)ToggleMute; + hashCode = (hashCode * 397) ^ (int)ResScaleUp; + hashCode = (hashCode * 397) ^ (int)ResScaleDown; + hashCode = (hashCode * 397) ^ (int)VolumeUp; + hashCode = (hashCode * 397) ^ (int)VolumeDown; + return hashCode; + } + } } } diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index b3cab7f5f..005fb5e1f 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -92,6 +92,7 @@ "LinuxVmMaxMapCountWarningTextPrimary": "Max amount of memory mappings is lower than recommended.", "LinuxVmMaxMapCountWarningTextSecondary": "The current value of vm.max_map_count ({0}) is lower than {1}. Some games might try to create more memory mappings than currently allowed. Ryujinx will crash as soon as this limit gets exceeded.\n\nYou might want to either manually increase the limit or install pkexec, which allows Ryujinx to assist with that.", "Settings": "Settings", + "SettingsDirty": "Unsaved Changes", "SettingsTabGeneral": "User Interface", "SettingsTabGeneralGeneral": "General", "SettingsTabGeneralEnableDiscordRichPresence": "Enable Discord Rich Presence", @@ -490,8 +491,8 @@ "DialogUserProfileUnsavedChangesTitle": "Warning - Unsaved Changes", "DialogUserProfileUnsavedChangesMessage": "You have made changes to this user profile that have not been saved.", "DialogUserProfileUnsavedChangesSubMessage": "Do you want to discard your changes?", - "DialogControllerSettingsModifiedConfirmMessage": "The current controller settings has been updated.", - "DialogControllerSettingsModifiedConfirmSubMessage": "Do you want to save?", + "DialogSettingsUnsavedChangesMessage": "You have made changes to settings that have not been saved.", + "DialogSettingsUnsavedChangesSubMessage": "Do you want to discard your changes?", "DialogLoadFileErrorMessage": "{0}. Errored File: {1}", "DialogModAlreadyExistsMessage": "Mod already exists", "DialogModInvalidMessage": "The specified directory does not contain a mod!", diff --git a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs index 15b7ddd14..2ff57322e 100644 --- a/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs +++ b/src/Ryujinx/UI/Helpers/ContentDialogHelper.cs @@ -28,7 +28,8 @@ namespace Ryujinx.Ava.UI.Helpers string closeButton, UserResult primaryButtonResult = UserResult.Ok, ManualResetEvent deferResetEvent = null, - TypedEventHandler deferCloseAction = null) + TypedEventHandler deferCloseAction = null, + Window parent = null) { UserResult result = UserResult.None; @@ -62,7 +63,7 @@ namespace Ryujinx.Ava.UI.Helpers contentDialog.PrimaryButtonClick += deferCloseAction; } - await ShowAsync(contentDialog); + await ShowAsync(contentDialog, parent); return result; } @@ -77,11 +78,21 @@ namespace Ryujinx.Ava.UI.Helpers int iconSymbol, UserResult primaryButtonResult = UserResult.Ok, ManualResetEvent deferResetEvent = null, - TypedEventHandler deferCloseAction = null) + TypedEventHandler deferCloseAction = null, + Window parent = null) { Grid content = CreateTextDialogContent(primaryText, secondaryText, iconSymbol); - return await ShowContentDialog(title, content, primaryButton, secondaryButton, closeButton, primaryButtonResult, deferResetEvent, deferCloseAction); + return await ShowContentDialog( + title, + content, + primaryButton, + secondaryButton, + closeButton, + primaryButtonResult, + deferResetEvent, + deferCloseAction, + parent); } public async static Task ShowDeferredContentDialog( @@ -94,7 +105,8 @@ namespace Ryujinx.Ava.UI.Helpers string closeButton, int iconSymbol, ManualResetEvent deferResetEvent, - Func doWhileDeferred = null) + Func doWhileDeferred = null, + Window parent = null) { bool startedDeferring = false; @@ -108,7 +120,8 @@ namespace Ryujinx.Ava.UI.Helpers iconSymbol, primaryButton == LocaleManager.Instance[LocaleKeys.InputDialogYes] ? UserResult.Yes : UserResult.Ok, deferResetEvent, - DeferClose); + DeferClose, + parent); async void DeferClose(ContentDialog sender, ContentDialogButtonClickEventArgs args) { @@ -199,7 +212,8 @@ namespace Ryujinx.Ava.UI.Helpers string secondaryText, string acceptButton, string closeButton, - string title) + string title, + Window parent = null) { return await ShowTextDialog( title, @@ -208,7 +222,8 @@ namespace Ryujinx.Ava.UI.Helpers acceptButton, "", closeButton, - (int)Symbol.Important); + (int)Symbol.Important, + parent: parent); } internal static async Task CreateConfirmationDialog( @@ -217,7 +232,8 @@ namespace Ryujinx.Ava.UI.Helpers string acceptButtonText, string cancelButtonText, string title, - UserResult primaryButtonResult = UserResult.Yes) + UserResult primaryButtonResult = UserResult.Yes, + Window parent = null) { return await ShowTextDialog( string.IsNullOrWhiteSpace(title) ? LocaleManager.Instance[LocaleKeys.DialogConfirmationTitle] : title, @@ -227,7 +243,8 @@ namespace Ryujinx.Ava.UI.Helpers "", cancelButtonText, (int)Symbol.Help, - primaryButtonResult); + primaryButtonResult, + parent: parent); } internal static async Task CreateUpdaterInfoDialog(string primary, string secondaryText) @@ -268,7 +285,11 @@ namespace Ryujinx.Ava.UI.Helpers (int)Symbol.Dismiss); } - internal static async Task CreateChoiceDialog(string title, string primary, string secondaryText) + internal static async Task CreateChoiceDialog( + string title, + string primary, + string secondaryText, + Window parent = null) { if (_isChoiceDialogOpen) { @@ -285,7 +306,8 @@ namespace Ryujinx.Ava.UI.Helpers "", LocaleManager.Instance[LocaleKeys.InputDialogNo], (int)Symbol.Help, - UserResult.Yes); + UserResult.Yes, + parent: parent); _isChoiceDialogOpen = false; @@ -308,69 +330,62 @@ namespace Ryujinx.Ava.UI.Helpers LocaleManager.Instance[LocaleKeys.DialogExitSubMessage]); } - public static async Task ShowAsync(ContentDialog contentDialog) + public static async Task ShowAsync(ContentDialog contentDialog, Window parent = null) { ContentDialogResult result; bool isTopDialog = true; - Window parent = GetMainWindow(); + parent ??= GetMainWindow(); if (_contentDialogOverlayWindow != null) { isTopDialog = false; } - if (parent is MainWindow window) + parent.Activate(); + + _contentDialogOverlayWindow = new ContentDialogOverlayWindow { - parent.Activate(); + Height = parent.Bounds.Height, + Width = parent.Bounds.Width, + Position = parent.PointToScreen(new Point()), + ShowInTaskbar = false, + }; - _contentDialogOverlayWindow = new ContentDialogOverlayWindow + parent.PositionChanged += OverlayOnPositionChanged; + + void OverlayOnPositionChanged(object sender, PixelPointEventArgs e) + { + if (_contentDialogOverlayWindow is null) { - Height = parent.Bounds.Height, - Width = parent.Bounds.Width, - Position = parent.PointToScreen(new Point()), - ShowInTaskbar = false, - }; - - parent.PositionChanged += OverlayOnPositionChanged; - - void OverlayOnPositionChanged(object sender, PixelPointEventArgs e) - { - if (_contentDialogOverlayWindow is null) - { - return; - } - - _contentDialogOverlayWindow.Position = parent.PointToScreen(new Point()); + return; } - _contentDialogOverlayWindow.ContentDialog = contentDialog; - - bool opened = false; - - _contentDialogOverlayWindow.Opened += OverlayOnActivated; - - async void OverlayOnActivated(object sender, EventArgs e) - { - if (opened) - { - return; - } - - opened = true; - - _contentDialogOverlayWindow.Position = parent.PointToScreen(new Point()); - - result = await ShowDialog(); - } - - result = await _contentDialogOverlayWindow.ShowDialog(parent); + _contentDialogOverlayWindow.Position = parent.PointToScreen(new Point()); } - else + + _contentDialogOverlayWindow.ContentDialog = contentDialog; + + bool opened = false; + + _contentDialogOverlayWindow.Opened += OverlayOnActivated; + + async void OverlayOnActivated(object sender, EventArgs e) { + if (opened) + { + return; + } + + opened = true; + + _contentDialogOverlayWindow.Position = parent.PointToScreen(new Point()); + result = await ShowDialog(); } + result = await _contentDialogOverlayWindow.ShowDialog(parent); + async Task ShowDialog() { if (_contentDialogOverlayWindow is not null) diff --git a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs index 6ee79a371..018af14ba 100644 --- a/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs @@ -1,5 +1,6 @@ using Avalonia.Svg.Skia; using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Ava.UI.ViewModels.Settings; using Ryujinx.Ava.UI.Views.Input; namespace Ryujinx.Ava.UI.ViewModels.Input @@ -54,9 +55,9 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } - public readonly InputViewModel ParentModel; + public readonly SettingsInputViewModel ParentModel; - public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config) + public ControllerInputViewModel(SettingsInputViewModel model, GamepadInputConfig config) { ParentModel = model; model.NotifyChangesEvent += OnParentModelChanged; diff --git a/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs index 0b530eb09..80a3403e3 100644 --- a/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/KeyboardInputViewModel.cs @@ -1,5 +1,6 @@ using Avalonia.Svg.Skia; using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Ava.UI.ViewModels.Settings; namespace Ryujinx.Ava.UI.ViewModels.Input { @@ -53,9 +54,9 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } - public readonly InputViewModel ParentModel; + public readonly SettingsInputViewModel ParentModel; - public KeyboardInputViewModel(InputViewModel model, KeyboardInputConfig config) + public KeyboardInputViewModel(SettingsInputViewModel model, KeyboardInputConfig config) { ParentModel = model; model.NotifyChangesEvent += OnParentModelChanged; diff --git a/src/Ryujinx/UI/ViewModels/Settings/SettingsAudioViewModel.cs b/src/Ryujinx/UI/ViewModels/Settings/SettingsAudioViewModel.cs new file mode 100644 index 000000000..e2c140429 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Settings/SettingsAudioViewModel.cs @@ -0,0 +1,89 @@ +using Avalonia.Threading; +using Ryujinx.Audio.Backends.OpenAL; +using Ryujinx.Audio.Backends.SDL2; +using Ryujinx.Audio.Backends.SoundIo; +using Ryujinx.Common.Logging; +using Ryujinx.UI.Common.Configuration; +using System; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.ViewModels.Settings +{ + public class SettingsAudioViewModel : BaseModel + { + public event Action DirtyEvent; + + private int _audioBackend; + public int AudioBackend + { + get => _audioBackend; + set + { + _audioBackend = value; + DirtyEvent?.Invoke(); + } + } + + private float _volume; + public float Volume + { + get => _volume; + set + { + _volume = value; + DirtyEvent?.Invoke(); + } + } + + public bool IsOpenAlEnabled { get; set; } + public bool IsSoundIoEnabled { get; set; } + public bool IsSDL2Enabled { get; set; } + + public SettingsAudioViewModel() + { + ConfigurationState config = ConfigurationState.Instance; + + Task.Run(CheckSoundBackends); + + AudioBackend = (int)config.System.AudioBackend.Value; + Volume = config.System.AudioVolume * 100; + } + + public async Task CheckSoundBackends() + { + IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported; + IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported; + IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported; + + await Dispatcher.UIThread.InvokeAsync(() => + { + OnPropertyChanged(nameof(IsOpenAlEnabled)); + OnPropertyChanged(nameof(IsSoundIoEnabled)); + OnPropertyChanged(nameof(IsSDL2Enabled)); + }); + } + + public bool CheckIfModified(ConfigurationState config) + { + bool isDirty = false; + + isDirty |= config.System.AudioBackend.Value != (AudioBackend)AudioBackend; + isDirty |= config.System.AudioVolume.Value != Volume / 100; + + return isDirty; + } + + public void Save(ConfigurationState config) + { + 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.System.AudioVolume.Value = Volume / 100; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Settings/SettingsCpuViewModel.cs b/src/Ryujinx/UI/ViewModels/Settings/SettingsCpuViewModel.cs new file mode 100644 index 000000000..a3ce0d241 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Settings/SettingsCpuViewModel.cs @@ -0,0 +1,74 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.UI.Common.Configuration; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Ava.UI.ViewModels.Settings +{ + public class SettingsCpuViewModel : BaseModel + { + public event Action DirtyEvent; + + public bool IsHypervisorAvailable => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; + + private bool _enablePptc; + public bool EnablePptc + { + get => _enablePptc; + set + { + _enablePptc = value; + DirtyEvent?.Invoke(); + } + } + + private bool _useHypervisor; + public bool UseHypervisor + { + get => _useHypervisor; + set + { + _useHypervisor = value; + DirtyEvent?.Invoke(); + } + } + + private int _memoryMode; + public int MemoryMode + { + get => _memoryMode; + set + { + _memoryMode = value; + DirtyEvent?.Invoke(); + } + } + + public SettingsCpuViewModel() + { + ConfigurationState config = ConfigurationState.Instance; + + EnablePptc = config.System.EnablePtc; + MemoryMode = (int)config.System.MemoryManagerMode.Value; + UseHypervisor = config.System.UseHypervisor; + } + + public bool CheckIfModified(ConfigurationState config) + { + bool isDirty = false; + + isDirty |= config.System.EnablePtc.Value != EnablePptc; + isDirty |= config.System.MemoryManagerMode.Value != (MemoryManagerMode)MemoryMode; + isDirty |= config.System.UseHypervisor.Value != UseHypervisor; + + return isDirty; + } + + public void Save(ConfigurationState config) + { + config.System.EnablePtc.Value = EnablePptc; + config.System.MemoryManagerMode.Value = (MemoryManagerMode)MemoryMode; + config.System.UseHypervisor.Value = UseHypervisor; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Settings/SettingsGraphicsViewModel.cs b/src/Ryujinx/UI/ViewModels/Settings/SettingsGraphicsViewModel.cs new file mode 100644 index 000000000..fe8fd248f --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Settings/SettingsGraphicsViewModel.cs @@ -0,0 +1,321 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.GraphicsDriver; +using Ryujinx.Graphics.Vulkan; +using Ryujinx.UI.Common.Configuration; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.ViewModels.Settings +{ + public class SettingsGraphicsViewModel : BaseModel + { + public event Action DirtyEvent; + + public ObservableCollection AvailableGpus { get; set; } + public bool ColorSpacePassthroughAvailable => OperatingSystem.IsMacOS(); + public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS(); + public bool IsCustomResolutionScaleActive => _resolutionScale == 4; + public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr; + public bool IsVulkanSelected => GraphicsBackendIndex == 0; + public string ScalingFilterLevelText => ScalingFilterLevel.ToString("0"); + private readonly List _gpuIds = new(); + + private int _graphicsBackendIndex; + public int GraphicsBackendIndex + { + get => _graphicsBackendIndex; + set + { + _graphicsBackendIndex = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsVulkanSelected)); + DirtyEvent?.Invoke(); + } + } + + private int _preferredGpuIndex; + public int PreferredGpuIndex + { + get => _preferredGpuIndex; + set + { + _preferredGpuIndex = value; + OnPropertyChanged(); + DirtyEvent?.Invoke(); + } + } + + + private bool _isVulkanAvailable = true; + public bool IsVulkanAvailable + { + get => _isVulkanAvailable; + set + { + _isVulkanAvailable = value; + OnPropertyChanged(); + } + } + + private bool _enableShaderCache; + public bool EnableShaderCache + { + get => _enableShaderCache; + set + { + _enableShaderCache = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableTextureRecompression; + public bool EnableTextureRecompression + { + get => _enableTextureRecompression; + set + { + _enableTextureRecompression = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableMacroHLE; + public bool EnableMacroHLE + { + get => _enableMacroHLE; + set + { + _enableMacroHLE = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableColorSpacePassthrough; + public bool EnableColorSpacePassthrough + { + get => _enableColorSpacePassthrough; + set + { + _enableColorSpacePassthrough = value; + DirtyEvent?.Invoke(); + } + } + + private int _resolutionScale; + public int ResolutionScale + { + get => _resolutionScale; + set + { + _resolutionScale = value; + + OnPropertyChanged(nameof(CustomResolutionScale)); + OnPropertyChanged(nameof(IsCustomResolutionScaleActive)); + DirtyEvent?.Invoke(); + } + } + + private float _customResolutionScale; + public float CustomResolutionScale + { + get => _customResolutionScale; + set + { + _customResolutionScale = MathF.Round(value, 1); + OnPropertyChanged(); + DirtyEvent?.Invoke(); + } + } + + private int _maxAnisotropy; + public int MaxAnisotropy + { + get => _maxAnisotropy; + set + { + _maxAnisotropy = value; + DirtyEvent?.Invoke(); + } + } + + private int _aspectRatio; + public int AspectRatio + { + get => _aspectRatio; + set + { + _aspectRatio = value; + DirtyEvent?.Invoke(); + } + } + + private int _graphicsBackendMultithreadingIndex; + public int GraphicsBackendMultithreadingIndex + { + get => _graphicsBackendMultithreadingIndex; + set + { + _graphicsBackendMultithreadingIndex = value; + OnPropertyChanged(); + DirtyEvent?.Invoke(); + } + } + + private string _shaderDumpPath; + public string ShaderDumpPath + { + get => _shaderDumpPath; + set + { + _shaderDumpPath = value; + DirtyEvent?.Invoke(); + } + } + + private int _antiAliasingEffect; + public int AntiAliasingEffect + { + get => _antiAliasingEffect; + set + { + _antiAliasingEffect = value; + DirtyEvent?.Invoke(); + } + } + + private int _scalingFilter; + public int ScalingFilter + { + get => _scalingFilter; + set + { + _scalingFilter = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsScalingFilterActive)); + DirtyEvent?.Invoke(); + } + } + + private int _scalingFilterLevel; + public int ScalingFilterLevel + { + get => _scalingFilterLevel; + set + { + _scalingFilterLevel = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ScalingFilterLevelText)); + DirtyEvent?.Invoke(); + } + } + + public SettingsGraphicsViewModel() + { + AvailableGpus = new ObservableCollection(); + + ConfigurationState config = ConfigurationState.Instance; + + GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value; + // Physical devices are queried asynchronously hence the preferred index config value is loaded in LoadAvailableGpus(). + EnableShaderCache = config.Graphics.EnableShaderCache; + EnableTextureRecompression = config.Graphics.EnableTextureRecompression; + EnableMacroHLE = config.Graphics.EnableMacroHLE; + EnableColorSpacePassthrough = config.Graphics.EnableColorSpacePassthrough; + ResolutionScale = config.Graphics.ResScale == -1 ? 4 : config.Graphics.ResScale - 1; + CustomResolutionScale = config.Graphics.ResScaleCustom; + MaxAnisotropy = config.Graphics.MaxAnisotropy == -1 ? 0 : (int)(MathF.Log2(config.Graphics.MaxAnisotropy)); + AspectRatio = (int)config.Graphics.AspectRatio.Value; + AntiAliasingEffect = (int)config.Graphics.AntiAliasing.Value; + ScalingFilter = (int)config.Graphics.ScalingFilter.Value; + ScalingFilterLevel = config.Graphics.ScalingFilterLevel.Value; + GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value; + ShaderDumpPath = config.Graphics.ShadersDumpPath; + + if (Program.PreviewerDetached) + { + Task.Run(LoadAvailableGpus); + } + } + + private async Task LoadAvailableGpus() + { + AvailableGpus.Clear(); + + var devices = VulkanRenderer.GetPhysicalDevices(); + + if (devices.Length == 0) + { + IsVulkanAvailable = false; + GraphicsBackendIndex = 1; + } + else + { + foreach (var device in devices) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + _gpuIds.Add(device.Id); + + AvailableGpus.Add(new ComboBoxItem { Content = $"{device.Name} {(device.IsDiscrete ? "(dGPU)" : "")}" }); + }); + } + } + + // GPU configuration needs to be loaded during the async method or it will always return 0. + PreferredGpuIndex = _gpuIds.Contains(ConfigurationState.Instance.Graphics.PreferredGpu) ? + _gpuIds.IndexOf(ConfigurationState.Instance.Graphics.PreferredGpu) : 0; + } + + public bool CheckIfModified(ConfigurationState config) + { + bool isDirty = false; + + isDirty |= config.Graphics.GraphicsBackend.Value != (GraphicsBackend)GraphicsBackendIndex; + isDirty |= config.Graphics.PreferredGpu.Value != _gpuIds.ElementAtOrDefault(PreferredGpuIndex); + isDirty |= config.Graphics.EnableShaderCache.Value != EnableShaderCache; + isDirty |= config.Graphics.EnableTextureRecompression.Value != EnableTextureRecompression; + isDirty |= config.Graphics.EnableMacroHLE.Value != EnableMacroHLE; + isDirty |= config.Graphics.EnableColorSpacePassthrough.Value != EnableColorSpacePassthrough; + isDirty |= config.Graphics.ResScale.Value != (ResolutionScale == 4 ? -1 : ResolutionScale + 1); + isDirty |= config.Graphics.ResScaleCustom.Value != CustomResolutionScale; + isDirty |= config.Graphics.MaxAnisotropy.Value != (MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy)); + isDirty |= config.Graphics.AspectRatio.Value != (AspectRatio)AspectRatio; + isDirty |= config.Graphics.AntiAliasing.Value != (AntiAliasing)AntiAliasingEffect; + isDirty |= config.Graphics.ScalingFilter.Value != (ScalingFilter)ScalingFilter; + isDirty |= config.Graphics.ScalingFilterLevel.Value != ScalingFilterLevel; + isDirty |= config.Graphics.BackendThreading.Value != (BackendThreading)GraphicsBackendMultithreadingIndex; + isDirty |= config.Graphics.ShadersDumpPath.Value != ShaderDumpPath; + + return isDirty; + } + + public void Save(ConfigurationState config) + { + config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex; + config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex); + config.Graphics.EnableShaderCache.Value = EnableShaderCache; + config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression; + config.Graphics.EnableMacroHLE.Value = EnableMacroHLE; + config.Graphics.EnableColorSpacePassthrough.Value = EnableColorSpacePassthrough; + config.Graphics.ResScale.Value = ResolutionScale == 4 ? -1 : ResolutionScale + 1; + config.Graphics.ResScaleCustom.Value = CustomResolutionScale; + config.Graphics.MaxAnisotropy.Value = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy); + config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio; + config.Graphics.AntiAliasing.Value = (AntiAliasing)AntiAliasingEffect; + config.Graphics.ScalingFilter.Value = (ScalingFilter)ScalingFilter; + config.Graphics.ScalingFilterLevel.Value = ScalingFilterLevel; + config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex; + config.Graphics.ShadersDumpPath.Value = ShaderDumpPath; + + if (config.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex) + { + DriverUtilities.ToggleOGLThreading(GraphicsBackendMultithreadingIndex == (int)BackendThreading.Off); + } + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Settings/SettingsHotkeysViewModel.cs b/src/Ryujinx/UI/ViewModels/Settings/SettingsHotkeysViewModel.cs new file mode 100644 index 000000000..18256a401 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Settings/SettingsHotkeysViewModel.cs @@ -0,0 +1,35 @@ +using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.UI.Common.Configuration; +using System; + +namespace Ryujinx.Ava.UI.ViewModels.Settings +{ + public class SettingsHotkeysViewModel : BaseModel + { + public event Action DirtyEvent; + + public HotkeyConfig KeyboardHotkey { get; set; } + + public SettingsHotkeysViewModel() + { + ConfigurationState config = ConfigurationState.Instance; + + KeyboardHotkey = new HotkeyConfig(config.Hid.Hotkeys.Value); + KeyboardHotkey.PropertyChanged += (_, _) => DirtyEvent?.Invoke(); + } + + public bool CheckIfModified(ConfigurationState config) + { + bool isDirty = false; + + isDirty |= !config.Hid.Hotkeys.Value.Equals(KeyboardHotkey.GetConfig()); + + return isDirty; + } + + public void Save(ConfigurationState config) + { + config.Hid.Hotkeys.Value = KeyboardHotkey.GetConfig(); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Settings/SettingsInputViewModel.cs similarity index 86% rename from src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs rename to src/Ryujinx/UI/ViewModels/Settings/SettingsInputViewModel.cs index 89cc6496d..f7a6cbdae 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Settings/SettingsInputViewModel.cs @@ -9,6 +9,7 @@ using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.Models; using Ryujinx.Ava.UI.Models.Input; +using Ryujinx.Ava.UI.ViewModels.Input; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -30,10 +31,12 @@ using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.Gamepad using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; using Key = Ryujinx.Common.Configuration.Hid.Key; -namespace Ryujinx.Ava.UI.ViewModels.Input +namespace Ryujinx.Ava.UI.ViewModels.Settings { - public class InputViewModel : BaseModel, IDisposable + public class SettingsInputViewModel : BaseModel, IDisposable { + public event Action DirtyEvent; + 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"; @@ -54,6 +57,17 @@ namespace Ryujinx.Ava.UI.ViewModels.Input private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + private bool _isModified; + public bool IsModified + { + get => _isModified; + set + { + _isModified = value; + DirtyEvent?.Invoke(); + } + } + public IGamepadDriver AvaloniaKeyboardDriver { get; } public IGamepad SelectedGamepad { get; private set; } @@ -70,7 +84,39 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public bool IsRight { get; set; } public bool IsLeft { get; set; } - public bool IsModified { get; set; } + private bool _enableDockedMode; + public bool EnableDockedMode + { + get => _enableDockedMode; + set + { + _enableDockedMode = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableKeyboard; + public bool EnableKeyboard + { + get => _enableKeyboard; + set + { + _enableKeyboard = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableMouse; + public bool EnableMouse + { + get => _enableMouse; + set + { + _enableMouse = value; + DirtyEvent?.Invoke(); + } + } + public event Action NotifyChangesEvent; public object ConfigViewModel @@ -89,12 +135,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input get => _playerId; set { - if (IsModified) - { - return; - } - - IsModified = false; _playerId = value; if (!Enum.IsDefined(typeof(PlayerIndex), _playerId)) @@ -231,7 +271,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input public InputConfig Config { get; set; } - public InputViewModel(UserControl owner) : this() + public SettingsInputViewModel(UserControl owner) : this() { if (Program.PreviewerDetached) { @@ -254,7 +294,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } - public InputViewModel() + public SettingsInputViewModel() { PlayerIndexes = new ObservableCollection(); Controllers = new ObservableCollection(); @@ -288,6 +328,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input { ConfigViewModel = new ControllerInputViewModel(this, new GamepadInputConfig(controllerInputConfig)); } + + EnableDockedMode = ConfigurationState.Instance.System.EnableDockedMode; + EnableKeyboard = ConfigurationState.Instance.Hid.EnableKeyboard; + EnableMouse = ConfigurationState.Instance.Hid.EnableMouse; } public void LoadDevice() @@ -740,37 +784,35 @@ namespace Ryujinx.Ava.UI.ViewModels.Input return; } + + 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(); + } 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(); - } - else - { - await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); - } + await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogProfileInvalidProfileNameErrorMessage]); } } @@ -801,19 +843,44 @@ namespace Ryujinx.Ava.UI.ViewModels.Input } } - public void Save() + public bool CheckIfModified(ConfigurationState config) { - IsModified = false; + bool isDirty = false; - List newConfig = new(); + isDirty |= IsModified; + isDirty |= config.System.EnableDockedMode.Value != EnableDockedMode; + isDirty |= config.Hid.EnableKeyboard.Value != EnableKeyboard; + isDirty |= config.Hid.EnableMouse.Value != EnableMouse; - newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); + return isDirty; + } - newConfig.Remove(newConfig.Find(x => x == null)); + public void Save(ConfigurationState config) + { + var newInputConfig = ConstructInputConfigList(); + + _mainWindow.ViewModel.AppHost?.NpadManager.ReloadConfiguration(newInputConfig, 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. + config.Hid.InputConfig.Value = newInputConfig; + + config.System.EnableDockedMode.Value = EnableDockedMode; + config.Hid.EnableKeyboard.Value = EnableKeyboard; + config.Hid.EnableMouse.Value = EnableMouse; + } + + public List ConstructInputConfigList() + { + List newInputConfig = new(); + + newInputConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); + + newInputConfig.Remove(newInputConfig.Find(x => x == null)); if (Device == 0) { - newConfig.Remove(newConfig.Find(x => x.PlayerIndex == this.PlayerId)); + newInputConfig.Remove(newInputConfig.Find(x => x.PlayerIndex == this.PlayerId)); } else { @@ -821,44 +888,33 @@ namespace Ryujinx.Ava.UI.ViewModels.Input if (device.Type == DeviceType.Keyboard) { - var inputConfig = (ConfigViewModel as KeyboardInputViewModel).Config; - inputConfig.Id = device.Id; + var keyboardConfig = (ConfigViewModel as KeyboardInputViewModel).Config; + keyboardConfig.Id = device.Id; } else { - var inputConfig = (ConfigViewModel as ControllerInputViewModel).Config; - inputConfig.Id = device.Id.Split(" ")[0]; + var controllerConfig = (ConfigViewModel as ControllerInputViewModel).Config; + controllerConfig.Id = device.Id.Split(" ")[0]; } - var config = !IsController + var inputConfig = !IsController ? (ConfigViewModel as KeyboardInputViewModel).Config.GetConfig() : (ConfigViewModel as ControllerInputViewModel).Config.GetConfig(); - config.ControllerType = Controllers[_controller].Type; - config.PlayerIndex = _playerId; + inputConfig.ControllerType = Controllers[_controller].Type; + inputConfig.PlayerIndex = _playerId; - int i = newConfig.FindIndex(x => x.PlayerIndex == PlayerId); + int i = newInputConfig.FindIndex(x => x.PlayerIndex == PlayerId); if (i == -1) { - newConfig.Add(config); + newInputConfig.Add(inputConfig); } else { - newConfig[i] = config; + newInputConfig[i] = inputConfig; } } - _mainWindow.ViewModel.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); + return newInputConfig; } public void NotifyChanges() @@ -870,6 +926,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input OnPropertyChanged(nameof(IsRight)); OnPropertyChanged(nameof(IsLeft)); NotifyChangesEvent?.Invoke(); + DirtyEvent?.Invoke(); } public void Dispose() diff --git a/src/Ryujinx/UI/ViewModels/Settings/SettingsLoggingViewModel.cs b/src/Ryujinx/UI/ViewModels/Settings/SettingsLoggingViewModel.cs new file mode 100644 index 000000000..7d87aba46 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Settings/SettingsLoggingViewModel.cs @@ -0,0 +1,183 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.UI.Common.Configuration; +using System; + +namespace Ryujinx.Ava.UI.ViewModels.Settings +{ + public class SettingsLoggingViewModel : BaseModel + { + public event Action DirtyEvent; + + private bool _enableFileLog; + public bool EnableFileLog + { + get => _enableFileLog; + set + { + _enableFileLog = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableStub; + public bool EnableStub + { + get => _enableStub; + set + { + _enableStub = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableInfo; + public bool EnableInfo + { + get => _enableInfo; + set + { + _enableInfo = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableWarn; + public bool EnableWarn + { + get => _enableWarn; + set + { + _enableWarn = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableError; + public bool EnableError + { + get => _enableError; + set + { + _enableError = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableTrace; + public bool EnableTrace + { + get => _enableTrace; + set + { + _enableTrace = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableGuest; + public bool EnableGuest + { + get => _enableGuest; + set + { + _enableGuest = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableFsAccessLog; + public bool EnableFsAccessLog + { + get => _enableFsAccessLog; + set + { + _enableFsAccessLog = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableDebug; + public bool EnableDebug + { + get => _enableDebug; + set + { + _enableDebug = value; + DirtyEvent?.Invoke(); + } + } + + private int _fsGlobalAccessLogMode; + public int FsGlobalAccessLogMode + { + get => _fsGlobalAccessLogMode; + set + { + _fsGlobalAccessLogMode = value; + DirtyEvent?.Invoke(); + } + } + + private int _openglDebugLevel; + public int OpenglDebugLevel + { + get => _openglDebugLevel; + set + { + _openglDebugLevel = value; + DirtyEvent?.Invoke(); + } + } + + public SettingsLoggingViewModel() + { + ConfigurationState config = ConfigurationState.Instance; + + 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; + FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; + OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; + } + + public bool CheckIfModified(ConfigurationState config) + { + bool isDirty = false; + + isDirty |= config.Logger.EnableFileLog.Value != EnableFileLog; + isDirty |= config.Logger.EnableStub.Value != EnableStub; + isDirty |= config.Logger.EnableInfo.Value != EnableInfo; + isDirty |= config.Logger.EnableWarn.Value != EnableWarn; + isDirty |= config.Logger.EnableError.Value != EnableError; + isDirty |= config.Logger.EnableTrace.Value != EnableTrace; + isDirty |= config.Logger.EnableGuest.Value != EnableGuest; + isDirty |= config.Logger.EnableDebug.Value != EnableDebug; + isDirty |= config.Logger.EnableFsAccessLog.Value != EnableFsAccessLog; + isDirty |= config.System.FsGlobalAccessLogMode.Value != FsGlobalAccessLogMode; + isDirty |= config.Logger.GraphicsDebugLevel.Value != (GraphicsDebugLevel)OpenglDebugLevel; + + return isDirty; + } + + public void Save(ConfigurationState config) + { + config.Logger.EnableFileLog.Value = EnableFileLog; + config.Logger.EnableStub.Value = EnableStub; + config.Logger.EnableInfo.Value = EnableInfo; + config.Logger.EnableWarn.Value = EnableWarn; + config.Logger.EnableError.Value = EnableError; + config.Logger.EnableTrace.Value = EnableTrace; + config.Logger.EnableGuest.Value = EnableGuest; + config.Logger.EnableDebug.Value = EnableDebug; + config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog; + config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; + config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Settings/SettingsNetworkViewModel.cs b/src/Ryujinx/UI/ViewModels/Settings/SettingsNetworkViewModel.cs new file mode 100644 index 000000000..372e6bd12 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Settings/SettingsNetworkViewModel.cs @@ -0,0 +1,104 @@ +using Avalonia.Collections; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Common.Configuration.Multiplayer; +using Ryujinx.UI.Common.Configuration; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.NetworkInformation; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.UI.ViewModels.Settings +{ + public class SettingsNetworkViewModel : BaseModel + { + public event Action DirtyEvent; + + private readonly Dictionary _networkInterfaces = new(); + public AvaloniaList NetworkInterfaceList + { + get => new(_networkInterfaces.Keys); + } + + private bool _enableInternetAccess; + public bool EnableInternetAccess + { + get => _enableInternetAccess; + set + { + _enableInternetAccess = value; + DirtyEvent?.Invoke(); + } + } + + private int _networkInterfaceIndex; + public int NetworkInterfaceIndex + { + get => _networkInterfaceIndex; + set + { + _networkInterfaceIndex = value != -1 ? value : 0; + OnPropertyChanged(); + DirtyEvent?.Invoke(); + } + } + + private int _multiplayerModeIndex; + public int MultiplayerModeIndex + { + get => _multiplayerModeIndex; + set + { + _multiplayerModeIndex = value; + DirtyEvent?.Invoke(); + } + } + + public SettingsNetworkViewModel() + { + ConfigurationState config = ConfigurationState.Instance; + + Task.Run(PopulateNetworkInterfaces); + + // LAN interface index is loaded asynchronously in PopulateNetworkInterfaces() + EnableInternetAccess = config.System.EnableInternetAccess; + MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; + } + + private async Task PopulateNetworkInterfaces() + { + _networkInterfaces.Clear(); + _networkInterfaces.Add(LocaleManager.Instance[LocaleKeys.NetworkInterfaceDefault], "0"); + + foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) + { + await Dispatcher.UIThread.InvokeAsync(() => + { + _networkInterfaces.Add(networkInterface.Name, networkInterface.Id); + }); + } + + // Network interface index needs to be loaded during the async method, or it will always return 0. + NetworkInterfaceIndex = _networkInterfaces.Values.ToList().IndexOf(ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value); + } + + public bool CheckIfModified(ConfigurationState config) + { + bool isDirty = false; + + isDirty |= config.System.EnableInternetAccess.Value != EnableInternetAccess; + isDirty |= config.Multiplayer.LanInterfaceId.Value != _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; + isDirty |= config.Multiplayer.Mode.Value != (MultiplayerMode)MultiplayerModeIndex; + + return isDirty; + } + + public void Save(ConfigurationState config) + { + config.System.EnableInternetAccess.Value = EnableInternetAccess; + config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; + config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Settings/SettingsSystemViewModel.cs b/src/Ryujinx/UI/ViewModels/Settings/SettingsSystemViewModel.cs new file mode 100644 index 000000000..a3881edc8 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Settings/SettingsSystemViewModel.cs @@ -0,0 +1,228 @@ +using Avalonia.Collections; +using Avalonia.Threading; +using LibHac.Tools.FsSystem; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Time.TimeZone; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Common.Configuration.System; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; + +namespace Ryujinx.Ava.UI.ViewModels.Settings +{ + public class SettingsSystemViewModel : BaseModel + { + public event Action DirtyEvent; + + private readonly List _validTzRegions = new(); + private readonly VirtualFileSystem _virtualFileSystem; + private readonly ContentManager _contentManager; + private TimeZoneContentManager _timeZoneContentManager; + + private int _region; + public int Region + { + get => _region; + set + { + _region = value; + DirtyEvent?.Invoke(); + } + } + + private int _language; + public int Language + { + get => _language; + set + { + _language = value; + DirtyEvent?.Invoke(); + } + } + + private string _timeZone; + public string TimeZone + { + get => _timeZone; + set + { + _timeZone = value; + OnPropertyChanged(); + DirtyEvent?.Invoke(); + } + } + + private DateTimeOffset _currentDate; + public DateTimeOffset CurrentDate + { + get => _currentDate; + set + { + _currentDate = value; + DirtyEvent?.Invoke(); + } + } + + private TimeSpan _currentTime; + public TimeSpan CurrentTime + { + get => _currentTime; + set + { + _currentTime = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableVsync; + public bool EnableVsync + { + get => _enableVsync; + set + { + _enableVsync = value; + DirtyEvent?.Invoke(); + } + } + + private bool _enableFsIntegrityChecks; + public bool EnableFsIntegrityChecks + { + get => _enableFsIntegrityChecks; + set + { + _enableFsIntegrityChecks = value; + DirtyEvent?.Invoke(); + } + } + + private bool _ignoreMissingServices; + public bool IgnoreMissingServices + { + get => _ignoreMissingServices; + set + { + _ignoreMissingServices = value; + DirtyEvent?.Invoke(); + } + } + + private bool _expandedDramSize; + public bool ExpandDramSize + { + get => _expandedDramSize; + set + { + _expandedDramSize = value; + DirtyEvent?.Invoke(); + } + } + + internal AvaloniaList TimeZones { get; set; } + + public SettingsSystemViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) + { + _virtualFileSystem = virtualFileSystem; + _contentManager = contentManager; + + ConfigurationState config = ConfigurationState.Instance; + + TimeZones = new(); + + if (Program.PreviewerDetached) + { + Task.Run(LoadTimeZones); + } + + Region = (int)config.System.Region.Value; + Language = (int)config.System.Language.Value; + TimeZone = config.System.TimeZone; + + DateTime currentHostDateTime = DateTime.Now; + TimeSpan systemDateTimeOffset = TimeSpan.FromSeconds(config.System.SystemTimeOffset); + DateTime currentDateTime = currentHostDateTime.Add(systemDateTimeOffset); + + CurrentDate = currentDateTime.Date; + CurrentTime = currentDateTime.TimeOfDay; + + EnableVsync = config.Graphics.EnableVsync; + EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; + ExpandDramSize = config.System.ExpandRam; + IgnoreMissingServices = config.System.IgnoreMissingServices; + } + + public async Task 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; + + await Dispatcher.UIThread.InvokeAsync(() => + { + 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; + } + } + + + public bool CheckIfModified(ConfigurationState config) + { + bool isDirty = false; + + isDirty |= config.System.Region.Value != (Region)Region; + isDirty |= config.System.Language.Value != (Language)Language; + + if (_validTzRegions.Contains(TimeZone)) + { + isDirty |= config.System.TimeZone.Value != TimeZone; + } + + // SystemTimeOffset will always have changed, so we don't check it here + + isDirty |= config.Graphics.EnableVsync.Value != EnableVsync; + isDirty |= config.System.EnableFsIntegrityChecks.Value != EnableFsIntegrityChecks; + isDirty |= config.System.ExpandRam.Value != ExpandDramSize; + isDirty |= config.System.IgnoreMissingServices.Value != IgnoreMissingServices; + + return isDirty; + } + + public void Save(ConfigurationState config) + { + config.System.Region.Value = (Region)Region; + config.System.Language.Value = (Language)Language; + + if (_validTzRegions.Contains(TimeZone)) + { + config.System.TimeZone.Value = TimeZone; + } + + config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); + + config.Graphics.EnableVsync.Value = EnableVsync; + config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; + config.System.ExpandRam.Value = ExpandDramSize; + config.System.IgnoreMissingServices.Value = IgnoreMissingServices; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Settings/SettingsUIViewModel.cs b/src/Ryujinx/UI/ViewModels/Settings/SettingsUIViewModel.cs new file mode 100644 index 000000000..b967afc0f --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Settings/SettingsUIViewModel.cs @@ -0,0 +1,148 @@ +using Avalonia.Collections; +using Ryujinx.Common.Configuration; +using Ryujinx.UI.Common.Configuration; +using System; +using System.Linq; + +namespace Ryujinx.Ava.UI.ViewModels.Settings +{ + public class SettingsUIViewModel : BaseModel + { + public event Action DirtyEvent; + + private bool _enableDiscordIntegration; + public bool EnableDiscordIntegration + { + get => _enableDiscordIntegration; + set + { + _enableDiscordIntegration = value; + DirtyEvent?.Invoke(); + } + } + + private bool _checkUpdatesOnStart; + public bool CheckUpdatesOnStart + { + get => _checkUpdatesOnStart; + set + { + _checkUpdatesOnStart = value; + DirtyEvent?.Invoke(); + } + } + + private bool _showConfirmExit; + public bool ShowConfirmExit + { + get => _showConfirmExit; + set + { + _showConfirmExit = value; + DirtyEvent?.Invoke(); + } + } + + private bool _rememberWindowState; + public bool RememberWindowState + { + get => _rememberWindowState; + set + { + _rememberWindowState = value; + DirtyEvent?.Invoke(); + } + } + + private int _hideCursor; + public int HideCursor + { + get => _hideCursor; + set + { + _hideCursor = value; + DirtyEvent?.Invoke(); + } + } + + private int _baseStyleIndex; + public int BaseStyleIndex + { + get => _baseStyleIndex; + set + { + _baseStyleIndex = value; + DirtyEvent?.Invoke(); + } + } + + public bool DirsChanged; + + public AvaloniaList GameDirectories { get; set; } + + public SettingsUIViewModel() + { + ConfigurationState config = ConfigurationState.Instance; + + GameDirectories = new(); + + EnableDiscordIntegration = config.EnableDiscordIntegration; + CheckUpdatesOnStart = config.CheckUpdatesOnStart; + ShowConfirmExit = config.ShowConfirmExit; + RememberWindowState = config.RememberWindowState; + HideCursor = (int)config.HideCursor.Value; + + GameDirectories.Clear(); + GameDirectories.AddRange(config.UI.GameDirs.Value); + GameDirectories.CollectionChanged += (_, _) => DirtyEvent?.Invoke(); + + BaseStyleIndex = config.UI.BaseStyle.Value switch + { + "Auto" => 0, + "Light" => 1, + "Dark" => 2, + _ => 0 + }; + } + + public bool CheckIfModified(ConfigurationState config) + { + bool isDirty = false; + + DirsChanged = !config.UI.GameDirs.Value.SequenceEqual(GameDirectories); + + isDirty |= config.EnableDiscordIntegration.Value != EnableDiscordIntegration; + isDirty |= config.CheckUpdatesOnStart.Value != CheckUpdatesOnStart; + isDirty |= config.ShowConfirmExit.Value != ShowConfirmExit; + isDirty |= config.RememberWindowState.Value != RememberWindowState; + isDirty |= config.HideCursor.Value != (HideCursorMode)HideCursor; + isDirty |= DirsChanged; + isDirty |= config.UI.BaseStyle.Value != BaseStyleIndex switch + { + 0 => "Auto", + 1 => "Light", + 2 => "Dark", + _ => "Auto" + }; + + return isDirty; + } + + public void Save(ConfigurationState config) + { + config.EnableDiscordIntegration.Value = EnableDiscordIntegration; + config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart; + config.ShowConfirmExit.Value = ShowConfirmExit; + config.RememberWindowState.Value = RememberWindowState; + config.HideCursor.Value = (HideCursorMode)HideCursor; + config.UI.GameDirs.Value = GameDirectories.ToList(); + config.UI.BaseStyle.Value = BaseStyleIndex switch + { + 0 => "Auto", + 1 => "Light", + 2 => "Dark", + _ => "Auto" + }; + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/Settings/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/Settings/SettingsViewModel.cs new file mode 100644 index 000000000..d72f9a709 --- /dev/null +++ b/src/Ryujinx/UI/ViewModels/Settings/SettingsViewModel.cs @@ -0,0 +1,118 @@ +using Ryujinx.Ava.UI.Windows; +using Ryujinx.UI.Common.Configuration; +using System; + +namespace Ryujinx.Ava.UI.ViewModels.Settings +{ + public class SettingsViewModel : BaseModel + { + private bool _isModified; + + public bool IsModified + { + get => _isModified; + private set + { + DirtyEvent?.Invoke(value); + _isModified = value; + } + } + + public event Action CloseWindow; + public event Action DirtyEvent; + public event Action ToggleButtons; + + public bool IsMacOS => OperatingSystem.IsMacOS(); + + private readonly SettingsAudioViewModel _audioViewModel; + private readonly SettingsCpuViewModel _cpuViewModel; + private readonly SettingsGraphicsViewModel _graphicsViewModel; + private readonly SettingsHotkeysViewModel _hotkeysViewModel; + private readonly SettingsInputViewModel _inputViewModel; + private readonly SettingsLoggingViewModel _loggingViewModel; + private readonly SettingsNetworkViewModel _networkViewModel; + private readonly SettingsSystemViewModel _systemViewModel; + private readonly SettingsUIViewModel _uiViewModel; + + public SettingsViewModel( + SettingsAudioViewModel audioViewModel, + SettingsCpuViewModel cpuViewModel, + SettingsGraphicsViewModel graphicsViewModel, + SettingsHotkeysViewModel hotkeysViewModel, + SettingsInputViewModel inputViewModel, + SettingsLoggingViewModel loggingViewModel, + SettingsNetworkViewModel networkViewModel, + SettingsSystemViewModel systemViewModel, + SettingsUIViewModel uiViewModel) + { + _audioViewModel = audioViewModel; + _cpuViewModel = cpuViewModel; + _graphicsViewModel = graphicsViewModel; + _hotkeysViewModel = hotkeysViewModel; + _inputViewModel = inputViewModel; + _loggingViewModel = loggingViewModel; + _networkViewModel = networkViewModel; + _systemViewModel = systemViewModel; + _uiViewModel = uiViewModel; + + _audioViewModel.DirtyEvent += CheckIfModified; + _cpuViewModel.DirtyEvent += CheckIfModified; + _graphicsViewModel.DirtyEvent += CheckIfModified; + _hotkeysViewModel.DirtyEvent += CheckIfModified; + _inputViewModel.DirtyEvent += CheckIfModified; + _loggingViewModel.DirtyEvent += CheckIfModified; + _networkViewModel.DirtyEvent += CheckIfModified; + _systemViewModel.DirtyEvent += CheckIfModified; + _uiViewModel.DirtyEvent += CheckIfModified; + } + + public void CheckIfModified() + { + bool isDirty = false; + + ConfigurationState config = ConfigurationState.Instance; + + isDirty |= _audioViewModel?.CheckIfModified(config) ?? false; + isDirty |= _cpuViewModel?.CheckIfModified(config) ?? false; + isDirty |= _graphicsViewModel?.CheckIfModified(config) ?? false; + isDirty |= _hotkeysViewModel?.CheckIfModified(config) ?? false; + isDirty |= _inputViewModel?.CheckIfModified(config) ?? false; + isDirty |= _loggingViewModel?.CheckIfModified(config) ?? false; + isDirty |= _networkViewModel?.CheckIfModified(config) ?? false; + isDirty |= _systemViewModel?.CheckIfModified(config) ?? false; + isDirty |= _uiViewModel?.CheckIfModified(config) ?? false; + + IsModified = isDirty; + } + + public void SaveSettings() + { + ConfigurationState config = ConfigurationState.Instance; + + _audioViewModel?.Save(config); + _cpuViewModel?.Save(config); + _graphicsViewModel?.Save(config); + _hotkeysViewModel?.Save(config); + _inputViewModel?.Save(config); + _loggingViewModel?.Save(config); + _networkViewModel?.Save(config); + _systemViewModel?.Save(config); + _uiViewModel?.Save(config); + + config.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + MainWindow.UpdateGraphicsConfig(); + } + + public void ApplyButton() + { + SaveSettings(); + } + + public void OkButton() + { + SaveSettings(); + CloseWindow?.Invoke(); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs deleted file mode 100644 index 70e5fa5c7..000000000 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ /dev/null @@ -1,615 +0,0 @@ -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.UI.Helpers; -using Ryujinx.Ava.UI.Models.Input; -using Ryujinx.Ava.UI.Windows; -using Ryujinx.Common.Configuration; -using Ryujinx.Common.Configuration.Multiplayer; -using Ryujinx.Common.GraphicsDriver; -using Ryujinx.Common.Logging; -using Ryujinx.Graphics.Vulkan; -using Ryujinx.HLE.FileSystem; -using Ryujinx.HLE.HOS.Services.Time.TimeZone; -using Ryujinx.UI.Common.Configuration; -using Ryujinx.UI.Common.Configuration.System; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Net.NetworkInformation; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; - -namespace Ryujinx.Ava.UI.ViewModels -{ - public class SettingsViewModel : BaseModel - { - private readonly VirtualFileSystem _virtualFileSystem; - private readonly ContentManager _contentManager; - private TimeZoneContentManager _timeZoneContentManager; - - private readonly List _validTzRegions; - - private readonly Dictionary _networkInterfaces; - - private float _customResolutionScale; - private int _resolutionScale; - private int _graphicsBackendMultithreadingIndex; - private float _volume; - private bool _isVulkanAvailable = true; - private bool _directoryChanged; - private readonly List _gpuIds = new(); - private int _graphicsBackendIndex; - private int _scalingFilter; - private int _scalingFilterLevel; - - public event Action CloseWindow; - public event Action SaveSettingsEvent; - private int _networkInterfaceIndex; - private int _multiplayerModeIndex; - - public int ResolutionScale - { - get => _resolutionScale; - set - { - _resolutionScale = value; - - OnPropertyChanged(nameof(CustomResolutionScale)); - OnPropertyChanged(nameof(IsCustomResolutionScaleActive)); - } - } - - public int GraphicsBackendMultithreadingIndex - { - get => _graphicsBackendMultithreadingIndex; - set - { - _graphicsBackendMultithreadingIndex = value; - - if (_graphicsBackendMultithreadingIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value) - { - Dispatcher.UIThread.InvokeAsync(() => - ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage], - "", - "", - LocaleManager.Instance[LocaleKeys.InputDialogOk], - LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle]) - ); - } - - OnPropertyChanged(); - } - } - - public float CustomResolutionScale - { - get => _customResolutionScale; - set - { - _customResolutionScale = MathF.Round(value, 1); - - OnPropertyChanged(); - } - } - - public bool IsVulkanAvailable - { - get => _isVulkanAvailable; - set - { - _isVulkanAvailable = value; - - OnPropertyChanged(); - } - } - - public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS(); - - public bool IsHypervisorAvailable => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; - - public bool DirectoryChanged - { - get => _directoryChanged; - set - { - _directoryChanged = value; - - OnPropertyChanged(); - } - } - - public bool IsMacOS => OperatingSystem.IsMacOS(); - - public bool EnableDiscordIntegration { get; set; } - public bool CheckUpdatesOnStart { get; set; } - public bool ShowConfirmExit { get; set; } - public bool RememberWindowState { get; set; } - public int HideCursor { 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 EnableTextureRecompression { get; set; } - public bool EnableMacroHLE { get; set; } - public bool EnableColorSpacePassthrough { get; set; } - public bool ColorSpacePassthroughAvailable => IsMacOS; - 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 IsCustomResolutionScaleActive => _resolutionScale == 4; - public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr; - - public bool IsVulkanSelected => GraphicsBackendIndex == 0; - public bool UseHypervisor { get; set; } - - public string TimeZone { get; set; } - public string ShaderDumpPath { 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 AntiAliasingEffect { get; set; } - public string ScalingFilterLevelText => ScalingFilterLevel.ToString("0"); - public int ScalingFilterLevel - { - get => _scalingFilterLevel; - set - { - _scalingFilterLevel = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(ScalingFilterLevelText)); - } - } - public int OpenglDebugLevel { get; set; } - public int MemoryMode { get; set; } - public int BaseStyleIndex { get; set; } - public int GraphicsBackendIndex - { - get => _graphicsBackendIndex; - set - { - _graphicsBackendIndex = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(IsVulkanSelected)); - } - } - public int ScalingFilter - { - get => _scalingFilter; - set - { - _scalingFilter = value; - OnPropertyChanged(); - OnPropertyChanged(nameof(IsScalingFilterActive)); - } - } - - public int PreferredGpuIndex { get; set; } - - public float Volume - { - get => _volume; - set - { - _volume = value; - - ConfigurationState.Instance.System.AudioVolume.Value = _volume / 100; - - OnPropertyChanged(); - } - } - - public DateTimeOffset CurrentDate { get; set; } - public TimeSpan CurrentTime { get; set; } - - internal AvaloniaList TimeZones { get; set; } - public AvaloniaList GameDirectories { get; set; } - public ObservableCollection AvailableGpus { get; set; } - - public AvaloniaList NetworkInterfaceList - { - get => new(_networkInterfaces.Keys); - } - - public HotkeyConfig KeyboardHotkey { get; set; } - - public int NetworkInterfaceIndex - { - get => _networkInterfaceIndex; - set - { - _networkInterfaceIndex = value != -1 ? value : 0; - ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[_networkInterfaceIndex]]; - } - } - - public int MultiplayerModeIndex - { - get => _multiplayerModeIndex; - set - { - _multiplayerModeIndex = value; - ConfigurationState.Instance.Multiplayer.Mode.Value = (MultiplayerMode)_multiplayerModeIndex; - } - } - - public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() - { - _virtualFileSystem = virtualFileSystem; - _contentManager = contentManager; - if (Program.PreviewerDetached) - { - Task.Run(LoadTimeZones); - } - } - - public SettingsViewModel() - { - GameDirectories = new AvaloniaList(); - TimeZones = new AvaloniaList(); - AvailableGpus = new ObservableCollection(); - _validTzRegions = new List(); - _networkInterfaces = new Dictionary(); - - Task.Run(CheckSoundBackends); - Task.Run(PopulateNetworkInterfaces); - - if (Program.PreviewerDetached) - { - Task.Run(LoadAvailableGpus); - LoadCurrentConfiguration(); - } - } - - public async Task CheckSoundBackends() - { - IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported; - IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported; - IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported; - - await Dispatcher.UIThread.InvokeAsync(() => - { - OnPropertyChanged(nameof(IsOpenAlEnabled)); - OnPropertyChanged(nameof(IsSoundIoEnabled)); - OnPropertyChanged(nameof(IsSDL2Enabled)); - }); - } - - private async Task LoadAvailableGpus() - { - AvailableGpus.Clear(); - - var devices = VulkanRenderer.GetPhysicalDevices(); - - if (devices.Length == 0) - { - IsVulkanAvailable = false; - GraphicsBackendIndex = 1; - } - else - { - foreach (var device in devices) - { - await Dispatcher.UIThread.InvokeAsync(() => - { - _gpuIds.Add(device.Id); - - AvailableGpus.Add(new ComboBoxItem { Content = $"{device.Name} {(device.IsDiscrete ? "(dGPU)" : "")}" }); - }); - } - } - - // GPU configuration needs to be loaded during the async method or it will always return 0. - PreferredGpuIndex = _gpuIds.Contains(ConfigurationState.Instance.Graphics.PreferredGpu) ? - _gpuIds.IndexOf(ConfigurationState.Instance.Graphics.PreferredGpu) : 0; - - Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(PreferredGpuIndex))); - } - - public async Task 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; - - await Dispatcher.UIThread.InvokeAsync(() => - { - TimeZones.Add(new TimeZone($"UTC{hours:+0#;-0#;+00}:{minutes:D2}", location, abbr2)); - - _validTzRegions.Add(location); - }); - } - - Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(TimeZone))); - } - - private async Task PopulateNetworkInterfaces() - { - _networkInterfaces.Clear(); - _networkInterfaces.Add(LocaleManager.Instance[LocaleKeys.NetworkInterfaceDefault], "0"); - - foreach (NetworkInterface networkInterface in NetworkInterface.GetAllNetworkInterfaces()) - { - await Dispatcher.UIThread.InvokeAsync(() => - { - _networkInterfaces.Add(networkInterface.Name, networkInterface.Id); - }); - } - - // Network interface index needs to be loaded during the async method or it will always return 0. - NetworkInterfaceIndex = _networkInterfaces.Values.ToList().IndexOf(ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value); - - Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(NetworkInterfaceIndex))); - } - - public void ValidateAndSetTimeZone(string location) - { - if (_validTzRegions.Contains(location)) - { - TimeZone = location; - } - } - - public void LoadCurrentConfiguration() - { - ConfigurationState config = ConfigurationState.Instance; - - // User Interface - EnableDiscordIntegration = config.EnableDiscordIntegration; - CheckUpdatesOnStart = config.CheckUpdatesOnStart; - ShowConfirmExit = config.ShowConfirmExit; - RememberWindowState = config.RememberWindowState; - HideCursor = (int)config.HideCursor.Value; - - GameDirectories.Clear(); - GameDirectories.AddRange(config.UI.GameDirs.Value); - - BaseStyleIndex = config.UI.BaseStyle.Value switch - { - "Auto" => 0, - "Light" => 1, - "Dark" => 2, - _ => 0 - }; - - // Input - EnableDockedMode = config.System.EnableDockedMode; - EnableKeyboard = config.Hid.EnableKeyboard; - EnableMouse = config.Hid.EnableMouse; - - // Keyboard Hotkeys - KeyboardHotkey = new HotkeyConfig(config.Hid.Hotkeys.Value); - - // System - Region = (int)config.System.Region.Value; - Language = (int)config.System.Language.Value; - TimeZone = config.System.TimeZone; - - DateTime currentHostDateTime = DateTime.Now; - TimeSpan systemDateTimeOffset = TimeSpan.FromSeconds(config.System.SystemTimeOffset); - DateTime currentDateTime = currentHostDateTime.Add(systemDateTimeOffset); - CurrentDate = currentDateTime.Date; - CurrentTime = currentDateTime.TimeOfDay; - - EnableVsync = config.Graphics.EnableVsync; - EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks; - ExpandDramSize = config.System.ExpandRam; - IgnoreMissingServices = config.System.IgnoreMissingServices; - - // CPU - EnablePptc = config.System.EnablePtc; - MemoryMode = (int)config.System.MemoryManagerMode.Value; - UseHypervisor = config.System.UseHypervisor; - - // Graphics - GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value; - // Physical devices are queried asynchronously hence the prefered index config value is loaded in LoadAvailableGpus(). - EnableShaderCache = config.Graphics.EnableShaderCache; - EnableTextureRecompression = config.Graphics.EnableTextureRecompression; - EnableMacroHLE = config.Graphics.EnableMacroHLE; - EnableColorSpacePassthrough = config.Graphics.EnableColorSpacePassthrough; - ResolutionScale = config.Graphics.ResScale == -1 ? 4 : config.Graphics.ResScale - 1; - CustomResolutionScale = config.Graphics.ResScaleCustom; - MaxAnisotropy = config.Graphics.MaxAnisotropy == -1 ? 0 : (int)(MathF.Log2(config.Graphics.MaxAnisotropy)); - AspectRatio = (int)config.Graphics.AspectRatio.Value; - GraphicsBackendMultithreadingIndex = (int)config.Graphics.BackendThreading.Value; - ShaderDumpPath = config.Graphics.ShadersDumpPath; - AntiAliasingEffect = (int)config.Graphics.AntiAliasing.Value; - ScalingFilter = (int)config.Graphics.ScalingFilter.Value; - ScalingFilterLevel = config.Graphics.ScalingFilterLevel.Value; - - // Audio - AudioBackend = (int)config.System.AudioBackend.Value; - Volume = config.System.AudioVolume * 100; - - // Network - EnableInternetAccess = config.System.EnableInternetAccess; - // LAN interface index is loaded asynchronously in PopulateNetworkInterfaces() - - // Logging - 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; - FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; - OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; - - MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; - } - - public void SaveSettings() - { - ConfigurationState config = ConfigurationState.Instance; - - // User Interface - config.EnableDiscordIntegration.Value = EnableDiscordIntegration; - config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart; - config.ShowConfirmExit.Value = ShowConfirmExit; - config.RememberWindowState.Value = RememberWindowState; - config.HideCursor.Value = (HideCursorMode)HideCursor; - - if (_directoryChanged) - { - List gameDirs = new(GameDirectories); - config.UI.GameDirs.Value = gameDirs; - } - - config.UI.BaseStyle.Value = BaseStyleIndex switch - { - 0 => "Auto", - 1 => "Light", - 2 => "Dark", - _ => "Auto" - }; - - // Input - config.System.EnableDockedMode.Value = EnableDockedMode; - config.Hid.EnableKeyboard.Value = EnableKeyboard; - config.Hid.EnableMouse.Value = EnableMouse; - - // Keyboard Hotkeys - config.Hid.Hotkeys.Value = KeyboardHotkey.GetConfig(); - - // System - config.System.Region.Value = (Region)Region; - config.System.Language.Value = (Language)Language; - - if (_validTzRegions.Contains(TimeZone)) - { - config.System.TimeZone.Value = TimeZone; - } - - config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds()); - config.Graphics.EnableVsync.Value = EnableVsync; - config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; - config.System.ExpandRam.Value = ExpandDramSize; - config.System.IgnoreMissingServices.Value = IgnoreMissingServices; - - // CPU - config.System.EnablePtc.Value = EnablePptc; - config.System.MemoryManagerMode.Value = (MemoryManagerMode)MemoryMode; - config.System.UseHypervisor.Value = UseHypervisor; - - // Graphics - config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex; - config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex); - config.Graphics.EnableShaderCache.Value = EnableShaderCache; - config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression; - config.Graphics.EnableMacroHLE.Value = EnableMacroHLE; - config.Graphics.EnableColorSpacePassthrough.Value = EnableColorSpacePassthrough; - config.Graphics.ResScale.Value = ResolutionScale == 4 ? -1 : ResolutionScale + 1; - config.Graphics.ResScaleCustom.Value = CustomResolutionScale; - config.Graphics.MaxAnisotropy.Value = MaxAnisotropy == 0 ? -1 : MathF.Pow(2, MaxAnisotropy); - config.Graphics.AspectRatio.Value = (AspectRatio)AspectRatio; - config.Graphics.AntiAliasing.Value = (AntiAliasing)AntiAliasingEffect; - config.Graphics.ScalingFilter.Value = (ScalingFilter)ScalingFilter; - config.Graphics.ScalingFilterLevel.Value = ScalingFilterLevel; - - if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex) - { - DriverUtilities.ToggleOGLThreading(GraphicsBackendMultithreadingIndex == (int)BackendThreading.Off); - } - - config.Graphics.BackendThreading.Value = (BackendThreading)GraphicsBackendMultithreadingIndex; - config.Graphics.ShadersDumpPath.Value = ShaderDumpPath; - - // Audio - 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.System.AudioVolume.Value = Volume / 100; - - // Network - config.System.EnableInternetAccess.Value = EnableInternetAccess; - - // Logging - config.Logger.EnableFileLog.Value = EnableFileLog; - config.Logger.EnableStub.Value = EnableStub; - config.Logger.EnableInfo.Value = EnableInfo; - config.Logger.EnableWarn.Value = EnableWarn; - config.Logger.EnableError.Value = EnableError; - config.Logger.EnableTrace.Value = EnableTrace; - config.Logger.EnableGuest.Value = EnableGuest; - config.Logger.EnableDebug.Value = EnableDebug; - config.Logger.EnableFsAccessLog.Value = EnableFsAccessLog; - config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; - config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; - - config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; - config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex; - - config.ToFileFormat().SaveConfig(Program.ConfigurationPath); - - MainWindow.UpdateGraphicsConfig(); - - SaveSettingsEvent?.Invoke(); - - _directoryChanged = false; - } - - private static void RevertIfNotSaved() - { - Program.ReloadConfig(); - } - - public void ApplyButton() - { - SaveSettings(); - } - - public void OkButton() - { - SaveSettings(); - CloseWindow?.Invoke(); - } - - public void CancelButton() - { - RevertIfNotSaved(); - CloseWindow?.Invoke(); - } - } -} diff --git a/src/Ryujinx/UI/Views/Input/InputView.axaml b/src/Ryujinx/UI/Views/Input/InputView.axaml deleted file mode 100644 index b4940941c..000000000 --- a/src/Ryujinx/UI/Views/Input/InputView.axaml +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/Ryujinx/UI/Views/Input/InputView.axaml.cs b/src/Ryujinx/UI/Views/Input/InputView.axaml.cs deleted file mode 100644 index 356381a8a..000000000 --- a/src/Ryujinx/UI/Views/Input/InputView.axaml.cs +++ /dev/null @@ -1,61 +0,0 @@ -using Avalonia.Controls; -using Ryujinx.Ava.Common.Locale; -using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.Models; -using Ryujinx.Ava.UI.ViewModels.Input; - -namespace Ryujinx.Ava.UI.Views.Input -{ - public partial class InputView : UserControl - { - private bool _dialogOpen; - private InputViewModel ViewModel { get; set; } - - public InputView() - { - DataContext = ViewModel = new InputViewModel(this); - - InitializeComponent(); - } - - public void SaveCurrentProfile() - { - ViewModel.Save(); - } - - private async void PlayerIndexBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (ViewModel.IsModified && !_dialogOpen) - { - _dialogOpen = true; - - var result = await ContentDialogHelper.CreateConfirmationDialog( - LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmMessage], - LocaleManager.Instance[LocaleKeys.DialogControllerSettingsModifiedConfirmSubMessage], - LocaleManager.Instance[LocaleKeys.InputDialogYes], - LocaleManager.Instance[LocaleKeys.InputDialogNo], - LocaleManager.Instance[LocaleKeys.RyujinxConfirm]); - - if (result == UserResult.Yes) - { - ViewModel.Save(); - } - - _dialogOpen = false; - - ViewModel.IsModified = false; - - if (e.AddedItems.Count > 0) - { - var player = (PlayerModel)e.AddedItems[0]; - ViewModel.PlayerId = player.Id; - } - } - } - - public void Dispose() - { - ViewModel.Dispose(); - } - } -} diff --git a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml index e4566f463..4c5aae098 100644 --- a/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml +++ b/src/Ryujinx/UI/Views/Input/KeyboardInputView.axaml @@ -672,4 +672,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs index 58a4b416b..4c91ec99a 100644 --- a/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Input/RumbleInputView.axaml.cs @@ -2,6 +2,7 @@ using Avalonia.Controls; using FluentAvalonia.UI.Controls; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.ViewModels.Input; +using Ryujinx.Ava.UI.ViewModels.Settings; using System.Threading.Tasks; namespace Ryujinx.Ava.UI.Views.Input diff --git a/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml index 657e07ee7..4f4c65a1b 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsAudioView.axaml @@ -7,11 +7,11 @@ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Settings" mc:Ignorable="d" - x:DataType="viewModels:SettingsViewModel"> + x:DataType="viewModels:SettingsAudioViewModel"> - + + x:DataType="viewModels:SettingsCpuViewModel"> diff --git a/src/Ryujinx/UI/Views/Settings/SettingsCpuView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsCpuView.axaml.cs new file mode 100644 index 000000000..6a53ad224 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsCpuView.axaml.cs @@ -0,0 +1,16 @@ +using Avalonia.Controls; +using Ryujinx.Ava.UI.ViewModels.Settings; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsCpuView : UserControl + { + public SettingsCpuViewModel ViewModel; + + public SettingsCpuView() + { + DataContext = ViewModel = new SettingsCpuViewModel(); + InitializeComponent(); + } + } +} diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml index 0a12575ad..f5d0af5ca 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml @@ -7,12 +7,12 @@ xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Settings" Design.Width="1000" mc:Ignorable="d" - x:DataType="viewModels:SettingsViewModel"> + x:DataType="viewModels:SettingsGraphicsViewModel"> - + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml.cs index 673413309..7a4aca007 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml.cs @@ -1,12 +1,40 @@ using Avalonia.Controls; +using Avalonia.Threading; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.UI.Helpers; +using Ryujinx.Ava.UI.ViewModels.Settings; +using Ryujinx.UI.Common.Configuration; namespace Ryujinx.Ava.UI.Views.Settings { public partial class SettingsGraphicsView : UserControl { + public SettingsGraphicsViewModel ViewModel; + public SettingsGraphicsView() { + DataContext = ViewModel = new SettingsGraphicsViewModel(); InitializeComponent(); } + + private void GraphicsBackendMultithreadingIndex_OnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (e.Source is not ComboBox comboBox) + { + return; + } + + if (comboBox.SelectedIndex != (int)ConfigurationState.Instance.Graphics.BackendThreading.Value) + { + Dispatcher.UIThread.InvokeAsync(() => + ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningMessage], + "", + "", + LocaleManager.Instance[LocaleKeys.InputDialogOk], + LocaleManager.Instance[LocaleKeys.DialogSettingsBackendThreadingWarningTitle], + parent: this.VisualRoot as Window) + ); + } + } } } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml index bffcada05..40b5d1b78 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml @@ -5,14 +5,14 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Settings" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" mc:Ignorable="d" - x:DataType="viewModels:SettingsViewModel" + x:DataType="viewModels:SettingsHotkeysViewModel" x:CompileBindings="True" Focusable="True"> - + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs index fb0fe2bb1..085957eec 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsHotkeysView.axaml.cs @@ -5,7 +5,7 @@ using Avalonia.Interactivity; using Avalonia.LogicalTree; using Ryujinx.Ava.Input; using Ryujinx.Ava.UI.Helpers; -using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.ViewModels.Settings; using Ryujinx.Input; using Ryujinx.Input.Assigner; using Key = Ryujinx.Common.Configuration.Hid.Key; @@ -14,11 +14,15 @@ namespace Ryujinx.Ava.UI.Views.Settings { public partial class SettingsHotkeysView : UserControl { + public SettingsHotkeysViewModel ViewModel; + private ButtonKeyAssigner _currentAssigner; private readonly IGamepadDriver _avaloniaKeyboardDriver; public SettingsHotkeysView() { + DataContext = ViewModel = new SettingsHotkeysViewModel(); + InitializeComponent(); foreach (ILogical visual in SettingButtons.GetLogicalDescendants()) @@ -77,37 +81,36 @@ namespace Ryujinx.Ava.UI.Views.Settings { if (e.ButtonValue.HasValue) { - var viewModel = (DataContext) as SettingsViewModel; var buttonValue = e.ButtonValue.Value; switch (button.Name) { case "ToggleVsync": - viewModel.KeyboardHotkey.ToggleVsync = buttonValue.AsHidType(); + ViewModel.KeyboardHotkey.ToggleVsync = buttonValue.AsHidType(); break; case "Screenshot": - viewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType(); + ViewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType(); break; case "ShowUI": - viewModel.KeyboardHotkey.ShowUI = buttonValue.AsHidType(); + ViewModel.KeyboardHotkey.ShowUI = buttonValue.AsHidType(); break; case "Pause": - viewModel.KeyboardHotkey.Pause = buttonValue.AsHidType(); + ViewModel.KeyboardHotkey.Pause = buttonValue.AsHidType(); break; case "ToggleMute": - viewModel.KeyboardHotkey.ToggleMute = buttonValue.AsHidType(); + ViewModel.KeyboardHotkey.ToggleMute = buttonValue.AsHidType(); break; case "ResScaleUp": - viewModel.KeyboardHotkey.ResScaleUp = buttonValue.AsHidType(); + ViewModel.KeyboardHotkey.ResScaleUp = buttonValue.AsHidType(); break; case "ResScaleDown": - viewModel.KeyboardHotkey.ResScaleDown = buttonValue.AsHidType(); + ViewModel.KeyboardHotkey.ResScaleDown = buttonValue.AsHidType(); break; case "VolumeUp": - viewModel.KeyboardHotkey.VolumeUp = buttonValue.AsHidType(); + ViewModel.KeyboardHotkey.VolumeUp = buttonValue.AsHidType(); break; case "VolumeDown": - viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType(); + ViewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType(); break; } } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml index 55c2ed6e3..6b9c233f5 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml @@ -2,15 +2,18 @@ x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsInputView" xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" xmlns:views="clr-namespace:Ryujinx.Ava.UI.Views.Input" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:inputViewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Settings" + xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models" mc:Ignorable="d" - x:DataType="viewModels:SettingsViewModel"> + x:DataType="viewModels:SettingsInputViewModel"> - + - - - - - - - - - - - - + Grid.Row="0" + HorizontalAlignment="Stretch" + VerticalAlignment="Stretch" + Orientation="Vertical"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - \ No newline at end of file + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs index 55b69af06..1b2d8e586 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsInputView.axaml.cs @@ -1,17 +1,22 @@ using Avalonia.Controls; +using Ryujinx.Ava.UI.ViewModels.Settings; namespace Ryujinx.Ava.UI.Views.Settings { public partial class SettingsInputView : UserControl { + public SettingsInputViewModel ViewModel; + public SettingsInputView() { + DataContext = ViewModel = new SettingsInputViewModel(this); + InitializeComponent(); } public void Dispose() { - InputView.Dispose(); + ViewModel.Dispose(); } } } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml index 0fc9ea1bb..5e90f5a12 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml @@ -6,11 +6,11 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Settings" mc:Ignorable="d" - x:DataType="viewModels:SettingsViewModel"> + x:DataType="viewModels:SettingsLoggingViewModel"> - + - \ No newline at end of file + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml.cs index c8df46b38..eb970dcce 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsLoggingView.axaml.cs @@ -1,11 +1,15 @@ using Avalonia.Controls; +using Ryujinx.Ava.UI.ViewModels.Settings; namespace Ryujinx.Ava.UI.Views.Settings { public partial class SettingsLoggingView : UserControl { + public SettingsLoggingViewModel ViewModel; + public SettingsLoggingView() { + DataContext = ViewModel = new SettingsLoggingViewModel(); InitializeComponent(); } } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml index 67fac192d..80c04c22f 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsNetworkView.axaml @@ -5,11 +5,11 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Settings" mc:Ignorable="d" - x:DataType="viewModels:SettingsViewModel"> + x:DataType="viewModels:SettingsNetworkViewModel"> - + + x:DataType="viewModels:SettingsSystemViewModel"> - + @@ -221,4 +221,4 @@ - \ No newline at end of file + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs index 2c9eac28c..83cab7d18 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs @@ -1,15 +1,17 @@ using Avalonia.Controls; -using Ryujinx.Ava.UI.ViewModels; +using Ryujinx.Ava.UI.ViewModels.Settings; +using Ryujinx.HLE.FileSystem; using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; namespace Ryujinx.Ava.UI.Views.Settings { public partial class SettingsSystemView : UserControl { - public SettingsViewModel ViewModel; + public SettingsSystemViewModel ViewModel; - public SettingsSystemView() + public SettingsSystemView(VirtualFileSystem virtualFileSystem, ContentManager contentManager) { + DataContext = ViewModel = new SettingsSystemViewModel(virtualFileSystem, contentManager); InitializeComponent(); } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml index f9b9be44b..9cd69e3a0 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml @@ -5,11 +5,11 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:locale="clr-namespace:Ryujinx.Ava.Common.Locale" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Settings" mc:Ignorable="d" - x:DataType="viewModels:SettingsViewModel"> + x:DataType="viewModels:SettingsUIViewModel"> - + 0) { ViewModel.GameDirectories.Add(result[0].Path.LocalPath); - ViewModel.DirectoryChanged = true; } } } @@ -53,7 +51,6 @@ namespace Ryujinx.Ava.UI.Views.Settings foreach (string path in new List(GameList.SelectedItems.Cast())) { ViewModel.GameDirectories.Remove(path); - ViewModel.DirectoryChanged = true; } if (GameList.ItemCount > 0) diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml index de3c2291a..9170a3750 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -7,8 +7,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:window="clr-namespace:Ryujinx.Ava.UI.Windows" - xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" - xmlns:settings="clr-namespace:Ryujinx.Ava.UI.Views.Settings" + xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Settings" xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers" Width="1100" Height="768" @@ -32,17 +31,6 @@ Grid.Row="1" IsVisible="False" KeyboardNavigation.IsTabStop="False"/> - - - - - - - - - - -