Compare commits

...

35 Commits

Author SHA1 Message Date
Isaac Marovitz
8fd9a707fc
Merge 43feea8cd5 into 5dbba07e33 2024-09-29 17:53:55 +02:00
e2dk4r
5dbba07e33
sdl: set app name (#7370)
Ryujinx was not hinting application name, so on some platforms (e.g.
Linux) volume control shows Ryujinx as 'SDL Application'. This can cause
confusion.

This commit fixes name in volume control applets on some platforms.

see: https://wiki.libsdl.org/SDL2/SDL_HINT_APP_NAME
2024-09-28 10:44:23 +02:00
MaxLastBreath
d86249cb0a
Convert MaxTextureCacheCapacity to Dynamic MaxTextureCacheCapacity for High Resolution Mod support. (#7307)
* Add Texture Size Capacity and 8GB Dram Build

* Update AutoDeleteCache.cs

* Dynamic Texture Cache (WIP)

* Change to float Multiplier, in-case it needs fine-tuning.

* Delete src/src.sln

* Update AutoDeleteCache.cs

* Format

* Fix Formatting

* Add DefaultTextureSizeCapacity and MemoryScaleFactor

- Also remove redundant New Lines

* Fix 4GB dram crashing

* Format newline

* Refractor

- Added Initialize() function to TextureCache and AutoDeleteCache
- Removed GetMaxTextureCapacity() function and instead added _maxCacheMemoryUsage
- Added private const MaxTextureSizeCapacity to AutoDelete Cache
- Added TextureCache.Initialize() to MemoryManager in order to fetch MaxGpuMemory at the right time.
- Moved and Changed Logger.Info for Gpu Memory to Logger.Notice and Moved it to PrintGpuInformation function.
- Opted to use a ternary operator for the Initialize function, I think it looks cleaner than bunch of if statements.

* Update src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* maxMemory to CacheMemory, use Clamp instead of Ternary. Changed MinTextureCapacity 1GiB to 512 MiB

* Update src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* Format comment

* comment context

* Increase TextureSize capacity for OpenGL back to 1024

- Added a new const ulong for OpenGLTextureSizeCapacity

* Fix changes from last commit.

* Adjust last OpenGL changes.

* Remove garbage VSC file

* Update src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* Update src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

* Update src/Ryujinx.Graphics.Gpu/Image/AutoDeleteCache.cs

Co-authored-by: gdkchan <gab.dark.100@gmail.com>

---------

Co-authored-by: gdkchan <gab.dark.100@gmail.com>
2024-09-26 14:33:38 -03:00
riperiperi
04d68ca616
GPU: Ensure all clip distances are initialized when used (#7363)
* GPU: Ensure all clip distances are initialized when used

* Shader cache version
2024-09-26 14:19:12 -03:00
Isaac Marovitz
43feea8cd5
Upstream config changes 2024-07-16 20:49:49 +01:00
Isaac Marovitz
3b60e8f590
Dirty on input map bindings change 2024-07-16 20:40:32 +01:00
Isaac Marovitz
a3b50fb28d
Implement IEquatable
Format
2024-07-16 20:40:32 +01:00
Isaac Marovitz
95e78f6ee1
Start InputVM Dirty Integration 2024-07-16 20:40:32 +01:00
Isaac Marovitz
3145934efc
Revert no longer necessary 2024-07-16 20:40:31 +01:00
Isaac Marovitz
8ffe522f53
Cleanup 2024-07-16 20:40:31 +01:00
Isaac Marovitz
47c491271a
Migrate UI to SettingsUIViewModel 2024-07-16 20:40:31 +01:00
Isaac Marovitz
9b6241985f
Migrate System to SettingsSystemViewModel 2024-07-16 20:40:31 +01:00
Isaac Marovitz
65ec957c4c
Migrate Network to SettingsNetworkViewModel 2024-07-16 20:40:30 +01:00
Isaac Marovitz
2d399b6d26
Null operators 2024-07-16 20:40:30 +01:00
Isaac Marovitz
2e48ef62fa
Adjust SettingsInputViewModel for new system 2024-07-16 20:40:30 +01:00
Isaac Marovitz
ddbdd0246a
Migrate Hotkeys to SettingsHoykeysViewModel 2024-07-16 20:40:30 +01:00
Isaac Marovitz
378cbf129d
Format 2024-07-16 20:40:30 +01:00
Isaac Marovitz
66205aa3a3
Migrate CPU to SettingsCpuViewModel 2024-07-16 20:40:29 +01:00
Isaac Marovitz
7f61ac3ab8
Rename SettingsCpuView 2024-07-16 20:40:29 +01:00
Isaac Marovitz
ae97783459
Migrate Graphics to SettingsGraphicsViewModel 2024-07-16 20:40:29 +01:00
Isaac Marovitz
7e19054de1
Migrate Logging to SettingsLoggingViewModel 2024-07-16 20:40:29 +01:00
Isaac Marovitz
388597b4e6
Migrate Audio to SettingsAudioViewModel 2024-07-16 20:40:28 +01:00
Isaac Marovitz
2d73107dc0
Cleanup SettingsViewModel usage 2024-07-16 20:40:28 +01:00
Isaac Marovitz
2281b3b59e
Move around VMs + Make most settings reactive 2024-07-16 20:40:28 +01:00
Isaac Marovitz
c628cd7af5
Default Apply to not enabled 2024-07-16 20:39:50 +01:00
Isaac Marovitz
286aebf70f
Move confimration dialogue to SettingsWindow 2024-07-16 20:39:50 +01:00
Isaac Marovitz
1ad9b27ed6
Remove InputViewModel.IsModified
Format
2024-07-16 20:39:49 +01:00
Isaac Marovitz
9167833f0a
Consolidate SettingsInputView & InputView 2024-07-16 20:39:49 +01:00
Isaac Marovitz
b3262302fc
Refactor SettingsWindow 2024-07-16 20:39:49 +01:00
Isaac Marovitz
7821d4581a
Enable/Disable Apply if dirty 2024-07-16 20:39:49 +01:00
Isaac Marovitz
417b4caa98
Add buttons interactibility toggle 2024-07-16 20:39:48 +01:00
Isaac Marovitz
fd33ebb42d
Use IsDefault and IsCancel instead of Hotkeys 2024-07-16 20:39:48 +01:00
Isaac Marovitz
ea80d922a6
Move all remaining input controls to Input VM 2024-07-16 20:39:48 +01:00
Isaac Marovitz
c141b248a8
Change SettingsWindow title on dirty 2024-07-16 20:39:48 +01:00
Isaac Marovitz
f195198608
Extend ContentDialogHelper to work on multiple windows 2024-07-16 20:39:47 +01:00
50 changed files with 2081 additions and 1176 deletions

View File

@ -1,6 +1,8 @@
using System;
namespace Ryujinx.Common.Configuration.Hid
{
public class KeyboardHotkeys
public class KeyboardHotkeys : IEquatable<KeyboardHotkeys>
{
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;
}
}
}
}

View File

@ -71,6 +71,8 @@ namespace Ryujinx.Graphics.GAL
public readonly int GatherBiasPrecision;
public readonly ulong MaximumGpuMemory;
public Capabilities(
TargetApi api,
string vendorName,
@ -131,7 +133,8 @@ namespace Ryujinx.Graphics.GAL
int shaderSubgroupSize,
int storageBufferOffsetAlignment,
int textureBufferOffsetAlignment,
int gatherBiasPrecision)
int gatherBiasPrecision,
ulong maximumGpuMemory)
{
Api = api;
VendorName = vendorName;
@ -193,6 +196,7 @@ namespace Ryujinx.Graphics.GAL
StorageBufferOffsetAlignment = storageBufferOffsetAlignment;
TextureBufferOffsetAlignment = textureBufferOffsetAlignment;
GatherBiasPrecision = gatherBiasPrecision;
MaximumGpuMemory = maximumGpuMemory;
}
}
}

View File

@ -1,3 +1,4 @@
using System;
using System.Collections;
using System.Collections.Generic;
@ -46,7 +47,11 @@ namespace Ryujinx.Graphics.Gpu.Image
{
private const int MinCountForDeletion = 32;
private const int MaxCapacity = 2048;
private const ulong MaxTextureSizeCapacity = 1024 * 1024 * 1024; // MB;
private const ulong MinTextureSizeCapacity = 512 * 1024 * 1024;
private const ulong MaxTextureSizeCapacity = 4UL * 1024 * 1024 * 1024;
private const ulong DefaultTextureSizeCapacity = 1UL * 1024 * 1024 * 1024;
private const float MemoryScaleFactor = 0.50f;
private ulong _maxCacheMemoryUsage = 0;
private readonly LinkedList<Texture> _textures;
private ulong _totalSize;
@ -56,6 +61,25 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly Dictionary<TextureDescriptor, ShortTextureCacheEntry> _shortCacheLookup;
/// <summary>
/// Initializes the cache, setting the maximum texture capacity for the specified GPU context.
/// </summary>
/// <remarks>
/// If the backend GPU has 0 memory capacity, the cache size defaults to `DefaultTextureSizeCapacity`.
/// </remarks>
/// <param name="context">The GPU context that the cache belongs to</param>
public void Initialize(GpuContext context)
{
var cacheMemory = (ulong)(context.Capabilities.MaximumGpuMemory * MemoryScaleFactor);
_maxCacheMemoryUsage = Math.Clamp(cacheMemory, MinTextureSizeCapacity, MaxTextureSizeCapacity);
if (context.Capabilities.MaximumGpuMemory == 0)
{
_maxCacheMemoryUsage = DefaultTextureSizeCapacity;
}
}
/// <summary>
/// Creates a new instance of the automatic deletion cache.
/// </summary>
@ -85,7 +109,7 @@ namespace Ryujinx.Graphics.Gpu.Image
texture.CacheNode = _textures.AddLast(texture);
if (_textures.Count > MaxCapacity ||
(_totalSize > MaxTextureSizeCapacity && _textures.Count >= MinCountForDeletion))
(_totalSize > _maxCacheMemoryUsage && _textures.Count >= MinCountForDeletion))
{
RemoveLeastUsedTexture();
}
@ -110,7 +134,7 @@ namespace Ryujinx.Graphics.Gpu.Image
_textures.AddLast(texture.CacheNode);
}
if (_totalSize > MaxTextureSizeCapacity && _textures.Count >= MinCountForDeletion)
if (_totalSize > _maxCacheMemoryUsage && _textures.Count >= MinCountForDeletion)
{
RemoveLeastUsedTexture();
}

View File

@ -68,6 +68,14 @@ namespace Ryujinx.Graphics.Gpu.Image
_cache = new AutoDeleteCache();
}
/// <summary>
/// Initializes the cache, setting the maximum texture capacity for the specified GPU context.
/// </summary>
public void Initialize()
{
_cache.Initialize(_context);
}
/// <summary>
/// Handles marking of textures written to a memory region being (partially) remapped.
/// </summary>

View File

@ -1,4 +1,5 @@
using Ryujinx.Common.Memory;
using Ryujinx.Graphics.Gpu.Image;
using Ryujinx.Memory;
using Ryujinx.Memory.Range;
using System;
@ -64,6 +65,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
MemoryUnmapped += Physical.BufferCache.MemoryUnmappedHandler;
MemoryUnmapped += VirtualRangeCache.MemoryUnmappedHandler;
MemoryUnmapped += CounterCache.MemoryUnmappedHandler;
Physical.TextureCache.Initialize();
}
/// <summary>

View File

@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
private const uint CodeGenVersion = 7331;
private const uint CodeGenVersion = 7353;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";

View File

@ -202,7 +202,8 @@ namespace Ryujinx.Graphics.OpenGL
shaderSubgroupSize: Constants.MaxSubgroupSize,
storageBufferOffsetAlignment: HwCapabilities.StorageBufferOffsetAlignment,
textureBufferOffsetAlignment: HwCapabilities.TextureBufferOffsetAlignment,
gatherBiasPrecision: intelWindows || amdWindows ? 8 : 0); // Precision is 8 for these vendors on Vulkan.
gatherBiasPrecision: intelWindows || amdWindows ? 8 : 0, // Precision is 8 for these vendors on Vulkan.
maximumGpuMemory: 0);
}
public void SetBufferData(BufferHandle buffer, int offset, ReadOnlySpan<byte> data)

View File

@ -190,7 +190,7 @@ namespace Ryujinx.Graphics.Shader.Translation
if (stage == ShaderStage.Vertex)
{
InitializePositionOutput(context);
InitializeVertexOutputs(context);
}
UInt128 usedAttributes = context.TranslatorContext.AttributeUsage.NextInputAttributesComponents;
@ -236,12 +236,20 @@ namespace Ryujinx.Graphics.Shader.Translation
}
}
private static void InitializePositionOutput(EmitterContext context)
private static void InitializeVertexOutputs(EmitterContext context)
{
for (int c = 0; c < 4; c++)
{
context.Store(StorageKind.Output, IoVariable.Position, null, Const(c), ConstF(c == 3 ? 1f : 0f));
}
if (context.Program.ClipDistancesWritten != 0)
{
for (int i = 0; i < 8; i++)
{
context.Store(StorageKind.Output, IoVariable.ClipDistance, null, Const(i), ConstF(0f));
}
}
}
private static void InitializeOutput(EmitterContext context, int location, bool perPatch)

View File

@ -781,7 +781,26 @@ namespace Ryujinx.Graphics.Vulkan
shaderSubgroupSize: (int)Capabilities.SubgroupSize,
storageBufferOffsetAlignment: (int)limits.MinStorageBufferOffsetAlignment,
textureBufferOffsetAlignment: (int)limits.MinTexelBufferOffsetAlignment,
gatherBiasPrecision: IsIntelWindows || IsAmdWindows ? (int)Capabilities.SubTexelPrecisionBits : 0);
gatherBiasPrecision: IsIntelWindows || IsAmdWindows ? (int)Capabilities.SubTexelPrecisionBits : 0,
maximumGpuMemory: GetTotalGPUMemory());
}
private ulong GetTotalGPUMemory()
{
ulong totalMemory = 0;
Api.GetPhysicalDeviceMemoryProperties(_physicalDevice.PhysicalDevice, out PhysicalDeviceMemoryProperties memoryProperties);
for (int i = 0; i < memoryProperties.MemoryHeapCount; i++)
{
var heap = memoryProperties.MemoryHeaps[i];
if ((heap.Flags & MemoryHeapFlags.DeviceLocalBit) == MemoryHeapFlags.DeviceLocalBit)
{
totalMemory += heap.Size;
}
}
return totalMemory;
}
public HardwareInfo GetHardwareInfo()
@ -865,6 +884,7 @@ namespace Ryujinx.Graphics.Vulkan
private void PrintGpuInformation()
{
Logger.Notice.Print(LogClass.Gpu, $"{GpuVendor} {GpuRenderer} ({GpuVersion})");
Logger.Notice.Print(LogClass.Gpu, $"GPU Memory: {GetTotalGPUMemory() / (1024 * 1024)} MiB");
}
public void Initialize(GraphicsDebugLevel logLevel)

View File

@ -53,6 +53,7 @@ namespace Ryujinx.SDL2.Common
return;
}
SDL_SetHint(SDL_HINT_APP_NAME, "Ryujinx");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");

View File

@ -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!",

View File

@ -28,7 +28,8 @@ namespace Ryujinx.Ava.UI.Helpers
string closeButton,
UserResult primaryButtonResult = UserResult.Ok,
ManualResetEvent deferResetEvent = null,
TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null)
TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> 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<ContentDialog, ContentDialogButtonClickEventArgs> deferCloseAction = null)
TypedEventHandler<ContentDialog, ContentDialogButtonClickEventArgs> 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<UserResult> ShowDeferredContentDialog(
@ -94,7 +105,8 @@ namespace Ryujinx.Ava.UI.Helpers
string closeButton,
int iconSymbol,
ManualResetEvent deferResetEvent,
Func<Window, Task> doWhileDeferred = null)
Func<Window, Task> 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<UserResult> 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<bool> CreateChoiceDialog(string title, string primary, string secondaryText)
internal static async Task<bool> 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<ContentDialogResult> ShowAsync(ContentDialog contentDialog)
public static async Task<ContentDialogResult> 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<ContentDialogResult>(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<ContentDialogResult>(parent);
async Task<ContentDialogResult> ShowDialog()
{
if (_contentDialogOverlayWindow is not null)

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<ComboBoxItem> 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<string> _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<ComboBoxItem>();
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);
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<PlayerModel>();
Controllers = new ObservableCollection<ControllerModel>();
@ -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<InputConfig> 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<InputConfig> ConstructInputConfigList()
{
List<InputConfig> 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()

View File

@ -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;
}
}
}

View File

@ -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<string, string> _networkInterfaces = new();
public AvaloniaList<string> 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;
}
}
}

View File

@ -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<string> _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<TimeZone> 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;
}
}
}

View File

@ -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<string> 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"
};
}
}
}

View File

@ -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<bool> DirtyEvent;
public event Action<bool> 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();
}
}
}

View File

@ -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<string> _validTzRegions;
private readonly Dictionary<string, string> _networkInterfaces;
private float _customResolutionScale;
private int _resolutionScale;
private int _graphicsBackendMultithreadingIndex;
private float _volume;
private bool _isVulkanAvailable = true;
private bool _directoryChanged;
private readonly List<string> _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<TimeZone> TimeZones { get; set; }
public AvaloniaList<string> GameDirectories { get; set; }
public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; }
public AvaloniaList<string> 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<string>();
TimeZones = new AvaloniaList<TimeZone>();
AvailableGpus = new ObservableCollection<ComboBoxItem>();
_validTzRegions = new List<string>();
_networkInterfaces = new Dictionary<string, string>();
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<string> 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();
}
}
}

View File

@ -1,225 +0,0 @@
<UserControl
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:models="clr-namespace:Ryujinx.Ava.UI.Models"
xmlns:views="clr-namespace:Ryujinx.Ava.UI.Views.Input"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
d:DesignHeight="800"
d:DesignWidth="800"
x:Class="Ryujinx.Ava.UI.Views.Input.InputView"
x:DataType="viewModels:InputViewModel"
x:CompileBindings="True"
mc:Ignorable="d"
Focusable="True">
<Design.DataContext>
<viewModels:InputViewModel />
</Design.DataContext>
<UserControl.Styles>
<Style Selector="ToggleButton">
<Setter Property="Width" Value="90" />
<Setter Property="Height" Value="27" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
</UserControl.Styles>
<StackPanel
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Orientation="Vertical">
<StackPanel
Margin="0 0 0 5"
Orientation="Vertical"
Spacing="5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Player Selection -->
<Grid
Grid.Column="0"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Margin="5,0,10,0"
Width="90"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsPlayer}" />
<ComboBox
Grid.Column="1"
Name="PlayerIndexBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
SelectionChanged="PlayerIndexBox_OnSelectionChanged"
ItemsSource="{Binding PlayerIndexes}"
SelectedIndex="{Binding PlayerId}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
<!-- Profile Selection -->
<Grid
Grid.Column="2"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Margin="5,0,10,0"
Width="90"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsProfile}" />
<ui:FAComboBox
Grid.Column="1"
IsEditable="True"
Name="ProfileBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
SelectedIndex="0"
ItemsSource="{Binding ProfilesList}"
Text="{Binding ProfileName, Mode=TwoWay}" />
<Button
Grid.Column="2"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ControllerSettingsLoadProfileToolTip}"
Command="{Binding LoadProfile}">
<ui:SymbolIcon
Symbol="Upload"
FontSize="15"
Height="20" />
</Button>
<Button
Grid.Column="3"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ControllerSettingsSaveProfileToolTip}"
Command="{Binding SaveProfile}">
<ui:SymbolIcon
Symbol="Save"
FontSize="15"
Height="20" />
</Button>
<Button
Grid.Column="4"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ControllerSettingsRemoveProfileToolTip}"
Command="{Binding RemoveProfile}">
<ui:SymbolIcon
Symbol="Delete"
FontSize="15"
Height="20" />
</Button>
</Grid>
</Grid>
<Separator />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Input Device -->
<Grid
Grid.Column="0"
Margin="2"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="5,0,10,0"
Width="90"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsInputDevice}" />
<ComboBox
Grid.Column="1"
Name="DeviceBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
ItemsSource="{Binding DeviceList}"
SelectedIndex="{Binding Device}" />
<Button
Grid.Column="2"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
Command="{Binding LoadDevices}">
<ui:SymbolIcon
Symbol="Refresh"
FontSize="15"
Height="20"/>
</Button>
</Grid>
<!-- Controller Type -->
<Grid
Grid.Column="2"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Margin="5,0,10,0"
Width="90"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsControllerType}" />
<ComboBox
Grid.Column="1"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Controllers}"
SelectedIndex="{Binding Controller}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="models:ControllerModel">
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Grid>
</StackPanel>
<ContentControl Content="{Binding ConfigViewModel}" IsVisible="{Binding ShowSettings}">
<ContentControl.DataTemplates>
<DataTemplate DataType="viewModels:ControllerInputViewModel">
<views:ControllerInputView />
</DataTemplate>
<DataTemplate DataType="viewModels:KeyboardInputViewModel">
<views:KeyboardInputView />
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</StackPanel>
</UserControl>

View File

@ -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();
}
}
}

View File

@ -672,4 +672,4 @@
</StackPanel>
</Grid>
</StackPanel>
</UserControl>
</UserControl>

View File

@ -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

View File

@ -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">
<Design.DataContext>
<viewModels:SettingsViewModel />
<viewModels:SettingsAudioViewModel />
</Design.DataContext>
<ScrollViewer
Name="AudioPage"

View File

@ -1,11 +1,15 @@
using Avalonia.Controls;
using Ryujinx.Ava.UI.ViewModels.Settings;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsAudioView : UserControl
{
public SettingsAudioViewModel ViewModel;
public SettingsAudioView()
{
DataContext = ViewModel = new SettingsAudioViewModel();
InitializeComponent();
}
}

View File

@ -1,12 +0,0 @@
using Avalonia.Controls;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsCPUView : UserControl
{
public SettingsCPUView()
{
InitializeComponent();
}
}
}

View File

@ -1,13 +1,13 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsCPUView"
x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsCpuView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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:SettingsCpuViewModel">
<Design.DataContext>
<viewModels:SettingsViewModel />
</Design.DataContext>

View File

@ -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();
}
}
}

View File

@ -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">
<Design.DataContext>
<viewModels:SettingsViewModel />
<viewModels:SettingsGraphicsViewModel />
</Design.DataContext>
<ScrollViewer
Name="GraphicsPage"
@ -268,6 +268,7 @@
<ComboBox Width="350"
HorizontalContentAlignment="Left"
ToolTip.Tip="{locale:Locale GalThreadingTooltip}"
SelectionChanged="GraphicsBackendMultithreadingIndex_OnSelectionChanged"
SelectedIndex="{Binding GraphicsBackendMultithreadingIndex}">
<ComboBoxItem>
<TextBlock Text="{locale:Locale CommonAuto}" />

View File

@ -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)
);
}
}
}
}

View File

@ -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">
<Design.DataContext>
<viewModels:SettingsViewModel />
<viewModels:SettingsHotkeysViewModel />
</Design.DataContext>
<UserControl.Resources>
<helpers:KeyValueConverter x:Key="Key" />

View File

@ -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<Key>();
ViewModel.KeyboardHotkey.ToggleVsync = buttonValue.AsHidType<Key>();
break;
case "Screenshot":
viewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType<Key>();
ViewModel.KeyboardHotkey.Screenshot = buttonValue.AsHidType<Key>();
break;
case "ShowUI":
viewModel.KeyboardHotkey.ShowUI = buttonValue.AsHidType<Key>();
ViewModel.KeyboardHotkey.ShowUI = buttonValue.AsHidType<Key>();
break;
case "Pause":
viewModel.KeyboardHotkey.Pause = buttonValue.AsHidType<Key>();
ViewModel.KeyboardHotkey.Pause = buttonValue.AsHidType<Key>();
break;
case "ToggleMute":
viewModel.KeyboardHotkey.ToggleMute = buttonValue.AsHidType<Key>();
ViewModel.KeyboardHotkey.ToggleMute = buttonValue.AsHidType<Key>();
break;
case "ResScaleUp":
viewModel.KeyboardHotkey.ResScaleUp = buttonValue.AsHidType<Key>();
ViewModel.KeyboardHotkey.ResScaleUp = buttonValue.AsHidType<Key>();
break;
case "ResScaleDown":
viewModel.KeyboardHotkey.ResScaleDown = buttonValue.AsHidType<Key>();
ViewModel.KeyboardHotkey.ResScaleDown = buttonValue.AsHidType<Key>();
break;
case "VolumeUp":
viewModel.KeyboardHotkey.VolumeUp = buttonValue.AsHidType<Key>();
ViewModel.KeyboardHotkey.VolumeUp = buttonValue.AsHidType<Key>();
break;
case "VolumeDown":
viewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType<Key>();
ViewModel.KeyboardHotkey.VolumeDown = buttonValue.AsHidType<Key>();
break;
}
}

View File

@ -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">
<Design.DataContext>
<viewModels:SettingsViewModel />
<viewModels:SettingsInputViewModel />
</Design.DataContext>
<ScrollViewer
Name="InputPage"
@ -27,41 +30,233 @@
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<views:InputView
Grid.Row="0"
Name="InputView" />
<StackPanel
Orientation="Vertical"
Grid.Row="2">
<Separator
Margin="0 10"
Height="1" />
<StackPanel
Orientation="Horizontal"
Spacing="10">
<CheckBox
ToolTip.Tip="{locale:Locale DockModeToggleTooltip}"
MinWidth="0"
IsChecked="{Binding EnableDockedMode}">
<TextBlock
Text="{locale:Locale SettingsTabInputEnableDockedMode}" />
</CheckBox>
<CheckBox
ToolTip.Tip="{locale:Locale DirectKeyboardTooltip}"
IsChecked="{Binding EnableKeyboard}">
<TextBlock
Text="{locale:Locale SettingsTabInputDirectKeyboardAccess}" />
</CheckBox>
<CheckBox
ToolTip.Tip="{locale:Locale DirectMouseTooltip}"
IsChecked="{Binding EnableMouse}">
<TextBlock
Text="{locale:Locale SettingsTabInputDirectMouseAccess}" />
</CheckBox>
Grid.Row="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Orientation="Vertical">
<StackPanel
Margin="0 0 0 5"
Orientation="Vertical"
Spacing="5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Player Selection -->
<Grid
Grid.Column="0"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Margin="5,0,10,0"
Width="90"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsPlayer}" />
<ComboBox
Grid.Column="1"
Name="PlayerIndexBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
ItemsSource="{Binding PlayerIndexes}"
SelectedIndex="{Binding PlayerId}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
<!-- Profile Selection -->
<Grid
Grid.Column="2"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock
Margin="5,0,10,0"
Width="90"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsProfile}" />
<ui:FAComboBox
Grid.Column="1"
IsEditable="True"
Name="ProfileBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
SelectedIndex="0"
ItemsSource="{Binding ProfilesList}"
Text="{Binding ProfileName, Mode=TwoWay}" />
<Button
Grid.Column="2"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ControllerSettingsLoadProfileToolTip}"
Command="{Binding LoadProfile}">
<ui:SymbolIcon
Symbol="Upload"
FontSize="15"
Height="20" />
</Button>
<Button
Grid.Column="3"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ControllerSettingsSaveProfileToolTip}"
Command="{Binding SaveProfile}">
<ui:SymbolIcon
Symbol="Save"
FontSize="15"
Height="20" />
</Button>
<Button
Grid.Column="4"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
ToolTip.Tip="{locale:Locale ControllerSettingsRemoveProfileToolTip}"
Command="{Binding RemoveProfile}">
<ui:SymbolIcon
Symbol="Delete"
FontSize="15"
Height="20" />
</Button>
</Grid>
</Grid>
<Separator />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="10" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Input Device -->
<Grid
Grid.Column="0"
Margin="2"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock
Grid.Column="0"
Margin="5,0,10,0"
Width="90"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsInputDevice}" />
<ComboBox
Grid.Column="1"
Name="DeviceBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
ItemsSource="{Binding DeviceList}"
SelectedIndex="{Binding Device}" />
<Button
Grid.Column="2"
MinWidth="0"
Margin="5,0,0,0"
VerticalAlignment="Center"
Command="{Binding LoadDevices}">
<ui:SymbolIcon
Symbol="Refresh"
FontSize="15"
Height="20"/>
</Button>
</Grid>
<!-- Controller Type -->
<Grid
Grid.Column="2"
Margin="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock
Margin="5,0,10,0"
Width="90"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{locale:Locale ControllerSettingsControllerType}" />
<ComboBox
Grid.Column="1"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Controllers}"
SelectedIndex="{Binding Controller}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="models:ControllerModel">
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</Grid>
</StackPanel>
<ContentControl Content="{Binding ConfigViewModel}" IsVisible="{Binding ShowSettings}">
<ContentControl.DataTemplates>
<DataTemplate DataType="inputViewModels:ControllerInputViewModel">
<views:ControllerInputView />
</DataTemplate>
<DataTemplate DataType="inputViewModels:KeyboardInputViewModel">
<views:KeyboardInputView />
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</StackPanel>
<StackPanel
Orientation="Vertical"
Grid.Row="2">
<Separator
Margin="0 10"
Height="1" />
<StackPanel
Orientation="Horizontal"
Spacing="10">
<CheckBox
ToolTip.Tip="{locale:Locale DockModeToggleTooltip}"
MinWidth="0"
IsChecked="{Binding EnableDockedMode}">
<TextBlock
Text="{locale:Locale SettingsTabInputEnableDockedMode}" />
</CheckBox>
<CheckBox
ToolTip.Tip="{locale:Locale DirectKeyboardTooltip}"
IsChecked="{Binding EnableKeyboard}">
<TextBlock
Text="{locale:Locale SettingsTabInputDirectKeyboardAccess}" />
</CheckBox>
<CheckBox
ToolTip.Tip="{locale:Locale DirectMouseTooltip}"
IsChecked="{Binding EnableMouse}">
<TextBlock
Text="{locale:Locale SettingsTabInputDirectMouseAccess}" />
</CheckBox>
</StackPanel>
</StackPanel>
</StackPanel>
</Grid>
</Panel>
</Border>
</ScrollViewer>
</UserControl>
</UserControl>

View File

@ -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();
}
}
}

View File

@ -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">
<Design.DataContext>
<viewModels:SettingsViewModel />
<viewModels:SettingsLoggingViewModel />
</Design.DataContext>
<ScrollViewer
Name="LoggingPage"
@ -117,4 +117,4 @@
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>
</UserControl>

View File

@ -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();
}
}

View File

@ -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">
<Design.DataContext>
<viewModels:SettingsViewModel />
<viewModels:SettingsNetworkViewModel />
</Design.DataContext>
<ScrollViewer
Name="NetworkPage"

View File

@ -1,11 +1,15 @@
using Avalonia.Controls;
using Ryujinx.Ava.UI.ViewModels.Settings;
namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsNetworkView : UserControl
{
public SettingsNetworkViewModel ViewModel;
public SettingsNetworkView()
{
DataContext = ViewModel = new SettingsNetworkViewModel();
InitializeComponent();
}
}

View File

@ -5,15 +5,15 @@
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:SettingsSystemViewModel">
<UserControl.Resources>
<helpers:TimeZoneConverter x:Key="TimeZone" />
</UserControl.Resources>
<Design.DataContext>
<viewModels:SettingsViewModel />
<viewModels:SettingsSystemViewModel />
</Design.DataContext>
<ScrollViewer
Name="SystemPage"
@ -154,7 +154,7 @@
SelectionChanged="TimeZoneBox_OnSelectionChanged"
Text="{Binding Path=TimeZone, Mode=OneWay}"
TextChanged="TimeZoneBox_OnTextChanged"
ToolTip.Tip="{locale:Locale TimezoneTooltip}"
ToolTip.Tip="{locale:Locale TimezoneTooltip}"
ValueMemberBinding="{Binding Mode=OneWay, Converter={StaticResource TimeZone}}" />
</StackPanel>
<StackPanel
@ -166,7 +166,7 @@
ToolTip.Tip="{locale:Locale TimeTooltip}"
Width="250"/>
<DatePicker
VerticalAlignment="Center"
VerticalAlignment="Center"
SelectedDate="{Binding CurrentDate}"
ToolTip.Tip="{locale:Locale TimeTooltip}"
Width="350" />
@ -221,4 +221,4 @@
</StackPanel>
</Border>
</ScrollViewer>
</UserControl>
</UserControl>

View File

@ -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();
}

View File

@ -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">
<Design.DataContext>
<viewModels:SettingsViewModel />
<viewModels:SettingsUIViewModel />
</Design.DataContext>
<ScrollViewer
Name="UiPage"

View File

@ -2,8 +2,7 @@ using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using Avalonia.VisualTree;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Settings;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -12,10 +11,11 @@ namespace Ryujinx.Ava.UI.Views.Settings
{
public partial class SettingsUiView : UserControl
{
public SettingsViewModel ViewModel;
public SettingsUIViewModel ViewModel;
public SettingsUiView()
{
DataContext = ViewModel = new SettingsUIViewModel();
InitializeComponent();
}
@ -26,7 +26,6 @@ namespace Ryujinx.Ava.UI.Views.Settings
if (!string.IsNullOrWhiteSpace(path) && Directory.Exists(path) && !ViewModel.GameDirectories.Contains(path))
{
ViewModel.GameDirectories.Add(path);
ViewModel.DirectoryChanged = true;
}
else
{
@ -40,7 +39,6 @@ namespace Ryujinx.Ava.UI.Views.Settings
if (result.Count > 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<string>(GameList.SelectedItems.Cast<string>()))
{
ViewModel.GameDirectories.Remove(path);
ViewModel.DirectoryChanged = true;
}
if (GameList.ItemCount > 0)

View File

@ -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"/>
<Grid Name="Pages" IsVisible="False" Grid.Row="2">
<settings:SettingsUiView Name="UiPage" />
<settings:SettingsInputView Name="InputPage" />
<settings:SettingsHotkeysView Name="HotkeysPage" />
<settings:SettingsSystemView Name="SystemPage" />
<settings:SettingsCPUView Name="CpuPage" />
<settings:SettingsGraphicsView Name="GraphicsPage" />
<settings:SettingsAudioView Name="AudioPage" />
<settings:SettingsNetworkView Name="NetworkPage" />
<settings:SettingsLoggingView Name="LoggingPage" />
</Grid>
<ui:NavigationView
Grid.Row="1"
IsSettingsVisible="False"
@ -108,21 +96,24 @@
</ui:NavigationView>
<ReversibleStackPanel
Grid.Row="2"
Name="Buttons"
Margin="10"
Spacing="10"
Orientation="Horizontal"
HorizontalAlignment="Right"
ReverseOrder="{Binding IsMacOS}">
<Button
HotKey="Enter"
IsDefault="True"
Classes="accent"
Content="{locale:Locale SettingsButtonOk}"
Command="{Binding OkButton}" />
<Button
HotKey="Escape"
IsCancel="True"
Content="{locale:Locale SettingsButtonCancel}"
Command="{Binding CancelButton}" />
Click="Cancel_OnClick" />
<Button
Name="Apply"
IsEnabled="False"
Content="{locale:Locale SettingsButtonApply}"
Command="{Binding ApplyButton}" />
</ReversibleStackPanel>

View File

@ -1,8 +1,11 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels.Settings;
using Ryujinx.Ava.UI.Views.Settings;
using Ryujinx.HLE.FileSystem;
using System;
@ -10,44 +13,79 @@ namespace Ryujinx.Ava.UI.Windows
{
public partial class SettingsWindow : StyleableWindow
{
internal SettingsViewModel ViewModel { get; set; }
private SettingsViewModel ViewModel { get; }
public readonly SettingsUiView UiPage;
public readonly SettingsInputView InputPage;
public readonly SettingsHotkeysView HotkeysPage;
public readonly SettingsSystemView SystemPage;
public readonly SettingsCpuView CpuPage;
public readonly SettingsGraphicsView GraphicsPage;
public readonly SettingsAudioView AudioPage;
public readonly SettingsNetworkView NetworkPage;
public readonly SettingsLoggingView LoggingPage;
public SettingsWindow(VirtualFileSystem virtualFileSystem, ContentManager contentManager)
{
Title = $"Ryujinx {Program.Version} - {LocaleManager.Instance[LocaleKeys.Settings]}";
Title = $"{LocaleManager.Instance[LocaleKeys.Settings]}";
AudioPage = new SettingsAudioView();
CpuPage = new SettingsCpuView();
GraphicsPage = new SettingsGraphicsView();
HotkeysPage = new SettingsHotkeysView();
InputPage = new SettingsInputView();
LoggingPage = new SettingsLoggingView();
NetworkPage = new SettingsNetworkView();
SystemPage = new SettingsSystemView(virtualFileSystem, contentManager);
UiPage = new SettingsUiView();
ViewModel = new SettingsViewModel(
AudioPage.ViewModel,
CpuPage.ViewModel,
GraphicsPage.ViewModel,
HotkeysPage.ViewModel,
InputPage.ViewModel,
LoggingPage.ViewModel,
NetworkPage.ViewModel,
SystemPage.ViewModel,
UiPage.ViewModel);
ViewModel = new SettingsViewModel(virtualFileSystem, contentManager);
DataContext = ViewModel;
ViewModel.CloseWindow += Close;
ViewModel.SaveSettingsEvent += SaveSettings;
ViewModel.DirtyEvent += UpdateDirtyTitle;
ViewModel.ToggleButtons += ToggleButtons;
InitializeComponent();
Load();
}
public SettingsWindow()
private void UpdateDirtyTitle(bool isDirty)
{
ViewModel = new SettingsViewModel();
DataContext = ViewModel;
InitializeComponent();
Load();
}
public void SaveSettings()
{
InputPage.InputView?.SaveCurrentProfile();
if (Owner is MainWindow window && ViewModel.DirectoryChanged)
if (!IsInitialized)
{
window.LoadApplications();
return;
}
if (isDirty)
{
Title = $"{LocaleManager.Instance[LocaleKeys.Settings]} - {LocaleManager.Instance[LocaleKeys.SettingsDirty]}";
Apply.IsEnabled = true;
}
else
{
Title = $"{LocaleManager.Instance[LocaleKeys.Settings]}";
Apply.IsEnabled = false;
}
}
private void ToggleButtons(bool enable)
{
Buttons.IsEnabled = enable;
}
private void Load()
{
Pages.Children.Clear();
NavPanel.SelectionChanged += NavPanelOnSelectionChanged;
NavPanel.SelectedItem = NavPanel.MenuItems.ElementAt(0);
}
@ -59,7 +97,6 @@ namespace Ryujinx.Ava.UI.Windows
switch (navItem.Tag.ToString())
{
case "UiPage":
UiPage.ViewModel = ViewModel;
NavPanel.Content = UiPage;
break;
case "InputPage":
@ -69,7 +106,6 @@ namespace Ryujinx.Ava.UI.Windows
NavPanel.Content = HotkeysPage;
break;
case "SystemPage":
SystemPage.ViewModel = ViewModel;
NavPanel.Content = SystemPage;
break;
case "CpuPage":
@ -93,8 +129,34 @@ namespace Ryujinx.Ava.UI.Windows
}
}
private async void Cancel_OnClick(object sender, RoutedEventArgs e)
{
if (ViewModel.IsModified)
{
var result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogSettingsUnsavedChangesMessage],
LocaleManager.Instance[LocaleKeys.DialogSettingsUnsavedChangesSubMessage],
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm],
parent: this);
if (result != UserResult.Yes)
{
return;
}
}
Close();
}
protected override void OnClosing(WindowClosingEventArgs e)
{
if (Owner is MainWindow window && UiPage.ViewModel.DirsChanged)
{
window.LoadApplications();
}
HotkeysPage.Dispose();
InputPage.Dispose();
base.OnClosing(e);