aboutsummaryrefslogtreecommitdiff
path: root/src/Ryujinx/Ui
diff options
context:
space:
mode:
authorTSR Berry <20988865+TSRBerry@users.noreply.github.com>2023-04-08 01:22:00 +0200
committerMary <thog@protonmail.com>2023-04-27 23:51:14 +0200
commitcee712105850ac3385cd0091a923438167433f9f (patch)
tree4a5274b21d8b7f938c0d0ce18736d3f2993b11b1 /src/Ryujinx/Ui
parentcd124bda587ef09668a971fa1cac1c3f0cfc9f21 (diff)
Move solution and projects to src
Diffstat (limited to 'src/Ryujinx/Ui')
-rw-r--r--src/Ryujinx/Ui/Applet/ErrorAppletDialog.cs31
-rw-r--r--src/Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs108
-rw-r--r--src/Ryujinx/Ui/Applet/GtkHostUiHandler.cs213
-rw-r--r--src/Ryujinx/Ui/Applet/GtkHostUiTheme.cs90
-rw-r--r--src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs89
-rw-r--r--src/Ryujinx/Ui/GLRenderer.cs135
-rw-r--r--src/Ryujinx/Ui/Helper/MetalHelper.cs134
-rw-r--r--src/Ryujinx/Ui/Helper/SortHelper.cs94
-rw-r--r--src/Ryujinx/Ui/Helper/ThemeHelper.cs35
-rw-r--r--src/Ryujinx/Ui/MainWindow.cs1834
-rw-r--r--src/Ryujinx/Ui/MainWindow.glade1006
-rw-r--r--src/Ryujinx/Ui/OpenToolkitBindingsContext.cs20
-rw-r--r--src/Ryujinx/Ui/RendererWidgetBase.cs778
-rw-r--r--src/Ryujinx/Ui/SPBOpenGLContext.cs47
-rw-r--r--src/Ryujinx/Ui/StatusUpdatedEventArgs.cs28
-rw-r--r--src/Ryujinx/Ui/VKRenderer.cs93
-rw-r--r--src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs220
-rw-r--r--src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs602
-rw-r--r--src/Ryujinx/Ui/Widgets/GtkDialog.cs114
-rw-r--r--src/Ryujinx/Ui/Widgets/GtkInputDialog.cs37
-rw-r--r--src/Ryujinx/Ui/Widgets/ProfileDialog.cs57
-rw-r--r--src/Ryujinx/Ui/Widgets/ProfileDialog.glade124
-rw-r--r--src/Ryujinx/Ui/Widgets/RawInputToTextEntry.cs27
-rw-r--r--src/Ryujinx/Ui/Widgets/UserErrorDialog.cs123
-rw-r--r--src/Ryujinx/Ui/Windows/AboutWindow.Designer.cs493
-rw-r--r--src/Ryujinx/Ui/Windows/AboutWindow.cs80
-rw-r--r--src/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs194
-rw-r--r--src/Ryujinx/Ui/Windows/AmiiboWindow.cs387
-rw-r--r--src/Ryujinx/Ui/Windows/AvatarWindow.cs294
-rw-r--r--src/Ryujinx/Ui/Windows/CheatWindow.cs154
-rw-r--r--src/Ryujinx/Ui/Windows/CheatWindow.glade135
-rw-r--r--src/Ryujinx/Ui/Windows/ControllerWindow.cs1222
-rw-r--r--src/Ryujinx/Ui/Windows/ControllerWindow.glade2241
-rw-r--r--src/Ryujinx/Ui/Windows/DlcWindow.cs273
-rw-r--r--src/Ryujinx/Ui/Windows/DlcWindow.glade202
-rw-r--r--src/Ryujinx/Ui/Windows/SettingsWindow.cs816
-rw-r--r--src/Ryujinx/Ui/Windows/SettingsWindow.glade3066
-rw-r--r--src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs208
-rw-r--r--src/Ryujinx/Ui/Windows/TitleUpdateWindow.glade214
-rw-r--r--src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs256
-rw-r--r--src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs331
41 files changed, 16605 insertions, 0 deletions
diff --git a/src/Ryujinx/Ui/Applet/ErrorAppletDialog.cs b/src/Ryujinx/Ui/Applet/ErrorAppletDialog.cs
new file mode 100644
index 00000000..d4cc7ccc
--- /dev/null
+++ b/src/Ryujinx/Ui/Applet/ErrorAppletDialog.cs
@@ -0,0 +1,31 @@
+using Gtk;
+using Ryujinx.Ui.Common.Configuration;
+using System.Reflection;
+
+namespace Ryujinx.Ui.Applet
+{
+ internal class ErrorAppletDialog : MessageDialog
+ {
+ public ErrorAppletDialog(Window parentWindow, DialogFlags dialogFlags, MessageType messageType, string[] buttons) : base(parentWindow, dialogFlags, messageType, ButtonsType.None, null)
+ {
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+
+ int responseId = 0;
+
+ if (buttons != null)
+ {
+ foreach (string buttonText in buttons)
+ {
+ AddButton(buttonText, responseId);
+ responseId++;
+ }
+ }
+ else
+ {
+ AddButton("OK", 0);
+ }
+
+ ShowAll();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs b/src/Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs
new file mode 100644
index 00000000..79df3cc7
--- /dev/null
+++ b/src/Ryujinx/Ui/Applet/GtkDynamicTextInputHandler.cs
@@ -0,0 +1,108 @@
+using Gtk;
+using Ryujinx.HLE.Ui;
+using Ryujinx.Input.GTK3;
+using Ryujinx.Ui.Widgets;
+using System.Threading;
+
+namespace Ryujinx.Ui.Applet
+{
+ /// <summary>
+ /// Class that forwards key events to a GTK Entry so they can be processed into text.
+ /// </summary>
+ internal class GtkDynamicTextInputHandler : IDynamicTextInputHandler
+ {
+ private readonly Window _parent;
+ private readonly OffscreenWindow _inputToTextWindow = new OffscreenWindow();
+ private readonly RawInputToTextEntry _inputToTextEntry = new RawInputToTextEntry();
+
+ private bool _canProcessInput;
+
+ public event DynamicTextChangedHandler TextChangedEvent;
+ public event KeyPressedHandler KeyPressedEvent;
+ public event KeyReleasedHandler KeyReleasedEvent;
+
+ public bool TextProcessingEnabled
+ {
+ get
+ {
+ return Volatile.Read(ref _canProcessInput);
+ }
+
+ set
+ {
+ Volatile.Write(ref _canProcessInput, value);
+ }
+ }
+
+ public GtkDynamicTextInputHandler(Window parent)
+ {
+ _parent = parent;
+ _parent.KeyPressEvent += HandleKeyPressEvent;
+ _parent.KeyReleaseEvent += HandleKeyReleaseEvent;
+
+ _inputToTextWindow.Add(_inputToTextEntry);
+
+ _inputToTextEntry.TruncateMultiline = true;
+
+ // Start with input processing turned off so the text box won't accumulate text
+ // if the user is playing on the keyboard.
+ _canProcessInput = false;
+ }
+
+ [GLib.ConnectBefore()]
+ private void HandleKeyPressEvent(object o, KeyPressEventArgs args)
+ {
+ var key = (Ryujinx.Common.Configuration.Hid.Key)GTK3MappingHelper.ToInputKey(args.Event.Key);
+
+ if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
+ {
+ return;
+ }
+
+ if (_canProcessInput)
+ {
+ _inputToTextEntry.SendKeyPressEvent(o, args);
+ _inputToTextEntry.GetSelectionBounds(out int selectionStart, out int selectionEnd);
+ TextChangedEvent?.Invoke(_inputToTextEntry.Text, selectionStart, selectionEnd, _inputToTextEntry.OverwriteMode);
+ }
+ }
+
+ [GLib.ConnectBefore()]
+ private void HandleKeyReleaseEvent(object o, KeyReleaseEventArgs args)
+ {
+ var key = (Ryujinx.Common.Configuration.Hid.Key)GTK3MappingHelper.ToInputKey(args.Event.Key);
+
+ if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
+ {
+ return;
+ }
+
+ if (_canProcessInput)
+ {
+ // TODO (caian): This solution may have problems if the pause is sent after a key press
+ // and before a key release. But for now GTK Entry does not seem to use release events.
+ _inputToTextEntry.SendKeyReleaseEvent(o, args);
+ _inputToTextEntry.GetSelectionBounds(out int selectionStart, out int selectionEnd);
+ TextChangedEvent?.Invoke(_inputToTextEntry.Text, selectionStart, selectionEnd, _inputToTextEntry.OverwriteMode);
+ }
+ }
+
+ public void SetText(string text, int cursorBegin)
+ {
+ _inputToTextEntry.Text = text;
+ _inputToTextEntry.Position = cursorBegin;
+ }
+
+ public void SetText(string text, int cursorBegin, int cursorEnd)
+ {
+ _inputToTextEntry.Text = text;
+ _inputToTextEntry.SelectRegion(cursorBegin, cursorEnd);
+ }
+
+ public void Dispose()
+ {
+ _parent.KeyPressEvent -= HandleKeyPressEvent;
+ _parent.KeyReleaseEvent -= HandleKeyReleaseEvent;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Applet/GtkHostUiHandler.cs b/src/Ryujinx/Ui/Applet/GtkHostUiHandler.cs
new file mode 100644
index 00000000..d81cbe3c
--- /dev/null
+++ b/src/Ryujinx/Ui/Applet/GtkHostUiHandler.cs
@@ -0,0 +1,213 @@
+using Gtk;
+using Ryujinx.HLE.HOS.Applets;
+using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
+using Ryujinx.HLE.Ui;
+using Ryujinx.Ui.Widgets;
+using System;
+using System.Threading;
+using Action = System.Action;
+
+namespace Ryujinx.Ui.Applet
+{
+ internal class GtkHostUiHandler : IHostUiHandler
+ {
+ private readonly Window _parent;
+
+ public IHostUiTheme HostUiTheme { get; }
+
+ public GtkHostUiHandler(Window parent)
+ {
+ _parent = parent;
+
+ HostUiTheme = new GtkHostUiTheme(parent);
+ }
+
+ public bool DisplayMessageDialog(ControllerAppletUiArgs args)
+ {
+ string playerCount = args.PlayerCountMin == args.PlayerCountMax ? $"exactly {args.PlayerCountMin}" : $"{args.PlayerCountMin}-{args.PlayerCountMax}";
+
+ string message = $"Application requests <b>{playerCount}</b> player(s) with:\n\n"
+ + $"<tt><b>TYPES:</b> {args.SupportedStyles}</tt>\n\n"
+ + $"<tt><b>PLAYERS:</b> {string.Join(", ", args.SupportedPlayers)}</tt>\n\n"
+ + (args.IsDocked ? "Docked mode set. <tt>Handheld</tt> is also invalid.\n\n" : "")
+ + "<i>Please reconfigure Input now and then press OK.</i>";
+
+ return DisplayMessageDialog("Controller Applet", message);
+ }
+
+ public bool DisplayMessageDialog(string title, string message)
+ {
+ ManualResetEvent dialogCloseEvent = new ManualResetEvent(false);
+
+ bool okPressed = false;
+
+ Application.Invoke(delegate
+ {
+ MessageDialog msgDialog = null;
+
+ try
+ {
+ msgDialog = new MessageDialog(_parent, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null)
+ {
+ Title = title,
+ Text = message,
+ UseMarkup = true
+ };
+
+ msgDialog.SetDefaultSize(400, 0);
+
+ msgDialog.Response += (object o, ResponseArgs args) =>
+ {
+ if (args.ResponseId == ResponseType.Ok)
+ {
+ okPressed = true;
+ }
+
+ dialogCloseEvent.Set();
+ msgDialog?.Dispose();
+ };
+
+ msgDialog.Show();
+ }
+ catch (Exception ex)
+ {
+ GtkDialog.CreateErrorDialog($"Error displaying Message Dialog: {ex}");
+
+ dialogCloseEvent.Set();
+ }
+ });
+
+ dialogCloseEvent.WaitOne();
+
+ return okPressed;
+ }
+
+ public bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText)
+ {
+ ManualResetEvent dialogCloseEvent = new ManualResetEvent(false);
+
+ bool okPressed = false;
+ bool error = false;
+ string inputText = args.InitialText ?? "";
+
+ Application.Invoke(delegate
+ {
+ try
+ {
+ var swkbdDialog = new SwkbdAppletDialog(_parent)
+ {
+ Title = "Software Keyboard",
+ Text = args.HeaderText,
+ SecondaryText = args.SubtitleText
+ };
+
+ swkbdDialog.InputEntry.Text = inputText;
+ swkbdDialog.InputEntry.PlaceholderText = args.GuideText;
+ swkbdDialog.OkButton.Label = args.SubmitText;
+
+ swkbdDialog.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
+
+ if (swkbdDialog.Run() == (int)ResponseType.Ok)
+ {
+ inputText = swkbdDialog.InputEntry.Text;
+ okPressed = true;
+ }
+
+ swkbdDialog.Dispose();
+ }
+ catch (Exception ex)
+ {
+ error = true;
+
+ GtkDialog.CreateErrorDialog($"Error displaying Software Keyboard: {ex}");
+ }
+ finally
+ {
+ dialogCloseEvent.Set();
+ }
+ });
+
+ dialogCloseEvent.WaitOne();
+
+ userText = error ? null : inputText;
+
+ return error || okPressed;
+ }
+
+ public void ExecuteProgram(HLE.Switch device, ProgramSpecifyKind kind, ulong value)
+ {
+ device.Configuration.UserChannelPersistence.ExecuteProgram(kind, value);
+ ((MainWindow)_parent).RendererWidget?.Exit();
+ }
+
+ public bool DisplayErrorAppletDialog(string title, string message, string[] buttons)
+ {
+ ManualResetEvent dialogCloseEvent = new ManualResetEvent(false);
+
+ bool showDetails = false;
+
+ Application.Invoke(delegate
+ {
+ try
+ {
+ ErrorAppletDialog msgDialog = new ErrorAppletDialog(_parent, DialogFlags.DestroyWithParent, MessageType.Error, buttons)
+ {
+ Title = title,
+ Text = message,
+ UseMarkup = true,
+ WindowPosition = WindowPosition.CenterAlways
+ };
+
+ msgDialog.SetDefaultSize(400, 0);
+
+ msgDialog.Response += (object o, ResponseArgs args) =>
+ {
+ if (buttons != null)
+ {
+ if (buttons.Length > 1)
+ {
+ if (args.ResponseId != (ResponseType)(buttons.Length - 1))
+ {
+ showDetails = true;
+ }
+ }
+ }
+
+ dialogCloseEvent.Set();
+ msgDialog?.Dispose();
+ };
+
+ msgDialog.Show();
+ }
+ catch (Exception ex)
+ {
+ GtkDialog.CreateErrorDialog($"Error displaying ErrorApplet Dialog: {ex}");
+
+ dialogCloseEvent.Set();
+ }
+ });
+
+ dialogCloseEvent.WaitOne();
+
+ return showDetails;
+ }
+
+ private void SynchronousGtkInvoke(Action action)
+ {
+ var waitHandle = new ManualResetEventSlim();
+
+ Application.Invoke(delegate
+ {
+ action();
+ waitHandle.Set();
+ });
+
+ waitHandle.Wait();
+ }
+
+ public IDynamicTextInputHandler CreateDynamicTextInputHandler()
+ {
+ return new GtkDynamicTextInputHandler(_parent);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Applet/GtkHostUiTheme.cs b/src/Ryujinx/Ui/Applet/GtkHostUiTheme.cs
new file mode 100644
index 00000000..f25da47c
--- /dev/null
+++ b/src/Ryujinx/Ui/Applet/GtkHostUiTheme.cs
@@ -0,0 +1,90 @@
+using Gtk;
+using Ryujinx.HLE.Ui;
+using System.Diagnostics;
+
+namespace Ryujinx.Ui.Applet
+{
+ internal class GtkHostUiTheme : IHostUiTheme
+ {
+ private const int RenderSurfaceWidth = 32;
+ private const int RenderSurfaceHeight = 32;
+
+ public string FontFamily { get; private set; }
+
+ public ThemeColor DefaultBackgroundColor { get; }
+ public ThemeColor DefaultForegroundColor { get; }
+ public ThemeColor DefaultBorderColor { get; }
+ public ThemeColor SelectionBackgroundColor { get; }
+ public ThemeColor SelectionForegroundColor { get; }
+
+ public GtkHostUiTheme(Window parent)
+ {
+ Entry entry = new Entry();
+ entry.SetStateFlags(StateFlags.Selected, true);
+
+ // Get the font and some colors directly from GTK.
+ FontFamily = entry.PangoContext.FontDescription.Family;
+
+ // Get foreground colors from the style context.
+
+ var defaultForegroundColor = entry.StyleContext.GetColor(StateFlags.Normal);
+ var selectedForegroundColor = entry.StyleContext.GetColor(StateFlags.Selected);
+
+ DefaultForegroundColor = new ThemeColor((float) defaultForegroundColor.Alpha, (float) defaultForegroundColor.Red, (float) defaultForegroundColor.Green, (float) defaultForegroundColor.Blue);
+ SelectionForegroundColor = new ThemeColor((float)selectedForegroundColor.Alpha, (float)selectedForegroundColor.Red, (float)selectedForegroundColor.Green, (float)selectedForegroundColor.Blue);
+
+ ListBoxRow row = new ListBoxRow();
+ row.SetStateFlags(StateFlags.Selected, true);
+
+ // Request the main thread to render some UI elements to an image to get an approximation for the color.
+ // NOTE (caian): This will only take the color of the top-left corner of the background, which may be incorrect
+ // if someone provides a custom style with a gradient or image.
+
+ using (var surface = new Cairo.ImageSurface(Cairo.Format.Argb32, RenderSurfaceWidth, RenderSurfaceHeight))
+ using (var context = new Cairo.Context(surface))
+ {
+ context.SetSourceRGBA(1, 1, 1, 1);
+ context.Rectangle(0, 0, RenderSurfaceWidth, RenderSurfaceHeight);
+ context.Fill();
+
+ // The background color must be from the main Window because entry uses a different color.
+ parent.StyleContext.RenderBackground(context, 0, 0, RenderSurfaceWidth, RenderSurfaceHeight);
+
+ DefaultBackgroundColor = ToThemeColor(surface.Data);
+
+ context.SetSourceRGBA(1, 1, 1, 1);
+ context.Rectangle(0, 0, RenderSurfaceWidth, RenderSurfaceHeight);
+ context.Fill();
+
+ // Use the background color of the list box row when selected as the text box frame color because they are the
+ // same in the default theme.
+ row.StyleContext.RenderBackground(context, 0, 0, RenderSurfaceWidth, RenderSurfaceHeight);
+
+ DefaultBorderColor = ToThemeColor(surface.Data);
+ }
+
+ // Use the border color as the text selection color.
+ SelectionBackgroundColor = DefaultBorderColor;
+ }
+
+ private ThemeColor ToThemeColor(byte[] data)
+ {
+ Debug.Assert(data.Length == 4 * RenderSurfaceWidth * RenderSurfaceHeight);
+
+ // Take the center-bottom pixel of the surface.
+ int position = 4 * (RenderSurfaceWidth * (RenderSurfaceHeight - 1) + RenderSurfaceWidth / 2);
+
+ if (position + 4 > data.Length)
+ {
+ return new ThemeColor(1, 0, 0, 0);
+ }
+
+ float a = data[position + 3] / 255.0f;
+ float r = data[position + 2] / 255.0f;
+ float g = data[position + 1] / 255.0f;
+ float b = data[position + 0] / 255.0f;
+
+ return new ThemeColor(a, r, g, b);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs b/src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs
new file mode 100644
index 00000000..7c14f0e8
--- /dev/null
+++ b/src/Ryujinx/Ui/Applet/SwkbdAppletDialog.cs
@@ -0,0 +1,89 @@
+using Gtk;
+using System;
+
+namespace Ryujinx.Ui.Applet
+{
+ public class SwkbdAppletDialog : MessageDialog
+ {
+ private int _inputMin;
+ private int _inputMax;
+
+ private Predicate<int> _checkLength;
+
+ private readonly Label _validationInfo;
+
+ public Entry InputEntry { get; }
+ public Button OkButton { get; }
+ public Button CancelButton { get; }
+
+ public SwkbdAppletDialog(Window parent) : base(parent, DialogFlags.Modal | DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.None, null)
+ {
+ SetDefaultSize(300, 0);
+
+ _validationInfo = new Label()
+ {
+ Visible = false
+ };
+
+ InputEntry = new Entry()
+ {
+ Visible = true
+ };
+
+ InputEntry.Activated += OnInputActivated;
+ InputEntry.Changed += OnInputChanged;
+
+ OkButton = (Button)AddButton("OK", ResponseType.Ok);
+ CancelButton = (Button)AddButton("Cancel", ResponseType.Cancel);
+
+ ((Box)MessageArea).PackEnd(_validationInfo, true, true, 0);
+ ((Box)MessageArea).PackEnd(InputEntry, true, true, 4);
+
+ SetInputLengthValidation(0, int.MaxValue); // Disable by default.
+ }
+
+ public void SetInputLengthValidation(int min, int max)
+ {
+ _inputMin = Math.Min(min, max);
+ _inputMax = Math.Max(min, max);
+
+ _validationInfo.Visible = false;
+
+ if (_inputMin <= 0 && _inputMax == int.MaxValue) // Disable.
+ {
+ _validationInfo.Visible = false;
+
+ _checkLength = (length) => true;
+ }
+ else if (_inputMin > 0 && _inputMax == int.MaxValue)
+ {
+ _validationInfo.Visible = true;
+ _validationInfo.Markup = $"<i>Must be at least {_inputMin} characters long</i>";
+
+ _checkLength = (length) => _inputMin <= length;
+ }
+ else
+ {
+ _validationInfo.Visible = true;
+ _validationInfo.Markup = $"<i>Must be {_inputMin}-{_inputMax} characters long</i>";
+
+ _checkLength = (length) => _inputMin <= length && length <= _inputMax;
+ }
+
+ OnInputChanged(this, EventArgs.Empty);
+ }
+
+ private void OnInputActivated(object sender, EventArgs e)
+ {
+ if (OkButton.IsSensitive)
+ {
+ Respond(ResponseType.Ok);
+ }
+ }
+
+ private void OnInputChanged(object sender, EventArgs e)
+ {
+ OkButton.Sensitive = _checkLength(InputEntry.Text.Length);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/GLRenderer.cs b/src/Ryujinx/Ui/GLRenderer.cs
new file mode 100644
index 00000000..e3a9804e
--- /dev/null
+++ b/src/Ryujinx/Ui/GLRenderer.cs
@@ -0,0 +1,135 @@
+using OpenTK.Graphics.OpenGL;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Graphics.OpenGL;
+using Ryujinx.Input.HLE;
+using SPB.Graphics;
+using SPB.Graphics.OpenGL;
+using SPB.Platform;
+using SPB.Platform.GLX;
+using SPB.Platform.WGL;
+using SPB.Windowing;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Ui
+{
+ public partial class GlRenderer : RendererWidgetBase
+ {
+ private GraphicsDebugLevel _glLogLevel;
+
+ private bool _initializedOpenGL;
+
+ private OpenGLContextBase _openGLContext;
+ private SwappableNativeWindowBase _nativeWindow;
+
+ public GlRenderer(InputManager inputManager, GraphicsDebugLevel glLogLevel) : base(inputManager, glLogLevel)
+ {
+ _glLogLevel = glLogLevel;
+ }
+
+ protected override bool OnDrawn(Cairo.Context cr)
+ {
+ if (!_initializedOpenGL)
+ {
+ IntializeOpenGL();
+ }
+
+ return true;
+ }
+
+ private void IntializeOpenGL()
+ {
+ _nativeWindow = RetrieveNativeWindow();
+
+ Window.EnsureNative();
+
+ _openGLContext = PlatformHelper.CreateOpenGLContext(GetGraphicsMode(), 3, 3, _glLogLevel == GraphicsDebugLevel.None ? OpenGLContextFlags.Compat : OpenGLContextFlags.Compat | OpenGLContextFlags.Debug);
+ _openGLContext.Initialize(_nativeWindow);
+ _openGLContext.MakeCurrent(_nativeWindow);
+
+ // Release the GL exclusivity that SPB gave us as we aren't going to use it in GTK Thread.
+ _openGLContext.MakeCurrent(null);
+
+ WaitEvent.Set();
+
+ _initializedOpenGL = true;
+ }
+
+ private SwappableNativeWindowBase RetrieveNativeWindow()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ IntPtr windowHandle = gdk_win32_window_get_handle(Window.Handle);
+
+ return new WGLWindow(new NativeHandle(windowHandle));
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ IntPtr displayHandle = gdk_x11_display_get_xdisplay(Display.Handle);
+ IntPtr windowHandle = gdk_x11_window_get_xid(Window.Handle);
+
+ return new GLXWindow(new NativeHandle(displayHandle), new NativeHandle(windowHandle));
+ }
+
+ throw new NotImplementedException();
+ }
+
+ [LibraryImport("libgdk-3-0.dll")]
+ private static partial IntPtr gdk_win32_window_get_handle(IntPtr d);
+
+ [LibraryImport("libgdk-3.so.0")]
+ private static partial IntPtr gdk_x11_display_get_xdisplay(IntPtr gdkDisplay);
+
+ [LibraryImport("libgdk-3.so.0")]
+ private static partial IntPtr gdk_x11_window_get_xid(IntPtr gdkWindow);
+
+ private static FramebufferFormat GetGraphicsMode()
+ {
+ return Environment.OSVersion.Platform == PlatformID.Unix ? new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false) : FramebufferFormat.Default;
+ }
+
+ public override void InitializeRenderer()
+ {
+ // First take exclusivity on the OpenGL context.
+ ((OpenGLRenderer)Renderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(_openGLContext));
+
+ _openGLContext.MakeCurrent(_nativeWindow);
+
+ GL.ClearColor(0, 0, 0, 1.0f);
+ GL.Clear(ClearBufferMask.ColorBufferBit);
+ SwapBuffers();
+ }
+
+ public override void SwapBuffers()
+ {
+ _nativeWindow.SwapBuffers();
+ }
+
+ protected override string GetGpuBackendName()
+ {
+ return "OpenGL";
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ // Try to bind the OpenGL context before calling the shutdown event
+ try
+ {
+ _openGLContext?.MakeCurrent(_nativeWindow);
+ }
+ catch (Exception) { }
+
+ Device?.DisposeGpu();
+ NpadManager.Dispose();
+
+ // Unbind context and destroy everything
+ try
+ {
+ _openGLContext?.MakeCurrent(null);
+ }
+ catch (Exception) { }
+
+ _openGLContext?.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/Helper/MetalHelper.cs b/src/Ryujinx/Ui/Helper/MetalHelper.cs
new file mode 100644
index 00000000..c2d4893e
--- /dev/null
+++ b/src/Ryujinx/Ui/Helper/MetalHelper.cs
@@ -0,0 +1,134 @@
+using Gdk;
+using System;
+using System.Runtime.InteropServices;
+using System.Runtime.Versioning;
+
+namespace Ryujinx.Ui.Helper
+{
+ public delegate void UpdateBoundsCallbackDelegate(Window window);
+
+ [SupportedOSPlatform("macos")]
+ static partial class MetalHelper
+ {
+ private const string LibObjCImport = "/usr/lib/libobjc.A.dylib";
+
+ private struct Selector
+ {
+ public readonly IntPtr NativePtr;
+
+ public unsafe Selector(string value)
+ {
+ int size = System.Text.Encoding.UTF8.GetMaxByteCount(value.Length);
+ byte* data = stackalloc byte[size];
+
+ fixed (char* pValue = value)
+ {
+ System.Text.Encoding.UTF8.GetBytes(pValue, value.Length, data, size);
+ }
+
+ NativePtr = sel_registerName(data);
+ }
+
+ public static implicit operator Selector(string value) => new Selector(value);
+ }
+
+ private static unsafe IntPtr GetClass(string value)
+ {
+ int size = System.Text.Encoding.UTF8.GetMaxByteCount(value.Length);
+ byte* data = stackalloc byte[size];
+
+ fixed (char* pValue = value)
+ {
+ System.Text.Encoding.UTF8.GetBytes(pValue, value.Length, data, size);
+ }
+
+ return objc_getClass(data);
+ }
+
+ private struct NSPoint
+ {
+ public double X;
+ public double Y;
+
+ public NSPoint(double x, double y)
+ {
+ X = x;
+ Y = y;
+ }
+ }
+
+ private struct NSRect
+ {
+ public NSPoint Pos;
+ public NSPoint Size;
+
+ public NSRect(double x, double y, double width, double height)
+ {
+ Pos = new NSPoint(x, y);
+ Size = new NSPoint(width, height);
+ }
+ }
+
+ public static IntPtr GetMetalLayer(Display display, Window window, out IntPtr nsView, out UpdateBoundsCallbackDelegate updateBounds)
+ {
+ nsView = gdk_quartz_window_get_nsview(window.Handle);
+
+ // Create a new CAMetalLayer.
+ IntPtr layerClass = GetClass("CAMetalLayer");
+ IntPtr metalLayer = IntPtr_objc_msgSend(layerClass, "alloc");
+ objc_msgSend(metalLayer, "init");
+
+ // Create a child NSView to render into.
+ IntPtr nsViewClass = GetClass("NSView");
+ IntPtr child = IntPtr_objc_msgSend(nsViewClass, "alloc");
+ objc_msgSend(child, "init", new NSRect());
+
+ // Add it as a child.
+ objc_msgSend(nsView, "addSubview:", child);
+
+ // Make its renderer our metal layer.
+ objc_msgSend(child, "setWantsLayer:", (byte)1);
+ objc_msgSend(child, "setLayer:", metalLayer);
+ objc_msgSend(metalLayer, "setContentsScale:", (double)display.GetMonitorAtWindow(window).ScaleFactor);
+
+ // Set the frame position/location.
+ updateBounds = (Window window) => {
+ window.GetPosition(out int x, out int y);
+ int width = window.Width;
+ int height = window.Height;
+ objc_msgSend(child, "setFrame:", new NSRect(x, y, width, height));
+ };
+
+ updateBounds(window);
+
+ return metalLayer;
+ }
+
+ [LibraryImport(LibObjCImport)]
+ private static unsafe partial IntPtr sel_registerName(byte* data);
+
+ [LibraryImport(LibObjCImport)]
+ private static unsafe partial IntPtr objc_getClass(byte* data);
+
+ [LibraryImport(LibObjCImport)]
+ private static partial void objc_msgSend(IntPtr receiver, Selector selector);
+
+ [LibraryImport(LibObjCImport)]
+ private static partial void objc_msgSend(IntPtr receiver, Selector selector, byte value);
+
+ [LibraryImport(LibObjCImport)]
+ private static partial void objc_msgSend(IntPtr receiver, Selector selector, IntPtr value);
+
+ [LibraryImport(LibObjCImport)]
+ private static partial void objc_msgSend(IntPtr receiver, Selector selector, NSRect point);
+
+ [LibraryImport(LibObjCImport)]
+ private static partial void objc_msgSend(IntPtr receiver, Selector selector, double value);
+
+ [LibraryImport(LibObjCImport, EntryPoint = "objc_msgSend")]
+ private static partial IntPtr IntPtr_objc_msgSend(IntPtr receiver, Selector selector);
+
+ [LibraryImport("libgdk-3.0.dylib")]
+ private static partial IntPtr gdk_quartz_window_get_nsview(IntPtr gdkWindow);
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Helper/SortHelper.cs b/src/Ryujinx/Ui/Helper/SortHelper.cs
new file mode 100644
index 00000000..0c0eefd2
--- /dev/null
+++ b/src/Ryujinx/Ui/Helper/SortHelper.cs
@@ -0,0 +1,94 @@
+using Gtk;
+using System;
+
+namespace Ryujinx.Ui.Helper
+{
+ static class SortHelper
+ {
+ public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b)
+ {
+ static string ReverseFormat(string time)
+ {
+ if (time == "Never")
+ {
+ return "00";
+ }
+
+ var numbers = time.Split(new char[] { 'd', 'h', 'm' });
+
+ time = time.Replace(" ", "").Replace("d", ".").Replace("h", ":").Replace("m", "");
+
+ if (numbers.Length == 2)
+ {
+ return $"00.00:{time}";
+ }
+ else if (numbers.Length == 3)
+ {
+ return $"00.{time}";
+ }
+
+ return time;
+ }
+
+ string aValue = ReverseFormat(model.GetValue(a, 5).ToString());
+ string bValue = ReverseFormat(model.GetValue(b, 5).ToString());
+
+ return TimeSpan.Compare(TimeSpan.Parse(aValue), TimeSpan.Parse(bValue));
+ }
+
+ public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b)
+ {
+ string aValue = model.GetValue(a, 6).ToString();
+ string bValue = model.GetValue(b, 6).ToString();
+
+ if (aValue == "Never")
+ {
+ aValue = DateTime.UnixEpoch.ToString();
+ }
+
+ if (bValue == "Never")
+ {
+ bValue = DateTime.UnixEpoch.ToString();
+ }
+
+ return DateTime.Compare(DateTime.Parse(bValue), DateTime.Parse(aValue));
+ }
+
+ public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b)
+ {
+ string aValue = model.GetValue(a, 8).ToString();
+ string bValue = model.GetValue(b, 8).ToString();
+
+ if (aValue[^3..] == "GiB")
+ {
+ aValue = (float.Parse(aValue[0..^3]) * 1024).ToString();
+ }
+ else
+ {
+ aValue = aValue[0..^3];
+ }
+
+ if (bValue[^3..] == "GiB")
+ {
+ bValue = (float.Parse(bValue[0..^3]) * 1024).ToString();
+ }
+ else
+ {
+ bValue = bValue[0..^3];
+ }
+
+ if (float.Parse(aValue) > float.Parse(bValue))
+ {
+ return -1;
+ }
+ else if (float.Parse(bValue) > float.Parse(aValue))
+ {
+ return 1;
+ }
+ else
+ {
+ return 0;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/Helper/ThemeHelper.cs b/src/Ryujinx/Ui/Helper/ThemeHelper.cs
new file mode 100644
index 00000000..448afcc9
--- /dev/null
+++ b/src/Ryujinx/Ui/Helper/ThemeHelper.cs
@@ -0,0 +1,35 @@
+using Gtk;
+using Ryujinx.Common.Logging;
+using Ryujinx.Ui.Common.Configuration;
+using System.IO;
+
+namespace Ryujinx.Ui.Helper
+{
+ static class ThemeHelper
+ {
+ public static void ApplyTheme()
+ {
+ if (!ConfigurationState.Instance.Ui.EnableCustomTheme)
+ {
+ return;
+ }
+
+ if (File.Exists(ConfigurationState.Instance.Ui.CustomThemePath) && (Path.GetExtension(ConfigurationState.Instance.Ui.CustomThemePath) == ".css"))
+ {
+ CssProvider cssProvider = new CssProvider();
+
+ cssProvider.LoadFromPath(ConfigurationState.Instance.Ui.CustomThemePath);
+
+ StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800);
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The \"custom_theme_path\" section in \"Config.json\" contains an invalid path: \"{ConfigurationState.Instance.Ui.CustomThemePath}\".");
+
+ ConfigurationState.Instance.Ui.CustomThemePath.Value = "";
+ ConfigurationState.Instance.Ui.EnableCustomTheme.Value = false;
+ ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/MainWindow.cs b/src/Ryujinx/Ui/MainWindow.cs
new file mode 100644
index 00000000..bf96e18a
--- /dev/null
+++ b/src/Ryujinx/Ui/MainWindow.cs
@@ -0,0 +1,1834 @@
+using ARMeilleure.Translation;
+using Gtk;
+using LibHac.Common;
+using LibHac.Common.Keys;
+using LibHac.Ncm;
+using LibHac.Ns;
+using LibHac.Tools.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Audio.Backends.Dummy;
+using Ryujinx.Audio.Backends.OpenAL;
+using Ryujinx.Audio.Backends.SDL2;
+using Ryujinx.Audio.Backends.SoundIo;
+using Ryujinx.Audio.Integration;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.SystemInterop;
+using Ryujinx.Cpu;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.GAL.Multithreading;
+using Ryujinx.Graphics.OpenGL;
+using Ryujinx.Graphics.Vulkan;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.HLE.HOS.SystemState;
+using Ryujinx.Input.GTK3;
+using Ryujinx.Input.HLE;
+using Ryujinx.Input.SDL2;
+using Ryujinx.Modules;
+using Ryujinx.Ui.App.Common;
+using Ryujinx.Ui.Applet;
+using Ryujinx.Ui.Common;
+using Ryujinx.Ui.Common.Configuration;
+using Ryujinx.Ui.Common.Helper;
+using Ryujinx.Ui.Helper;
+using Ryujinx.Ui.Widgets;
+using Ryujinx.Ui.Windows;
+using Silk.NET.Vulkan;
+using SPB.Graphics.Vulkan;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using GUI = Gtk.Builder.ObjectAttribute;
+using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState;
+
+namespace Ryujinx.Ui
+{
+ public class MainWindow : Window
+ {
+ private readonly VirtualFileSystem _virtualFileSystem;
+ private readonly ContentManager _contentManager;
+ private readonly AccountManager _accountManager;
+ private readonly LibHacHorizonManager _libHacHorizonManager;
+
+ private UserChannelPersistence _userChannelPersistence;
+
+ private HLE.Switch _emulationContext;
+
+ private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
+
+ private readonly ApplicationLibrary _applicationLibrary;
+ private readonly GtkHostUiHandler _uiHandler;
+ private readonly AutoResetEvent _deviceExitStatus;
+ private readonly ListStore _tableStore;
+
+ private bool _updatingGameTable;
+ private bool _gameLoaded;
+ private bool _ending;
+
+ private string _currentEmulatedGamePath = null;
+
+ private string _lastScannedAmiiboId = "";
+ private bool _lastScannedAmiiboShowAll = false;
+
+ public RendererWidgetBase RendererWidget;
+ public InputManager InputManager;
+
+ public bool IsFocused;
+
+#pragma warning disable CS0169, CS0649, IDE0044
+
+ [GUI] public MenuItem ExitMenuItem;
+ [GUI] public MenuItem UpdateMenuItem;
+ [GUI] MenuBar _menuBar;
+ [GUI] Box _footerBox;
+ [GUI] Box _statusBar;
+ [GUI] MenuItem _optionMenu;
+ [GUI] MenuItem _manageUserProfiles;
+ [GUI] MenuItem _fileMenu;
+ [GUI] MenuItem _loadApplicationFile;
+ [GUI] MenuItem _loadApplicationFolder;
+ [GUI] MenuItem _appletMenu;
+ [GUI] MenuItem _actionMenu;
+ [GUI] MenuItem _pauseEmulation;
+ [GUI] MenuItem _resumeEmulation;
+ [GUI] MenuItem _stopEmulation;
+ [GUI] MenuItem _simulateWakeUpMessage;
+ [GUI] MenuItem _scanAmiibo;
+ [GUI] MenuItem _takeScreenshot;
+ [GUI] MenuItem _hideUi;
+ [GUI] MenuItem _fullScreen;
+ [GUI] CheckMenuItem _startFullScreen;
+ [GUI] CheckMenuItem _showConsole;
+ [GUI] CheckMenuItem _favToggle;
+ [GUI] MenuItem _firmwareInstallDirectory;
+ [GUI] MenuItem _firmwareInstallFile;
+ [GUI] MenuItem _fileTypesSubMenu;
+ [GUI] Label _fifoStatus;
+ [GUI] CheckMenuItem _iconToggle;
+ [GUI] CheckMenuItem _developerToggle;
+ [GUI] CheckMenuItem _appToggle;
+ [GUI] CheckMenuItem _timePlayedToggle;
+ [GUI] CheckMenuItem _versionToggle;
+ [GUI] CheckMenuItem _lastPlayedToggle;
+ [GUI] CheckMenuItem _fileExtToggle;
+ [GUI] CheckMenuItem _pathToggle;
+ [GUI] CheckMenuItem _fileSizeToggle;
+ [GUI] CheckMenuItem _nspShown;
+ [GUI] CheckMenuItem _pfs0Shown;
+ [GUI] CheckMenuItem _xciShown;
+ [GUI] CheckMenuItem _ncaShown;
+ [GUI] CheckMenuItem _nroShown;
+ [GUI] CheckMenuItem _nsoShown;
+ [GUI] Label _gpuBackend;
+ [GUI] Label _dockedMode;
+ [GUI] Label _aspectRatio;
+ [GUI] Label _gameStatus;
+ [GUI] TreeView _gameTable;
+ [GUI] TreeSelection _gameTableSelection;
+ [GUI] ScrolledWindow _gameTableWindow;
+ [GUI] Label _gpuName;
+ [GUI] Label _progressLabel;
+ [GUI] Label _firmwareVersionLabel;
+ [GUI] Gtk.ProgressBar _progressBar;
+ [GUI] Box _viewBox;
+ [GUI] Label _vSyncStatus;
+ [GUI] Label _volumeStatus;
+ [GUI] Box _listStatusBox;
+ [GUI] Label _loadingStatusLabel;
+ [GUI] Gtk.ProgressBar _loadingStatusBar;
+
+#pragma warning restore CS0649, IDE0044, CS0169
+
+ public MainWindow() : this(new Builder("Ryujinx.Ui.MainWindow.glade")) { }
+
+ private MainWindow(Builder builder) : base(builder.GetRawOwnedObject("_mainWin"))
+ {
+ builder.Autoconnect(this);
+
+ // Apply custom theme if needed.
+ ThemeHelper.ApplyTheme();
+ Gdk.Monitor monitor = Display.GetMonitor(0);
+ // Sets overridden fields.
+ int monitorWidth = monitor.Geometry.Width * monitor.ScaleFactor;
+ int monitorHeight = monitor.Geometry.Height * monitor.ScaleFactor;
+
+ DefaultWidth = monitorWidth < 1280 ? monitorWidth : 1280;
+ DefaultHeight = monitorHeight < 760 ? monitorHeight : 760;
+
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+ Title = $"Ryujinx {Program.Version}";
+
+ // Hide emulation context status bar.
+ _statusBar.Hide();
+
+ // Instantiate HLE objects.
+ _virtualFileSystem = VirtualFileSystem.CreateInstance();
+ _libHacHorizonManager = new LibHacHorizonManager();
+
+ _libHacHorizonManager.InitializeFsServer(_virtualFileSystem);
+ _libHacHorizonManager.InitializeArpServer();
+ _libHacHorizonManager.InitializeBcatServer();
+ _libHacHorizonManager.InitializeSystemClients();
+
+ // Save data created before we supported extra data in directory save data will not work properly if
+ // given empty extra data. Luckily some of that extra data can be created using the data from the
+ // save data indexer, which should be enough to check access permissions for user saves.
+ // Every single save data's extra data will be checked and fixed if needed each time the emulator is opened.
+ // Consider removing this at some point in the future when we don't need to worry about old saves.
+ VirtualFileSystem.FixExtraData(_libHacHorizonManager.RyujinxClient);
+
+ _contentManager = new ContentManager(_virtualFileSystem);
+ _accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile);
+ _userChannelPersistence = new UserChannelPersistence();
+
+ // Instantiate GUI objects.
+ _applicationLibrary = new ApplicationLibrary(_virtualFileSystem);
+ _uiHandler = new GtkHostUiHandler(this);
+ _deviceExitStatus = new AutoResetEvent(false);
+
+ WindowStateEvent += WindowStateEvent_Changed;
+ DeleteEvent += Window_Close;
+ FocusInEvent += MainWindow_FocusInEvent;
+ FocusOutEvent += MainWindow_FocusOutEvent;
+
+ _applicationLibrary.ApplicationAdded += Application_Added;
+ _applicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated;
+
+ _fileMenu.StateChanged += FileMenu_StateChanged;
+ _actionMenu.StateChanged += ActionMenu_StateChanged;
+ _optionMenu.StateChanged += OptionMenu_StateChanged;
+
+ _gameTable.ButtonReleaseEvent += Row_Clicked;
+ _fullScreen.Activated += FullScreen_Toggled;
+
+ RendererWidgetBase.StatusUpdatedEvent += Update_StatusBar;
+
+ ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
+ ConfigurationState.Instance.Graphics.AspectRatio.Event += UpdateAspectRatioState;
+ ConfigurationState.Instance.System.EnableDockedMode.Event += UpdateDockedModeState;
+ ConfigurationState.Instance.System.AudioVolume.Event += UpdateAudioVolumeState;
+
+ if (ConfigurationState.Instance.Ui.StartFullscreen)
+ {
+ _startFullScreen.Active = true;
+ }
+
+ _showConsole.Active = ConfigurationState.Instance.Ui.ShowConsole.Value;
+ _showConsole.Visible = ConsoleHelper.SetConsoleWindowStateSupported;
+
+ _actionMenu.Sensitive = false;
+ _pauseEmulation.Sensitive = false;
+ _resumeEmulation.Sensitive = false;
+
+ _nspShown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.NSP.Value;
+ _pfs0Shown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.PFS0.Value;
+ _xciShown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.XCI.Value;
+ _ncaShown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.NCA.Value;
+ _nroShown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.NRO.Value;
+ _nsoShown.Active = ConfigurationState.Instance.Ui.ShownFileTypes.NSO.Value;
+
+ _nspShown.Toggled += NSP_Shown_Toggled;
+ _pfs0Shown.Toggled += PFS0_Shown_Toggled;
+ _xciShown.Toggled += XCI_Shown_Toggled;
+ _ncaShown.Toggled += NCA_Shown_Toggled;
+ _nroShown.Toggled += NRO_Shown_Toggled;
+ _nsoShown.Toggled += NSO_Shown_Toggled;
+
+ _fileTypesSubMenu.Visible = FileAssociationHelper.IsTypeAssociationSupported;
+
+ if (ConfigurationState.Instance.Ui.GuiColumns.FavColumn) _favToggle.Active = true;
+ if (ConfigurationState.Instance.Ui.GuiColumns.IconColumn) _iconToggle.Active = true;
+ if (ConfigurationState.Instance.Ui.GuiColumns.AppColumn) _appToggle.Active = true;
+ if (ConfigurationState.Instance.Ui.GuiColumns.DevColumn) _developerToggle.Active = true;
+ if (ConfigurationState.Instance.Ui.GuiColumns.VersionColumn) _versionToggle.Active = true;
+ if (ConfigurationState.Instance.Ui.GuiColumns.TimePlayedColumn) _timePlayedToggle.Active = true;
+ if (ConfigurationState.Instance.Ui.GuiColumns.LastPlayedColumn) _lastPlayedToggle.Active = true;
+ if (ConfigurationState.Instance.Ui.GuiColumns.FileExtColumn) _fileExtToggle.Active = true;
+ if (ConfigurationState.Instance.Ui.GuiColumns.FileSizeColumn) _fileSizeToggle.Active = true;
+ if (ConfigurationState.Instance.Ui.GuiColumns.PathColumn) _pathToggle.Active = true;
+
+ _favToggle.Toggled += Fav_Toggled;
+ _iconToggle.Toggled += Icon_Toggled;
+ _appToggle.Toggled += App_Toggled;
+ _developerToggle.Toggled += Developer_Toggled;
+ _versionToggle.Toggled += Version_Toggled;
+ _timePlayedToggle.Toggled += TimePlayed_Toggled;
+ _lastPlayedToggle.Toggled += LastPlayed_Toggled;
+ _fileExtToggle.Toggled += FileExt_Toggled;
+ _fileSizeToggle.Toggled += FileSize_Toggled;
+ _pathToggle.Toggled += Path_Toggled;
+
+ _gameTable.Model = _tableStore = new ListStore(
+ typeof(bool),
+ typeof(Gdk.Pixbuf),
+ typeof(string),
+ typeof(string),
+ typeof(string),
+ typeof(string),
+ typeof(string),
+ typeof(string),
+ typeof(string),
+ typeof(string),
+ typeof(BlitStruct<ApplicationControlProperty>));
+
+ _tableStore.SetSortFunc(5, SortHelper.TimePlayedSort);
+ _tableStore.SetSortFunc(6, SortHelper.LastPlayedSort);
+ _tableStore.SetSortFunc(8, SortHelper.FileSizeSort);
+
+ int columnId = ConfigurationState.Instance.Ui.ColumnSort.SortColumnId;
+ bool ascending = ConfigurationState.Instance.Ui.ColumnSort.SortAscending;
+
+ _tableStore.SetSortColumnId(columnId, ascending ? SortType.Ascending : SortType.Descending);
+
+ _gameTable.EnableSearch = true;
+ _gameTable.SearchColumn = 2;
+ _gameTable.SearchEqualFunc = (model, col, key, iter) => !((string)model.GetValue(iter, col)).Contains(key, StringComparison.InvariantCultureIgnoreCase);
+
+ _hideUi.Label = _hideUi.Label.Replace("SHOWUIKEY", ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUi.ToString());
+
+ UpdateColumns();
+ UpdateGameTable();
+
+ ConfigurationState.Instance.Ui.GameDirs.Event += (sender, args) =>
+ {
+ if (args.OldValue != args.NewValue)
+ {
+ UpdateGameTable();
+ }
+ };
+
+ Task.Run(RefreshFirmwareLabel);
+
+ InputManager = new InputManager(new GTK3KeyboardDriver(this), new SDL2GamepadDriver());
+ }
+
+ private void UpdateIgnoreMissingServicesState(object sender, ReactiveEventArgs<bool> args)
+ {
+ if (_emulationContext != null)
+ {
+ _emulationContext.Configuration.IgnoreMissingServices = args.NewValue;
+ }
+ }
+
+ private void UpdateAspectRatioState(object sender, ReactiveEventArgs<AspectRatio> args)
+ {
+ if (_emulationContext != null)
+ {
+ _emulationContext.Configuration.AspectRatio = args.NewValue;
+ }
+ }
+
+ private void UpdateDockedModeState(object sender, ReactiveEventArgs<bool> e)
+ {
+ if (_emulationContext != null)
+ {
+ _emulationContext.System.ChangeDockedModeState(e.NewValue);
+ }
+ }
+
+ private void UpdateAudioVolumeState(object sender, ReactiveEventArgs<float> e)
+ {
+ _emulationContext?.SetVolume(e.NewValue);
+ }
+
+ private void WindowStateEvent_Changed(object o, WindowStateEventArgs args)
+ {
+ _fullScreen.Label = args.Event.NewWindowState.HasFlag(Gdk.WindowState.Fullscreen) ? "Exit Fullscreen" : "Enter Fullscreen";
+ }
+
+ private void MainWindow_FocusOutEvent(object o, FocusOutEventArgs args)
+ {
+ IsFocused = false;
+ }
+
+ private void MainWindow_FocusInEvent(object o, FocusInEventArgs args)
+ {
+ IsFocused = true;
+ }
+
+ private void UpdateColumns()
+ {
+ foreach (TreeViewColumn column in _gameTable.Columns)
+ {
+ _gameTable.RemoveColumn(column);
+ }
+
+ CellRendererToggle favToggle = new CellRendererToggle();
+ favToggle.Toggled += FavToggle_Toggled;
+
+ if (ConfigurationState.Instance.Ui.GuiColumns.FavColumn) _gameTable.AppendColumn("Fav", favToggle, "active", 0);
+ if (ConfigurationState.Instance.Ui.GuiColumns.IconColumn) _gameTable.AppendColumn("Icon", new CellRendererPixbuf(), "pixbuf", 1);
+ if (ConfigurationState.Instance.Ui.GuiColumns.AppColumn) _gameTable.AppendColumn("Application", new CellRendererText(), "text", 2);
+ if (ConfigurationState.Instance.Ui.GuiColumns.DevColumn) _gameTable.AppendColumn("Developer", new CellRendererText(), "text", 3);
+ if (ConfigurationState.Instance.Ui.GuiColumns.VersionColumn) _gameTable.AppendColumn("Version", new CellRendererText(), "text", 4);
+ if (ConfigurationState.Instance.Ui.GuiColumns.TimePlayedColumn) _gameTable.AppendColumn("Time Played", new CellRendererText(), "text", 5);
+ if (ConfigurationState.Instance.Ui.GuiColumns.LastPlayedColumn) _gameTable.AppendColumn("Last Played", new CellRendererText(), "text", 6);
+ if (ConfigurationState.Instance.Ui.GuiColumns.FileExtColumn) _gameTable.AppendColumn("File Ext", new CellRendererText(), "text", 7);
+ if (ConfigurationState.Instance.Ui.GuiColumns.FileSizeColumn) _gameTable.AppendColumn("File Size", new CellRendererText(), "text", 8);
+ if (ConfigurationState.Instance.Ui.GuiColumns.PathColumn) _gameTable.AppendColumn("Path", new CellRendererText(), "text", 9);
+
+ foreach (TreeViewColumn column in _gameTable.Columns)
+ {
+ switch (column.Title)
+ {
+ case "Fav":
+ column.SortColumnId = 0;
+ column.Clicked += Column_Clicked;
+ break;
+ case "Application":
+ column.SortColumnId = 2;
+ column.Clicked += Column_Clicked;
+ break;
+ case "Developer":
+ column.SortColumnId = 3;
+ column.Clicked += Column_Clicked;
+ break;
+ case "Version":
+ column.SortColumnId = 4;
+ column.Clicked += Column_Clicked;
+ break;
+ case "Time Played":
+ column.SortColumnId = 5;
+ column.Clicked += Column_Clicked;
+ break;
+ case "Last Played":
+ column.SortColumnId = 6;
+ column.Clicked += Column_Clicked;
+ break;
+ case "File Ext":
+ column.SortColumnId = 7;
+ column.Clicked += Column_Clicked;
+ break;
+ case "File Size":
+ column.SortColumnId = 8;
+ column.Clicked += Column_Clicked;
+ break;
+ case "Path":
+ column.SortColumnId = 9;
+ column.Clicked += Column_Clicked;
+ break;
+ }
+ }
+ }
+
+ protected override void OnDestroyed()
+ {
+ InputManager.Dispose();
+ }
+
+ private void InitializeSwitchInstance()
+ {
+ _virtualFileSystem.ReloadKeySet();
+
+ IRenderer renderer;
+
+ if (ConfigurationState.Instance.Graphics.GraphicsBackend == GraphicsBackend.Vulkan)
+ {
+ string preferredGpu = ConfigurationState.Instance.Graphics.PreferredGpu.Value;
+ renderer = new VulkanRenderer(CreateVulkanSurface, VulkanHelper.GetRequiredInstanceExtensions, preferredGpu);
+ }
+ else
+ {
+ renderer = new OpenGLRenderer();
+ }
+
+ BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading;
+
+ bool threadedGAL = threadingMode == BackendThreading.On || (threadingMode == BackendThreading.Auto && renderer.PreferThreading);
+
+ if (threadedGAL)
+ {
+ renderer = new ThreadedRenderer(renderer);
+ }
+
+ Logger.Info?.PrintMsg(LogClass.Gpu, $"Backend Threading ({threadingMode}): {threadedGAL}");
+
+ IHardwareDeviceDriver deviceDriver = new DummyHardwareDeviceDriver();
+
+ if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SDL2)
+ {
+ if (SDL2HardwareDeviceDriver.IsSupported)
+ {
+ deviceDriver = new SDL2HardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SDL2 is not supported, trying to fall back to OpenAL.");
+
+ if (OpenALHardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl;
+ SaveConfig();
+
+ deviceDriver = new OpenALHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, trying to fall back to SoundIO.");
+
+ if (SoundIoHardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo;
+ SaveConfig();
+
+ deviceDriver = new SoundIoHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, falling back to dummy audio out.");
+ }
+ }
+ }
+ }
+ else if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.SoundIo)
+ {
+ if (SoundIoHardwareDeviceDriver.IsSupported)
+ {
+ deviceDriver = new SoundIoHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, trying to fall back to SDL2.");
+
+ if (SDL2HardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2;
+ SaveConfig();
+
+ deviceDriver = new SDL2HardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SDL2 is not supported, trying to fall back to OpenAL.");
+
+ if (OpenALHardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found OpenAL, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.OpenAl;
+ SaveConfig();
+
+ deviceDriver = new OpenALHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, falling back to dummy audio out.");
+ }
+ }
+ }
+ }
+ else if (ConfigurationState.Instance.System.AudioBackend.Value == AudioBackend.OpenAl)
+ {
+ if (OpenALHardwareDeviceDriver.IsSupported)
+ {
+ deviceDriver = new OpenALHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "OpenAL is not supported, trying to fall back to SDL2.");
+
+ if (SDL2HardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found SDL2, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SDL2;
+ SaveConfig();
+
+ deviceDriver = new SDL2HardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SDL2 is not supported, trying to fall back to SoundIO.");
+
+ if (SoundIoHardwareDeviceDriver.IsSupported)
+ {
+ Logger.Warning?.Print(LogClass.Audio, "Found SoundIO, changing configuration.");
+
+ ConfigurationState.Instance.System.AudioBackend.Value = AudioBackend.SoundIo;
+ SaveConfig();
+
+ deviceDriver = new SoundIoHardwareDeviceDriver();
+ }
+ else
+ {
+ Logger.Warning?.Print(LogClass.Audio, "SoundIO is not supported, falling back to dummy audio out.");
+ }
+ }
+ }
+ }
+
+ var memoryConfiguration = ConfigurationState.Instance.System.ExpandRam.Value
+ ? HLE.MemoryConfiguration.MemoryConfiguration6GiB
+ : HLE.MemoryConfiguration.MemoryConfiguration4GiB;
+
+ IntegrityCheckLevel fsIntegrityCheckLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks ? IntegrityCheckLevel.ErrorOnInvalid : IntegrityCheckLevel.None;
+
+ HLE.HLEConfiguration configuration = new HLE.HLEConfiguration(_virtualFileSystem,
+ _libHacHorizonManager,
+ _contentManager,
+ _accountManager,
+ _userChannelPersistence,
+ renderer,
+ deviceDriver,
+ memoryConfiguration,
+ _uiHandler,
+ (SystemLanguage)ConfigurationState.Instance.System.Language.Value,
+ (RegionCode)ConfigurationState.Instance.System.Region.Value,
+ ConfigurationState.Instance.Graphics.EnableVsync,
+ ConfigurationState.Instance.System.EnableDockedMode,
+ ConfigurationState.Instance.System.EnablePtc,
+ ConfigurationState.Instance.System.EnableInternetAccess,
+ fsIntegrityCheckLevel,
+ ConfigurationState.Instance.System.FsGlobalAccessLogMode,
+ ConfigurationState.Instance.System.SystemTimeOffset,
+ ConfigurationState.Instance.System.TimeZone,
+ ConfigurationState.Instance.System.MemoryManagerMode,
+ ConfigurationState.Instance.System.IgnoreMissingServices,
+ ConfigurationState.Instance.Graphics.AspectRatio,
+ ConfigurationState.Instance.System.AudioVolume,
+ ConfigurationState.Instance.System.UseHypervisor,
+ ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value);
+
+ _emulationContext = new HLE.Switch(configuration);
+ }
+
+ private SurfaceKHR CreateVulkanSurface(Instance instance, Vk vk)
+ {
+ return new SurfaceKHR((ulong)((VKRenderer)RendererWidget).CreateWindowSurface(instance.Handle));
+ }
+
+ private void SetupProgressUiHandlers()
+ {
+ if (_emulationContext.Processes.ActiveApplication.DiskCacheLoadState != null)
+ {
+ _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged -= ProgressHandler;
+ _emulationContext.Processes.ActiveApplication.DiskCacheLoadState.StateChanged += ProgressHandler;
+ }
+
+ _emulationContext.Gpu.ShaderCacheStateChanged -= ProgressHandler;
+ _emulationContext.Gpu.ShaderCacheStateChanged += ProgressHandler;
+ }
+
+ private void ProgressHandler<T>(T state, int current, int total) where T : Enum
+ {
+ bool visible;
+ string label;
+
+ switch (state)
+ {
+ case LoadState ptcState:
+ visible = ptcState != LoadState.Loaded;
+ label = $"PTC : {current}/{total}";
+ break;
+ case ShaderCacheLoadingState shaderCacheState:
+ visible = shaderCacheState != ShaderCacheLoadingState.Loaded;
+ label = $"Shaders : {current}/{total}";
+ break;
+ default:
+ throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}");
+ }
+
+ Application.Invoke(delegate
+ {
+ _loadingStatusLabel.Text = label;
+ _loadingStatusBar.Fraction = total > 0 ? (double)current / total : 0;
+ _loadingStatusBar.Visible = visible;
+ _loadingStatusLabel.Visible = visible;
+ });
+ }
+
+ public void UpdateGameTable()
+ {
+ if (_updatingGameTable || _gameLoaded)
+ {
+ return;
+ }
+
+ _updatingGameTable = true;
+
+ _tableStore.Clear();
+
+ Thread applicationLibraryThread = new Thread(() =>
+ {
+ _applicationLibrary.LoadApplications(ConfigurationState.Instance.Ui.GameDirs, ConfigurationState.Instance.System.Language);
+
+ _updatingGameTable = false;
+ });
+ applicationLibraryThread.Name = "GUI.ApplicationLibraryThread";
+ applicationLibraryThread.IsBackground = true;
+ applicationLibraryThread.Start();
+ }
+
+ [Conditional("RELEASE")]
+ public void PerformanceCheck()
+ {
+ if (ConfigurationState.Instance.Logger.EnableTrace.Value)
+ {
+ MessageDialog debugWarningDialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null)
+ {
+ Title = "Ryujinx - Warning",
+ Text = "You have trace logging enabled, which is designed to be used by developers only.",
+ SecondaryText = "For optimal performance, it's recommended to disable trace logging. Would you like to disable trace logging now?"
+ };
+
+ if (debugWarningDialog.Run() == (int)ResponseType.Yes)
+ {
+ ConfigurationState.Instance.Logger.EnableTrace.Value = false;
+ SaveConfig();
+ }
+
+ debugWarningDialog.Dispose();
+ }
+
+ if (!string.IsNullOrWhiteSpace(ConfigurationState.Instance.Graphics.ShadersDumpPath.Value))
+ {
+ MessageDialog shadersDumpWarningDialog = new MessageDialog(this, DialogFlags.Modal, MessageType.Warning, ButtonsType.YesNo, null)
+ {
+ Title = "Ryujinx - Warning",
+ Text = "You have shader dumping enabled, which is designed to be used by developers only.",
+ SecondaryText = "For optimal performance, it's recommended to disable shader dumping. Would you like to disable shader dumping now?"
+ };
+
+ if (shadersDumpWarningDialog.Run() == (int)ResponseType.Yes)
+ {
+ ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = "";
+ SaveConfig();
+ }
+
+ shadersDumpWarningDialog.Dispose();
+ }
+ }
+
+ private bool LoadApplication(string path, bool isFirmwareTitle)
+ {
+ SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
+
+ if (!SetupValidator.CanStartApplication(_contentManager, path, out UserError userError))
+ {
+ if (SetupValidator.CanFixStartApplication(_contentManager, path, userError, out firmwareVersion))
+ {
+ string message = $"Would you like to install the firmware embedded in this game? (Firmware {firmwareVersion.VersionString})";
+
+ ResponseType responseDialog = (ResponseType)GtkDialog.CreateConfirmationDialog("No Firmware Installed", message).Run();
+
+ if (responseDialog != ResponseType.Yes || !SetupValidator.TryFixStartApplication(_contentManager, path, userError, out _))
+ {
+ UserErrorDialog.CreateUserErrorDialog(userError);
+
+ return false;
+ }
+
+ // Tell the user that we installed a firmware for them.
+
+ firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
+
+ RefreshFirmwareLabel();
+
+ message = $"No installed firmware was found but Ryujinx was able to install firmware {firmwareVersion.VersionString} from the provided game.\nThe emulator will now start.";
+
+ GtkDialog.CreateInfoDialog($"Firmware {firmwareVersion.VersionString} was installed", message);
+ }
+ else
+ {
+ UserErrorDialog.CreateUserErrorDialog(userError);
+
+ return false;
+ }
+ }
+
+ Logger.Notice.Print(LogClass.Application, $"Using Firmware Version: {firmwareVersion?.VersionString}");
+
+ if (isFirmwareTitle)
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA).");
+
+ return _emulationContext.LoadNca(path);
+ }
+
+ if (Directory.Exists(path))
+ {
+ string[] romFsFiles = Directory.GetFiles(path, "*.istorage");
+
+ if (romFsFiles.Length == 0)
+ {
+ romFsFiles = Directory.GetFiles(path, "*.romfs");
+ }
+
+ if (romFsFiles.Length > 0)
+ {
+ Logger.Info?.Print(LogClass.Application, "Loading as cart with RomFS.");
+
+ return _emulationContext.LoadCart(path, romFsFiles[0]);
+ }
+
+ Logger.Info?.Print(LogClass.Application, "Loading as cart WITHOUT RomFS.");
+
+ return _emulationContext.LoadCart(path);
+ }
+
+ if (File.Exists(path))
+ {
+ switch (System.IO.Path.GetExtension(path).ToLowerInvariant())
+ {
+ case ".xci":
+ Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
+
+ return _emulationContext.LoadXci(path);
+ case ".nca":
+ Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
+
+ return _emulationContext.LoadNca(path);
+ case ".nsp":
+ case ".pfs0":
+ Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
+
+ return _emulationContext.LoadNsp(path);
+ default:
+ Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
+ try
+ {
+ return _emulationContext.LoadProgram(path);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ Logger.Error?.Print(LogClass.Application, "The specified file is not supported by Ryujinx.");
+
+ return false;
+ }
+ }
+ }
+
+ Logger.Warning?.Print(LogClass.Application, "Please specify a valid XCI/NCA/NSP/PFS0/NRO file.");
+
+ return false;
+ }
+
+ public void RunApplication(string path, bool startFullscreen = false)
+ {
+ if (_gameLoaded)
+ {
+ GtkDialog.CreateInfoDialog("A game has already been loaded", "Please stop emulation or close the emulator before launching another game.");
+ }
+ else
+ {
+ PerformanceCheck();
+
+ Logger.RestartTime();
+
+ RendererWidget = CreateRendererWidget();
+
+ SwitchToRenderWidget(startFullscreen);
+
+ InitializeSwitchInstance();
+
+ UpdateGraphicsConfig();
+
+ bool isFirmwareTitle = false;
+
+ if (path.StartsWith("@SystemContent"))
+ {
+ path = _virtualFileSystem.SwitchPathToSystemPath(path);
+
+ isFirmwareTitle = true;
+ }
+
+ if (!LoadApplication(path, isFirmwareTitle))
+ {
+ _emulationContext.Dispose();
+ SwitchToGameTable();
+
+ return;
+ }
+
+ SetupProgressUiHandlers();
+
+ _currentEmulatedGamePath = path;
+
+ _deviceExitStatus.Reset();
+
+ Translator.IsReadyForTranslation.Reset();
+
+ Thread windowThread = new(CreateGameWindow)
+ {
+ Name = "GUI.WindowThread"
+ };
+
+ windowThread.Start();
+
+ _gameLoaded = true;
+ _actionMenu.Sensitive = true;
+ UpdateMenuItem.Sensitive = false;
+
+ _lastScannedAmiiboId = "";
+
+ _firmwareInstallFile.Sensitive = false;
+ _firmwareInstallDirectory.Sensitive = false;
+
+ DiscordIntegrationModule.SwitchToPlayingState(_emulationContext.Processes.ActiveApplication.ProgramIdText,
+ _emulationContext.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString());
+
+ _applicationLibrary.LoadAndSaveMetaData(_emulationContext.Processes.ActiveApplication.ProgramIdText, appMetadata =>
+ {
+ appMetadata.LastPlayed = DateTime.UtcNow.ToString();
+ });
+ }
+ }
+
+ private RendererWidgetBase CreateRendererWidget()
+ {
+ if (ConfigurationState.Instance.Graphics.GraphicsBackend == GraphicsBackend.Vulkan)
+ {
+ return new VKRenderer(InputManager, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
+ }
+ else
+ {
+ return new GlRenderer(InputManager, ConfigurationState.Instance.Logger.GraphicsDebugLevel);
+ }
+ }
+
+ private void SwitchToRenderWidget(bool startFullscreen = false)
+ {
+ _viewBox.Remove(_gameTableWindow);
+ RendererWidget.Expand = true;
+ _viewBox.Child = RendererWidget;
+
+ RendererWidget.ShowAll();
+ EditFooterForGameRenderer();
+
+ if (Window.State.HasFlag(Gdk.WindowState.Fullscreen))
+ {
+ ToggleExtraWidgets(false);
+ }
+ else if (startFullscreen || ConfigurationState.Instance.Ui.StartFullscreen.Value)
+ {
+ FullScreen_Toggled(null, null);
+ }
+ }
+
+ private void SwitchToGameTable()
+ {
+ if (Window.State.HasFlag(Gdk.WindowState.Fullscreen))
+ {
+ ToggleExtraWidgets(true);
+ }
+
+ RendererWidget.Exit();
+
+ if (RendererWidget.Window != Window && RendererWidget.Window != null)
+ {
+ RendererWidget.Window.Dispose();
+ }
+
+ RendererWidget.Dispose();
+
+ if (OperatingSystem.IsWindows())
+ {
+ _windowsMultimediaTimerResolution?.Dispose();
+ _windowsMultimediaTimerResolution = null;
+ }
+
+ DisplaySleep.Restore();
+
+ _viewBox.Remove(RendererWidget);
+ _viewBox.Add(_gameTableWindow);
+
+ _gameTableWindow.Expand = true;
+
+ Window.Title = $"Ryujinx {Program.Version}";
+
+ _emulationContext = null;
+ _gameLoaded = false;
+ RendererWidget = null;
+
+ DiscordIntegrationModule.SwitchToMainMenu();
+
+ RecreateFooterForMenu();
+
+ UpdateColumns();
+ UpdateGameTable();
+
+ RefreshFirmwareLabel();
+ HandleRelaunch();
+ }
+
+ private void CreateGameWindow()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ _windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution(1);
+ }
+
+ DisplaySleep.Prevent();
+
+ RendererWidget.Initialize(_emulationContext);
+
+ RendererWidget.WaitEvent.WaitOne();
+
+ RendererWidget.Start();
+
+ _emulationContext.Dispose();
+ _deviceExitStatus.Set();
+
+ // NOTE: Everything that is here will not be executed when you close the UI.
+ Application.Invoke(delegate
+ {
+ SwitchToGameTable();
+ });
+ }
+
+ private void RecreateFooterForMenu()
+ {
+ _listStatusBox.Show();
+ _statusBar.Hide();
+ }
+
+ private void EditFooterForGameRenderer()
+ {
+ _listStatusBox.Hide();
+ _statusBar.Show();
+ }
+
+ public void ToggleExtraWidgets(bool show)
+ {
+ if (RendererWidget != null)
+ {
+ if (show)
+ {
+ _menuBar.ShowAll();
+ _footerBox.Show();
+ _statusBar.Show();
+ }
+ else
+ {
+ _menuBar.Hide();
+ _footerBox.Hide();
+ }
+ }
+ }
+
+ private void UpdateGameMetadata(string titleId)
+ {
+ if (_gameLoaded)
+ {
+ _applicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
+ {
+ DateTime lastPlayedDateTime = DateTime.Parse(appMetadata.LastPlayed);
+ double sessionTimePlayed = DateTime.UtcNow.Subtract(lastPlayedDateTime).TotalSeconds;
+
+ appMetadata.TimePlayed += Math.Round(sessionTimePlayed, MidpointRounding.AwayFromZero);
+ });
+ }
+ }
+
+ public void UpdateGraphicsConfig()
+ {
+ int resScale = ConfigurationState.Instance.Graphics.ResScale;
+ float resScaleCustom = ConfigurationState.Instance.Graphics.ResScaleCustom;
+
+ Graphics.Gpu.GraphicsConfig.ResScale = (resScale == -1) ? resScaleCustom : resScale;
+ Graphics.Gpu.GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy;
+ Graphics.Gpu.GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath;
+ Graphics.Gpu.GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache;
+ Graphics.Gpu.GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression;
+ Graphics.Gpu.GraphicsConfig.EnableMacroHLE = ConfigurationState.Instance.Graphics.EnableMacroHLE;
+ }
+
+ public void SaveConfig()
+ {
+ ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
+ }
+
+ private void End()
+ {
+ if (_ending)
+ {
+ return;
+ }
+
+ _ending = true;
+
+ if (_emulationContext != null)
+ {
+ UpdateGameMetadata(_emulationContext.Processes.ActiveApplication.ProgramIdText);
+
+ if (RendererWidget != null)
+ {
+ // We tell the widget that we are exiting.
+ RendererWidget.Exit();
+
+ // Wait for the other thread to dispose the HLE context before exiting.
+ _deviceExitStatus.WaitOne();
+ RendererWidget.Dispose();
+ }
+ }
+
+ Dispose();
+
+ Program.Exit();
+ Application.Quit();
+ }
+
+ //
+ // Events
+ //
+ private void Application_Added(object sender, ApplicationAddedEventArgs args)
+ {
+ Application.Invoke(delegate
+ {
+ _tableStore.AppendValues(
+ args.AppData.Favorite,
+ new Gdk.Pixbuf(args.AppData.Icon, 75, 75),
+ $"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}",
+ args.AppData.Developer,
+ args.AppData.Version,
+ args.AppData.TimePlayed,
+ args.AppData.LastPlayed,
+ args.AppData.FileExtension,
+ args.AppData.FileSize,
+ args.AppData.Path,
+ args.AppData.ControlHolder);
+ });
+ }
+
+ private void ApplicationCount_Updated(object sender, ApplicationCountUpdatedEventArgs args)
+ {
+ Application.Invoke(delegate
+ {
+ _progressLabel.Text = $"{args.NumAppsLoaded}/{args.NumAppsFound} Games Loaded";
+ float barValue = 0;
+
+ if (args.NumAppsFound != 0)
+ {
+ barValue = (float)args.NumAppsLoaded / args.NumAppsFound;
+ }
+
+ _progressBar.Fraction = barValue;
+
+ // Reset the vertical scrollbar to the top when titles finish loading
+ if (args.NumAppsLoaded == args.NumAppsFound)
+ {
+ _gameTableWindow.Vadjustment.Value = 0;
+ }
+ });
+ }
+
+ private void Update_StatusBar(object sender, StatusUpdatedEventArgs args)
+ {
+ Application.Invoke(delegate
+ {
+ _gameStatus.Text = args.GameStatus;
+ _fifoStatus.Text = args.FifoStatus;
+ _gpuName.Text = args.GpuName;
+ _dockedMode.Text = args.DockedMode;
+ _aspectRatio.Text = args.AspectRatio;
+ _gpuBackend.Text = args.GpuBackend;
+ _volumeStatus.Text = GetVolumeLabelText(args.Volume);
+
+ if (args.VSyncEnabled)
+ {
+ _vSyncStatus.Attributes = new Pango.AttrList();
+ _vSyncStatus.Attributes.Insert(new Pango.AttrForeground(11822, 60138, 51657));
+ }
+ else
+ {
+ _vSyncStatus.Attributes = new Pango.AttrList();
+ _vSyncStatus.Attributes.Insert(new Pango.AttrForeground(ushort.MaxValue, 17733, 21588));
+ }
+ });
+ }
+
+ private void FavToggle_Toggled(object sender, ToggledArgs args)
+ {
+ _tableStore.GetIter(out TreeIter treeIter, new TreePath(args.Path));
+
+ string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower();
+ bool newToggleValue = !(bool)_tableStore.GetValue(treeIter, 0);
+
+ _tableStore.SetValue(treeIter, 0, newToggleValue);
+
+ _applicationLibrary.LoadAndSaveMetaData(titleId, appMetadata =>
+ {
+ appMetadata.Favorite = newToggleValue;
+ });
+ }
+
+ private void Column_Clicked(object sender, EventArgs args)
+ {
+ TreeViewColumn column = (TreeViewColumn)sender;
+
+ ConfigurationState.Instance.Ui.ColumnSort.SortColumnId.Value = column.SortColumnId;
+ ConfigurationState.Instance.Ui.ColumnSort.SortAscending.Value = column.SortOrder == SortType.Ascending;
+
+ SaveConfig();
+ }
+
+ private void Row_Activated(object sender, RowActivatedArgs args)
+ {
+ _gameTableSelection.GetSelected(out TreeIter treeIter);
+
+ string path = (string)_tableStore.GetValue(treeIter, 9);
+
+ RunApplication(path);
+ }
+
+ private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args)
+ {
+ _emulationContext.EnableDeviceVsync = !_emulationContext.EnableDeviceVsync;
+
+ Logger.Info?.Print(LogClass.Application, $"VSync toggled to: {_emulationContext.EnableDeviceVsync}");
+ }
+
+ private void DockedMode_Clicked(object sender, ButtonReleaseEventArgs args)
+ {
+ ConfigurationState.Instance.System.EnableDockedMode.Value = !ConfigurationState.Instance.System.EnableDockedMode.Value;
+ }
+
+ private string GetVolumeLabelText(float volume)
+ {
+ string icon = volume == 0 ? "🔇" : "🔊";
+
+ return $"{icon} {(int)(volume * 100)}%";
+ }
+
+ private void VolumeStatus_Clicked(object sender, ButtonReleaseEventArgs args)
+ {
+ if (_emulationContext != null)
+ {
+ if (_emulationContext.IsAudioMuted())
+ {
+ _emulationContext.SetVolume(ConfigurationState.Instance.System.AudioVolume);
+ }
+ else
+ {
+ _emulationContext.SetVolume(0);
+ }
+ }
+ }
+
+ private void AspectRatio_Clicked(object sender, ButtonReleaseEventArgs args)
+ {
+ AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value;
+
+ ConfigurationState.Instance.Graphics.AspectRatio.Value = ((int)aspectRatio + 1) > Enum.GetNames<AspectRatio>().Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1;
+ }
+
+ private void Row_Clicked(object sender, ButtonReleaseEventArgs args)
+ {
+ if (args.Event.Button != 3 /* Right Click */)
+ {
+ return;
+ }
+
+ _gameTableSelection.GetSelected(out TreeIter treeIter);
+
+ if (treeIter.UserData == IntPtr.Zero)
+ {
+ return;
+ }
+
+ string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString();
+ string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0];
+ string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower();
+
+ BlitStruct<ApplicationControlProperty> controlData = (BlitStruct<ApplicationControlProperty>)_tableStore.GetValue(treeIter, 10);
+
+ _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData);
+ }
+
+ private void Load_Application_File(object sender, EventArgs args)
+ {
+ using (FileChooserNative fileChooser = new FileChooserNative("Choose the file to open", this, FileChooserAction.Open, "Open", "Cancel"))
+ {
+ FileFilter filter = new FileFilter()
+ {
+ Name = "Switch Executables"
+ };
+ filter.AddPattern("*.xci");
+ filter.AddPattern("*.nsp");
+ filter.AddPattern("*.pfs0");
+ filter.AddPattern("*.nca");
+ filter.AddPattern("*.nro");
+ filter.AddPattern("*.nso");
+
+ fileChooser.AddFilter(filter);
+
+ if (fileChooser.Run() == (int)ResponseType.Accept)
+ {
+ RunApplication(fileChooser.Filename);
+ }
+ }
+ }
+
+ private void Load_Application_Folder(object sender, EventArgs args)
+ {
+ using (FileChooserNative fileChooser = new FileChooserNative("Choose the folder to open", this, FileChooserAction.SelectFolder, "Open", "Cancel"))
+ {
+ if (fileChooser.Run() == (int)ResponseType.Accept)
+ {
+ RunApplication(fileChooser.Filename);
+ }
+ }
+ }
+
+ private void FileMenu_StateChanged(object o, StateChangedArgs args)
+ {
+ _appletMenu.Sensitive = _emulationContext == null && _contentManager.GetCurrentFirmwareVersion() != null && _contentManager.GetCurrentFirmwareVersion().Major > 3;
+ _loadApplicationFile.Sensitive = _emulationContext == null;
+ _loadApplicationFolder.Sensitive = _emulationContext == null;
+ }
+
+ private void Load_Mii_Edit_Applet(object sender, EventArgs args)
+ {
+ string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
+
+ RunApplication(contentPath);
+ }
+
+ private void Open_Ryu_Folder(object sender, EventArgs args)
+ {
+ OpenHelper.OpenFolder(AppDataManager.BaseDirPath);
+ }
+
+ private void OpenLogsFolder_Pressed(object sender, EventArgs args)
+ {
+ string logPath = System.IO.Path.Combine(ReleaseInformation.GetBaseApplicationDirectory(), "Logs");
+
+ new DirectoryInfo(logPath).Create();
+
+ OpenHelper.OpenFolder(logPath);
+ }
+
+ private void Exit_Pressed(object sender, EventArgs args)
+ {
+ if (!_gameLoaded || !ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog())
+ {
+ End();
+ }
+ }
+
+ private void Window_Close(object sender, DeleteEventArgs args)
+ {
+ if (!_gameLoaded || !ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog())
+ {
+ End();
+ }
+ else
+ {
+ args.RetVal = true;
+ }
+ }
+
+ private void StopEmulation_Pressed(object sender, EventArgs args)
+ {
+ if (_emulationContext != null)
+ {
+ UpdateGameMetadata(_emulationContext.Processes.ActiveApplication.ProgramIdText);
+ }
+
+ _pauseEmulation.Sensitive = false;
+ _resumeEmulation.Sensitive = false;
+ UpdateMenuItem.Sensitive = true;
+ RendererWidget?.Exit();
+ }
+
+ private void PauseEmulation_Pressed(object sender, EventArgs args)
+ {
+ _pauseEmulation.Sensitive = false;
+ _resumeEmulation.Sensitive = true;
+ _emulationContext.System.TogglePauseEmulation(true);
+ }
+
+ private void ResumeEmulation_Pressed(object sender, EventArgs args)
+ {
+ _pauseEmulation.Sensitive = true;
+ _resumeEmulation.Sensitive = false;
+ _emulationContext.System.TogglePauseEmulation(false);
+ }
+
+ public void ActivatePauseMenu()
+ {
+ _pauseEmulation.Sensitive = true;
+ _resumeEmulation.Sensitive = false;
+ }
+
+ public void TogglePause()
+ {
+ _pauseEmulation.Sensitive ^= true;
+ _resumeEmulation.Sensitive ^= true;
+ _emulationContext.System.TogglePauseEmulation(_resumeEmulation.Sensitive);
+ }
+
+ private void Installer_File_Pressed(object o, EventArgs args)
+ {
+ FileChooserNative fileChooser = new FileChooserNative("Choose the firmware file to open", this, FileChooserAction.Open, "Open", "Cancel");
+
+ FileFilter filter = new FileFilter
+ {
+ Name = "Switch Firmware Files"
+ };
+ filter.AddPattern("*.zip");
+ filter.AddPattern("*.xci");
+
+ fileChooser.AddFilter(filter);
+
+ HandleInstallerDialog(fileChooser);
+ }
+
+ private void Installer_Directory_Pressed(object o, EventArgs args)
+ {
+ FileChooserNative directoryChooser = new FileChooserNative("Choose the firmware directory to open", this, FileChooserAction.SelectFolder, "Open", "Cancel");
+
+ HandleInstallerDialog(directoryChooser);
+ }
+
+ private void HandleInstallerDialog(FileChooserNative fileChooser)
+ {
+ if (fileChooser.Run() == (int)ResponseType.Accept)
+ {
+ try
+ {
+ string filename = fileChooser.Filename;
+
+ fileChooser.Dispose();
+
+ SystemVersion firmwareVersion = _contentManager.VerifyFirmwarePackage(filename);
+
+ if (firmwareVersion is null)
+ {
+ GtkDialog.CreateErrorDialog($"A valid system firmware was not found in {filename}.");
+
+ return;
+ }
+
+ string dialogTitle = $"Install Firmware {firmwareVersion.VersionString}";
+
+ SystemVersion currentVersion = _contentManager.GetCurrentFirmwareVersion();
+
+ string dialogMessage = $"System version {firmwareVersion.VersionString} will be installed.";
+
+ if (currentVersion != null)
+ {
+ dialogMessage += $"\n\nThis will replace the current system version {currentVersion.VersionString}. ";
+ }
+
+ dialogMessage += "\n\nDo you want to continue?";
+
+ ResponseType responseInstallDialog = (ResponseType)GtkDialog.CreateConfirmationDialog(dialogTitle, dialogMessage).Run();
+
+ MessageDialog waitingDialog = GtkDialog.CreateWaitingDialog(dialogTitle, "Installing firmware...");
+
+ if (responseInstallDialog == ResponseType.Yes)
+ {
+ Logger.Info?.Print(LogClass.Application, $"Installing firmware {firmwareVersion.VersionString}");
+
+ Thread thread = new Thread(() =>
+ {
+ Application.Invoke(delegate
+ {
+ waitingDialog.Run();
+
+ });
+
+ try
+ {
+ _contentManager.InstallFirmware(filename);
+
+ Application.Invoke(delegate
+ {
+ waitingDialog.Dispose();
+
+ string message = $"System version {firmwareVersion.VersionString} successfully installed.";
+
+ GtkDialog.CreateInfoDialog(dialogTitle, message);
+ Logger.Info?.Print(LogClass.Application, message);
+
+ // Purge Applet Cache.
+
+ DirectoryInfo miiEditorCacheFolder = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, "0100000000001009", "cache"));
+
+ if (miiEditorCacheFolder.Exists)
+ {
+ miiEditorCacheFolder.Delete(true);
+ }
+ });
+ }
+ catch (Exception ex)
+ {
+ Application.Invoke(delegate
+ {
+ waitingDialog.Dispose();
+
+ GtkDialog.CreateErrorDialog(ex.Message);
+ });
+ }
+ finally
+ {
+ RefreshFirmwareLabel();
+ }
+ });
+
+ thread.Name = "GUI.FirmwareInstallerThread";
+ thread.Start();
+ }
+ }
+ catch (MissingKeyException ex)
+ {
+ Logger.Error?.Print(LogClass.Application, ex.ToString());
+ UserErrorDialog.CreateUserErrorDialog(UserError.FirmwareParsingFailed);
+ }
+ catch (Exception ex)
+ {
+ GtkDialog.CreateErrorDialog(ex.Message);
+ }
+ }
+ else
+ {
+ fileChooser.Dispose();
+ }
+ }
+
+ private void RefreshFirmwareLabel()
+ {
+ SystemVersion currentFirmware = _contentManager.GetCurrentFirmwareVersion();
+
+ Application.Invoke(delegate
+ {
+ _firmwareVersionLabel.Text = currentFirmware != null ? currentFirmware.VersionString : "0.0.0";
+ });
+ }
+
+ private void InstallFileTypes_Pressed(object sender, EventArgs e)
+ {
+ if (FileAssociationHelper.Install())
+ {
+ GtkDialog.CreateInfoDialog("Install file types", "File types successfully installed!");
+ }
+ else
+ {
+ GtkDialog.CreateErrorDialog("Failed to install file types.");
+ }
+ }
+
+ private void UninstallFileTypes_Pressed(object sender, EventArgs e)
+ {
+ if (FileAssociationHelper.Uninstall())
+ {
+ GtkDialog.CreateInfoDialog("Uninstall file types", "File types successfully uninstalled!");
+ }
+ else
+ {
+ GtkDialog.CreateErrorDialog("Failed to uninstall file types.");
+ }
+ }
+
+ private void HandleRelaunch()
+ {
+ if (_userChannelPersistence.PreviousIndex != -1 && _userChannelPersistence.ShouldRestart)
+ {
+ _userChannelPersistence.ShouldRestart = false;
+
+ RunApplication(_currentEmulatedGamePath);
+ }
+ else
+ {
+ // otherwise, clear state.
+ _userChannelPersistence = new UserChannelPersistence();
+ _currentEmulatedGamePath = null;
+ _actionMenu.Sensitive = false;
+ _firmwareInstallFile.Sensitive = true;
+ _firmwareInstallDirectory.Sensitive = true;
+ }
+ }
+
+ private void FullScreen_Toggled(object sender, EventArgs args)
+ {
+ if (!Window.State.HasFlag(Gdk.WindowState.Fullscreen))
+ {
+ Fullscreen();
+
+ ToggleExtraWidgets(false);
+ }
+ else
+ {
+ Unfullscreen();
+
+ ToggleExtraWidgets(true);
+ }
+ }
+
+ private void StartFullScreen_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.StartFullscreen.Value = _startFullScreen.Active;
+
+ SaveConfig();
+ }
+
+ private void ShowConsole_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.ShowConsole.Value = _showConsole.Active;
+
+ SaveConfig();
+ }
+
+ private void OptionMenu_StateChanged(object o, StateChangedArgs args)
+ {
+ _manageUserProfiles.Sensitive = _emulationContext == null;
+ }
+
+ private void Settings_Pressed(object sender, EventArgs args)
+ {
+ SettingsWindow settingsWindow = new SettingsWindow(this, _virtualFileSystem, _contentManager);
+
+ settingsWindow.SetSizeRequest((int)(settingsWindow.DefaultWidth * Program.WindowScaleFactor), (int)(settingsWindow.DefaultHeight * Program.WindowScaleFactor));
+ settingsWindow.Show();
+ }
+
+ private void HideUi_Pressed(object sender, EventArgs args)
+ {
+ ToggleExtraWidgets(false);
+ }
+
+ private void ManageCheats_Pressed(object sender, EventArgs args)
+ {
+ var window = new CheatWindow(_virtualFileSystem,
+ _emulationContext.Processes.ActiveApplication.ProgramId,
+ _emulationContext.Processes.ActiveApplication.ApplicationControlProperties.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString());
+
+ window.Destroyed += CheatWindow_Destroyed;
+ window.Show();
+ }
+
+ private void CheatWindow_Destroyed(object sender, EventArgs e)
+ {
+ _emulationContext.EnableCheats();
+ (sender as CheatWindow).Destroyed -= CheatWindow_Destroyed;
+ }
+
+ private void ManageUserProfiles_Pressed(object sender, EventArgs args)
+ {
+ UserProfilesManagerWindow userProfilesManagerWindow = new UserProfilesManagerWindow(_accountManager, _contentManager, _virtualFileSystem);
+
+ userProfilesManagerWindow.SetSizeRequest((int)(userProfilesManagerWindow.DefaultWidth * Program.WindowScaleFactor), (int)(userProfilesManagerWindow.DefaultHeight * Program.WindowScaleFactor));
+ userProfilesManagerWindow.Show();
+ }
+
+ private void Simulate_WakeUp_Message_Pressed(object sender, EventArgs args)
+ {
+ if (_emulationContext != null)
+ {
+ _emulationContext.System.SimulateWakeUpMessage();
+ }
+ }
+
+ private void ActionMenu_StateChanged(object o, StateChangedArgs args)
+ {
+ _scanAmiibo.Sensitive = _emulationContext != null && _emulationContext.System.SearchingForAmiibo(out int _);
+ _takeScreenshot.Sensitive = _emulationContext != null;
+ }
+
+ private void Scan_Amiibo(object sender, EventArgs args)
+ {
+ if (_emulationContext.System.SearchingForAmiibo(out int deviceId))
+ {
+ AmiiboWindow amiiboWindow = new AmiiboWindow
+ {
+ LastScannedAmiiboShowAll = _lastScannedAmiiboShowAll,
+ LastScannedAmiiboId = _lastScannedAmiiboId,
+ DeviceId = deviceId,
+ TitleId = _emulationContext.Processes.ActiveApplication.ProgramIdText.ToUpper()
+ };
+
+ amiiboWindow.DeleteEvent += AmiiboWindow_DeleteEvent;
+
+ amiiboWindow.Show();
+ }
+ else
+ {
+ GtkDialog.CreateInfoDialog($"Amiibo", "The game is currently not ready to receive Amiibo scan data. Ensure that you have an Amiibo-compatible game open and ready to receive Amiibo scan data.");
+ }
+ }
+
+ private void Take_Screenshot(object sender, EventArgs args)
+ {
+ if (_emulationContext != null && RendererWidget != null)
+ {
+ RendererWidget.ScreenshotRequested = true;
+ }
+ }
+
+ private void AmiiboWindow_DeleteEvent(object sender, DeleteEventArgs args)
+ {
+ if (((AmiiboWindow)sender).AmiiboId != "" && ((AmiiboWindow)sender).Response == ResponseType.Ok)
+ {
+ _lastScannedAmiiboId = ((AmiiboWindow)sender).AmiiboId;
+ _lastScannedAmiiboShowAll = ((AmiiboWindow)sender).LastScannedAmiiboShowAll;
+
+ _emulationContext.System.ScanAmiibo(((AmiiboWindow)sender).DeviceId, ((AmiiboWindow)sender).AmiiboId, ((AmiiboWindow)sender).UseRandomUuid);
+ }
+ }
+
+ private void Update_Pressed(object sender, EventArgs args)
+ {
+ if (Updater.CanUpdate(true))
+ {
+ Updater.BeginParse(this, true).ContinueWith(task =>
+ {
+ Logger.Error?.Print(LogClass.Application, $"Updater error: {task.Exception}");
+ }, TaskContinuationOptions.OnlyOnFaulted);
+ }
+ }
+
+ private void About_Pressed(object sender, EventArgs args)
+ {
+ AboutWindow aboutWindow = new AboutWindow();
+
+ aboutWindow.SetSizeRequest((int)(aboutWindow.DefaultWidth * Program.WindowScaleFactor), (int)(aboutWindow.DefaultHeight * Program.WindowScaleFactor));
+ aboutWindow.Show();
+ }
+
+ private void Fav_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.GuiColumns.FavColumn.Value = _favToggle.Active;
+
+ SaveConfig();
+ UpdateColumns();
+ }
+
+ private void Icon_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.GuiColumns.IconColumn.Value = _iconToggle.Active;
+
+ SaveConfig();
+ UpdateColumns();
+ }
+
+ private void App_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.GuiColumns.AppColumn.Value = _appToggle.Active;
+
+ SaveConfig();
+ UpdateColumns();
+ }
+
+ private void Developer_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.GuiColumns.DevColumn.Value = _developerToggle.Active;
+
+ SaveConfig();
+ UpdateColumns();
+ }
+
+ private void Version_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.GuiColumns.VersionColumn.Value = _versionToggle.Active;
+
+ SaveConfig();
+ UpdateColumns();
+ }
+
+ private void TimePlayed_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.GuiColumns.TimePlayedColumn.Value = _timePlayedToggle.Active;
+
+ SaveConfig();
+ UpdateColumns();
+ }
+
+ private void LastPlayed_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.GuiColumns.LastPlayedColumn.Value = _lastPlayedToggle.Active;
+
+ SaveConfig();
+ UpdateColumns();
+ }
+
+ private void FileExt_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.GuiColumns.FileExtColumn.Value = _fileExtToggle.Active;
+
+ SaveConfig();
+ UpdateColumns();
+ }
+
+ private void FileSize_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.GuiColumns.FileSizeColumn.Value = _fileSizeToggle.Active;
+
+ SaveConfig();
+ UpdateColumns();
+ }
+
+ private void Path_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.GuiColumns.PathColumn.Value = _pathToggle.Active;
+
+ SaveConfig();
+ UpdateColumns();
+ }
+
+ private void NSP_Shown_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.ShownFileTypes.NSP.Value = _nspShown.Active;
+
+ SaveConfig();
+ UpdateGameTable();
+ }
+
+ private void PFS0_Shown_Toggled(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.ShownFileTypes.PFS0.Value = _pfs0Shown.Active;
+
+ SaveConfig();
+ UpdateGameTable();
+ }
+
+ private void XCI_Shown_Toggled (object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.ShownFileTypes.XCI.Value = _xciShown.Active;
+
+ SaveConfig();
+ UpdateGameTable();
+ }
+
+ private void NCA_Shown_Toggled (object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.ShownFileTypes.NCA.Value = _ncaShown.Active;
+
+ SaveConfig();
+ UpdateGameTable();
+ }
+
+ private void NRO_Shown_Toggled (object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.ShownFileTypes.NRO.Value = _nroShown.Active;
+
+ SaveConfig();
+ UpdateGameTable();
+ }
+
+ private void NSO_Shown_Toggled (object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.Ui.ShownFileTypes.NSO.Value = _nsoShown.Active;
+
+ SaveConfig();
+ UpdateGameTable();
+ }
+
+ private void RefreshList_Pressed(object sender, ButtonReleaseEventArgs args)
+ {
+ UpdateGameTable();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/MainWindow.glade b/src/Ryujinx/Ui/MainWindow.glade
new file mode 100644
index 00000000..58d5d955
--- /dev/null
+++ b/src/Ryujinx/Ui/MainWindow.glade
@@ -0,0 +1,1006 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.40.0 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkApplicationWindow" id="_mainWin">
+ <property name="can-focus">False</property>
+ <property name="title" translatable="yes">Ryujinx</property>
+ <property name="window-position">center</property>
+ <child>
+ <object class="GtkBox" id="_box">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkMenuBar" id="_menuBar">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="_fileMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">File</property>
+ <property name="use-underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="_loadApplicationFile">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Open a file explorer to choose a Switch compatible file to load</property>
+ <property name="label" translatable="yes">Load Application from File</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Load_Application_File" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_loadApplicationFolder">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Open a file explorer to choose a Switch compatible, unpacked application to load</property>
+ <property name="label" translatable="yes">Load Unpacked Game</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Load_Application_Folder" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_appletMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Load Applet</property>
+ <property name="use-underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="LoadMiiEditApplet">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Open Mii Editor Applet in Standalone mode</property>
+ <property name="label" translatable="yes">Mii Editor</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Load_Mii_Edit_Applet" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="OpenRyuFolder">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Open Ryujinx filesystem folder</property>
+ <property name="label" translatable="yes">Open Ryujinx Folder</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Open_Ryu_Folder" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="OpenLogsFolder">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Opens the folder where logs are written to.</property>
+ <property name="label" translatable="yes">Open Logs Folder</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="OpenLogsFolder_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="ExitMenuItem">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Exit Ryujinx</property>
+ <property name="label" translatable="yes">Exit</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Exit_Pressed" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_optionMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Options</property>
+ <property name="use-underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="_fullScreen">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Enter Fullscreen</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_startFullScreen">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Start Games in Fullscreen Mode</property>
+ <property name="use-underline">True</property>
+ <signal name="toggled" handler="StartFullScreen_Toggled" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_showConsole">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Show Log Console</property>
+ <property name="use-underline">True</property>
+ <signal name="toggled" handler="ShowConsole_Toggled" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="GUIColumns">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Select which GUI columns to enable</property>
+ <property name="label" translatable="yes">Enable GUI Columns</property>
+ <property name="use-underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkCheckMenuItem" id="_favToggle">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or Disable Favorite Games Column in the game list</property>
+ <property name="label" translatable="yes">Enable Favorite Games Column</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_iconToggle">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or Disable Icon Column in the game list</property>
+ <property name="label" translatable="yes">Enable Icon Column</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_appToggle">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or Disable Title Name/ID Column in the game list</property>
+ <property name="label" translatable="yes">Enable Title Name/ID Column</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_developerToggle">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or Disable Developer Column in the game list</property>
+ <property name="label" translatable="yes">Enable Developer Column</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_versionToggle">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or Disable Version Column in the game list</property>
+ <property name="label" translatable="yes">Enable Version Column</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_timePlayedToggle">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or Disable Time Played Column in the game list</property>
+ <property name="label" translatable="yes">Enable Time Played Column</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_lastPlayedToggle">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or Disable Last Played Column in the game list</property>
+ <property name="label" translatable="yes">Enable Last Played Column</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_fileExtToggle">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or Disable file extension column in the game list</property>
+ <property name="label" translatable="yes">Enable File Ext Column</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_fileSizeToggle">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or Disable File Size Column in the game list</property>
+ <property name="label" translatable="yes">Enable File Size Column</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_pathToggle">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or Disable Path Column in the game list</property>
+ <property name="label" translatable="yes">Enable Path Column</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="ShownFileTypes">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Select which file types to show</property>
+ <property name="label" translatable="yes">Show File Types</property>
+ <property name="use-underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkCheckMenuItem" id="_nspShown">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Shows .NSP files in the games list</property>
+ <property name="label" translatable="yes">.NSP</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_pfs0Shown">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Shows .PFS0 files in the games list</property>
+ <property name="label" translatable="yes">.PFS0</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_xciShown">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Shows .XCI files in the games list</property>
+ <property name="label" translatable="yes">.XCI</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_ncaShown">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Shows .NCA files in the games list</property>
+ <property name="label" translatable="yes">.NCA</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_nroShown">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Shows .NRO files in the games list</property>
+ <property name="label" translatable="yes">.NRO</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkCheckMenuItem" id="_nsoShown">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Shows .NSO files in the games list</property>
+ <property name="label" translatable="yes">.NSO</property>
+ <property name="use-underline">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="SettingsMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Open settings window</property>
+ <property name="label" translatable="yes">Settings</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Settings_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_manageUserProfiles">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Open User Profiles Manager window</property>
+ <property name="label" translatable="yes">Manage User Profiles</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="ManageUserProfiles_Pressed" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_actionMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Actions</property>
+ <property name="use-underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="_pauseEmulation">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Pause emulation</property>
+ <property name="label" translatable="yes">Pause Emulation</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="PauseEmulation_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_resumeEmulation">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Resume emulation</property>
+ <property name="label" translatable="yes">Resume Emulation</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="ResumeEmulation_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_stopEmulation">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Stop emulation of the current game and return to game selection</property>
+ <property name="label" translatable="yes">Stop Emulation</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="StopEmulation_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_simulateWakeUpMessage">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Simulate a Wake-up Message</property>
+ <property name="label" translatable="yes">Simulate Wake-up Message</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Simulate_WakeUp_Message_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_scanAmiibo">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Scan an Amiibo</property>
+ <property name="label" translatable="yes">Scan an Amiibo</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Scan_Amiibo" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_takeScreenshot">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Take a screenshot</property>
+ <property name="label" translatable="yes">Take Screenshot</property>
+ <signal name="activate" handler="Take_Screenshot" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_hideUi">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Hide UI (SHOWUIKEY to show)</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="HideUi_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_manageCheats">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Manage Cheats</property>
+ <signal name="activate" handler="ManageCheats_Pressed" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_toolsMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Tools</property>
+ <property name="use-underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="FirmwareSubMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Install Firmware</property>
+ <property name="use-underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="_firmwareInstallFile">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Install a firmware from XCI or ZIP</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Installer_File_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_firmwareInstallDirectory">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Install a firmware from a directory</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Installer_Directory_Pressed" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_fileTypesSubMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Manage file types</property>
+ <property name="use-underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="_installFileTypes">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Install file types</property>
+ <signal name="activate" handler="InstallFileTypes_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="_uninstallFileTypes">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Uninstall file types</property>
+ <signal name="activate" handler="UninstallFileTypes_Pressed" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="HelpMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Help</property>
+ <property name="use-underline">True</property>
+ <child type="submenu">
+ <object class="GtkMenu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkMenuItem" id="UpdateMenuItem">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Check for updates to Ryujinx</property>
+ <property name="label" translatable="yes">Check for Updates</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="Update_Pressed" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparatorMenuItem">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuItem" id="About">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Open about window</property>
+ <property name="label" translatable="yes">About</property>
+ <property name="use-underline">True</property>
+ <signal name="activate" handler="About_Pressed" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_mainBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="_viewBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow" id="_gameTableWindow">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="shadow-type">in</property>
+ <child>
+ <object class="GtkTreeView" id="_gameTable">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="reorderable">True</property>
+ <property name="hover-selection">True</property>
+ <signal name="row-activated" handler="Row_Activated" swapped="no"/>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection" id="_gameTableSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_footerBox">
+ <property name="height-request">19</property>
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkBox" id="_listStatusBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkEventBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <signal name="button-release-event" handler="RefreshList_Pressed" swapped="no"/>
+ <child>
+ <object class="GtkImage">
+ <property name="name">RefreshList</property>
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="stock">gtk-refresh</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="_progressLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">5</property>
+ <property name="margin-top">2</property>
+ <property name="margin-bottom">2</property>
+ <property name="label" translatable="yes">0/0 Games Loaded</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkProgressBar" id="_progressBar">
+ <property name="width-request">200</property>
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">5</property>
+ <property name="margin-bottom">6</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_statusBar">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkEventBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <signal name="button-release-event" handler="VSyncStatus_Clicked" swapped="no"/>
+ <child>
+ <object class="GtkLabel" id="_vSyncStatus">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="label" translatable="yes">VSync</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEventBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <signal name="button-release-event" handler="DockedMode_Clicked" swapped="no"/>
+ <child>
+ <object class="GtkLabel" id="_dockedMode">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEventBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <signal name="button-release-event" handler="VolumeStatus_Clicked" swapped="no"/>
+ <child>
+ <object class="GtkLabel" id="_volumeStatus">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEventBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <signal name="button-release-event" handler="AspectRatio_Clicked" swapped="no"/>
+ <child>
+ <object class="GtkLabel" id="_aspectRatio">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="_gameStatus">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">9</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="_fifoStatus">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">10</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">11</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="_gpuBackend">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">12</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">13</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="_gpuName">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">14</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">System Version</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="_firmwareVersionLabel">
+ <property name="width-request">50</property>
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack-type">end</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="_loadingStatusLabel">
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="label" translatable="yes">0/0 </property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">11</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkProgressBar" id="_loadingStatusBar">
+ <property name="width-request">200</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="margin-bottom">6</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">12</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/src/Ryujinx/Ui/OpenToolkitBindingsContext.cs b/src/Ryujinx/Ui/OpenToolkitBindingsContext.cs
new file mode 100644
index 00000000..ec4111fa
--- /dev/null
+++ b/src/Ryujinx/Ui/OpenToolkitBindingsContext.cs
@@ -0,0 +1,20 @@
+using SPB.Graphics;
+using System;
+
+namespace Ryujinx.Ui
+{
+ public class OpenToolkitBindingsContext : OpenTK.IBindingsContext
+ {
+ private IBindingsContext _bindingContext;
+
+ public OpenToolkitBindingsContext(IBindingsContext bindingsContext)
+ {
+ _bindingContext = bindingsContext;
+ }
+
+ public IntPtr GetProcAddress(string procName)
+ {
+ return _bindingContext.GetProcAddress(procName);
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/RendererWidgetBase.cs b/src/Ryujinx/Ui/RendererWidgetBase.cs
new file mode 100644
index 00000000..65afa6e4
--- /dev/null
+++ b/src/Ryujinx/Ui/RendererWidgetBase.cs
@@ -0,0 +1,778 @@
+using ARMeilleure.Translation;
+using Gdk;
+using Gtk;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.Graphics.GAL;
+using Ryujinx.Graphics.GAL.Multithreading;
+using Ryujinx.Graphics.Gpu;
+using Ryujinx.Input;
+using Ryujinx.Input.GTK3;
+using Ryujinx.Input.HLE;
+using Ryujinx.Ui.Common.Configuration;
+using Ryujinx.Ui.Widgets;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ui
+{
+ using Image = SixLabors.ImageSharp.Image;
+ using Key = Input.Key;
+ using ScalingFilter = Graphics.GAL.ScalingFilter;
+ using Switch = HLE.Switch;
+
+ public abstract class RendererWidgetBase : DrawingArea
+ {
+ private const int SwitchPanelWidth = 1280;
+ private const int SwitchPanelHeight = 720;
+ private const int TargetFps = 60;
+ private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
+ private const float VolumeDelta = 0.05f;
+
+ public ManualResetEvent WaitEvent { get; set; }
+ public NpadManager NpadManager { get; }
+ public TouchScreenManager TouchScreenManager { get; }
+ public Switch Device { get; private set; }
+ public IRenderer Renderer { get; private set; }
+
+ public bool ScreenshotRequested { get; set; }
+ protected int WindowWidth { get; private set; }
+ protected int WindowHeight { get; private set; }
+
+ public static event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
+
+ private bool _isActive;
+ private bool _isStopped;
+
+ private bool _toggleFullscreen;
+ private bool _toggleDockedMode;
+
+ private readonly long _ticksPerFrame;
+
+ private long _ticks = 0;
+ private float _newVolume;
+
+ private readonly Stopwatch _chrono;
+
+ private KeyboardHotkeyState _prevHotkeyState;
+
+ private readonly ManualResetEvent _exitEvent;
+
+ private readonly CancellationTokenSource _gpuCancellationTokenSource;
+
+ // Hide Cursor
+ const int CursorHideIdleTime = 5; // seconds
+ private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
+ private long _lastCursorMoveTime;
+ private bool _hideCursorOnIdle;
+ private InputManager _inputManager;
+ private IKeyboard _keyboardInterface;
+ private GraphicsDebugLevel _glLogLevel;
+ private string _gpuBackendName;
+ private string _gpuVendorName;
+ private bool _isMouseInClient;
+
+ public RendererWidgetBase(InputManager inputManager, GraphicsDebugLevel glLogLevel)
+ {
+ var mouseDriver = new GTK3MouseDriver(this);
+
+ _inputManager = inputManager;
+ _inputManager.SetMouseDriver(mouseDriver);
+ NpadManager = _inputManager.CreateNpadManager();
+ TouchScreenManager = _inputManager.CreateTouchScreenManager();
+ _keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
+
+ WaitEvent = new ManualResetEvent(false);
+
+ _glLogLevel = glLogLevel;
+
+ Destroyed += Renderer_Destroyed;
+
+ _chrono = new Stopwatch();
+
+ _ticksPerFrame = Stopwatch.Frequency / TargetFps;
+
+ AddEvents((int)(EventMask.ButtonPressMask
+ | EventMask.ButtonReleaseMask
+ | EventMask.PointerMotionMask
+ | EventMask.ScrollMask
+ | EventMask.EnterNotifyMask
+ | EventMask.LeaveNotifyMask
+ | EventMask.KeyPressMask
+ | EventMask.KeyReleaseMask));
+
+ _exitEvent = new ManualResetEvent(false);
+
+ _gpuCancellationTokenSource = new CancellationTokenSource();
+
+ _hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+
+ ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorStateChanged;
+ ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAnriAliasing;
+ ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
+ ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
+ }
+
+ private void UpdateScalingFilterLevel(object sender, ReactiveEventArgs<int> e)
+ {
+ Renderer.Window.SetScalingFilter((ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
+ Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
+ }
+
+ private void UpdateScalingFilter(object sender, ReactiveEventArgs<Ryujinx.Common.Configuration.ScalingFilter> e)
+ {
+ Renderer.Window.SetScalingFilter((ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
+ Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
+ }
+
+ public abstract void InitializeRenderer();
+
+ public abstract void SwapBuffers();
+
+ protected abstract string GetGpuBackendName();
+
+ private string GetGpuVendorName()
+ {
+ return Renderer.GetHardwareInfo().GpuVendor;
+ }
+
+ private void HideCursorStateChanged(object sender, ReactiveEventArgs<bool> state)
+ {
+ Application.Invoke(delegate
+ {
+ _hideCursorOnIdle = state.NewValue;
+
+ if (_hideCursorOnIdle)
+ {
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+ }
+ else
+ {
+ Window.Cursor = null;
+ }
+ });
+ }
+
+ private void Renderer_Destroyed(object sender, EventArgs e)
+ {
+ ConfigurationState.Instance.HideCursorOnIdle.Event -= HideCursorStateChanged;
+ ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAnriAliasing;
+ ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter;
+ ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel;
+
+ NpadManager.Dispose();
+ Dispose();
+ }
+
+ private void UpdateAnriAliasing(object sender, ReactiveEventArgs<Ryujinx.Common.Configuration.AntiAliasing> e)
+ {
+ Renderer?.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)e.NewValue);
+ }
+
+ protected override bool OnMotionNotifyEvent(EventMotion evnt)
+ {
+ if (_hideCursorOnIdle)
+ {
+ _lastCursorMoveTime = Stopwatch.GetTimestamp();
+ }
+
+ if (ConfigurationState.Instance.Hid.EnableMouse)
+ {
+ Window.Cursor = _invisibleCursor;
+ }
+
+ _isMouseInClient = true;
+
+ return false;
+ }
+
+ protected override bool OnEnterNotifyEvent(EventCrossing evnt)
+ {
+ Window.Cursor = ConfigurationState.Instance.Hid.EnableMouse ? _invisibleCursor : null;
+
+ _isMouseInClient = true;
+
+ return base.OnEnterNotifyEvent(evnt);
+ }
+
+ protected override bool OnLeaveNotifyEvent(EventCrossing evnt)
+ {
+ Window.Cursor = null;
+
+ _isMouseInClient = false;
+
+ return base.OnLeaveNotifyEvent(evnt);
+ }
+
+ protected override void OnGetPreferredHeight(out int minimumHeight, out int naturalHeight)
+ {
+ Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
+
+ // If the monitor is at least 1080p, use the Switch panel size as minimal size.
+ if (monitor.Geometry.Height >= 1080)
+ {
+ minimumHeight = SwitchPanelHeight;
+ }
+ // Otherwise, we default minimal size to 480p 16:9.
+ else
+ {
+ minimumHeight = 480;
+ }
+
+ naturalHeight = minimumHeight;
+ }
+
+ protected override void OnGetPreferredWidth(out int minimumWidth, out int naturalWidth)
+ {
+ Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
+
+ // If the monitor is at least 1080p, use the Switch panel size as minimal size.
+ if (monitor.Geometry.Height >= 1080)
+ {
+ minimumWidth = SwitchPanelWidth;
+ }
+ // Otherwise, we default minimal size to 480p 16:9.
+ else
+ {
+ minimumWidth = 854;
+ }
+
+ naturalWidth = minimumWidth;
+ }
+
+ protected override bool OnConfigureEvent(EventConfigure evnt)
+ {
+ bool result = base.OnConfigureEvent(evnt);
+
+ Gdk.Monitor monitor = Display.GetMonitorAtWindow(Window);
+
+ WindowWidth = evnt.Width * monitor.ScaleFactor;
+ WindowHeight = evnt.Height * monitor.ScaleFactor;
+
+ Renderer?.Window?.SetSize(WindowWidth, WindowHeight);
+
+ return result;
+ }
+
+ private void HandleScreenState(KeyboardStateSnapshot keyboard)
+ {
+ bool toggleFullscreen = keyboard.IsPressed(Key.F11)
+ || ((keyboard.IsPressed(Key.AltLeft)
+ || keyboard.IsPressed(Key.AltRight))
+ && keyboard.IsPressed(Key.Enter))
+ || keyboard.IsPressed(Key.Escape);
+
+ bool fullScreenToggled = ParentWindow.State.HasFlag(WindowState.Fullscreen);
+
+ if (toggleFullscreen != _toggleFullscreen)
+ {
+ if (toggleFullscreen)
+ {
+ if (fullScreenToggled)
+ {
+ ParentWindow.Unfullscreen();
+ (Toplevel as MainWindow)?.ToggleExtraWidgets(true);
+ }
+ else
+ {
+ if (keyboard.IsPressed(Key.Escape))
+ {
+ if (!ConfigurationState.Instance.ShowConfirmExit || GtkDialog.CreateExitDialog())
+ {
+ Exit();
+ }
+ }
+ else
+ {
+ ParentWindow.Fullscreen();
+ (Toplevel as MainWindow)?.ToggleExtraWidgets(false);
+ }
+ }
+ }
+ }
+
+ _toggleFullscreen = toggleFullscreen;
+
+ bool toggleDockedMode = keyboard.IsPressed(Key.F9);
+
+ if (toggleDockedMode != _toggleDockedMode)
+ {
+ if (toggleDockedMode)
+ {
+ ConfigurationState.Instance.System.EnableDockedMode.Value =
+ !ConfigurationState.Instance.System.EnableDockedMode.Value;
+ }
+ }
+
+ _toggleDockedMode = toggleDockedMode;
+
+ if (_hideCursorOnIdle && !ConfigurationState.Instance.Hid.EnableMouse)
+ {
+ long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
+ Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null;
+ }
+
+ if (ConfigurationState.Instance.Hid.EnableMouse && _isMouseInClient)
+ {
+ Window.Cursor = _invisibleCursor;
+ }
+ }
+
+ public void Initialize(Switch device)
+ {
+ Device = device;
+
+ IRenderer renderer = Device.Gpu.Renderer;
+
+ if (renderer is ThreadedRenderer tr)
+ {
+ renderer = tr.BaseRenderer;
+ }
+
+ Renderer = renderer;
+ Renderer?.Window?.SetSize(WindowWidth, WindowHeight);
+
+ if (Renderer != null)
+ {
+ Renderer.ScreenCaptured += Renderer_ScreenCaptured;
+ }
+
+ NpadManager.Initialize(device, ConfigurationState.Instance.Hid.InputConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse);
+ TouchScreenManager.Initialize(device);
+ }
+
+ private unsafe void Renderer_ScreenCaptured(object sender, ScreenCaptureImageInfo e)
+ {
+ if (e.Data.Length > 0 && e.Height > 0 && e.Width > 0)
+ {
+ Task.Run(() =>
+ {
+ lock (this)
+ {
+ var currentTime = DateTime.Now;
+ string filename = $"ryujinx_capture_{currentTime.Year}-{currentTime.Month:D2}-{currentTime.Day:D2}_{currentTime.Hour:D2}-{currentTime.Minute:D2}-{currentTime.Second:D2}.png";
+ string directory = AppDataManager.Mode switch
+ {
+ AppDataManager.LaunchMode.Portable => System.IO.Path.Combine(AppDataManager.BaseDirPath, "screenshots"),
+ _ => System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "Ryujinx")
+ };
+
+ string path = System.IO.Path.Combine(directory, filename);
+
+ try
+ {
+ Directory.CreateDirectory(directory);
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Failed to create directory at path {directory}. Error : {ex.GetType().Name}", "Screenshot");
+
+ return;
+ }
+
+ Image image = e.IsBgra ? Image.LoadPixelData<Bgra32>(e.Data, e.Width, e.Height)
+ : Image.LoadPixelData<Rgba32>(e.Data, e.Width, e.Height);
+
+ if (e.FlipX)
+ {
+ image.Mutate(x => x.Flip(FlipMode.Horizontal));
+ }
+
+ if (e.FlipY)
+ {
+ image.Mutate(x => x.Flip(FlipMode.Vertical));
+ }
+
+ image.SaveAsPng(path, new PngEncoder()
+ {
+ ColorType = PngColorType.Rgb
+ });
+
+ image.Dispose();
+
+ Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
+ }
+ });
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Screenshot is empty. Size : {e.Data.Length} bytes. Resolution : {e.Width}x{e.Height}", "Screenshot");
+ }
+ }
+
+ public void Render()
+ {
+ Gtk.Window parent = Toplevel as Gtk.Window;
+ parent.Present();
+
+ InitializeRenderer();
+
+ Device.Gpu.Renderer.Initialize(_glLogLevel);
+
+ Renderer.Window.SetAntiAliasing((Graphics.GAL.AntiAliasing)ConfigurationState.Instance.Graphics.AntiAliasing.Value);
+ Renderer.Window.SetScalingFilter((Graphics.GAL.ScalingFilter)ConfigurationState.Instance.Graphics.ScalingFilter.Value);
+ Renderer.Window.SetScalingFilterLevel(ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value);
+
+ _gpuBackendName = GetGpuBackendName();
+ _gpuVendorName = GetGpuVendorName();
+
+ Device.Gpu.Renderer.RunLoop(() =>
+ {
+ Device.Gpu.SetGpuThread();
+ Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
+ Translator.IsReadyForTranslation.Set();
+
+ Renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
+
+ (Toplevel as MainWindow)?.ActivatePauseMenu();
+
+ while (_isActive)
+ {
+ if (_isStopped)
+ {
+ return;
+ }
+
+ _ticks += _chrono.ElapsedTicks;
+
+ _chrono.Restart();
+
+ if (Device.WaitFifo())
+ {
+ Device.Statistics.RecordFifoStart();
+ Device.ProcessFrame();
+ Device.Statistics.RecordFifoEnd();
+ }
+
+ while (Device.ConsumeFrameAvailable())
+ {
+ Device.PresentFrame(SwapBuffers);
+ }
+
+ if (_ticks >= _ticksPerFrame)
+ {
+ string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? "Docked" : "Handheld";
+ float scale = GraphicsConfig.ResScale;
+ if (scale != 1)
+ {
+ dockedMode += $" ({scale}x)";
+ }
+
+ StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
+ Device.EnableDeviceVsync,
+ Device.GetVolume(),
+ _gpuBackendName,
+ dockedMode,
+ ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
+ $"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
+ $"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
+ $"GPU: {_gpuVendorName}"));
+
+ _ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
+ }
+ }
+ });
+ }
+
+ public void Start()
+ {
+ _chrono.Restart();
+
+ _isActive = true;
+
+ Gtk.Window parent = Toplevel as Gtk.Window;
+
+ Application.Invoke(delegate
+ {
+ parent.Present();
+
+ var activeProcess = Device.Processes.ActiveApplication;
+
+ string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : $" {activeProcess.Name}";
+ string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $" v{activeProcess.DisplayVersion}";
+ string titleIdSection = $" ({activeProcess.ProgramIdText.ToUpper()})";
+ string titleArchSection = activeProcess.Is64Bit ? " (64-bit)" : " (32-bit)";
+
+ parent.Title = $"Ryujinx {Program.Version} -{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}";
+ });
+
+ Thread renderLoopThread = new Thread(Render)
+ {
+ Name = "GUI.RenderLoop"
+ };
+ renderLoopThread.Start();
+
+ Thread nvStutterWorkaround = null;
+ if (Renderer is Graphics.OpenGL.OpenGLRenderer)
+ {
+ nvStutterWorkaround = new Thread(NVStutterWorkaround)
+ {
+ Name = "GUI.NVStutterWorkaround"
+ };
+ nvStutterWorkaround.Start();
+ }
+
+ MainLoop();
+
+ renderLoopThread.Join();
+ nvStutterWorkaround?.Join();
+
+ Exit();
+ }
+
+ public void Exit()
+ {
+ TouchScreenManager?.Dispose();
+ NpadManager?.Dispose();
+
+ if (_isStopped)
+ {
+ return;
+ }
+
+ _gpuCancellationTokenSource.Cancel();
+
+ _isStopped = true;
+
+ if (_isActive)
+ {
+ _isActive = false;
+
+ _exitEvent.WaitOne();
+ _exitEvent.Dispose();
+ }
+ }
+
+ private void NVStutterWorkaround()
+ {
+ while (_isActive)
+ {
+ // When NVIDIA Threaded Optimization is on, the driver will snapshot all threads in the system whenever the application creates any new ones.
+ // The ThreadPool has something called a "GateThread" which terminates itself after some inactivity.
+ // However, it immediately starts up again, since the rules regarding when to terminate and when to start differ.
+ // This creates a new thread every second or so.
+ // The main problem with this is that the thread snapshot can take 70ms, is on the OpenGL thread and will delay rendering any graphics.
+ // This is a little over budget on a frame time of 16ms, so creates a large stutter.
+ // The solution is to keep the ThreadPool active so that it never has a reason to terminate the GateThread.
+
+ // TODO: This should be removed when the issue with the GateThread is resolved.
+
+ ThreadPool.QueueUserWorkItem((state) => { });
+ Thread.Sleep(300);
+ }
+ }
+
+ public void MainLoop()
+ {
+ while (_isActive)
+ {
+ UpdateFrame();
+
+ // Polling becomes expensive if it's not slept
+ Thread.Sleep(1);
+ }
+
+ _exitEvent.Set();
+ }
+
+ private bool UpdateFrame()
+ {
+ if (!_isActive)
+ {
+ return true;
+ }
+
+ if (_isStopped)
+ {
+ return false;
+ }
+
+ if ((Toplevel as MainWindow).IsFocused)
+ {
+ Application.Invoke(delegate
+ {
+ KeyboardStateSnapshot keyboard = _keyboardInterface.GetKeyboardStateSnapshot();
+
+ HandleScreenState(keyboard);
+
+ if (keyboard.IsPressed(Key.Delete))
+ {
+ if (!ParentWindow.State.HasFlag(WindowState.Fullscreen))
+ {
+ Device.Processes.ActiveApplication.DiskCacheLoadState?.Cancel();
+ }
+ }
+ });
+ }
+
+ NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
+
+ if ((Toplevel as MainWindow).IsFocused)
+ {
+ KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleVSync))
+ {
+ Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
+ }
+
+ if ((currentHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Screenshot)) || ScreenshotRequested)
+ {
+ ScreenshotRequested = false;
+
+ Renderer.Screenshot();
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ShowUi) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ShowUi))
+ {
+ (Toplevel as MainWindow).ToggleExtraWidgets(true);
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.Pause) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.Pause))
+ {
+ (Toplevel as MainWindow)?.TogglePause();
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ToggleMute))
+ {
+ if (Device.IsAudioMuted())
+ {
+ Device.SetVolume(ConfigurationState.Instance.System.AudioVolume);
+ }
+ else
+ {
+ Device.SetVolume(0);
+ }
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleUp))
+ {
+ GraphicsConfig.ResScale = GraphicsConfig.ResScale % MaxResolutionScale + 1;
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.ResScaleDown))
+ {
+ GraphicsConfig.ResScale =
+ (MaxResolutionScale + GraphicsConfig.ResScale - 2) % MaxResolutionScale + 1;
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.VolumeUp) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.VolumeUp))
+ {
+ _newVolume = MathF.Round((Device.GetVolume() + VolumeDelta), 2);
+ Device.SetVolume(_newVolume);
+ }
+
+ if (currentHotkeyState.HasFlag(KeyboardHotkeyState.VolumeDown) &&
+ !_prevHotkeyState.HasFlag(KeyboardHotkeyState.VolumeDown))
+ {
+ _newVolume = MathF.Round((Device.GetVolume() - VolumeDelta), 2);
+ Device.SetVolume(_newVolume);
+ }
+
+ _prevHotkeyState = currentHotkeyState;
+ }
+
+ // Touchscreen
+ bool hasTouch = false;
+
+ // Get screen touch position
+ if ((Toplevel as MainWindow).IsFocused && !ConfigurationState.Instance.Hid.EnableMouse)
+ {
+ hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as GTK3MouseDriver).IsButtonPressed(MouseButton.Button1), ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
+ }
+
+ if (!hasTouch)
+ {
+ TouchScreenManager.Update(false);
+ }
+
+ Device.Hid.DebugPad.Update();
+
+ return true;
+ }
+
+ [Flags]
+ private enum KeyboardHotkeyState
+ {
+ None = 0,
+ ToggleVSync = 1 << 0,
+ Screenshot = 1 << 1,
+ ShowUi = 1 << 2,
+ Pause = 1 << 3,
+ ToggleMute = 1 << 4,
+ ResScaleUp = 1 << 5,
+ ResScaleDown = 1 << 6,
+ VolumeUp = 1 << 7,
+ VolumeDown = 1 << 8
+ }
+
+ private KeyboardHotkeyState GetHotkeyState()
+ {
+ KeyboardHotkeyState state = KeyboardHotkeyState.None;
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
+ {
+ state |= KeyboardHotkeyState.ToggleVSync;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
+ {
+ state |= KeyboardHotkeyState.Screenshot;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ShowUi))
+ {
+ state |= KeyboardHotkeyState.ShowUi;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Pause))
+ {
+ state |= KeyboardHotkeyState.Pause;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleMute))
+ {
+ state |= KeyboardHotkeyState.ToggleMute;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleUp))
+ {
+ state |= KeyboardHotkeyState.ResScaleUp;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ResScaleDown))
+ {
+ state |= KeyboardHotkeyState.ResScaleDown;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeUp))
+ {
+ state |= KeyboardHotkeyState.VolumeUp;
+ }
+
+ if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.VolumeDown))
+ {
+ state |= KeyboardHotkeyState.VolumeDown;
+ }
+
+ return state;
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/SPBOpenGLContext.cs b/src/Ryujinx/Ui/SPBOpenGLContext.cs
new file mode 100644
index 00000000..97644269
--- /dev/null
+++ b/src/Ryujinx/Ui/SPBOpenGLContext.cs
@@ -0,0 +1,47 @@
+using OpenTK.Graphics.OpenGL;
+using Ryujinx.Graphics.OpenGL;
+using SPB.Graphics;
+using SPB.Graphics.OpenGL;
+using SPB.Platform;
+using SPB.Windowing;
+
+namespace Ryujinx.Ui
+{
+ class SPBOpenGLContext : IOpenGLContext
+ {
+ private OpenGLContextBase _context;
+ private NativeWindowBase _window;
+
+ private SPBOpenGLContext(OpenGLContextBase context, NativeWindowBase window)
+ {
+ _context = context;
+ _window = window;
+ }
+
+ public void Dispose()
+ {
+ _context.Dispose();
+ _window.Dispose();
+ }
+
+ public void MakeCurrent()
+ {
+ _context.MakeCurrent(_window);
+ }
+
+ public static SPBOpenGLContext CreateBackgroundContext(OpenGLContextBase sharedContext)
+ {
+ OpenGLContextBase context = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, 3, 3, OpenGLContextFlags.Compat, true, sharedContext);
+ NativeWindowBase window = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100);
+
+ context.Initialize(window);
+ context.MakeCurrent(window);
+
+ GL.LoadBindings(new OpenToolkitBindingsContext(context));
+
+ context.MakeCurrent(null);
+
+ return new SPBOpenGLContext(context, window);
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs b/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs
new file mode 100644
index 00000000..046597b0
--- /dev/null
+++ b/src/Ryujinx/Ui/StatusUpdatedEventArgs.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace Ryujinx.Ui
+{
+ public class StatusUpdatedEventArgs : EventArgs
+ {
+ public bool VSyncEnabled;
+ public float Volume;
+ public string DockedMode;
+ public string AspectRatio;
+ public string GameStatus;
+ public string FifoStatus;
+ public string GpuName;
+ public string GpuBackend;
+
+ public StatusUpdatedEventArgs(bool vSyncEnabled, float volume, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName)
+ {
+ VSyncEnabled = vSyncEnabled;
+ Volume = volume;
+ GpuBackend = gpuBackend;
+ DockedMode = dockedMode;
+ AspectRatio = aspectRatio;
+ GameStatus = gameStatus;
+ FifoStatus = fifoStatus;
+ GpuName = gpuName;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/VKRenderer.cs b/src/Ryujinx/Ui/VKRenderer.cs
new file mode 100644
index 00000000..d2106c58
--- /dev/null
+++ b/src/Ryujinx/Ui/VKRenderer.cs
@@ -0,0 +1,93 @@
+using Gdk;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Input.HLE;
+using Ryujinx.Ui.Helper;
+using SPB.Graphics.Vulkan;
+using SPB.Platform.Metal;
+using SPB.Platform.Win32;
+using SPB.Platform.X11;
+using SPB.Windowing;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Ui
+{
+ public partial class VKRenderer : RendererWidgetBase
+ {
+ public NativeWindowBase NativeWindow { get; private set; }
+ private UpdateBoundsCallbackDelegate _updateBoundsCallback;
+
+ public VKRenderer(InputManager inputManager, GraphicsDebugLevel glLogLevel) : base(inputManager, glLogLevel) { }
+
+ private NativeWindowBase RetrieveNativeWindow()
+ {
+ if (OperatingSystem.IsWindows())
+ {
+ IntPtr windowHandle = gdk_win32_window_get_handle(Window.Handle);
+
+ return new SimpleWin32Window(new NativeHandle(windowHandle));
+ }
+ else if (OperatingSystem.IsLinux())
+ {
+ IntPtr displayHandle = gdk_x11_display_get_xdisplay(Display.Handle);
+ IntPtr windowHandle = gdk_x11_window_get_xid(Window.Handle);
+
+ return new SimpleX11Window(new NativeHandle(displayHandle), new NativeHandle(windowHandle));
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ IntPtr metalLayer = MetalHelper.GetMetalLayer(Display, Window, out IntPtr nsView, out _updateBoundsCallback);
+
+ return new SimpleMetalWindow(new NativeHandle(nsView), new NativeHandle(metalLayer));
+ }
+
+ throw new NotImplementedException();
+ }
+
+ [LibraryImport("libgdk-3-0.dll")]
+ private static partial IntPtr gdk_win32_window_get_handle(IntPtr d);
+
+ [LibraryImport("libgdk-3.so.0")]
+ private static partial IntPtr gdk_x11_display_get_xdisplay(IntPtr gdkDisplay);
+
+ [LibraryImport("libgdk-3.so.0")]
+ private static partial IntPtr gdk_x11_window_get_xid(IntPtr gdkWindow);
+
+ protected override bool OnConfigureEvent(EventConfigure evnt)
+ {
+ if (NativeWindow == null)
+ {
+ NativeWindow = RetrieveNativeWindow();
+
+ WaitEvent.Set();
+ }
+
+ bool result = base.OnConfigureEvent(evnt);
+
+ _updateBoundsCallback?.Invoke(Window);
+
+ return result;
+ }
+
+ public unsafe IntPtr CreateWindowSurface(IntPtr instance)
+ {
+ return VulkanHelper.CreateWindowSurface(instance, NativeWindow);
+ }
+
+ public override void InitializeRenderer() { }
+
+ public override void SwapBuffers() { }
+
+ protected override string GetGpuBackendName()
+ {
+ return "Vulkan";
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ Device?.DisposeGpu();
+
+ NpadManager.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs
new file mode 100644
index 00000000..5a0563d9
--- /dev/null
+++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.Designer.cs
@@ -0,0 +1,220 @@
+using Gtk;
+
+namespace Ryujinx.Ui.Widgets
+{
+ public partial class GameTableContextMenu : Menu
+ {
+ private MenuItem _openSaveUserDirMenuItem;
+ private MenuItem _openSaveDeviceDirMenuItem;
+ private MenuItem _openSaveBcatDirMenuItem;
+ private MenuItem _manageTitleUpdatesMenuItem;
+ private MenuItem _manageDlcMenuItem;
+ private MenuItem _manageCheatMenuItem;
+ private MenuItem _openTitleModDirMenuItem;
+ private MenuItem _openTitleSdModDirMenuItem;
+ private Menu _extractSubMenu;
+ private MenuItem _extractMenuItem;
+ private MenuItem _extractRomFsMenuItem;
+ private MenuItem _extractExeFsMenuItem;
+ private MenuItem _extractLogoMenuItem;
+ private Menu _manageSubMenu;
+ private MenuItem _manageCacheMenuItem;
+ private MenuItem _purgePtcCacheMenuItem;
+ private MenuItem _purgeShaderCacheMenuItem;
+ private MenuItem _openPtcDirMenuItem;
+ private MenuItem _openShaderCacheDirMenuItem;
+
+ private void InitializeComponent()
+ {
+ //
+ // _openSaveUserDirMenuItem
+ //
+ _openSaveUserDirMenuItem = new MenuItem("Open User Save Directory")
+ {
+ TooltipText = "Open the directory which contains Application's User Saves."
+ };
+ _openSaveUserDirMenuItem.Activated += OpenSaveUserDir_Clicked;
+
+ //
+ // _openSaveDeviceDirMenuItem
+ //
+ _openSaveDeviceDirMenuItem = new MenuItem("Open Device Save Directory")
+ {
+ TooltipText = "Open the directory which contains Application's Device Saves."
+ };
+ _openSaveDeviceDirMenuItem.Activated += OpenSaveDeviceDir_Clicked;
+
+ //
+ // _openSaveBcatDirMenuItem
+ //
+ _openSaveBcatDirMenuItem = new MenuItem("Open BCAT Save Directory")
+ {
+ TooltipText = "Open the directory which contains Application's BCAT Saves."
+ };
+ _openSaveBcatDirMenuItem.Activated += OpenSaveBcatDir_Clicked;
+
+ //
+ // _manageTitleUpdatesMenuItem
+ //
+ _manageTitleUpdatesMenuItem = new MenuItem("Manage Title Updates")
+ {
+ TooltipText = "Open the Title Update management window"
+ };
+ _manageTitleUpdatesMenuItem.Activated += ManageTitleUpdates_Clicked;
+
+ //
+ // _manageDlcMenuItem
+ //
+ _manageDlcMenuItem = new MenuItem("Manage DLC")
+ {
+ TooltipText = "Open the DLC management window"
+ };
+ _manageDlcMenuItem.Activated += ManageDlc_Clicked;
+
+ //
+ // _manageCheatMenuItem
+ //
+ _manageCheatMenuItem = new MenuItem("Manage Cheats")
+ {
+ TooltipText = "Open the Cheat management window"
+ };
+ _manageCheatMenuItem.Activated += ManageCheats_Clicked;
+
+ //
+ // _openTitleModDirMenuItem
+ //
+ _openTitleModDirMenuItem = new MenuItem("Open Mods Directory")
+ {
+ TooltipText = "Open the directory which contains Application's Mods."
+ };
+ _openTitleModDirMenuItem.Activated += OpenTitleModDir_Clicked;
+
+ //
+ // _openTitleSdModDirMenuItem
+ //
+ _openTitleSdModDirMenuItem = new MenuItem("Open Atmosphere Mods Directory")
+ {
+ TooltipText = "Open the alternative SD card atmosphere directory which contains the Application's Mods."
+ };
+ _openTitleSdModDirMenuItem.Activated += OpenTitleSdModDir_Clicked;
+
+ //
+ // _extractSubMenu
+ //
+ _extractSubMenu = new Menu();
+
+ //
+ // _extractMenuItem
+ //
+ _extractMenuItem = new MenuItem("Extract Data")
+ {
+ Submenu = _extractSubMenu
+ };
+
+ //
+ // _extractRomFsMenuItem
+ //
+ _extractRomFsMenuItem = new MenuItem("RomFS")
+ {
+ TooltipText = "Extract the RomFS section from Application's current config (including updates)."
+ };
+ _extractRomFsMenuItem.Activated += ExtractRomFs_Clicked;
+
+ //
+ // _extractExeFsMenuItem
+ //
+ _extractExeFsMenuItem = new MenuItem("ExeFS")
+ {
+ TooltipText = "Extract the ExeFS section from Application's current config (including updates)."
+ };
+ _extractExeFsMenuItem.Activated += ExtractExeFs_Clicked;
+
+ //
+ // _extractLogoMenuItem
+ //
+ _extractLogoMenuItem = new MenuItem("Logo")
+ {
+ TooltipText = "Extract the Logo section from Application's current config (including updates)."
+ };
+ _extractLogoMenuItem.Activated += ExtractLogo_Clicked;
+
+ //
+ // _manageSubMenu
+ //
+ _manageSubMenu = new Menu();
+
+ //
+ // _manageCacheMenuItem
+ //
+ _manageCacheMenuItem = new MenuItem("Cache Management")
+ {
+ Submenu = _manageSubMenu
+ };
+
+ //
+ // _purgePtcCacheMenuItem
+ //
+ _purgePtcCacheMenuItem = new MenuItem("Queue PPTC Rebuild")
+ {
+ TooltipText = "Trigger PPTC to rebuild at boot time on the next game launch."
+ };
+ _purgePtcCacheMenuItem.Activated += PurgePtcCache_Clicked;
+
+ //
+ // _purgeShaderCacheMenuItem
+ //
+ _purgeShaderCacheMenuItem = new MenuItem("Purge Shader Cache")
+ {
+ TooltipText = "Delete the Application's shader cache."
+ };
+ _purgeShaderCacheMenuItem.Activated += PurgeShaderCache_Clicked;
+
+ //
+ // _openPtcDirMenuItem
+ //
+ _openPtcDirMenuItem = new MenuItem("Open PPTC Directory")
+ {
+ TooltipText = "Open the directory which contains the Application's PPTC cache."
+ };
+ _openPtcDirMenuItem.Activated += OpenPtcDir_Clicked;
+
+ //
+ // _openShaderCacheDirMenuItem
+ //
+ _openShaderCacheDirMenuItem = new MenuItem("Open Shader Cache Directory")
+ {
+ TooltipText = "Open the directory which contains the Application's shader cache."
+ };
+ _openShaderCacheDirMenuItem.Activated += OpenShaderCacheDir_Clicked;
+
+ ShowComponent();
+ }
+
+ private void ShowComponent()
+ {
+ _extractSubMenu.Append(_extractExeFsMenuItem);
+ _extractSubMenu.Append(_extractRomFsMenuItem);
+ _extractSubMenu.Append(_extractLogoMenuItem);
+
+ _manageSubMenu.Append(_purgePtcCacheMenuItem);
+ _manageSubMenu.Append(_purgeShaderCacheMenuItem);
+ _manageSubMenu.Append(_openPtcDirMenuItem);
+ _manageSubMenu.Append(_openShaderCacheDirMenuItem);
+
+ Add(_openSaveUserDirMenuItem);
+ Add(_openSaveDeviceDirMenuItem);
+ Add(_openSaveBcatDirMenuItem);
+ Add(new SeparatorMenuItem());
+ Add(_manageTitleUpdatesMenuItem);
+ Add(_manageDlcMenuItem);
+ Add(_manageCheatMenuItem);
+ Add(_openTitleModDirMenuItem);
+ Add(_openTitleSdModDirMenuItem);
+ Add(new SeparatorMenuItem());
+ Add(_manageCacheMenuItem);
+ Add(_extractMenuItem);
+
+ ShowAll();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
new file mode 100644
index 00000000..558288aa
--- /dev/null
+++ b/src/Ryujinx/Ui/Widgets/GameTableContextMenu.cs
@@ -0,0 +1,602 @@
+using Gtk;
+using LibHac;
+using LibHac.Account;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.Fs.Shim;
+using LibHac.FsSystem;
+using LibHac.Ns;
+using LibHac.Tools.Fs;
+using LibHac.Tools.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.Ui.App.Common;
+using Ryujinx.Ui.Common.Configuration;
+using Ryujinx.Ui.Common.Helper;
+using Ryujinx.Ui.Windows;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+
+namespace Ryujinx.Ui.Widgets
+{
+ public partial class GameTableContextMenu : Menu
+ {
+ private readonly MainWindow _parent;
+ private readonly VirtualFileSystem _virtualFileSystem;
+ private readonly AccountManager _accountManager;
+ private readonly HorizonClient _horizonClient;
+ private readonly BlitStruct<ApplicationControlProperty> _controlData;
+
+ private readonly string _titleFilePath;
+ private readonly string _titleName;
+ private readonly string _titleIdText;
+ private readonly ulong _titleId;
+
+ private MessageDialog _dialog;
+ private bool _cancel;
+
+ public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct<ApplicationControlProperty> controlData)
+ {
+ _parent = parent;
+
+ InitializeComponent();
+
+ _virtualFileSystem = virtualFileSystem;
+ _accountManager = accountManager;
+ _horizonClient = horizonClient;
+ _titleFilePath = titleFilePath;
+ _titleName = titleName;
+ _titleIdText = titleId;
+ _controlData = controlData;
+
+ if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId))
+ {
+ GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id");
+
+ return;
+ }
+
+ _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
+ _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
+ _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0;
+
+ string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower();
+ bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
+
+ _extractRomFsMenuItem.Sensitive = hasNca;
+ _extractExeFsMenuItem.Sensitive = hasNca;
+ _extractLogoMenuItem.Sensitive = hasNca;
+
+ PopupAtPointer(null);
+ }
+
+ private bool TryFindSaveData(string titleName, ulong titleId, BlitStruct<ApplicationControlProperty> controlHolder, in SaveDataFilter filter, out ulong saveDataId)
+ {
+ saveDataId = default;
+
+ Result result = _horizonClient.Fs.FindSaveDataWithFilter(out SaveDataInfo saveDataInfo, SaveDataSpaceId.User, in filter);
+
+ if (ResultFs.TargetNotFound.Includes(result))
+ {
+ ref ApplicationControlProperty control = ref controlHolder.Value;
+
+ Logger.Info?.Print(LogClass.Application, $"Creating save directory for Title: {titleName} [{titleId:x16}]");
+
+ if (Utilities.IsZeros(controlHolder.ByteSpan))
+ {
+ // If the current application doesn't have a loaded control property, create a dummy one
+ // and set the savedata sizes so a user savedata will be created.
+ control = ref new BlitStruct<ApplicationControlProperty>(1).Value;
+
+ // The set sizes don't actually matter as long as they're non-zero because we use directory savedata.
+ control.UserAccountSaveDataSize = 0x4000;
+ control.UserAccountSaveDataJournalSize = 0x4000;
+
+ Logger.Warning?.Print(LogClass.Application, "No control file was found for this game. Using a dummy one instead. This may cause inaccuracies in some games.");
+ }
+
+ Uid user = new Uid((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
+
+ result = _horizonClient.Fs.EnsureApplicationSaveData(out _, new LibHac.Ncm.ApplicationId(titleId), in control, in user);
+
+ if (result.IsFailure())
+ {
+ GtkDialog.CreateErrorDialog($"There was an error creating the specified savedata: {result.ToStringWithName()}");
+
+ return false;
+ }
+
+ // Try to find the savedata again after creating it
+ result = _horizonClient.Fs.FindSaveDataWithFilter(out saveDataInfo, SaveDataSpaceId.User, in filter);
+ }
+
+ if (result.IsSuccess())
+ {
+ saveDataId = saveDataInfo.SaveDataId;
+
+ return true;
+ }
+
+ GtkDialog.CreateErrorDialog($"There was an error finding the specified savedata: {result.ToStringWithName()}");
+
+ return false;
+ }
+
+ private void OpenSaveDir(in SaveDataFilter saveDataFilter)
+ {
+ if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId))
+ {
+ return;
+ }
+
+ string saveRootPath = System.IO.Path.Combine(_virtualFileSystem.GetNandPath(), $"user/save/{saveDataId:x16}");
+
+ if (!Directory.Exists(saveRootPath))
+ {
+ // Inconsistent state. Create the directory
+ Directory.CreateDirectory(saveRootPath);
+ }
+
+ string committedPath = System.IO.Path.Combine(saveRootPath, "0");
+ string workingPath = System.IO.Path.Combine(saveRootPath, "1");
+
+ // If the committed directory exists, that path will be loaded the next time the savedata is mounted
+ if (Directory.Exists(committedPath))
+ {
+ OpenHelper.OpenFolder(committedPath);
+ }
+ else
+ {
+ // If the working directory exists and the committed directory doesn't,
+ // the working directory will be loaded the next time the savedata is mounted
+ if (!Directory.Exists(workingPath))
+ {
+ Directory.CreateDirectory(workingPath);
+ }
+
+ OpenHelper.OpenFolder(workingPath);
+ }
+ }
+
+ private void ExtractSection(NcaSectionType ncaSectionType, int programIndex = 0)
+ {
+ FileChooserNative fileChooser = new FileChooserNative("Choose the folder to extract into", _parent, FileChooserAction.SelectFolder, "Extract", "Cancel");
+
+ ResponseType response = (ResponseType)fileChooser.Run();
+ string destination = fileChooser.Filename;
+
+ fileChooser.Dispose();
+
+ if (response == ResponseType.Accept)
+ {
+ Thread extractorThread = new Thread(() =>
+ {
+ Gtk.Application.Invoke(delegate
+ {
+ _dialog = new MessageDialog(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Cancel, null)
+ {
+ Title = "Ryujinx - NCA Section Extractor",
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"),
+ SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...",
+ WindowPosition = WindowPosition.Center
+ };
+
+ int dialogResponse = _dialog.Run();
+ if (dialogResponse == (int)ResponseType.Cancel || dialogResponse == (int)ResponseType.DeleteEvent)
+ {
+ _cancel = true;
+ _dialog.Dispose();
+ }
+ });
+
+ using (FileStream file = new FileStream(_titleFilePath, FileMode.Open, FileAccess.Read))
+ {
+ Nca mainNca = null;
+ Nca patchNca = null;
+
+ if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") ||
+ (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") ||
+ (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci"))
+ {
+ PartitionFileSystem pfs;
+
+ if (System.IO.Path.GetExtension(_titleFilePath) == ".xci")
+ {
+ Xci xci = new Xci(_virtualFileSystem.KeySet, file.AsStorage());
+
+ pfs = xci.OpenPartition(XciPartitionType.Secure);
+ }
+ else
+ {
+ pfs = new PartitionFileSystem(file.AsStorage());
+ }
+
+ foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
+ {
+ using var ncaFile = new UniqueRef<IFile>();
+
+ pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ Nca nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Release().AsStorage());
+
+ if (nca.Header.ContentType == NcaContentType.Program)
+ {
+ int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
+
+ if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())
+ {
+ patchNca = nca;
+ }
+ else
+ {
+ mainNca = nca;
+ }
+ }
+ }
+ }
+ else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca")
+ {
+ mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
+ }
+
+ if (mainNca == null)
+ {
+ Logger.Error?.Print(LogClass.Application, "Extraction failure. The main NCA is not present in the selected file.");
+
+ Gtk.Application.Invoke(delegate
+ {
+ GtkDialog.CreateErrorDialog("Extraction failure. The main NCA is not present in the selected file.");
+ });
+
+ return;
+ }
+
+ (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
+
+ if (updatePatchNca != null)
+ {
+ patchNca = updatePatchNca;
+ }
+
+ int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType);
+
+ IFileSystem ncaFileSystem = patchNca != null ? mainNca.OpenFileSystemWithPatch(patchNca, index, IntegrityCheckLevel.ErrorOnInvalid)
+ : mainNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid);
+
+ FileSystemClient fsClient = _horizonClient.Fs;
+
+ string source = DateTime.Now.ToFileTime().ToString()[10..];
+ string output = DateTime.Now.ToFileTime().ToString()[10..];
+
+ using var uniqueSourceFs = new UniqueRef<IFileSystem>(ncaFileSystem);
+ using var uniqueOutputFs = new UniqueRef<IFileSystem>(new LocalFileSystem(destination));
+
+ fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref);
+ fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref);
+
+ (Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/");
+
+ if (!canceled)
+ {
+ if (resultCode.Value.IsFailure())
+ {
+ Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}");
+
+ Gtk.Application.Invoke(delegate
+ {
+ _dialog?.Dispose();
+
+ GtkDialog.CreateErrorDialog("Extraction failed. Read the log file for further information.");
+ });
+ }
+ else if (resultCode.Value.IsSuccess())
+ {
+ Gtk.Application.Invoke(delegate
+ {
+ _dialog?.Dispose();
+
+ MessageDialog dialog = new MessageDialog(null, DialogFlags.DestroyWithParent, MessageType.Info, ButtonsType.Ok, null)
+ {
+ Title = "Ryujinx - NCA Section Extractor",
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png"),
+ SecondaryText = "Extraction completed successfully.",
+ WindowPosition = WindowPosition.Center
+ };
+
+ dialog.Run();
+ dialog.Dispose();
+ });
+ }
+ }
+
+ fsClient.Unmount(source.ToU8Span());
+ fsClient.Unmount(output.ToU8Span());
+ }
+ });
+
+ extractorThread.Name = "GUI.NcaSectionExtractorThread";
+ extractorThread.IsBackground = true;
+ extractorThread.Start();
+ }
+ }
+
+ private (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath)
+ {
+ Result rc = fs.OpenDirectory(out DirectoryHandle sourceHandle, sourcePath.ToU8Span(), OpenDirectoryMode.All);
+ if (rc.IsFailure()) return (rc, false);
+
+ using (sourceHandle)
+ {
+ foreach (DirectoryEntryEx entry in fs.EnumerateEntries(sourcePath, "*", SearchOptions.Default))
+ {
+ if (_cancel)
+ {
+ return (null, true);
+ }
+
+ string subSrcPath = PathTools.Normalize(PathTools.Combine(sourcePath, entry.Name));
+ string subDstPath = PathTools.Normalize(PathTools.Combine(destPath, entry.Name));
+
+ if (entry.Type == DirectoryEntryType.Directory)
+ {
+ fs.EnsureDirectoryExists(subDstPath);
+
+ (Result? result, bool canceled) = CopyDirectory(fs, subSrcPath, subDstPath);
+ if (canceled || result.Value.IsFailure())
+ {
+ return (result, canceled);
+ }
+ }
+
+ if (entry.Type == DirectoryEntryType.File)
+ {
+ fs.CreateOrOverwriteFile(subDstPath, entry.Size);
+
+ rc = CopyFile(fs, subSrcPath, subDstPath);
+ if (rc.IsFailure()) return (rc, false);
+ }
+ }
+ }
+
+ return (Result.Success, false);
+ }
+
+ public Result CopyFile(FileSystemClient fs, string sourcePath, string destPath)
+ {
+ Result rc = fs.OpenFile(out FileHandle sourceHandle, sourcePath.ToU8Span(), OpenMode.Read);
+ if (rc.IsFailure()) return rc;
+
+ using (sourceHandle)
+ {
+ rc = fs.OpenFile(out FileHandle destHandle, destPath.ToU8Span(), OpenMode.Write | OpenMode.AllowAppend);
+ if (rc.IsFailure()) return rc;
+
+ using (destHandle)
+ {
+ const int maxBufferSize = 1024 * 1024;
+
+ rc = fs.GetFileSize(out long fileSize, sourceHandle);
+ if (rc.IsFailure()) return rc;
+
+ int bufferSize = (int)Math.Min(maxBufferSize, fileSize);
+
+ byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
+ try
+ {
+ for (long offset = 0; offset < fileSize; offset += bufferSize)
+ {
+ int toRead = (int)Math.Min(fileSize - offset, bufferSize);
+ Span<byte> buf = buffer.AsSpan(0, toRead);
+
+ rc = fs.ReadFile(out long _, sourceHandle, offset, buf);
+ if (rc.IsFailure()) return rc;
+
+ rc = fs.WriteFile(destHandle, offset, buf, WriteOption.None);
+ if (rc.IsFailure()) return rc;
+ }
+ }
+ finally
+ {
+ ArrayPool<byte>.Shared.Return(buffer);
+ }
+
+ rc = fs.FlushFile(destHandle);
+ if (rc.IsFailure()) return rc;
+ }
+ }
+
+ return Result.Success;
+ }
+
+ //
+ // Events
+ //
+ private void OpenSaveUserDir_Clicked(object sender, EventArgs args)
+ {
+ var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
+ var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default);
+
+ OpenSaveDir(in saveDataFilter);
+ }
+
+ private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
+ {
+ var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default);
+
+ OpenSaveDir(in saveDataFilter);
+ }
+
+ private void OpenSaveBcatDir_Clicked(object sender, EventArgs args)
+ {
+ var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
+
+ OpenSaveDir(in saveDataFilter);
+ }
+
+ private void ManageTitleUpdates_Clicked(object sender, EventArgs args)
+ {
+ new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show();
+ }
+
+ private void ManageDlc_Clicked(object sender, EventArgs args)
+ {
+ new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show();
+ }
+
+ private void ManageCheats_Clicked(object sender, EventArgs args)
+ {
+ new CheatWindow(_virtualFileSystem, _titleId, _titleName).Show();
+ }
+
+ private void OpenTitleModDir_Clicked(object sender, EventArgs args)
+ {
+ string modsBasePath = _virtualFileSystem.ModLoader.GetModsBasePath();
+ string titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, _titleIdText);
+
+ OpenHelper.OpenFolder(titleModsPath);
+ }
+
+ private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
+ {
+ string sdModsBasePath = _virtualFileSystem.ModLoader.GetSdModsBasePath();
+ string titleModsPath = _virtualFileSystem.ModLoader.GetTitleDir(sdModsBasePath, _titleIdText);
+
+ OpenHelper.OpenFolder(titleModsPath);
+ }
+
+ private void ExtractRomFs_Clicked(object sender, EventArgs args)
+ {
+ ExtractSection(NcaSectionType.Data);
+ }
+
+ private void ExtractExeFs_Clicked(object sender, EventArgs args)
+ {
+ ExtractSection(NcaSectionType.Code);
+ }
+
+ private void ExtractLogo_Clicked(object sender, EventArgs args)
+ {
+ ExtractSection(NcaSectionType.Logo);
+ }
+
+ private void OpenPtcDir_Clicked(object sender, EventArgs args)
+ {
+ string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu");
+
+ string mainPath = System.IO.Path.Combine(ptcDir, "0");
+ string backupPath = System.IO.Path.Combine(ptcDir, "1");
+
+ if (!Directory.Exists(ptcDir))
+ {
+ Directory.CreateDirectory(ptcDir);
+ Directory.CreateDirectory(mainPath);
+ Directory.CreateDirectory(backupPath);
+ }
+
+ OpenHelper.OpenFolder(ptcDir);
+ }
+
+ private void OpenShaderCacheDir_Clicked(object sender, EventArgs args)
+ {
+ string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader");
+
+ if (!Directory.Exists(shaderCacheDir))
+ {
+ Directory.CreateDirectory(shaderCacheDir);
+ }
+
+ OpenHelper.OpenFolder(shaderCacheDir);
+ }
+
+ private void PurgePtcCache_Clicked(object sender, EventArgs args)
+ {
+ DirectoryInfo mainDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0"));
+ DirectoryInfo backupDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1"));
+
+ MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n<b>{_titleName}</b>\n\nAre you sure you want to proceed?");
+
+ List<FileInfo> cacheFiles = new List<FileInfo>();
+
+ if (mainDir.Exists)
+ {
+ cacheFiles.AddRange(mainDir.EnumerateFiles("*.cache"));
+ }
+
+ if (backupDir.Exists)
+ {
+ cacheFiles.AddRange(backupDir.EnumerateFiles("*.cache"));
+ }
+
+ if (cacheFiles.Count > 0 && warningDialog.Run() == (int)ResponseType.Yes)
+ {
+ foreach (FileInfo file in cacheFiles)
+ {
+ try
+ {
+ file.Delete();
+ }
+ catch(Exception e)
+ {
+ GtkDialog.CreateErrorDialog($"Error purging PPTC cache {file.Name}: {e}");
+ }
+ }
+ }
+
+ warningDialog.Dispose();
+ }
+
+ private void PurgeShaderCache_Clicked(object sender, EventArgs args)
+ {
+ DirectoryInfo shaderCacheDir = new DirectoryInfo(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"));
+
+ using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n<b>{_titleName}</b>\n\nAre you sure you want to proceed?");
+
+ List<DirectoryInfo> oldCacheDirectories = new List<DirectoryInfo>();
+ List<FileInfo> newCacheFiles = new List<FileInfo>();
+
+ if (shaderCacheDir.Exists)
+ {
+ oldCacheDirectories.AddRange(shaderCacheDir.EnumerateDirectories("*"));
+ newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.toc"));
+ newCacheFiles.AddRange(shaderCacheDir.GetFiles("*.data"));
+ }
+
+ if ((oldCacheDirectories.Count > 0 || newCacheFiles.Count > 0) && warningDialog.Run() == (int)ResponseType.Yes)
+ {
+ foreach (DirectoryInfo directory in oldCacheDirectories)
+ {
+ try
+ {
+ directory.Delete(true);
+ }
+ catch (Exception e)
+ {
+ GtkDialog.CreateErrorDialog($"Error purging shader cache at {directory.Name}: {e}");
+ }
+ }
+
+ foreach (FileInfo file in newCacheFiles)
+ {
+ try
+ {
+ file.Delete();
+ }
+ catch (Exception e)
+ {
+ GtkDialog.CreateErrorDialog($"Error purging shader cache at {file.Name}: {e}");
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/Widgets/GtkDialog.cs b/src/Ryujinx/Ui/Widgets/GtkDialog.cs
new file mode 100644
index 00000000..d2cab219
--- /dev/null
+++ b/src/Ryujinx/Ui/Widgets/GtkDialog.cs
@@ -0,0 +1,114 @@
+using Gtk;
+using Ryujinx.Common.Logging;
+using Ryujinx.Ui.Common.Configuration;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace Ryujinx.Ui.Widgets
+{
+ internal class GtkDialog : MessageDialog
+ {
+ private static bool _isChoiceDialogOpen;
+
+ private GtkDialog(string title, string mainText, string secondaryText, MessageType messageType = MessageType.Other, ButtonsType buttonsType = ButtonsType.Ok)
+ : base(null, DialogFlags.Modal, messageType, buttonsType, null)
+ {
+ Title = title;
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+ Text = mainText;
+ SecondaryText = secondaryText;
+ WindowPosition = WindowPosition.Center;
+ SecondaryUseMarkup = true;
+
+ Response += GtkDialog_Response;
+
+ SetSizeRequest(200, 20);
+ }
+
+ private void GtkDialog_Response(object sender, ResponseArgs args)
+ {
+ Dispose();
+ }
+
+ internal static void CreateInfoDialog(string mainText, string secondaryText)
+ {
+ new GtkDialog("Ryujinx - Info", mainText, secondaryText, MessageType.Info).Run();
+ }
+
+ internal static void CreateUpdaterInfoDialog(string mainText, string secondaryText)
+ {
+ new GtkDialog("Ryujinx - Updater", mainText, secondaryText, MessageType.Info).Run();
+ }
+
+ internal static MessageDialog CreateWaitingDialog(string mainText, string secondaryText)
+ {
+ return new GtkDialog("Ryujinx - Waiting", mainText, secondaryText, MessageType.Info, ButtonsType.None);
+ }
+
+ internal static void CreateWarningDialog(string mainText, string secondaryText)
+ {
+ new GtkDialog("Ryujinx - Warning", mainText, secondaryText, MessageType.Warning).Run();
+ }
+
+ internal static void CreateErrorDialog(string errorMessage)
+ {
+ Logger.Error?.Print(LogClass.Application, errorMessage);
+
+ new GtkDialog("Ryujinx - Error", "Ryujinx has encountered an error", errorMessage, MessageType.Error).Run();
+ }
+
+ internal static MessageDialog CreateConfirmationDialog(string mainText, string secondaryText = "")
+ {
+ return new GtkDialog("Ryujinx - Confirmation", mainText, secondaryText, MessageType.Question, ButtonsType.YesNo);
+ }
+
+ internal static bool CreateChoiceDialog(string title, string mainText, string secondaryText)
+ {
+ if (_isChoiceDialogOpen)
+ {
+ return false;
+ }
+
+ _isChoiceDialogOpen = true;
+
+ ResponseType response = (ResponseType)new GtkDialog(title, mainText, secondaryText, MessageType.Question, ButtonsType.YesNo).Run();
+
+ _isChoiceDialogOpen = false;
+
+ return response == ResponseType.Yes;
+ }
+
+ internal static ResponseType CreateCustomDialog(string title, string mainText, string secondaryText, Dictionary<int, string> buttons, MessageType messageType = MessageType.Other)
+ {
+ GtkDialog gtkDialog = new GtkDialog(title, mainText, secondaryText, messageType, ButtonsType.None);
+
+ foreach (var button in buttons)
+ {
+ gtkDialog.AddButton(button.Value, button.Key);
+ }
+
+ return (ResponseType)gtkDialog.Run();
+ }
+
+ internal static string CreateInputDialog(Window parent, string title, string mainText, uint inputMax)
+ {
+ GtkInputDialog gtkDialog = new GtkInputDialog(parent, title, mainText, inputMax);
+ ResponseType response = (ResponseType)gtkDialog.Run();
+ string responseText = gtkDialog.InputEntry.Text.TrimEnd();
+
+ gtkDialog.Dispose();
+
+ if (response == ResponseType.Ok)
+ {
+ return responseText;
+ }
+
+ return "";
+ }
+
+ internal static bool CreateExitDialog()
+ {
+ return CreateChoiceDialog("Ryujinx - Exit", "Are you sure you want to close Ryujinx?", "All unsaved data will be lost!");
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/Widgets/GtkInputDialog.cs b/src/Ryujinx/Ui/Widgets/GtkInputDialog.cs
new file mode 100644
index 00000000..21b34937
--- /dev/null
+++ b/src/Ryujinx/Ui/Widgets/GtkInputDialog.cs
@@ -0,0 +1,37 @@
+using Gtk;
+
+namespace Ryujinx.Ui.Widgets
+{
+ public class GtkInputDialog : MessageDialog
+ {
+ public Entry InputEntry { get; }
+
+ public GtkInputDialog(Window parent, string title, string mainText, uint inputMax) : base(parent, DialogFlags.Modal | DialogFlags.DestroyWithParent, MessageType.Question, ButtonsType.OkCancel, null)
+ {
+ SetDefaultSize(300, 0);
+
+ Title = title;
+
+ Label mainTextLabel = new Label
+ {
+ Text = mainText
+ };
+
+ InputEntry = new Entry
+ {
+ MaxLength = (int)inputMax
+ };
+
+ Label inputMaxTextLabel = new Label
+ {
+ Text = $"(Max length: {inputMax})"
+ };
+
+ ((Box)MessageArea).PackStart(mainTextLabel, true, true, 0);
+ ((Box)MessageArea).PackStart(InputEntry, true, true, 5);
+ ((Box)MessageArea).PackStart(inputMaxTextLabel, true, true, 0);
+
+ ShowAll();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Widgets/ProfileDialog.cs b/src/Ryujinx/Ui/Widgets/ProfileDialog.cs
new file mode 100644
index 00000000..242e8bd7
--- /dev/null
+++ b/src/Ryujinx/Ui/Widgets/ProfileDialog.cs
@@ -0,0 +1,57 @@
+using Gtk;
+using Ryujinx.Ui.Common.Configuration;
+using System;
+using System.Reflection;
+using GUI = Gtk.Builder.ObjectAttribute;
+
+namespace Ryujinx.Ui.Widgets
+{
+ public class ProfileDialog : Dialog
+ {
+ public string FileName { get; private set; }
+
+#pragma warning disable CS0649, IDE0044
+ [GUI] Entry _profileEntry;
+ [GUI] Label _errorMessage;
+#pragma warning restore CS0649, IDE0044
+
+ public ProfileDialog() : this(new Builder("Ryujinx.Ui.Widgets.ProfileDialog.glade")) { }
+
+ private ProfileDialog(Builder builder) : base(builder.GetRawOwnedObject("_profileDialog"))
+ {
+ builder.Autoconnect(this);
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+ }
+
+ private void OkToggle_Activated(object sender, EventArgs args)
+ {
+ ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
+
+ bool validFileName = true;
+
+ foreach (char invalidChar in System.IO.Path.GetInvalidFileNameChars())
+ {
+ if (_profileEntry.Text.Contains(invalidChar))
+ {
+ validFileName = false;
+ }
+ }
+
+ if (validFileName && !string.IsNullOrEmpty(_profileEntry.Text))
+ {
+ FileName = $"{_profileEntry.Text}.json";
+
+ Respond(ResponseType.Ok);
+ }
+ else
+ {
+ _errorMessage.Text = "The file name contains invalid characters. Please try again.";
+ }
+ }
+
+ private void CancelToggle_Activated(object sender, EventArgs args)
+ {
+ Respond(ResponseType.Cancel);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Widgets/ProfileDialog.glade b/src/Ryujinx/Ui/Widgets/ProfileDialog.glade
new file mode 100644
index 00000000..adaf6608
--- /dev/null
+++ b/src/Ryujinx/Ui/Widgets/ProfileDialog.glade
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkDialog" id="_profileDialog">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Ryujinx - Profile Dialog</property>
+ <property name="modal">True</property>
+ <property name="window_position">center</property>
+ <property name="default_width">400</property>
+ <property name="type_hint">dialog</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child internal-child="vbox">
+ <object class="GtkBox">
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">2</property>
+ <child internal-child="action_area">
+ <object class="GtkButtonBox">
+ <property name="can_focus">False</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkToggleButton" id="OkToggle">
+ <property name="label" translatable="yes">OK</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="toggled" handler="OkToggle_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="CancelToggle">
+ <property name="label" translatable="yes">Cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="toggled" handler="CancelToggle_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">20</property>
+ <property name="margin_bottom">10</property>
+ <property name="label" translatable="yes">Enter a name for the new profile:</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="_profileEntry">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_left">20</property>
+ <property name="margin_right">20</property>
+ <property name="margin_top">20</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="_errorMessage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin_left">20</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <attributes>
+ <attribute name="foreground" value="#ffff00000000"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/src/Ryujinx/Ui/Widgets/RawInputToTextEntry.cs b/src/Ryujinx/Ui/Widgets/RawInputToTextEntry.cs
new file mode 100644
index 00000000..a0092f29
--- /dev/null
+++ b/src/Ryujinx/Ui/Widgets/RawInputToTextEntry.cs
@@ -0,0 +1,27 @@
+using Gtk;
+
+namespace Ryujinx.Ui.Widgets
+{
+ public class RawInputToTextEntry : Entry
+ {
+ public void SendKeyPressEvent(object o, KeyPressEventArgs args)
+ {
+ base.OnKeyPressEvent(args.Event);
+ }
+
+ public void SendKeyReleaseEvent(object o, KeyReleaseEventArgs args)
+ {
+ base.OnKeyReleaseEvent(args.Event);
+ }
+
+ public void SendButtonPressEvent(object o, ButtonPressEventArgs args)
+ {
+ base.OnButtonPressEvent(args.Event);
+ }
+
+ public void SendButtonReleaseEvent(object o, ButtonReleaseEventArgs args)
+ {
+ base.OnButtonReleaseEvent(args.Event);
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/Widgets/UserErrorDialog.cs b/src/Ryujinx/Ui/Widgets/UserErrorDialog.cs
new file mode 100644
index 00000000..fc3938d4
--- /dev/null
+++ b/src/Ryujinx/Ui/Widgets/UserErrorDialog.cs
@@ -0,0 +1,123 @@
+using Gtk;
+using Ryujinx.Ui.Common;
+using Ryujinx.Ui.Common.Helper;
+
+namespace Ryujinx.Ui.Widgets
+{
+ internal class UserErrorDialog : MessageDialog
+ {
+ private const string SetupGuideUrl = "https://github.com/Ryujinx/Ryujinx/wiki/Ryujinx-Setup-&-Configuration-Guide";
+ private const int OkResponseId = 0;
+ private const int SetupGuideResponseId = 1;
+
+ private readonly UserError _userError;
+
+ private UserErrorDialog(UserError error) : base(null, DialogFlags.Modal, MessageType.Error, ButtonsType.None, null)
+ {
+ _userError = error;
+
+ WindowPosition = WindowPosition.Center;
+ SecondaryUseMarkup = true;
+
+ Response += UserErrorDialog_Response;
+
+ SetSizeRequest(120, 50);
+
+ AddButton("OK", OkResponseId);
+
+ bool isInSetupGuide = IsCoveredBySetupGuide(error);
+
+ if (isInSetupGuide)
+ {
+ AddButton("Open the Setup Guide", SetupGuideResponseId);
+ }
+
+ string errorCode = GetErrorCode(error);
+
+ SecondaryUseMarkup = true;
+
+ Title = $"Ryujinx error ({errorCode})";
+ Text = $"{errorCode}: {GetErrorTitle(error)}";
+ SecondaryText = GetErrorDescription(error);
+
+ if (isInSetupGuide)
+ {
+ SecondaryText += "\n<b>For more information on how to fix this error, follow our Setup Guide.</b>";
+ }
+ }
+
+ private string GetErrorCode(UserError error)
+ {
+ return $"RYU-{(uint)error:X4}";
+ }
+
+ private string GetErrorTitle(UserError error)
+ {
+ return error switch
+ {
+ UserError.NoKeys => "Keys not found",
+ UserError.NoFirmware => "Firmware not found",
+ UserError.FirmwareParsingFailed => "Firmware parsing error",
+ UserError.ApplicationNotFound => "Application not found",
+ UserError.Unknown => "Unknown error",
+ _ => "Undefined error",
+ };
+ }
+
+ private string GetErrorDescription(UserError error)
+ {
+ return error switch
+ {
+ UserError.NoKeys => "Ryujinx was unable to find your 'prod.keys' file",
+ UserError.NoFirmware => "Ryujinx was unable to find any firmwares installed",
+ UserError.FirmwareParsingFailed => "Ryujinx was unable to parse the provided firmware. This is usually caused by outdated keys.",
+ UserError.ApplicationNotFound => "Ryujinx couldn't find a valid application at the given path.",
+ UserError.Unknown => "An unknown error occured!",
+ _ => "An undefined error occured! This shouldn't happen, please contact a dev!",
+ };
+ }
+
+ private static bool IsCoveredBySetupGuide(UserError error)
+ {
+ return error switch
+ {
+ UserError.NoKeys or
+ UserError.NoFirmware or
+ UserError.FirmwareParsingFailed => true,
+ _ => false,
+ };
+ }
+
+ private static string GetSetupGuideUrl(UserError error)
+ {
+ if (!IsCoveredBySetupGuide(error))
+ {
+ return null;
+ }
+
+ return error switch
+ {
+ UserError.NoKeys => SetupGuideUrl + "#initial-setup---placement-of-prodkeys",
+ UserError.NoFirmware => SetupGuideUrl + "#initial-setup-continued---installation-of-firmware",
+ _ => SetupGuideUrl,
+ };
+ }
+
+ private void UserErrorDialog_Response(object sender, ResponseArgs args)
+ {
+ int responseId = (int)args.ResponseId;
+
+ if (responseId == SetupGuideResponseId)
+ {
+ OpenHelper.OpenUrl(GetSetupGuideUrl(_userError));
+ }
+
+ Dispose();
+ }
+
+ public static void CreateUserErrorDialog(UserError error)
+ {
+ new UserErrorDialog(error).Run();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Windows/AboutWindow.Designer.cs b/src/Ryujinx/Ui/Windows/AboutWindow.Designer.cs
new file mode 100644
index 00000000..fa1a0657
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/AboutWindow.Designer.cs
@@ -0,0 +1,493 @@
+using Gtk;
+using Pango;
+using Ryujinx.Ui.Common.Configuration;
+using System.Reflection;
+
+namespace Ryujinx.Ui.Windows
+{
+ public partial class AboutWindow : Window
+ {
+ private Box _mainBox;
+ private Box _leftBox;
+ private Box _logoBox;
+ private Image _ryujinxLogo;
+ private Box _logoTextBox;
+ private Label _ryujinxLabel;
+ private Label _ryujinxPhoneticLabel;
+ private EventBox _ryujinxLink;
+ private Label _ryujinxLinkLabel;
+ private Label _versionLabel;
+ private Label _disclaimerLabel;
+ private EventBox _amiiboApiLink;
+ private Label _amiiboApiLinkLabel;
+ private Box _socialBox;
+ private EventBox _patreonEventBox;
+ private Box _patreonBox;
+ private Image _patreonLogo;
+ private Label _patreonLabel;
+ private EventBox _githubEventBox;
+ private Box _githubBox;
+ private Image _githubLogo;
+ private Label _githubLabel;
+ private Box _discordBox;
+ private EventBox _discordEventBox;
+ private Image _discordLogo;
+ private Label _discordLabel;
+ private EventBox _twitterEventBox;
+ private Box _twitterBox;
+ private Image _twitterLogo;
+ private Label _twitterLabel;
+ private Separator _separator;
+ private Box _rightBox;
+ private Label _aboutLabel;
+ private Label _aboutDescriptionLabel;
+ private Label _createdByLabel;
+ private TextView _createdByText;
+ private EventBox _contributorsEventBox;
+ private Label _contributorsLinkLabel;
+ private Label _patreonNamesLabel;
+ private ScrolledWindow _patreonNamesScrolled;
+ private TextView _patreonNamesText;
+
+ private void InitializeComponent()
+ {
+
+#pragma warning disable CS0612
+
+ //
+ // AboutWindow
+ //
+ CanFocus = false;
+ Resizable = false;
+ Modal = true;
+ WindowPosition = WindowPosition.Center;
+ DefaultWidth = 800;
+ DefaultHeight = 450;
+ TypeHint = Gdk.WindowTypeHint.Dialog;
+
+ //
+ // _mainBox
+ //
+ _mainBox = new Box(Orientation.Horizontal, 0);
+
+ //
+ // _leftBox
+ //
+ _leftBox = new Box(Orientation.Vertical, 0)
+ {
+ Margin = 15,
+ MarginLeft = 30,
+ MarginRight = 0
+ };
+
+ //
+ // _logoBox
+ //
+ _logoBox = new Box(Orientation.Horizontal, 0);
+
+ //
+ // _ryujinxLogo
+ //
+ _ryujinxLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png", 100, 100))
+ {
+ Margin = 10,
+ MarginLeft = 15
+ };
+
+ //
+ // _logoTextBox
+ //
+ _logoTextBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _ryujinxLabel
+ //
+ _ryujinxLabel = new Label("Ryujinx")
+ {
+ MarginTop = 15,
+ Justify = Justification.Center,
+ Attributes = new AttrList()
+ };
+ _ryujinxLabel.Attributes.Insert(new Pango.AttrScale(2.7f));
+
+ //
+ // _ryujinxPhoneticLabel
+ //
+ _ryujinxPhoneticLabel = new Label("(REE-YOU-JINX)")
+ {
+ Justify = Justification.Center
+ };
+
+ //
+ // _ryujinxLink
+ //
+ _ryujinxLink = new EventBox()
+ {
+ Margin = 5
+ };
+ _ryujinxLink.ButtonPressEvent += RyujinxButton_Pressed;
+
+ //
+ // _ryujinxLinkLabel
+ //
+ _ryujinxLinkLabel = new Label("www.ryujinx.org")
+ {
+ TooltipText = "Click to open the Ryujinx website in your default browser.",
+ Justify = Justification.Center,
+ Attributes = new AttrList()
+ };
+ _ryujinxLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
+
+ //
+ // _versionLabel
+ //
+ _versionLabel = new Label(Program.Version)
+ {
+ Expand = true,
+ Justify = Justification.Center,
+ Margin = 5
+ };
+
+ //
+ // _disclaimerLabel
+ //
+ _disclaimerLabel = new Label("Ryujinx is not affiliated with Nintendo™,\nor any of its partners, in any way.")
+ {
+ Expand = true,
+ Justify = Justification.Center,
+ Margin = 5,
+ Attributes = new AttrList()
+ };
+ _disclaimerLabel.Attributes.Insert(new Pango.AttrScale(0.8f));
+
+ //
+ // _amiiboApiLink
+ //
+ _amiiboApiLink = new EventBox()
+ {
+ Margin = 5
+ };
+ _amiiboApiLink.ButtonPressEvent += AmiiboApiButton_Pressed;
+
+ //
+ // _amiiboApiLinkLabel
+ //
+ _amiiboApiLinkLabel = new Label("AmiiboAPI (www.amiiboapi.com) is used\nin our Amiibo emulation.")
+ {
+ TooltipText = "Click to open the AmiiboAPI website in your default browser.",
+ Justify = Justification.Center,
+ Attributes = new AttrList()
+ };
+ _amiiboApiLinkLabel.Attributes.Insert(new Pango.AttrScale(0.9f));
+
+ //
+ // _socialBox
+ //
+ _socialBox = new Box(Orientation.Horizontal, 0)
+ {
+ Margin = 25,
+ MarginBottom = 10
+ };
+
+ //
+ // _patreonEventBox
+ //
+ _patreonEventBox = new EventBox()
+ {
+ TooltipText = "Click to open the Ryujinx Patreon page in your default browser."
+ };
+ _patreonEventBox.ButtonPressEvent += PatreonButton_Pressed;
+
+ //
+ // _patreonBox
+ //
+ _patreonBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _patreonLogo
+ //
+ _patreonLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Patreon_Light.png", 30, 30))
+ {
+ Margin = 10
+ };
+
+ //
+ // _patreonLabel
+ //
+ _patreonLabel = new Label("Patreon")
+ {
+ Justify = Justification.Center
+ };
+
+ //
+ // _githubEventBox
+ //
+ _githubEventBox = new EventBox()
+ {
+ TooltipText = "Click to open the Ryujinx GitHub page in your default browser."
+ };
+ _githubEventBox.ButtonPressEvent += GitHubButton_Pressed;
+
+ //
+ // _githubBox
+ //
+ _githubBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _githubLogo
+ //
+ _githubLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_GitHub_Light.png", 30, 30))
+ {
+ Margin = 10
+ };
+
+ //
+ // _githubLabel
+ //
+ _githubLabel = new Label("GitHub")
+ {
+ Justify = Justification.Center
+ };
+
+ //
+ // _discordBox
+ //
+ _discordBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _discordEventBox
+ //
+ _discordEventBox = new EventBox()
+ {
+ TooltipText = "Click to open an invite to the Ryujinx Discord server in your default browser."
+ };
+ _discordEventBox.ButtonPressEvent += DiscordButton_Pressed;
+
+ //
+ // _discordLogo
+ //
+ _discordLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Discord_Light.png", 30, 30))
+ {
+ Margin = 10
+ };
+
+ //
+ // _discordLabel
+ //
+ _discordLabel = new Label("Discord")
+ {
+ Justify = Justification.Center
+ };
+
+ //
+ // _twitterEventBox
+ //
+ _twitterEventBox = new EventBox()
+ {
+ TooltipText = "Click to open the Ryujinx Twitter page in your default browser."
+ };
+ _twitterEventBox.ButtonPressEvent += TwitterButton_Pressed;
+
+ //
+ // _twitterBox
+ //
+ _twitterBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _twitterLogo
+ //
+ _twitterLogo = new Image(new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Twitter_Light.png", 30, 30))
+ {
+ Margin = 10
+ };
+
+ //
+ // _twitterLabel
+ //
+ _twitterLabel = new Label("Twitter")
+ {
+ Justify = Justification.Center
+ };
+
+ //
+ // _separator
+ //
+ _separator = new Separator(Orientation.Vertical)
+ {
+ Margin = 15
+ };
+
+ //
+ // _rightBox
+ //
+ _rightBox = new Box(Orientation.Vertical, 0)
+ {
+ Margin = 15,
+ MarginTop = 40
+ };
+
+ //
+ // _aboutLabel
+ //
+ _aboutLabel = new Label("About :")
+ {
+ Halign = Align.Start,
+ Attributes = new AttrList()
+ };
+ _aboutLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold));
+ _aboutLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
+
+ //
+ // _aboutDescriptionLabel
+ //
+ _aboutDescriptionLabel = new Label("Ryujinx is an emulator for the Nintendo Switch™.\n" +
+ "Please support us on Patreon.\n" +
+ "Get all the latest news on our Twitter or Discord.\n" +
+ "Developers interested in contributing can find out more on our GitHub or Discord.")
+ {
+ Margin = 15,
+ Halign = Align.Start
+ };
+
+ //
+ // _createdByLabel
+ //
+ _createdByLabel = new Label("Maintained by :")
+ {
+ Halign = Align.Start,
+ Attributes = new AttrList()
+ };
+ _createdByLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold));
+ _createdByLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
+
+ //
+ // _createdByText
+ //
+ _createdByText = new TextView()
+ {
+ WrapMode = Gtk.WrapMode.Word,
+ Editable = false,
+ CursorVisible = false,
+ Margin = 15,
+ MarginRight = 30
+ };
+ _createdByText.Buffer.Text = "gdkchan, Ac_K, Thog, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, Xpl0itR, GoffyDude, »jD« and more...";
+
+ //
+ // _contributorsEventBox
+ //
+ _contributorsEventBox = new EventBox();
+ _contributorsEventBox.ButtonPressEvent += ContributorsButton_Pressed;
+
+ //
+ // _contributorsLinkLabel
+ //
+ _contributorsLinkLabel = new Label("See All Contributors...")
+ {
+ TooltipText = "Click to open the Contributors page in your default browser.",
+ MarginRight = 30,
+ Halign = Align.End,
+ Attributes = new AttrList()
+ };
+ _contributorsLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
+
+ //
+ // _patreonNamesLabel
+ //
+ _patreonNamesLabel = new Label("Supported on Patreon by :")
+ {
+ Halign = Align.Start,
+ Attributes = new AttrList()
+ };
+ _patreonNamesLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold));
+ _patreonNamesLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
+
+ //
+ // _patreonNamesScrolled
+ //
+ _patreonNamesScrolled = new ScrolledWindow()
+ {
+ Margin = 15,
+ MarginRight = 30,
+ Expand = true,
+ ShadowType = ShadowType.In
+ };
+ _patreonNamesScrolled.SetPolicy(PolicyType.Never, PolicyType.Automatic);
+
+ //
+ // _patreonNamesText
+ //
+ _patreonNamesText = new TextView()
+ {
+ WrapMode = Gtk.WrapMode.Word
+ };
+ _patreonNamesText.Buffer.Text = "Loading...";
+ _patreonNamesText.SetProperty("editable", new GLib.Value(false));
+
+#pragma warning restore CS0612
+
+ ShowComponent();
+ }
+
+ private void ShowComponent()
+ {
+ _logoBox.Add(_ryujinxLogo);
+
+ _ryujinxLink.Add(_ryujinxLinkLabel);
+
+ _logoTextBox.Add(_ryujinxLabel);
+ _logoTextBox.Add(_ryujinxPhoneticLabel);
+ _logoTextBox.Add(_ryujinxLink);
+
+ _logoBox.Add(_logoTextBox);
+
+ _amiiboApiLink.Add(_amiiboApiLinkLabel);
+
+ _patreonBox.Add(_patreonLogo);
+ _patreonBox.Add(_patreonLabel);
+ _patreonEventBox.Add(_patreonBox);
+
+ _githubBox.Add(_githubLogo);
+ _githubBox.Add(_githubLabel);
+ _githubEventBox.Add(_githubBox);
+
+ _discordBox.Add(_discordLogo);
+ _discordBox.Add(_discordLabel);
+ _discordEventBox.Add(_discordBox);
+
+ _twitterBox.Add(_twitterLogo);
+ _twitterBox.Add(_twitterLabel);
+ _twitterEventBox.Add(_twitterBox);
+
+ _socialBox.Add(_patreonEventBox);
+ _socialBox.Add(_githubEventBox);
+ _socialBox.Add(_discordEventBox);
+ _socialBox.Add(_twitterEventBox);
+
+ _leftBox.Add(_logoBox);
+ _leftBox.Add(_versionLabel);
+ _leftBox.Add(_disclaimerLabel);
+ _leftBox.Add(_amiiboApiLink);
+ _leftBox.Add(_socialBox);
+
+ _contributorsEventBox.Add(_contributorsLinkLabel);
+ _patreonNamesScrolled.Add(_patreonNamesText);
+
+ _rightBox.Add(_aboutLabel);
+ _rightBox.Add(_aboutDescriptionLabel);
+ _rightBox.Add(_createdByLabel);
+ _rightBox.Add(_createdByText);
+ _rightBox.Add(_contributorsEventBox);
+ _rightBox.Add(_patreonNamesLabel);
+ _rightBox.Add(_patreonNamesScrolled);
+
+ _mainBox.Add(_leftBox);
+ _mainBox.Add(_separator);
+ _mainBox.Add(_rightBox);
+
+ Add(_mainBox);
+
+ ShowAll();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Windows/AboutWindow.cs b/src/Ryujinx/Ui/Windows/AboutWindow.cs
new file mode 100644
index 00000000..41cf9c01
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/AboutWindow.cs
@@ -0,0 +1,80 @@
+using Gtk;
+using Ryujinx.Common.Utilities;
+using Ryujinx.Ui.Common.Helper;
+using System.Net.Http;
+using System.Net.NetworkInformation;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Ryujinx.Ui.Windows
+{
+ public partial class AboutWindow : Window
+ {
+ public AboutWindow() : base($"Ryujinx {Program.Version} - About")
+ {
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(OpenHelper)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+ InitializeComponent();
+
+ _ = DownloadPatronsJson();
+ }
+
+ private async Task DownloadPatronsJson()
+ {
+ if (!NetworkInterface.GetIsNetworkAvailable())
+ {
+ _patreonNamesText.Buffer.Text = "Connection Error.";
+ }
+
+ HttpClient httpClient = new HttpClient();
+
+ try
+ {
+ string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
+
+ _patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize(patreonJsonString, CommonJsonContext.Default.StringArray));
+ }
+ catch
+ {
+ _patreonNamesText.Buffer.Text = "API Error.";
+ }
+ }
+
+ //
+ // Events
+ //
+ private void RyujinxButton_Pressed(object sender, ButtonPressEventArgs args)
+ {
+ OpenHelper.OpenUrl("https://ryujinx.org");
+ }
+
+ private void AmiiboApiButton_Pressed(object sender, ButtonPressEventArgs args)
+ {
+ OpenHelper.OpenUrl("https://amiiboapi.com");
+ }
+
+ private void PatreonButton_Pressed(object sender, ButtonPressEventArgs args)
+ {
+ OpenHelper.OpenUrl("https://www.patreon.com/ryujinx");
+ }
+
+ private void GitHubButton_Pressed(object sender, ButtonPressEventArgs args)
+ {
+ OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx");
+ }
+
+ private void DiscordButton_Pressed(object sender, ButtonPressEventArgs args)
+ {
+ OpenHelper.OpenUrl("https://discordapp.com/invite/N2FmfVc");
+ }
+
+ private void TwitterButton_Pressed(object sender, ButtonPressEventArgs args)
+ {
+ OpenHelper.OpenUrl("https://twitter.com/RyujinxEmu");
+ }
+
+ private void ContributorsButton_Pressed(object sender, ButtonPressEventArgs args)
+ {
+ OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx/graphs/contributors?type=a");
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs b/src/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs
new file mode 100644
index 00000000..3480c6e8
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/AmiiboWindow.Designer.cs
@@ -0,0 +1,194 @@
+using Gtk;
+
+namespace Ryujinx.Ui.Windows
+{
+ public partial class AmiiboWindow : Window
+ {
+ private Box _mainBox;
+ private ButtonBox _buttonBox;
+ private Button _scanButton;
+ private Button _cancelButton;
+ private CheckButton _randomUuidCheckBox;
+ private Box _amiiboBox;
+ private Box _amiiboHeadBox;
+ private Box _amiiboSeriesBox;
+ private Label _amiiboSeriesLabel;
+ private ComboBoxText _amiiboSeriesComboBox;
+ private Box _amiiboCharsBox;
+ private Label _amiiboCharsLabel;
+ private ComboBoxText _amiiboCharsComboBox;
+ private CheckButton _showAllCheckBox;
+ private Image _amiiboImage;
+ private Label _gameUsageLabel;
+
+ private void InitializeComponent()
+ {
+#pragma warning disable CS0612
+
+ //
+ // AmiiboWindow
+ //
+ CanFocus = false;
+ Resizable = false;
+ Modal = true;
+ WindowPosition = WindowPosition.Center;
+ DefaultWidth = 600;
+ DefaultHeight = 470;
+ TypeHint = Gdk.WindowTypeHint.Dialog;
+
+ //
+ // _mainBox
+ //
+ _mainBox = new Box(Orientation.Vertical, 2);
+
+ //
+ // _buttonBox
+ //
+ _buttonBox = new ButtonBox(Orientation.Horizontal)
+ {
+ Margin = 20,
+ LayoutStyle = ButtonBoxStyle.End
+ };
+
+ //
+ // _scanButton
+ //
+ _scanButton = new Button()
+ {
+ Label = "Scan It!",
+ CanFocus = true,
+ ReceivesDefault = true,
+ MarginLeft = 10
+ };
+ _scanButton.Clicked += ScanButton_Pressed;
+
+ //
+ // _randomUuidCheckBox
+ //
+ _randomUuidCheckBox = new CheckButton()
+ {
+ Label = "Hack: Use Random Tag Uuid",
+ TooltipText = "This allows multiple scans of a single Amiibo.\n(used in The Legend of Zelda: Breath of the Wild)"
+ };
+
+ //
+ // _cancelButton
+ //
+ _cancelButton = new Button()
+ {
+ Label = "Cancel",
+ CanFocus = true,
+ ReceivesDefault = true,
+ MarginLeft = 10
+ };
+ _cancelButton.Clicked += CancelButton_Pressed;
+
+ //
+ // _amiiboBox
+ //
+ _amiiboBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _amiiboHeadBox
+ //
+ _amiiboHeadBox = new Box(Orientation.Horizontal, 0)
+ {
+ Margin = 20,
+ Hexpand = true
+ };
+
+ //
+ // _amiiboSeriesBox
+ //
+ _amiiboSeriesBox = new Box(Orientation.Horizontal, 0)
+ {
+ Hexpand = true
+ };
+
+ //
+ // _amiiboSeriesLabel
+ //
+ _amiiboSeriesLabel = new Label("Amiibo Series:");
+
+ //
+ // _amiiboSeriesComboBox
+ //
+ _amiiboSeriesComboBox = new ComboBoxText();
+
+ //
+ // _amiiboCharsBox
+ //
+ _amiiboCharsBox = new Box(Orientation.Horizontal, 0)
+ {
+ Hexpand = true
+ };
+
+ //
+ // _amiiboCharsLabel
+ //
+ _amiiboCharsLabel = new Label("Character:");
+
+ //
+ // _amiiboCharsComboBox
+ //
+ _amiiboCharsComboBox = new ComboBoxText();
+
+ //
+ // _showAllCheckBox
+ //
+ _showAllCheckBox = new CheckButton()
+ {
+ Label = "Show All Amiibo"
+ };
+
+ //
+ // _amiiboImage
+ //
+ _amiiboImage = new Image()
+ {
+ HeightRequest = 350,
+ WidthRequest = 350
+ };
+
+ //
+ // _gameUsageLabel
+ //
+ _gameUsageLabel = new Label("")
+ {
+ MarginTop = 20
+ };
+
+#pragma warning restore CS0612
+
+ ShowComponent();
+ }
+
+ private void ShowComponent()
+ {
+ _buttonBox.Add(_showAllCheckBox);
+ _buttonBox.Add(_randomUuidCheckBox);
+ _buttonBox.Add(_scanButton);
+ _buttonBox.Add(_cancelButton);
+
+ _amiiboSeriesBox.Add(_amiiboSeriesLabel);
+ _amiiboSeriesBox.Add(_amiiboSeriesComboBox);
+
+ _amiiboCharsBox.Add(_amiiboCharsLabel);
+ _amiiboCharsBox.Add(_amiiboCharsComboBox);
+
+ _amiiboHeadBox.Add(_amiiboSeriesBox);
+ _amiiboHeadBox.Add(_amiiboCharsBox);
+
+ _amiiboBox.PackStart(_amiiboHeadBox, true, true, 0);
+ _amiiboBox.PackEnd(_gameUsageLabel, false, false, 0);
+ _amiiboBox.PackEnd(_amiiboImage, false, false, 0);
+
+ _mainBox.Add(_amiiboBox);
+ _mainBox.PackEnd(_buttonBox, false, false, 0);
+
+ Add(_mainBox);
+
+ ShowAll();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Windows/AmiiboWindow.cs b/src/Ryujinx/Ui/Windows/AmiiboWindow.cs
new file mode 100644
index 00000000..11a566d8
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/AmiiboWindow.cs
@@ -0,0 +1,387 @@
+using Gtk;
+using Ryujinx.Common;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.Ui.Common.Configuration;
+using Ryujinx.Ui.Common.Models.Amiibo;
+using Ryujinx.Ui.Widgets;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Net.Http;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using AmiiboApi = Ryujinx.Ui.Common.Models.Amiibo.AmiiboApi;
+using AmiiboJsonSerializerContext = Ryujinx.Ui.Common.Models.Amiibo.AmiiboJsonSerializerContext;
+
+namespace Ryujinx.Ui.Windows
+{
+ public partial class AmiiboWindow : Window
+ {
+ private const string DEFAULT_JSON = "{ \"amiibo\": [] }";
+
+ public string AmiiboId { get; private set; }
+
+ public int DeviceId { get; set; }
+ public string TitleId { get; set; }
+ public string LastScannedAmiiboId { get; set; }
+ public bool LastScannedAmiiboShowAll { get; set; }
+
+ public ResponseType Response { get; private set; }
+
+ public bool UseRandomUuid
+ {
+ get
+ {
+ return _randomUuidCheckBox.Active;
+ }
+ }
+
+ private readonly HttpClient _httpClient;
+ private readonly string _amiiboJsonPath;
+
+ private readonly byte[] _amiiboLogoBytes;
+
+ private List<AmiiboApi> _amiiboList;
+
+ private static readonly AmiiboJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+ public AmiiboWindow() : base($"Ryujinx {Program.Version} - Amiibo")
+ {
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+
+ InitializeComponent();
+
+ _httpClient = new HttpClient()
+ {
+ Timeout = TimeSpan.FromSeconds(30)
+ };
+
+ Directory.CreateDirectory(System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "amiibo"));
+
+ _amiiboJsonPath = System.IO.Path.Join(AppDataManager.BaseDirPath, "system", "amiibo", "Amiibo.json");
+ _amiiboList = new List<AmiiboApi>();
+
+ _amiiboLogoBytes = EmbeddedResources.Read("Ryujinx.Ui.Common/Resources/Logo_Amiibo.png");
+ _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes);
+
+ _scanButton.Sensitive = false;
+ _randomUuidCheckBox.Sensitive = false;
+
+ _ = LoadContentAsync();
+ }
+
+ private async Task LoadContentAsync()
+ {
+ string amiiboJsonString = DEFAULT_JSON;
+
+ if (File.Exists(_amiiboJsonPath))
+ {
+ amiiboJsonString = await File.ReadAllTextAsync(_amiiboJsonPath);
+
+ if (await NeedsUpdate(JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).LastUpdated))
+ {
+ amiiboJsonString = await DownloadAmiiboJson();
+ }
+ }
+ else
+ {
+ try
+ {
+ amiiboJsonString = await DownloadAmiiboJson();
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Failed to download amiibo data: {ex}");
+
+ ShowInfoDialog();
+
+ Close();
+ }
+ }
+
+ _amiiboList = JsonHelper.Deserialize(amiiboJsonString, SerializerContext.AmiiboJson).Amiibo;
+ _amiiboList = _amiiboList.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
+
+ if (LastScannedAmiiboShowAll)
+ {
+ _showAllCheckBox.Click();
+ }
+
+ ParseAmiiboData();
+
+ _showAllCheckBox.Clicked += ShowAllCheckBox_Clicked;
+ }
+
+ private void ParseAmiiboData()
+ {
+ List<string> comboxItemList = new List<string>();
+
+ for (int i = 0; i < _amiiboList.Count; i++)
+ {
+ if (!comboxItemList.Contains(_amiiboList[i].AmiiboSeries))
+ {
+ if (!_showAllCheckBox.Active)
+ {
+ foreach (var game in _amiiboList[i].GamesSwitch)
+ {
+ if (game != null)
+ {
+ if (game.GameId.Contains(TitleId))
+ {
+ comboxItemList.Add(_amiiboList[i].AmiiboSeries);
+ _amiiboSeriesComboBox.Append(_amiiboList[i].AmiiboSeries, _amiiboList[i].AmiiboSeries);
+
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ comboxItemList.Add(_amiiboList[i].AmiiboSeries);
+ _amiiboSeriesComboBox.Append(_amiiboList[i].AmiiboSeries, _amiiboList[i].AmiiboSeries);
+ }
+ }
+ }
+
+ _amiiboSeriesComboBox.Changed += SeriesComboBox_Changed;
+ _amiiboCharsComboBox.Changed += CharacterComboBox_Changed;
+
+ if (LastScannedAmiiboId != "")
+ {
+ SelectLastScannedAmiibo();
+ }
+ else
+ {
+ _amiiboSeriesComboBox.Active = 0;
+ }
+ }
+
+ private void SelectLastScannedAmiibo()
+ {
+ bool isSet = _amiiboSeriesComboBox.SetActiveId(_amiiboList.FirstOrDefault(amiibo => amiibo.Head + amiibo.Tail == LastScannedAmiiboId).AmiiboSeries);
+ isSet = _amiiboCharsComboBox.SetActiveId(LastScannedAmiiboId);
+
+ if (isSet == false)
+ {
+ _amiiboSeriesComboBox.Active = 0;
+ }
+ }
+
+ private async Task<bool> NeedsUpdate(DateTime oldLastModified)
+ {
+ try
+ {
+ HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://amiibo.ryujinx.org/"));
+
+ if (response.IsSuccessStatusCode)
+ {
+ return response.Content.Headers.LastModified != oldLastModified;
+ }
+
+ return false;
+ }
+ catch (Exception ex)
+ {
+ Logger.Error?.Print(LogClass.Application, $"Failed to check for amiibo updates: {ex}");
+
+ ShowInfoDialog();
+
+ return false;
+ }
+ }
+
+ private async Task<string> DownloadAmiiboJson()
+ {
+ HttpResponseMessage response = await _httpClient.GetAsync("https://amiibo.ryujinx.org/");
+
+ if (response.IsSuccessStatusCode)
+ {
+ string amiiboJsonString = await response.Content.ReadAsStringAsync();
+
+ using (FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough))
+ {
+ dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
+ }
+
+ return amiiboJsonString;
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Failed to download amiibo data. Response status code: {response.StatusCode}");
+
+ GtkDialog.CreateInfoDialog($"Amiibo API", "An error occured while fetching information from the API.");
+
+ Close();
+ }
+
+ return DEFAULT_JSON;
+ }
+
+ private async Task UpdateAmiiboPreview(string imageUrl)
+ {
+ HttpResponseMessage response = await _httpClient.GetAsync(imageUrl);
+
+ if (response.IsSuccessStatusCode)
+ {
+ byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync();
+ Gdk.Pixbuf amiiboPreview = new Gdk.Pixbuf(amiiboPreviewBytes);
+
+ float ratio = Math.Min((float)_amiiboImage.AllocatedWidth / amiiboPreview.Width,
+ (float)_amiiboImage.AllocatedHeight / amiiboPreview.Height);
+
+ int resizeHeight = (int)(amiiboPreview.Height * ratio);
+ int resizeWidth = (int)(amiiboPreview.Width * ratio);
+
+ _amiiboImage.Pixbuf = amiiboPreview.ScaleSimple(resizeWidth, resizeHeight, Gdk.InterpType.Bilinear);
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Failed to get amiibo preview. Response status code: {response.StatusCode}");
+ }
+ }
+
+ private void ShowInfoDialog()
+ {
+ GtkDialog.CreateInfoDialog($"Amiibo API", "Unable to connect to Amiibo API server. The service may be down or you may need to verify your internet connection is online.");
+ }
+
+ //
+ // Events
+ //
+ private void SeriesComboBox_Changed(object sender, EventArgs args)
+ {
+ _amiiboCharsComboBox.Changed -= CharacterComboBox_Changed;
+
+ _amiiboCharsComboBox.RemoveAll();
+
+ List<AmiiboApi> amiiboSortedList = _amiiboList.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeriesComboBox.ActiveId).OrderBy(amiibo => amiibo.Name).ToList();
+
+ List<string> comboxItemList = new List<string>();
+
+ for (int i = 0; i < amiiboSortedList.Count; i++)
+ {
+ if (!comboxItemList.Contains(amiiboSortedList[i].Head + amiiboSortedList[i].Tail))
+ {
+ if (!_showAllCheckBox.Active)
+ {
+ foreach (var game in amiiboSortedList[i].GamesSwitch)
+ {
+ if (game != null)
+ {
+ if (game.GameId.Contains(TitleId))
+ {
+ comboxItemList.Add(amiiboSortedList[i].Head + amiiboSortedList[i].Tail);
+ _amiiboCharsComboBox.Append(amiiboSortedList[i].Head + amiiboSortedList[i].Tail, amiiboSortedList[i].Name);
+
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ comboxItemList.Add(amiiboSortedList[i].Head + amiiboSortedList[i].Tail);
+ _amiiboCharsComboBox.Append(amiiboSortedList[i].Head + amiiboSortedList[i].Tail, amiiboSortedList[i].Name);
+ }
+ }
+ }
+
+ _amiiboCharsComboBox.Changed += CharacterComboBox_Changed;
+
+ _amiiboCharsComboBox.Active = 0;
+
+ _scanButton.Sensitive = true;
+ _randomUuidCheckBox.Sensitive = true;
+ }
+
+ private void CharacterComboBox_Changed(object sender, EventArgs args)
+ {
+ AmiiboId = _amiiboCharsComboBox.ActiveId;
+
+ _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes);
+
+ string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Head + amiibo.Tail == _amiiboCharsComboBox.ActiveId).Image;
+
+ var usageStringBuilder = new StringBuilder();
+
+ for (int i = 0; i < _amiiboList.Count; i++)
+ {
+ if (_amiiboList[i].Head + _amiiboList[i].Tail == _amiiboCharsComboBox.ActiveId)
+ {
+ bool writable = false;
+
+ foreach (var item in _amiiboList[i].GamesSwitch)
+ {
+ if (item.GameId.Contains(TitleId))
+ {
+ foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
+ {
+ usageStringBuilder.Append(Environment.NewLine);
+ usageStringBuilder.Append($"- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}");
+
+ writable = usageItem.Write;
+ }
+ }
+ }
+
+ if (usageStringBuilder.Length == 0)
+ {
+ usageStringBuilder.Append("Unknown.");
+ }
+
+ _gameUsageLabel.Text = $"Usage{(writable ? " (Writable)" : "")} : {usageStringBuilder}";
+ }
+ }
+
+ _ = UpdateAmiiboPreview(imageUrl);
+ }
+
+ private void ShowAllCheckBox_Clicked(object sender, EventArgs e)
+ {
+ _amiiboImage.Pixbuf = new Gdk.Pixbuf(_amiiboLogoBytes);
+
+ _amiiboSeriesComboBox.Changed -= SeriesComboBox_Changed;
+ _amiiboCharsComboBox.Changed -= CharacterComboBox_Changed;
+
+ _amiiboSeriesComboBox.RemoveAll();
+ _amiiboCharsComboBox.RemoveAll();
+
+ _scanButton.Sensitive = false;
+ _randomUuidCheckBox.Sensitive = false;
+
+ new Task(() => ParseAmiiboData()).Start();
+ }
+
+ private void ScanButton_Pressed(object sender, EventArgs args)
+ {
+ LastScannedAmiiboShowAll = _showAllCheckBox.Active;
+
+ Response = ResponseType.Ok;
+
+ Close();
+ }
+
+ private void CancelButton_Pressed(object sender, EventArgs args)
+ {
+ AmiiboId = "";
+ LastScannedAmiiboId = "";
+ LastScannedAmiiboShowAll = false;
+
+ Response = ResponseType.Cancel;
+
+ Close();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ _httpClient.Dispose();
+
+ base.Dispose(disposing);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Windows/AvatarWindow.cs b/src/Ryujinx/Ui/Windows/AvatarWindow.cs
new file mode 100644
index 00000000..0cda890f
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/AvatarWindow.cs
@@ -0,0 +1,294 @@
+using Gtk;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Ncm;
+using LibHac.Tools.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Common.Memory;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.Ui.Common.Configuration;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Formats.Png;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+
+using Image = SixLabors.ImageSharp.Image;
+
+namespace Ryujinx.Ui.Windows
+{
+ public class AvatarWindow : Window
+ {
+ public byte[] SelectedProfileImage;
+ public bool NewUser;
+
+ private static Dictionary<string, byte[]> _avatarDict = new Dictionary<string, byte[]>();
+
+ private ListStore _listStore;
+ private IconView _iconView;
+ private Button _setBackgroungColorButton;
+ private Gdk.RGBA _backgroundColor;
+
+ public AvatarWindow() : base($"Ryujinx {Program.Version} - Manage Accounts - Avatar")
+ {
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+
+ CanFocus = false;
+ Resizable = false;
+ Modal = true;
+ TypeHint = Gdk.WindowTypeHint.Dialog;
+
+ SetDefaultSize(740, 400);
+ SetPosition(WindowPosition.Center);
+
+ Box vbox = new(Orientation.Vertical, 0);
+ Add(vbox);
+
+ ScrolledWindow scrolledWindow = new ScrolledWindow
+ {
+ ShadowType = ShadowType.EtchedIn
+ };
+ scrolledWindow.SetPolicy(PolicyType.Automatic, PolicyType.Automatic);
+
+ Box hbox = new(Orientation.Horizontal, 0);
+
+ Button chooseButton = new Button()
+ {
+ Label = "Choose",
+ CanFocus = true,
+ ReceivesDefault = true
+ };
+ chooseButton.Clicked += ChooseButton_Pressed;
+
+ _setBackgroungColorButton = new Button()
+ {
+ Label = "Set Background Color",
+ CanFocus = true
+ };
+ _setBackgroungColorButton.Clicked += SetBackgroungColorButton_Pressed;
+
+ _backgroundColor.Red = 1;
+ _backgroundColor.Green = 1;
+ _backgroundColor.Blue = 1;
+ _backgroundColor.Alpha = 1;
+
+ Button closeButton = new Button()
+ {
+ Label = "Close",
+ CanFocus = true
+ };
+ closeButton.Clicked += CloseButton_Pressed;
+
+ vbox.PackStart(scrolledWindow, true, true, 0);
+ hbox.PackStart(chooseButton, true, true, 0);
+ hbox.PackStart(_setBackgroungColorButton, true, true, 0);
+ hbox.PackStart(closeButton, true, true, 0);
+ vbox.PackStart(hbox, false, false, 0);
+
+ _listStore = new ListStore(typeof(string), typeof(Gdk.Pixbuf));
+ _listStore.SetSortColumnId(0, SortType.Ascending);
+
+ _iconView = new IconView(_listStore);
+ _iconView.ItemWidth = 64;
+ _iconView.ItemPadding = 10;
+ _iconView.PixbufColumn = 1;
+
+ _iconView.SelectionChanged += IconView_SelectionChanged;
+
+ scrolledWindow.Add(_iconView);
+
+ _iconView.GrabFocus();
+
+ ProcessAvatars();
+
+ ShowAll();
+ }
+
+ public static void PreloadAvatars(ContentManager contentManager, VirtualFileSystem virtualFileSystem)
+ {
+ if (_avatarDict.Count > 0)
+ {
+ return;
+ }
+
+ string contentPath = contentManager.GetInstalledContentPath(0x010000000000080A, StorageId.BuiltInSystem, NcaContentType.Data);
+ string avatarPath = virtualFileSystem.SwitchPathToSystemPath(contentPath);
+
+ if (!string.IsNullOrWhiteSpace(avatarPath))
+ {
+ using (IStorage ncaFileStream = new LocalStorage(avatarPath, FileAccess.Read, FileMode.Open))
+ {
+ Nca nca = new Nca(virtualFileSystem.KeySet, ncaFileStream);
+ IFileSystem romfs = nca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.ErrorOnInvalid);
+
+ foreach (var item in romfs.EnumerateEntries())
+ {
+ // TODO: Parse DatabaseInfo.bin and table.bin files for more accuracy.
+
+ if (item.Type == DirectoryEntryType.File && item.FullPath.Contains("chara") && item.FullPath.Contains("szs"))
+ {
+ using var file = new UniqueRef<IFile>();
+
+ romfs.OpenFile(ref file.Ref, ("/" + item.FullPath).ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ using (MemoryStream stream = MemoryStreamManager.Shared.GetStream())
+ using (MemoryStream streamPng = MemoryStreamManager.Shared.GetStream())
+ {
+ file.Get.AsStream().CopyTo(stream);
+
+ stream.Position = 0;
+
+ Image avatarImage = Image.LoadPixelData<Rgba32>(DecompressYaz0(stream), 256, 256);
+
+ avatarImage.SaveAsPng(streamPng);
+
+ _avatarDict.Add(item.FullPath, streamPng.ToArray());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private void ProcessAvatars()
+ {
+ _listStore.Clear();
+
+ foreach (var avatar in _avatarDict)
+ {
+ _listStore.AppendValues(avatar.Key, new Gdk.Pixbuf(ProcessImage(avatar.Value), 96, 96));
+ }
+
+ _iconView.SelectPath(new TreePath(new int[] { 0 }));
+ }
+
+ private byte[] ProcessImage(byte[] data)
+ {
+ using (MemoryStream streamJpg = MemoryStreamManager.Shared.GetStream())
+ {
+ Image avatarImage = Image.Load(data, new PngDecoder());
+
+ avatarImage.Mutate(x => x.BackgroundColor(new Rgba32((byte)(_backgroundColor.Red * 255),
+ (byte)(_backgroundColor.Green * 255),
+ (byte)(_backgroundColor.Blue * 255),
+ (byte)(_backgroundColor.Alpha * 255))));
+ avatarImage.SaveAsJpeg(streamJpg);
+
+ return streamJpg.ToArray();
+ }
+ }
+
+ private void CloseButton_Pressed(object sender, EventArgs e)
+ {
+ SelectedProfileImage = null;
+
+ Close();
+ }
+
+ private void IconView_SelectionChanged(object sender, EventArgs e)
+ {
+ if (_iconView.SelectedItems.Length > 0)
+ {
+ _listStore.GetIter(out TreeIter iter, _iconView.SelectedItems[0]);
+
+ SelectedProfileImage = ProcessImage(_avatarDict[(string)_listStore.GetValue(iter, 0)]);
+ }
+ }
+
+ private void SetBackgroungColorButton_Pressed(object sender, EventArgs e)
+ {
+ using (ColorChooserDialog colorChooserDialog = new ColorChooserDialog("Set Background Color", this))
+ {
+ colorChooserDialog.UseAlpha = false;
+ colorChooserDialog.Rgba = _backgroundColor;
+
+ if (colorChooserDialog.Run() == (int)ResponseType.Ok)
+ {
+ _backgroundColor = colorChooserDialog.Rgba;
+
+ ProcessAvatars();
+ }
+
+ colorChooserDialog.Hide();
+ }
+ }
+
+ private void ChooseButton_Pressed(object sender, EventArgs e)
+ {
+ Close();
+ }
+
+ private static byte[] DecompressYaz0(Stream stream)
+ {
+ using (BinaryReader reader = new BinaryReader(stream))
+ {
+ reader.ReadInt32(); // Magic
+
+ uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
+
+ reader.ReadInt64(); // Padding
+
+ byte[] input = new byte[stream.Length - stream.Position];
+ stream.Read(input, 0, input.Length);
+
+ long inputOffset = 0;
+
+ byte[] output = new byte[decodedLength];
+ long outputOffset = 0;
+
+ ushort mask = 0;
+ byte header = 0;
+
+ while (outputOffset < decodedLength)
+ {
+ if ((mask >>= 1) == 0)
+ {
+ header = input[inputOffset++];
+ mask = 0x80;
+ }
+
+ if ((header & mask) > 0)
+ {
+ if (outputOffset == output.Length)
+ {
+ break;
+ }
+
+ output[outputOffset++] = input[inputOffset++];
+ }
+ else
+ {
+ byte byte1 = input[inputOffset++];
+ byte byte2 = input[inputOffset++];
+
+ int dist = ((byte1 & 0xF) << 8) | byte2;
+ int position = (int)outputOffset - (dist + 1);
+
+ int length = byte1 >> 4;
+ if (length == 0)
+ {
+ length = input[inputOffset++] + 0x12;
+ }
+ else
+ {
+ length += 2;
+ }
+
+ while (length-- > 0)
+ {
+ output[outputOffset++] = output[position++];
+ }
+ }
+ }
+
+ return output;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.cs b/src/Ryujinx/Ui/Windows/CheatWindow.cs
new file mode 100644
index 00000000..917603b2
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/CheatWindow.cs
@@ -0,0 +1,154 @@
+using Gtk;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+using GUI = Gtk.Builder.ObjectAttribute;
+
+namespace Ryujinx.Ui.Windows
+{
+ public class CheatWindow : Window
+ {
+ private readonly string _enabledCheatsPath;
+ private readonly bool _noCheatsFound;
+
+#pragma warning disable CS0649, IDE0044
+ [GUI] Label _baseTitleInfoLabel;
+ [GUI] TreeView _cheatTreeView;
+ [GUI] Button _saveButton;
+#pragma warning restore CS0649, IDE0044
+
+ public CheatWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.CheatWindow.glade"), virtualFileSystem, titleId, titleName) { }
+
+ private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName) : base(builder.GetRawOwnedObject("_cheatWindow"))
+ {
+ builder.Autoconnect(this);
+ _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
+
+ string modsBasePath = virtualFileSystem.ModLoader.GetModsBasePath();
+ string titleModsPath = virtualFileSystem.ModLoader.GetTitleDir(modsBasePath, titleId.ToString("X16"));
+
+ _enabledCheatsPath = System.IO.Path.Combine(titleModsPath, "cheats", "enabled.txt");
+
+ _cheatTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string), typeof(string));
+
+ CellRendererToggle enableToggle = new CellRendererToggle();
+ enableToggle.Toggled += (sender, args) =>
+ {
+ _cheatTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path));
+ bool newValue = !(bool)_cheatTreeView.Model.GetValue(treeIter, 0);
+ _cheatTreeView.Model.SetValue(treeIter, 0, newValue);
+
+ if (_cheatTreeView.Model.IterChildren(out TreeIter childIter, treeIter))
+ {
+ do
+ {
+ _cheatTreeView.Model.SetValue(childIter, 0, newValue);
+ }
+ while (_cheatTreeView.Model.IterNext(ref childIter));
+ }
+ };
+
+ _cheatTreeView.AppendColumn("Enabled", enableToggle, "active", 0);
+ _cheatTreeView.AppendColumn("Name", new CellRendererText(), "text", 1);
+ _cheatTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
+
+ var buildIdColumn = _cheatTreeView.AppendColumn("Build Id", new CellRendererText(), "text", 3);
+ buildIdColumn.Visible = false;
+
+ string[] enabled = { };
+
+ if (File.Exists(_enabledCheatsPath))
+ {
+ enabled = File.ReadAllLines(_enabledCheatsPath);
+ }
+
+ int cheatAdded = 0;
+
+ var mods = new ModLoader.ModCache();
+
+ ModLoader.QueryContentsDir(mods, new DirectoryInfo(System.IO.Path.Combine(modsBasePath, "contents")), titleId);
+
+ string currentCheatFile = string.Empty;
+ string buildId = string.Empty;
+ TreeIter parentIter = default;
+
+ foreach (var cheat in mods.Cheats)
+ {
+ if (cheat.Path.FullName != currentCheatFile)
+ {
+ currentCheatFile = cheat.Path.FullName;
+ string parentPath = currentCheatFile.Replace(titleModsPath, "");
+
+ buildId = System.IO.Path.GetFileNameWithoutExtension(currentCheatFile).ToUpper();
+ parentIter = ((TreeStore)_cheatTreeView.Model).AppendValues(false, buildId, parentPath, "");
+ }
+
+ string cleanName = cheat.Name.Substring(1, cheat.Name.Length - 8);
+ ((TreeStore)_cheatTreeView.Model).AppendValues(parentIter, enabled.Contains($"{buildId}-{cheat.Name}"), cleanName, "", buildId);
+
+ cheatAdded++;
+ }
+
+ if (cheatAdded == 0)
+ {
+ ((TreeStore)_cheatTreeView.Model).AppendValues(false, "No Cheats Found", "", "");
+ _cheatTreeView.GetColumn(0).Visible = false;
+
+ _noCheatsFound = true;
+
+ _saveButton.Visible = false;
+ }
+
+ _cheatTreeView.ExpandAll();
+ }
+
+ private void SaveButton_Clicked(object sender, EventArgs args)
+ {
+ if (_noCheatsFound)
+ {
+ return;
+ }
+
+ List<string> enabledCheats = new List<string>();
+
+ if (_cheatTreeView.Model.GetIterFirst(out TreeIter parentIter))
+ {
+ do
+ {
+ if (_cheatTreeView.Model.IterChildren(out TreeIter childIter, parentIter))
+ {
+ do
+ {
+ var enabled = (bool)_cheatTreeView.Model.GetValue(childIter, 0);
+
+ if (enabled)
+ {
+ var name = _cheatTreeView.Model.GetValue(childIter, 1).ToString();
+ var buildId = _cheatTreeView.Model.GetValue(childIter, 3).ToString();
+
+ enabledCheats.Add($"{buildId}-<{name} Cheat>");
+ }
+ }
+ while (_cheatTreeView.Model.IterNext(ref childIter));
+ }
+ }
+ while (_cheatTreeView.Model.IterNext(ref parentIter));
+ }
+
+ Directory.CreateDirectory(System.IO.Path.GetDirectoryName(_enabledCheatsPath));
+
+ File.WriteAllLines(_enabledCheatsPath, enabledCheats);
+
+ Dispose();
+ }
+
+ private void CancelButton_Clicked(object sender, EventArgs args)
+ {
+ Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/Windows/CheatWindow.glade b/src/Ryujinx/Ui/Windows/CheatWindow.glade
new file mode 100644
index 00000000..37b1cbe0
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/CheatWindow.glade
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.21.0 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkWindow" id="_cheatWindow">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Ryujinx - Cheat Manager</property>
+ <property name="default_width">440</property>
+ <property name="default_height">550</property>
+ <child>
+ <object class="GtkBox" id="MainBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="CheatBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="_baseTitleInfoLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="label" translatable="yes">Available Cheats</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkViewport">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkTreeView" id="_cheatTreeView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkButtonBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="_saveButton">
+ <property name="label" translatable="yes">Save</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">2</property>
+ <property name="margin_bottom">2</property>
+ <signal name="clicked" handler="SaveButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="_cancelButton">
+ <property name="label" translatable="yes">Cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">2</property>
+ <property name="margin_bottom">2</property>
+ <signal name="clicked" handler="CancelButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ </object>
+</interface>
diff --git a/src/Ryujinx/Ui/Windows/ControllerWindow.cs b/src/Ryujinx/Ui/Windows/ControllerWindow.cs
new file mode 100644
index 00000000..9b4befd8
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/ControllerWindow.cs
@@ -0,0 +1,1222 @@
+using Gtk;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.Configuration.Hid.Controller;
+using Ryujinx.Common.Configuration.Hid.Controller.Motion;
+using Ryujinx.Common.Configuration.Hid.Keyboard;
+using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.Input;
+using Ryujinx.Input.Assigner;
+using Ryujinx.Input.GTK3;
+using Ryujinx.Ui.Common.Configuration;
+using Ryujinx.Ui.Widgets;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Text.Json;
+using System.Threading;
+using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
+using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
+using GUI = Gtk.Builder.ObjectAttribute;
+using Key = Ryujinx.Common.Configuration.Hid.Key;
+
+namespace Ryujinx.Ui.Windows
+{
+ public class ControllerWindow : Window
+ {
+ private readonly PlayerIndex _playerIndex;
+ private readonly InputConfig _inputConfig;
+
+ private bool _isWaitingForInput;
+
+#pragma warning disable CS0649, IDE0044
+ [GUI] Adjustment _controllerStrongRumble;
+ [GUI] Adjustment _controllerWeakRumble;
+ [GUI] Adjustment _controllerDeadzoneLeft;
+ [GUI] Adjustment _controllerDeadzoneRight;
+ [GUI] Adjustment _controllerRangeLeft;
+ [GUI] Adjustment _controllerRangeRight;
+ [GUI] Adjustment _controllerTriggerThreshold;
+ [GUI] Adjustment _slotNumber;
+ [GUI] Adjustment _altSlotNumber;
+ [GUI] Adjustment _sensitivity;
+ [GUI] Adjustment _gyroDeadzone;
+ [GUI] CheckButton _enableMotion;
+ [GUI] CheckButton _enableCemuHook;
+ [GUI] CheckButton _mirrorInput;
+ [GUI] Entry _dsuServerHost;
+ [GUI] Entry _dsuServerPort;
+ [GUI] ComboBoxText _inputDevice;
+ [GUI] ComboBoxText _profile;
+ [GUI] Box _settingsBox;
+ [GUI] Box _motionAltBox;
+ [GUI] Box _motionBox;
+ [GUI] Box _dsuServerHostBox;
+ [GUI] Box _dsuServerPortBox;
+ [GUI] Box _motionControllerSlot;
+ [GUI] Grid _leftStickKeyboard;
+ [GUI] Grid _leftStickController;
+ [GUI] Box _deadZoneLeftBox;
+ [GUI] Box _rangeLeftBox;
+ [GUI] Grid _rightStickKeyboard;
+ [GUI] Grid _rightStickController;
+ [GUI] Box _deadZoneRightBox;
+ [GUI] Box _rangeRightBox;
+ [GUI] Grid _leftSideTriggerBox;
+ [GUI] Grid _rightSideTriggerBox;
+ [GUI] Box _triggerThresholdBox;
+ [GUI] ComboBoxText _controllerType;
+ [GUI] ToggleButton _lStick;
+ [GUI] CheckButton _invertLStickX;
+ [GUI] CheckButton _invertLStickY;
+ [GUI] CheckButton _rotateL90CW;
+ [GUI] ToggleButton _lStickUp;
+ [GUI] ToggleButton _lStickDown;
+ [GUI] ToggleButton _lStickLeft;
+ [GUI] ToggleButton _lStickRight;
+ [GUI] ToggleButton _lStickButton;
+ [GUI] ToggleButton _dpadUp;
+ [GUI] ToggleButton _dpadDown;
+ [GUI] ToggleButton _dpadLeft;
+ [GUI] ToggleButton _dpadRight;
+ [GUI] ToggleButton _minus;
+ [GUI] ToggleButton _l;
+ [GUI] ToggleButton _zL;
+ [GUI] ToggleButton _rStick;
+ [GUI] CheckButton _invertRStickX;
+ [GUI] CheckButton _invertRStickY;
+ [GUI] CheckButton _rotateR90CW;
+ [GUI] ToggleButton _rStickUp;
+ [GUI] ToggleButton _rStickDown;
+ [GUI] ToggleButton _rStickLeft;
+ [GUI] ToggleButton _rStickRight;
+ [GUI] ToggleButton _rStickButton;
+ [GUI] ToggleButton _a;
+ [GUI] ToggleButton _b;
+ [GUI] ToggleButton _x;
+ [GUI] ToggleButton _y;
+ [GUI] ToggleButton _plus;
+ [GUI] ToggleButton _r;
+ [GUI] ToggleButton _zR;
+ [GUI] ToggleButton _lSl;
+ [GUI] ToggleButton _lSr;
+ [GUI] ToggleButton _rSl;
+ [GUI] ToggleButton _rSr;
+ [GUI] Image _controllerImage;
+ [GUI] CheckButton _enableRumble;
+ [GUI] Box _rumbleBox;
+#pragma warning restore CS0649, IDE0044
+
+ private MainWindow _mainWindow;
+ private IGamepadDriver _gtk3KeyboardDriver;
+ private IGamepad _selectedGamepad;
+ private bool _mousePressed;
+ private bool _middleMousePressed;
+
+ private static readonly InputConfigJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+ public ControllerWindow(MainWindow mainWindow, PlayerIndex controllerId) : this(mainWindow, new Builder("Ryujinx.Ui.Windows.ControllerWindow.glade"), controllerId) { }
+
+ private ControllerWindow(MainWindow mainWindow, Builder builder, PlayerIndex controllerId) : base(builder.GetRawOwnedObject("_controllerWin"))
+ {
+ _mainWindow = mainWindow;
+ _selectedGamepad = null;
+
+ // NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused...
+ _gtk3KeyboardDriver = new GTK3KeyboardDriver(this);
+
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+
+ builder.Autoconnect(this);
+
+ _playerIndex = controllerId;
+ _inputConfig = ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerIndex);
+
+ Title = $"Ryujinx - Controller Settings - {_playerIndex}";
+
+ if (_playerIndex == PlayerIndex.Handheld)
+ {
+ _controllerType.Append(ControllerType.Handheld.ToString(), "Handheld");
+ _controllerType.Sensitive = false;
+ }
+ else
+ {
+ _controllerType.Append(ControllerType.ProController.ToString(), "Pro Controller");
+ _controllerType.Append(ControllerType.JoyconPair.ToString(), "Joycon Pair");
+ _controllerType.Append(ControllerType.JoyconLeft.ToString(), "Joycon Left");
+ _controllerType.Append(ControllerType.JoyconRight.ToString(), "Joycon Right");
+ }
+
+ _controllerType.Active = 0; // Set initial value to first in list.
+
+ // Bind Events.
+ _lStick.Clicked += ButtonForStick_Pressed;
+ _lStickUp.Clicked += Button_Pressed;
+ _lStickDown.Clicked += Button_Pressed;
+ _lStickLeft.Clicked += Button_Pressed;
+ _lStickRight.Clicked += Button_Pressed;
+ _lStickButton.Clicked += Button_Pressed;
+ _dpadUp.Clicked += Button_Pressed;
+ _dpadDown.Clicked += Button_Pressed;
+ _dpadLeft.Clicked += Button_Pressed;
+ _dpadRight.Clicked += Button_Pressed;
+ _minus.Clicked += Button_Pressed;
+ _l.Clicked += Button_Pressed;
+ _zL.Clicked += Button_Pressed;
+ _lSl.Clicked += Button_Pressed;
+ _lSr.Clicked += Button_Pressed;
+ _rStick.Clicked += ButtonForStick_Pressed;
+ _rStickUp.Clicked += Button_Pressed;
+ _rStickDown.Clicked += Button_Pressed;
+ _rStickLeft.Clicked += Button_Pressed;
+ _rStickRight.Clicked += Button_Pressed;
+ _rStickButton.Clicked += Button_Pressed;
+ _a.Clicked += Button_Pressed;
+ _b.Clicked += Button_Pressed;
+ _x.Clicked += Button_Pressed;
+ _y.Clicked += Button_Pressed;
+ _plus.Clicked += Button_Pressed;
+ _r.Clicked += Button_Pressed;
+ _zR.Clicked += Button_Pressed;
+ _rSl.Clicked += Button_Pressed;
+ _rSr.Clicked += Button_Pressed;
+ _enableCemuHook.Clicked += CemuHookCheckButtonPressed;
+
+ // Setup current values.
+ UpdateInputDeviceList();
+ SetAvailableOptions();
+
+ ClearValues();
+ if (_inputDevice.ActiveId != null)
+ {
+ SetCurrentValues();
+ }
+
+ mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
+ mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
+
+ if (_mainWindow.RendererWidget != null)
+ {
+ _mainWindow.RendererWidget.NpadManager.BlockInputUpdates();
+ }
+ }
+
+ private void CemuHookCheckButtonPressed(object sender, EventArgs e)
+ {
+ UpdateCemuHookSpecificFieldsVisibility();
+ }
+
+ private void HandleOnGamepadDisconnected(string id)
+ {
+ Application.Invoke(delegate
+ {
+ UpdateInputDeviceList();
+ });
+ }
+
+ private void HandleOnGamepadConnected(string id)
+ {
+ Application.Invoke(delegate
+ {
+ UpdateInputDeviceList();
+ });
+ }
+
+ protected override void OnDestroyed()
+ {
+ _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected;
+ _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected;
+
+ if (_mainWindow.RendererWidget != null)
+ {
+ _mainWindow.RendererWidget.NpadManager.UnblockInputUpdates();
+ }
+
+ _selectedGamepad?.Dispose();
+
+ _gtk3KeyboardDriver.Dispose();
+ }
+
+ private static string GetShortGamepadName(string str)
+ {
+ const string ShrinkChars = "...";
+ const int MaxSize = 50;
+
+ if (str.Length > MaxSize)
+ {
+ return $"{str.AsSpan(0, MaxSize - ShrinkChars.Length)}{ShrinkChars}";
+ }
+
+ return str;
+ }
+
+ private void UpdateInputDeviceList()
+ {
+ _inputDevice.RemoveAll();
+ _inputDevice.Append("disabled", "Disabled");
+ _inputDevice.SetActiveId("disabled");
+
+ foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds)
+ {
+ IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
+
+ if (gamepad != null)
+ {
+ _inputDevice.Append($"keyboard/{id}", GetShortGamepadName($"{gamepad.Name} ({id})"));
+
+ gamepad.Dispose();
+ }
+ }
+
+ foreach (string id in _mainWindow.InputManager.GamepadDriver.GamepadsIds)
+ {
+ IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
+
+ if (gamepad != null)
+ {
+ _inputDevice.Append($"controller/{id}", GetShortGamepadName($"{gamepad.Name} ({id})"));
+
+ gamepad.Dispose();
+ }
+ }
+
+ switch (_inputConfig)
+ {
+ case StandardKeyboardInputConfig keyboard:
+ _inputDevice.SetActiveId($"keyboard/{keyboard.Id}");
+ break;
+ case StandardControllerInputConfig controller:
+ _inputDevice.SetActiveId($"controller/{controller.Id}");
+ break;
+ }
+ }
+
+ private void UpdateCemuHookSpecificFieldsVisibility()
+ {
+ if (_enableCemuHook.Active)
+ {
+ _dsuServerHostBox.Show();
+ _dsuServerPortBox.Show();
+ _motionControllerSlot.Show();
+ _motionAltBox.Show();
+ _mirrorInput.Show();
+ }
+ else
+ {
+ _dsuServerHostBox.Hide();
+ _dsuServerPortBox.Hide();
+ _motionControllerSlot.Hide();
+ _motionAltBox.Hide();
+ _mirrorInput.Hide();
+ }
+ }
+
+ private void SetAvailableOptions()
+ {
+ if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("keyboard"))
+ {
+ ShowAll();
+ _leftStickController.Hide();
+ _rightStickController.Hide();
+ _deadZoneLeftBox.Hide();
+ _deadZoneRightBox.Hide();
+ _rangeLeftBox.Hide();
+ _rangeRightBox.Hide();
+ _triggerThresholdBox.Hide();
+ _motionBox.Hide();
+ _rumbleBox.Hide();
+ }
+ else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller"))
+ {
+ ShowAll();
+ _leftStickKeyboard.Hide();
+ _rightStickKeyboard.Hide();
+
+ UpdateCemuHookSpecificFieldsVisibility();
+ }
+ else
+ {
+ _settingsBox.Hide();
+ }
+
+ ClearValues();
+ }
+
+ private void SetCurrentValues()
+ {
+ SetControllerSpecificFields();
+
+ SetProfiles();
+
+ if (_inputDevice.ActiveId.StartsWith("keyboard") && _inputConfig is StandardKeyboardInputConfig)
+ {
+ SetValues(_inputConfig);
+ }
+ else if (_inputDevice.ActiveId.StartsWith("controller") && _inputConfig is StandardControllerInputConfig)
+ {
+ SetValues(_inputConfig);
+ }
+ }
+
+ private void SetControllerSpecificFields()
+ {
+ _leftSideTriggerBox.Hide();
+ _rightSideTriggerBox.Hide();
+ _motionAltBox.Hide();
+
+ switch (_controllerType.ActiveId)
+ {
+ case "JoyconLeft":
+ _leftSideTriggerBox.Show();
+ break;
+ case "JoyconRight":
+ _rightSideTriggerBox.Show();
+ break;
+ case "JoyconPair":
+ _motionAltBox.Show();
+ break;
+ }
+
+ if (!OperatingSystem.IsMacOS())
+ {
+ _controllerImage.Pixbuf = _controllerType.ActiveId switch
+ {
+ "ProController" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_ProCon.svg", 400, 400),
+ "JoyconLeft" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConLeft.svg", 400, 500),
+ "JoyconRight" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConRight.svg", 400, 500),
+ _ => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Controller_JoyConPair.svg", 400, 500),
+ };
+ }
+ }
+
+ private void ClearValues()
+ {
+ _lStick.Label = "Unbound";
+ _lStickUp.Label = "Unbound";
+ _lStickDown.Label = "Unbound";
+ _lStickLeft.Label = "Unbound";
+ _lStickRight.Label = "Unbound";
+ _lStickButton.Label = "Unbound";
+ _dpadUp.Label = "Unbound";
+ _dpadDown.Label = "Unbound";
+ _dpadLeft.Label = "Unbound";
+ _dpadRight.Label = "Unbound";
+ _minus.Label = "Unbound";
+ _l.Label = "Unbound";
+ _zL.Label = "Unbound";
+ _lSl.Label = "Unbound";
+ _lSr.Label = "Unbound";
+ _rStick.Label = "Unbound";
+ _rStickUp.Label = "Unbound";
+ _rStickDown.Label = "Unbound";
+ _rStickLeft.Label = "Unbound";
+ _rStickRight.Label = "Unbound";
+ _rStickButton.Label = "Unbound";
+ _a.Label = "Unbound";
+ _b.Label = "Unbound";
+ _x.Label = "Unbound";
+ _y.Label = "Unbound";
+ _plus.Label = "Unbound";
+ _r.Label = "Unbound";
+ _zR.Label = "Unbound";
+ _rSl.Label = "Unbound";
+ _rSr.Label = "Unbound";
+ _controllerStrongRumble.Value = 1;
+ _controllerWeakRumble.Value = 1;
+ _controllerDeadzoneLeft.Value = 0;
+ _controllerDeadzoneRight.Value = 0;
+ _controllerRangeLeft.Value = 1;
+ _controllerRangeRight.Value = 1;
+ _controllerTriggerThreshold.Value = 0;
+ _mirrorInput.Active = false;
+ _enableMotion.Active = false;
+ _enableCemuHook.Active = false;
+ _slotNumber.Value = 0;
+ _altSlotNumber.Value = 0;
+ _sensitivity.Value = 100;
+ _gyroDeadzone.Value = 1;
+ _dsuServerHost.Buffer.Text = "";
+ _dsuServerPort.Buffer.Text = "";
+ _enableRumble.Active = false;
+ }
+
+ private void SetValues(InputConfig config)
+ {
+ switch (config)
+ {
+ case StandardKeyboardInputConfig keyboardConfig:
+ if (!_controllerType.SetActiveId(keyboardConfig.ControllerType.ToString()))
+ {
+ _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld
+ ? ControllerType.Handheld.ToString()
+ : ControllerType.ProController.ToString());
+ }
+
+ _lStickUp.Label = keyboardConfig.LeftJoyconStick.StickUp.ToString();
+ _lStickDown.Label = keyboardConfig.LeftJoyconStick.StickDown.ToString();
+ _lStickLeft.Label = keyboardConfig.LeftJoyconStick.StickLeft.ToString();
+ _lStickRight.Label = keyboardConfig.LeftJoyconStick.StickRight.ToString();
+ _lStickButton.Label = keyboardConfig.LeftJoyconStick.StickButton.ToString();
+ _dpadUp.Label = keyboardConfig.LeftJoycon.DpadUp.ToString();
+ _dpadDown.Label = keyboardConfig.LeftJoycon.DpadDown.ToString();
+ _dpadLeft.Label = keyboardConfig.LeftJoycon.DpadLeft.ToString();
+ _dpadRight.Label = keyboardConfig.LeftJoycon.DpadRight.ToString();
+ _minus.Label = keyboardConfig.LeftJoycon.ButtonMinus.ToString();
+ _l.Label = keyboardConfig.LeftJoycon.ButtonL.ToString();
+ _zL.Label = keyboardConfig.LeftJoycon.ButtonZl.ToString();
+ _lSl.Label = keyboardConfig.LeftJoycon.ButtonSl.ToString();
+ _lSr.Label = keyboardConfig.LeftJoycon.ButtonSr.ToString();
+ _rStickUp.Label = keyboardConfig.RightJoyconStick.StickUp.ToString();
+ _rStickDown.Label = keyboardConfig.RightJoyconStick.StickDown.ToString();
+ _rStickLeft.Label = keyboardConfig.RightJoyconStick.StickLeft.ToString();
+ _rStickRight.Label = keyboardConfig.RightJoyconStick.StickRight.ToString();
+ _rStickButton.Label = keyboardConfig.RightJoyconStick.StickButton.ToString();
+ _a.Label = keyboardConfig.RightJoycon.ButtonA.ToString();
+ _b.Label = keyboardConfig.RightJoycon.ButtonB.ToString();
+ _x.Label = keyboardConfig.RightJoycon.ButtonX.ToString();
+ _y.Label = keyboardConfig.RightJoycon.ButtonY.ToString();
+ _plus.Label = keyboardConfig.RightJoycon.ButtonPlus.ToString();
+ _r.Label = keyboardConfig.RightJoycon.ButtonR.ToString();
+ _zR.Label = keyboardConfig.RightJoycon.ButtonZr.ToString();
+ _rSl.Label = keyboardConfig.RightJoycon.ButtonSl.ToString();
+ _rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString();
+ break;
+
+ case StandardControllerInputConfig controllerConfig:
+ if (!_controllerType.SetActiveId(controllerConfig.ControllerType.ToString()))
+ {
+ _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld
+ ? ControllerType.Handheld.ToString()
+ : ControllerType.ProController.ToString());
+ }
+
+ _lStick.Label = controllerConfig.LeftJoyconStick.Joystick.ToString();
+ _invertLStickX.Active = controllerConfig.LeftJoyconStick.InvertStickX;
+ _invertLStickY.Active = controllerConfig.LeftJoyconStick.InvertStickY;
+ _rotateL90CW.Active = controllerConfig.LeftJoyconStick.Rotate90CW;
+ _lStickButton.Label = controllerConfig.LeftJoyconStick.StickButton.ToString();
+ _dpadUp.Label = controllerConfig.LeftJoycon.DpadUp.ToString();
+ _dpadDown.Label = controllerConfig.LeftJoycon.DpadDown.ToString();
+ _dpadLeft.Label = controllerConfig.LeftJoycon.DpadLeft.ToString();
+ _dpadRight.Label = controllerConfig.LeftJoycon.DpadRight.ToString();
+ _minus.Label = controllerConfig.LeftJoycon.ButtonMinus.ToString();
+ _l.Label = controllerConfig.LeftJoycon.ButtonL.ToString();
+ _zL.Label = controllerConfig.LeftJoycon.ButtonZl.ToString();
+ _lSl.Label = controllerConfig.LeftJoycon.ButtonSl.ToString();
+ _lSr.Label = controllerConfig.LeftJoycon.ButtonSr.ToString();
+ _rStick.Label = controllerConfig.RightJoyconStick.Joystick.ToString();
+ _invertRStickX.Active = controllerConfig.RightJoyconStick.InvertStickX;
+ _invertRStickY.Active = controllerConfig.RightJoyconStick.InvertStickY;
+ _rotateR90CW.Active = controllerConfig.RightJoyconStick.Rotate90CW;
+ _rStickButton.Label = controllerConfig.RightJoyconStick.StickButton.ToString();
+ _a.Label = controllerConfig.RightJoycon.ButtonA.ToString();
+ _b.Label = controllerConfig.RightJoycon.ButtonB.ToString();
+ _x.Label = controllerConfig.RightJoycon.ButtonX.ToString();
+ _y.Label = controllerConfig.RightJoycon.ButtonY.ToString();
+ _plus.Label = controllerConfig.RightJoycon.ButtonPlus.ToString();
+ _r.Label = controllerConfig.RightJoycon.ButtonR.ToString();
+ _zR.Label = controllerConfig.RightJoycon.ButtonZr.ToString();
+ _rSl.Label = controllerConfig.RightJoycon.ButtonSl.ToString();
+ _rSr.Label = controllerConfig.RightJoycon.ButtonSr.ToString();
+ _controllerStrongRumble.Value = controllerConfig.Rumble.StrongRumble;
+ _controllerWeakRumble.Value = controllerConfig.Rumble.WeakRumble;
+ _enableRumble.Active = controllerConfig.Rumble.EnableRumble;
+ _controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft;
+ _controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight;
+ _controllerRangeLeft.Value = controllerConfig.RangeLeft;
+ _controllerRangeRight.Value = controllerConfig.RangeRight;
+ _controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold;
+ _sensitivity.Value = controllerConfig.Motion.Sensitivity;
+ _gyroDeadzone.Value = controllerConfig.Motion.GyroDeadzone;
+ _enableMotion.Active = controllerConfig.Motion.EnableMotion;
+ _enableCemuHook.Active = controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook;
+
+ // If both stick ranges are 0 (usually indicative of an outdated profile load) then both sticks will be set to 1.0.
+ if (_controllerRangeLeft.Value <= 0.0 && _controllerRangeRight.Value <= 0.0)
+ {
+ _controllerRangeLeft.Value = 1.0;
+ _controllerRangeRight.Value = 1.0;
+
+ Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} stick range reset. Save the profile now to update your configuration");
+ }
+
+ if (controllerConfig.Motion is CemuHookMotionConfigController cemuHookMotionConfig)
+ {
+ _slotNumber.Value = cemuHookMotionConfig.Slot;
+ _altSlotNumber.Value = cemuHookMotionConfig.AltSlot;
+ _mirrorInput.Active = cemuHookMotionConfig.MirrorInput;
+ _dsuServerHost.Buffer.Text = cemuHookMotionConfig.DsuServerHost;
+ _dsuServerPort.Buffer.Text = cemuHookMotionConfig.DsuServerPort.ToString();
+ }
+
+ break;
+ }
+ }
+
+ private InputConfig GetValues()
+ {
+ if (_inputDevice.ActiveId.StartsWith("keyboard"))
+ {
+ Enum.TryParse(_lStickUp.Label, out Key lStickUp);
+ Enum.TryParse(_lStickDown.Label, out Key lStickDown);
+ Enum.TryParse(_lStickLeft.Label, out Key lStickLeft);
+ Enum.TryParse(_lStickRight.Label, out Key lStickRight);
+ Enum.TryParse(_lStickButton.Label, out Key lStickButton);
+ Enum.TryParse(_dpadUp.Label, out Key lDPadUp);
+ Enum.TryParse(_dpadDown.Label, out Key lDPadDown);
+ Enum.TryParse(_dpadLeft.Label, out Key lDPadLeft);
+ Enum.TryParse(_dpadRight.Label, out Key lDPadRight);
+ Enum.TryParse(_minus.Label, out Key lButtonMinus);
+ Enum.TryParse(_l.Label, out Key lButtonL);
+ Enum.TryParse(_zL.Label, out Key lButtonZl);
+ Enum.TryParse(_lSl.Label, out Key lButtonSl);
+ Enum.TryParse(_lSr.Label, out Key lButtonSr);
+
+ Enum.TryParse(_rStickUp.Label, out Key rStickUp);
+ Enum.TryParse(_rStickDown.Label, out Key rStickDown);
+ Enum.TryParse(_rStickLeft.Label, out Key rStickLeft);
+ Enum.TryParse(_rStickRight.Label, out Key rStickRight);
+ Enum.TryParse(_rStickButton.Label, out Key rStickButton);
+ Enum.TryParse(_a.Label, out Key rButtonA);
+ Enum.TryParse(_b.Label, out Key rButtonB);
+ Enum.TryParse(_x.Label, out Key rButtonX);
+ Enum.TryParse(_y.Label, out Key rButtonY);
+ Enum.TryParse(_plus.Label, out Key rButtonPlus);
+ Enum.TryParse(_r.Label, out Key rButtonR);
+ Enum.TryParse(_zR.Label, out Key rButtonZr);
+ Enum.TryParse(_rSl.Label, out Key rButtonSl);
+ Enum.TryParse(_rSr.Label, out Key rButtonSr);
+
+ return new StandardKeyboardInputConfig
+ {
+ Backend = InputBackendType.WindowKeyboard,
+ Version = InputConfig.CurrentVersion,
+ Id = _inputDevice.ActiveId.Split("/")[1],
+ ControllerType = Enum.Parse<ControllerType>(_controllerType.ActiveId),
+ PlayerIndex = _playerIndex,
+ LeftJoycon = new LeftJoyconCommonConfig<Key>
+ {
+ ButtonMinus = lButtonMinus,
+ ButtonL = lButtonL,
+ ButtonZl = lButtonZl,
+ ButtonSl = lButtonSl,
+ ButtonSr = lButtonSr,
+ DpadUp = lDPadUp,
+ DpadDown = lDPadDown,
+ DpadLeft = lDPadLeft,
+ DpadRight = lDPadRight
+ },
+ LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
+ {
+ StickUp = lStickUp,
+ StickDown = lStickDown,
+ StickLeft = lStickLeft,
+ StickRight = lStickRight,
+ StickButton = lStickButton,
+ },
+ RightJoycon = new RightJoyconCommonConfig<Key>
+ {
+ ButtonA = rButtonA,
+ ButtonB = rButtonB,
+ ButtonX = rButtonX,
+ ButtonY = rButtonY,
+ ButtonPlus = rButtonPlus,
+ ButtonR = rButtonR,
+ ButtonZr = rButtonZr,
+ ButtonSl = rButtonSl,
+ ButtonSr = rButtonSr
+ },
+ RightJoyconStick = new JoyconConfigKeyboardStick<Key>
+ {
+ StickUp = rStickUp,
+ StickDown = rStickDown,
+ StickLeft = rStickLeft,
+ StickRight = rStickRight,
+ StickButton = rStickButton,
+ },
+ };
+ }
+
+ if (_inputDevice.ActiveId.StartsWith("controller"))
+ {
+ Enum.TryParse(_lStick.Label, out ConfigStickInputId lStick);
+ Enum.TryParse(_lStickButton.Label, out ConfigGamepadInputId lStickButton);
+ Enum.TryParse(_minus.Label, out ConfigGamepadInputId lButtonMinus);
+ Enum.TryParse(_l.Label, out ConfigGamepadInputId lButtonL);
+ Enum.TryParse(_zL.Label, out ConfigGamepadInputId lButtonZl);
+ Enum.TryParse(_lSl.Label, out ConfigGamepadInputId lButtonSl);
+ Enum.TryParse(_lSr.Label, out ConfigGamepadInputId lButtonSr);
+ Enum.TryParse(_dpadUp.Label, out ConfigGamepadInputId lDPadUp);
+ Enum.TryParse(_dpadDown.Label, out ConfigGamepadInputId lDPadDown);
+ Enum.TryParse(_dpadLeft.Label, out ConfigGamepadInputId lDPadLeft);
+ Enum.TryParse(_dpadRight.Label, out ConfigGamepadInputId lDPadRight);
+
+ Enum.TryParse(_rStick.Label, out ConfigStickInputId rStick);
+ Enum.TryParse(_rStickButton.Label, out ConfigGamepadInputId rStickButton);
+ Enum.TryParse(_a.Label, out ConfigGamepadInputId rButtonA);
+ Enum.TryParse(_b.Label, out ConfigGamepadInputId rButtonB);
+ Enum.TryParse(_x.Label, out ConfigGamepadInputId rButtonX);
+ Enum.TryParse(_y.Label, out ConfigGamepadInputId rButtonY);
+ Enum.TryParse(_plus.Label, out ConfigGamepadInputId rButtonPlus);
+ Enum.TryParse(_r.Label, out ConfigGamepadInputId rButtonR);
+ Enum.TryParse(_zR.Label, out ConfigGamepadInputId rButtonZr);
+ Enum.TryParse(_rSl.Label, out ConfigGamepadInputId rButtonSl);
+ Enum.TryParse(_rSr.Label, out ConfigGamepadInputId rButtonSr);
+
+ int.TryParse(_dsuServerPort.Buffer.Text, out int port);
+
+ MotionConfigController motionConfig;
+
+ if (_enableCemuHook.Active)
+ {
+ motionConfig = new CemuHookMotionConfigController
+ {
+ MotionBackend = MotionInputBackendType.CemuHook,
+ EnableMotion = _enableMotion.Active,
+ Sensitivity = (int)_sensitivity.Value,
+ GyroDeadzone = _gyroDeadzone.Value,
+ MirrorInput = _mirrorInput.Active,
+ Slot = (int)_slotNumber.Value,
+ AltSlot = (int)_altSlotNumber.Value,
+ DsuServerHost = _dsuServerHost.Buffer.Text,
+ DsuServerPort = port
+ };
+ }
+ else
+ {
+ motionConfig = new StandardMotionConfigController
+ {
+ MotionBackend = MotionInputBackendType.GamepadDriver,
+ EnableMotion = _enableMotion.Active,
+ Sensitivity = (int)_sensitivity.Value,
+ GyroDeadzone = _gyroDeadzone.Value,
+ };
+ }
+
+ return new StandardControllerInputConfig
+ {
+ Backend = InputBackendType.GamepadSDL2,
+ Version = InputConfig.CurrentVersion,
+ Id = _inputDevice.ActiveId.Split("/")[1].Split(" ")[0],
+ ControllerType = Enum.Parse<ControllerType>(_controllerType.ActiveId),
+ PlayerIndex = _playerIndex,
+ DeadzoneLeft = (float)_controllerDeadzoneLeft.Value,
+ DeadzoneRight = (float)_controllerDeadzoneRight.Value,
+ RangeLeft = (float)_controllerRangeLeft.Value,
+ RangeRight = (float)_controllerRangeRight.Value,
+ TriggerThreshold = (float)_controllerTriggerThreshold.Value,
+ LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
+ {
+ ButtonMinus = lButtonMinus,
+ ButtonL = lButtonL,
+ ButtonZl = lButtonZl,
+ ButtonSl = lButtonSl,
+ ButtonSr = lButtonSr,
+ DpadUp = lDPadUp,
+ DpadDown = lDPadDown,
+ DpadLeft = lDPadLeft,
+ DpadRight = lDPadRight
+ },
+ LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
+ {
+ InvertStickX = _invertLStickX.Active,
+ Joystick = lStick,
+ InvertStickY = _invertLStickY.Active,
+ StickButton = lStickButton,
+ Rotate90CW = _rotateL90CW.Active,
+ },
+ RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
+ {
+ ButtonA = rButtonA,
+ ButtonB = rButtonB,
+ ButtonX = rButtonX,
+ ButtonY = rButtonY,
+ ButtonPlus = rButtonPlus,
+ ButtonR = rButtonR,
+ ButtonZr = rButtonZr,
+ ButtonSl = rButtonSl,
+ ButtonSr = rButtonSr
+ },
+ RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
+ {
+ InvertStickX = _invertRStickX.Active,
+ Joystick = rStick,
+ InvertStickY = _invertRStickY.Active,
+ StickButton = rStickButton,
+ Rotate90CW = _rotateR90CW.Active,
+ },
+ Motion = motionConfig,
+ Rumble = new RumbleConfigController
+ {
+ StrongRumble = (float)_controllerStrongRumble.Value,
+ WeakRumble = (float)_controllerWeakRumble.Value,
+ EnableRumble = _enableRumble.Active
+ }
+ };
+ }
+
+ if (!_inputDevice.ActiveId.StartsWith("disabled"))
+ {
+ GtkDialog.CreateErrorDialog("Invalid data detected in one or more fields; the configuration was not saved.");
+ }
+
+ return null;
+ }
+
+ private string GetProfileBasePath()
+ {
+ if (_inputDevice.ActiveId.StartsWith("keyboard"))
+ {
+ return System.IO.Path.Combine(AppDataManager.ProfilesDirPath, "keyboard");
+ }
+ else if (_inputDevice.ActiveId.StartsWith("controller"))
+ {
+ return System.IO.Path.Combine(AppDataManager.ProfilesDirPath, "controller");
+ }
+
+ return AppDataManager.ProfilesDirPath;
+ }
+
+ //
+ // Events
+ //
+ private void InputDevice_Changed(object sender, EventArgs args)
+ {
+ SetAvailableOptions();
+ SetControllerSpecificFields();
+
+ _selectedGamepad?.Dispose();
+ _selectedGamepad = null;
+
+ if (_inputDevice.ActiveId != null)
+ {
+ SetProfiles();
+
+ string id = GetCurrentGamepadId();
+
+ if (_inputDevice.ActiveId.StartsWith("keyboard"))
+ {
+ if (_inputConfig is StandardKeyboardInputConfig)
+ {
+ SetValues(_inputConfig);
+ }
+
+ if (_mainWindow.InputManager.KeyboardDriver is GTK3KeyboardDriver)
+ {
+ // NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused...
+ _selectedGamepad = _gtk3KeyboardDriver.GetGamepad(id);
+ }
+ else
+ {
+ _selectedGamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
+ }
+ }
+ else if (_inputDevice.ActiveId.StartsWith("controller"))
+ {
+ if (_inputConfig is StandardControllerInputConfig)
+ {
+ SetValues(_inputConfig);
+ }
+
+ _selectedGamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id);
+ }
+ }
+ }
+
+ private string GetCurrentGamepadId()
+ {
+ if (_inputDevice.ActiveId == null || _inputDevice.ActiveId == "disabled")
+ {
+ return null;
+ }
+
+ return _inputDevice.ActiveId.Split("/")[1].Split(" ")[0];
+ }
+
+ private void Controller_Changed(object sender, EventArgs args)
+ {
+ SetControllerSpecificFields();
+ }
+
+ private IButtonAssigner CreateButtonAssigner(bool forStick)
+ {
+ IButtonAssigner assigner;
+
+ if (_inputDevice.ActiveId.StartsWith("keyboard"))
+ {
+ assigner = new KeyboardKeyAssigner((IKeyboard)_selectedGamepad);
+ }
+ else if (_inputDevice.ActiveId.StartsWith("controller"))
+ {
+ assigner = new GamepadButtonAssigner(_selectedGamepad, (float)_controllerTriggerThreshold.Value, forStick);
+ }
+ else
+ {
+ throw new Exception("Controller not supported");
+ }
+
+ return assigner;
+ }
+
+ private void HandleButtonPressed(ToggleButton button, bool forStick)
+ {
+ if (_isWaitingForInput)
+ {
+ button.Active = false;
+
+ return;
+ }
+
+ _mousePressed = false;
+
+ ButtonPressEvent += MouseClick;
+
+ IButtonAssigner assigner = CreateButtonAssigner(forStick);
+
+ _isWaitingForInput = true;
+
+ // Open GTK3 keyboard for cancel operations
+ IKeyboard keyboard = (IKeyboard)_gtk3KeyboardDriver.GetGamepad("0");
+
+ Thread inputThread = new Thread(() =>
+ {
+ assigner.Initialize();
+
+ while (true)
+ {
+ Thread.Sleep(10);
+ assigner.ReadInput();
+
+ if (_mousePressed || keyboard.IsPressed(Ryujinx.Input.Key.Escape) || assigner.HasAnyButtonPressed() || assigner.ShouldCancel())
+ {
+ break;
+ }
+ }
+
+ string pressedButton = assigner.GetPressedButton();
+
+ Application.Invoke(delegate
+ {
+ if (_middleMousePressed)
+ {
+ button.Label = "Unbound";
+ }
+ else if (pressedButton != "")
+ {
+ button.Label = pressedButton;
+ }
+
+ _middleMousePressed = false;
+
+ ButtonPressEvent -= MouseClick;
+ keyboard.Dispose();
+
+ button.Active = false;
+ _isWaitingForInput = false;
+ });
+ });
+
+ inputThread.Name = "GUI.InputThread";
+ inputThread.IsBackground = true;
+ inputThread.Start();
+ }
+
+ private void Button_Pressed(object sender, EventArgs args)
+ {
+ HandleButtonPressed((ToggleButton)sender, false);
+ }
+
+ private void ButtonForStick_Pressed(object sender, EventArgs args)
+ {
+ HandleButtonPressed((ToggleButton)sender, true);
+ }
+
+ private void MouseClick(object sender, ButtonPressEventArgs args)
+ {
+ _mousePressed = true;
+ _middleMousePressed = args.Event.Button == 2;
+ }
+
+ private void SetProfiles()
+ {
+ _profile.RemoveAll();
+
+ string basePath = GetProfileBasePath();
+
+ if (!Directory.Exists(basePath))
+ {
+ Directory.CreateDirectory(basePath);
+ }
+
+ if (_inputDevice.ActiveId == null|| _inputDevice.ActiveId.Equals("disabled"))
+ {
+ _profile.Append("default", "None");
+ }
+ else
+ {
+ _profile.Append("default", "Default");
+
+ foreach (string profile in Directory.GetFiles(basePath, "*.*", SearchOption.AllDirectories))
+ {
+ _profile.Append(System.IO.Path.GetFileName(profile), System.IO.Path.GetFileNameWithoutExtension(profile));
+ }
+ }
+
+ _profile.SetActiveId("default");
+ }
+
+ private void ProfileLoad_Activated(object sender, EventArgs args)
+ {
+ ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
+
+ if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == null) return;
+
+ InputConfig config = null;
+ int pos = _profile.Active;
+
+ if (_profile.ActiveId == "default")
+ {
+ if (_inputDevice.ActiveId.StartsWith("keyboard"))
+ {
+ config = new StandardKeyboardInputConfig
+ {
+ Version = InputConfig.CurrentVersion,
+ Backend = InputBackendType.WindowKeyboard,
+ Id = null,
+ ControllerType = ControllerType.ProController,
+ LeftJoycon = new LeftJoyconCommonConfig<Key>
+ {
+ DpadUp = Key.Up,
+ DpadDown = Key.Down,
+ DpadLeft = Key.Left,
+ DpadRight = Key.Right,
+ ButtonMinus = Key.Minus,
+ ButtonL = Key.E,
+ ButtonZl = Key.Q,
+ ButtonSl = Key.Unbound,
+ ButtonSr = Key.Unbound
+ },
+
+ LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
+ {
+ StickUp = Key.W,
+ StickDown = Key.S,
+ StickLeft = Key.A,
+ StickRight = Key.D,
+ StickButton = Key.F,
+ },
+
+ RightJoycon = new RightJoyconCommonConfig<Key>
+ {
+ ButtonA = Key.Z,
+ ButtonB = Key.X,
+ ButtonX = Key.C,
+ ButtonY = Key.V,
+ ButtonPlus = Key.Plus,
+ ButtonR = Key.U,
+ ButtonZr = Key.O,
+ ButtonSl = Key.Unbound,
+ ButtonSr = Key.Unbound
+ },
+
+ RightJoyconStick = new JoyconConfigKeyboardStick<Key>
+ {
+ StickUp = Key.I,
+ StickDown = Key.K,
+ StickLeft = Key.J,
+ StickRight = Key.L,
+ StickButton = Key.H,
+ }
+ };
+ }
+ else if (_inputDevice.ActiveId.StartsWith("controller"))
+ {
+ bool isNintendoStyle = _inputDevice.ActiveText.Contains("Nintendo");
+
+ config = new StandardControllerInputConfig
+ {
+ Version = InputConfig.CurrentVersion,
+ Backend = InputBackendType.GamepadSDL2,
+ Id = null,
+ ControllerType = ControllerType.JoyconPair,
+ DeadzoneLeft = 0.1f,
+ DeadzoneRight = 0.1f,
+ RangeLeft = 1.0f,
+ RangeRight = 1.0f,
+ TriggerThreshold = 0.5f,
+ LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
+ {
+ DpadUp = ConfigGamepadInputId.DpadUp,
+ DpadDown = ConfigGamepadInputId.DpadDown,
+ DpadLeft = ConfigGamepadInputId.DpadLeft,
+ DpadRight = ConfigGamepadInputId.DpadRight,
+ ButtonMinus = ConfigGamepadInputId.Minus,
+ ButtonL = ConfigGamepadInputId.LeftShoulder,
+ ButtonZl = ConfigGamepadInputId.LeftTrigger,
+ ButtonSl = ConfigGamepadInputId.Unbound,
+ ButtonSr = ConfigGamepadInputId.Unbound,
+ },
+
+ LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
+ {
+ Joystick = ConfigStickInputId.Left,
+ StickButton = ConfigGamepadInputId.LeftStick,
+ InvertStickX = false,
+ InvertStickY = false,
+ Rotate90CW = false,
+ },
+
+ RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
+ {
+ ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
+ ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
+ ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
+ ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
+ ButtonPlus = ConfigGamepadInputId.Plus,
+ ButtonR = ConfigGamepadInputId.RightShoulder,
+ ButtonZr = ConfigGamepadInputId.RightTrigger,
+ ButtonSl = ConfigGamepadInputId.Unbound,
+ ButtonSr = ConfigGamepadInputId.Unbound,
+ },
+
+ RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
+ {
+ Joystick = ConfigStickInputId.Right,
+ StickButton = ConfigGamepadInputId.RightStick,
+ InvertStickX = false,
+ InvertStickY = false,
+ Rotate90CW = false,
+ },
+
+ Motion = new StandardMotionConfigController
+ {
+ MotionBackend = MotionInputBackendType.GamepadDriver,
+ EnableMotion = true,
+ Sensitivity = 100,
+ GyroDeadzone = 1,
+ },
+ Rumble = new RumbleConfigController
+ {
+ StrongRumble = 1f,
+ WeakRumble = 1f,
+ EnableRumble = false
+ }
+ };
+ }
+ }
+ else
+ {
+ string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId);
+
+ if (!File.Exists(path))
+ {
+ if (pos >= 0)
+ {
+ _profile.Remove(pos);
+ }
+
+ return;
+ }
+
+ try
+ {
+ config = JsonHelper.DeserializeFromFile(path, SerializerContext.InputConfig);
+ }
+ catch (JsonException) { }
+ }
+
+ SetValues(config);
+ }
+
+ private void ProfileAdd_Activated(object sender, EventArgs args)
+ {
+ ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
+
+ if (_inputDevice.ActiveId == "disabled") return;
+
+ InputConfig inputConfig = GetValues();
+ ProfileDialog profileDialog = new ProfileDialog();
+
+ if (inputConfig == null) return;
+
+ if (profileDialog.Run() == (int)ResponseType.Ok)
+ {
+ string path = System.IO.Path.Combine(GetProfileBasePath(), profileDialog.FileName);
+ string jsonString = JsonHelper.Serialize(inputConfig, SerializerContext.InputConfig);
+
+ File.WriteAllText(path, jsonString);
+ }
+
+ profileDialog.Dispose();
+
+ SetProfiles();
+ }
+
+ private void ProfileRemove_Activated(object sender, EventArgs args)
+ {
+ ((ToggleButton) sender).SetStateFlags(StateFlags.Normal, true);
+
+ if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == "default" || _profile.ActiveId == null) return;
+
+ MessageDialog confirmDialog = GtkDialog.CreateConfirmationDialog("Deleting Profile", "This action is irreversible, are you sure you want to continue?");
+
+ if (confirmDialog.Run() == (int)ResponseType.Yes)
+ {
+ string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId);
+
+ if (File.Exists(path))
+ {
+ File.Delete(path);
+ }
+
+ SetProfiles();
+ }
+ }
+
+ private void SaveToggle_Activated(object sender, EventArgs args)
+ {
+ InputConfig inputConfig = GetValues();
+
+ var newConfig = new List<InputConfig>();
+ newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value);
+
+ if (_inputConfig == null && inputConfig != null)
+ {
+ newConfig.Add(inputConfig);
+ }
+ else
+ {
+ if (_inputDevice.ActiveId == "disabled")
+ {
+ newConfig.Remove(_inputConfig);
+ }
+ else if (inputConfig != null)
+ {
+ int index = newConfig.IndexOf(_inputConfig);
+
+ newConfig[index] = inputConfig;
+ }
+ }
+
+ if (_mainWindow.RendererWidget != null)
+ {
+ _mainWindow.RendererWidget.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);
+
+ Dispose();
+ }
+
+ private void CloseToggle_Activated(object sender, EventArgs args)
+ {
+ Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/Windows/ControllerWindow.glade b/src/Ryujinx/Ui/Windows/ControllerWindow.glade
new file mode 100644
index 00000000..e433f5cc
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/ControllerWindow.glade
@@ -0,0 +1,2241 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkAdjustment" id="_altSlotNumber">
+ <property name="upper">4</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">4</property>
+ </object>
+ <object class="GtkAdjustment" id="_controllerStrongRumble">
+ <property name="lower">0.1</property>
+ <property name="upper">10</property>
+ <property name="value">1.0</property>
+ <property name="step_increment">0.1</property>
+ <property name="page_increment">1.0</property>
+ </object>
+ <object class="GtkAdjustment" id="_controllerWeakRumble">
+ <property name="lower">0.1</property>
+ <property name="upper">10</property>
+ <property name="value">1.0</property>
+ <property name="step_increment">0.1</property>
+ <property name="page_increment">1.0</property>
+ </object>
+ <object class="GtkAdjustment" id="_controllerDeadzoneLeft">
+ <property name="upper">1</property>
+ <property name="value">0.050000000000000003</property>
+ <property name="step_increment">0.01</property>
+ <property name="page_increment">0.10000000000000001</property>
+ </object>
+ <object class="GtkAdjustment" id="_controllerDeadzoneRight">
+ <property name="upper">1</property>
+ <property name="value">0.050000000000000003</property>
+ <property name="step_increment">0.01</property>
+ <property name="page_increment">0.10000000000000001</property>
+ </object>
+ <object class="GtkAdjustment" id="_controllerRangeLeft">
+ <property name="upper">2</property>
+ <property name="value">1.000000000000000003</property>
+ <property name="step_increment">0.01</property>
+ <property name="page_increment">0.10000000000000001</property>
+ </object>
+ <object class="GtkAdjustment" id="_controllerRangeRight">
+ <property name="upper">2</property>
+ <property name="value">1.000000000000000003</property>
+ <property name="step_increment">0.01</property>
+ <property name="page_increment">0.10000000000000001</property>
+ </object>
+ <object class="GtkAdjustment" id="_controllerTriggerThreshold">
+ <property name="upper">1</property>
+ <property name="value">0.5</property>
+ <property name="step_increment">0.01</property>
+ <property name="page_increment">0.10000000000000001</property>
+ </object>
+ <object class="GtkAdjustment" id="_gyroDeadzone">
+ <property name="upper">100</property>
+ <property name="value">0.01</property>
+ <property name="step_increment">0.01</property>
+ <property name="page_increment">0.10000000000000001</property>
+ <property name="page_size">0.10000000000000001</property>
+ </object>
+ <object class="GtkAdjustment" id="_sensitivity">
+ <property name="upper">1000</property>
+ <property name="value">100</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">4</property>
+ </object>
+ <object class="GtkAdjustment" id="_slotNumber">
+ <property name="upper">4</property>
+ <property name="step_increment">1</property>
+ <property name="page_increment">4</property>
+ </object>
+ <object class="GtkWindow" id="_controllerWin">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Ryujinx - Controller Settings</property>
+ <property name="modal">True</property>
+ <property name="window_position">center</property>
+ <property name="default_width">1200</property>
+ <property name="default_height">720</property>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkViewport">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="HeadBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <child>
+ <object class="GtkBox" id="DeviceBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_right">5</property>
+ <property name="label" translatable="yes">Input Device</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_inputDevice">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="active">0</property>
+ <property name="active_id">disabled</property>
+ <items>
+ <item id="disabled" translatable="yes">Disabled</item>
+ </items>
+ <signal name="changed" handler="InputDevice_Changed" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="ControllerTypeBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">20</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">The controller's type</property>
+ <property name="halign">center</property>
+ <property name="margin_right">5</property>
+ <property name="label" translatable="yes">Controller Type:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_controllerType">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="tooltip_text" translatable="yes">The controller's type</property>
+ <property name="active">0</property>
+ <signal name="changed" handler="Controller_Changed" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="ProfileBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">20</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_right">5</property>
+ <property name="label" translatable="yes">Profile:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_profile">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_right">5</property>
+ <property name="active">0</property>
+ <property name="active_id">default</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton">
+ <property name="label" translatable="yes">Load</property>
+ <property name="width_request">60</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="margin_right">5</property>
+ <signal name="toggled" handler="ProfileLoad_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton">
+ <property name="label" translatable="yes">Add</property>
+ <property name="width_request">60</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="margin_right">5</property>
+ <signal name="toggled" handler="ProfileAdd_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton">
+ <property name="label" translatable="yes">Remove</property>
+ <property name="width_request">60</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="toggled" handler="ProfileRemove_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_settingsBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_top">5</property>
+ <child>
+ <object class="GtkBox" id="ButtonsBox">
+ <property name="width_request">156</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">5</property>
+ <property name="margin_bottom">5</property>
+ <property name="label" translatable="yes">Buttons</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">A</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">B</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">X</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Y</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_a">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">70</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_b">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">70</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_x">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">70</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_y">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">70</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">+</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">-</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_minus">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">70</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_plus">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">70</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">4</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="LeftStickBox">
+ <property name="width_request">160</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">5</property>
+ <property name="margin_bottom">5</property>
+ <property name="label" translatable="yes">Left Stick</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">5</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">LStick Button</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_lStickButton">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="_leftStickKeyboard">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkToggleButton" id="_lStickDown">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_lStickUp">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_lStickLeft">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_lStickRight">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">LStick Down</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">LStick Up</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">LStick Right</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">LStick Left</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="_leftStickController">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">LStick</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_lStick">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_invertLStickX">
+ <property name="label" translatable="yes">Invert Stick X</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_invertLStickY">
+ <property name="label" translatable="yes">Invert Stick Y</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_rotateL90CW">
+ <property name="label" translatable="yes">Rotate 90° Clockwise</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_deadZoneLeftBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Deadzone Left</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">_controllerDeadzoneLeft</property>
+ <property name="round_digits">2</property>
+ <property name="digits">2</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_rangeLeftBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Range Left</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">_controllerRangeLeft</property>
+ <property name="round_digits">2</property>
+ <property name="digits">2</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="TriggerBox">
+ <property name="width_request">150</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">5</property>
+ <property name="margin_bottom">5</property>
+ <property name="label" translatable="yes">Triggers</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">L</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">R</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_l">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_r">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">ZL</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">ZR</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_zL">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_zR">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="_leftSideTriggerBox">
+ <property name="name">_sideTriggerBox</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">5</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Left SL</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Left SR</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_lSl">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_lSr">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="_rightSideTriggerBox">
+ <property name="name">_sideTriggerBox</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">5</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Right SL</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Right SR</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_rSl">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_rSr">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_triggerThresholdBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin_left">10</property>
+ <property name="label" translatable="yes">Trigger Threshold</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">_controllerTriggerThreshold</property>
+ <property name="round_digits">2</property>
+ <property name="digits">2</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <child>
+ <object class="GtkBox" id="DPadBox">
+ <property name="width_request">156</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">5</property>
+ <property name="margin_bottom">5</property>
+ <property name="label" translatable="yes">Directional Pad</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Dpad Up</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Dpad Down</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Dpad Left</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Dpad Right</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_dpadUp">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">70</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_dpadDown">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">70</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_dpadLeft">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">70</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_dpadRight">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">70</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_rumbleBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">5</property>
+ <property name="label" translatable="yes">Rumble</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_enableRumble">
+ <property name="label" translatable="yes">Enable</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_StrongMultiBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Strong rumble multiplier</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">_controllerStrongRumble</property>
+ <property name="round_digits">1</property>
+ <property name="digits">1</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_WeakMultiBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Weak rumble multiplier</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">_controllerWeakRumble</property>
+ <property name="round_digits">1</property>
+ <property name="digits">1</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="RightStickBox">
+ <property name="width_request">160</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">5</property>
+ <property name="margin_bottom">5</property>
+ <property name="label" translatable="yes">Right Stick</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_bottom">5</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">RStick Button</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_rStickButton">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="_rightStickKeyboard">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">RStick Up</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">RStick Down</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">RStick Left</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">RStick Right</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_rStickUp">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_rStickDown">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_rStickLeft">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_rStickRight">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkGrid" id="_rightStickController">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="row_spacing">3</property>
+ <property name="column_spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="width_request">80</property>
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">RStick</property>
+ <property name="xalign">0</property>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_rStick">
+ <property name="label" translatable="yes"> </property>
+ <property name="width_request">65</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_invertRStickX">
+ <property name="label" translatable="yes">Invert Stick X</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_invertRStickY">
+ <property name="label" translatable="yes">Invert Stick Y</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_rotateR90CW">
+ <property name="label" translatable="yes">Rotate 90° Clockwise</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_deadZoneRightBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Deadzone Right</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">_controllerDeadzoneRight</property>
+ <property name="round_digits">2</property>
+ <property name="digits">2</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_rangeRightBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Range Right</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">_controllerRangeRight</property>
+ <property name="round_digits">2</property>
+ <property name="digits">2</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_motionBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">5</property>
+ <property name="margin_bottom">5</property>
+ <property name="label" translatable="yes">Motion</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_enableMotion">
+ <property name="label" translatable="yes">Enable Motion Controls</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_enableCemuHook">
+ <property name="label" translatable="yes">Use CemuHook compatible motion</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_motionControllerSlot">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_right">17</property>
+ <property name="label" translatable="yes">Controller Slot</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="_slot">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_left">10</property>
+ <property name="adjustment">_slotNumber</property>
+ <property name="climb_rate">1</property>
+ <property name="snap_to_ticks">True</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_right">5</property>
+ <property name="label" translatable="yes">Gyro Sensitivity %</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="text" translatable="yes">0</property>
+ <property name="adjustment">_sensitivity</property>
+ <property name="climb_rate">1</property>
+ <property name="snap_to_ticks">True</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_motionAltBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="_mirrorInput">
+ <property name="label" translatable="yes">Mirror Input</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">10</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Right JoyCon Slot</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="_slotRight">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="text" translatable="yes">0</property>
+ <property name="adjustment">_altSlotNumber</property>
+ <property name="climb_rate">1</property>
+ <property name="snap_to_ticks">True</property>
+ <property name="numeric">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_dsuServerHostBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">30</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Server Host</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="_dsuServerHost">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_dsuServerPortBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="spacing">30</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Server Port</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="_dsuServerPort">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="label" translatable="yes">Gyro Deadzone</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="adjustment">_gyroDeadzone</property>
+ <property name="round_digits">2</property>
+ <property name="digits">2</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">9</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="_controllerImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">20</property>
+ <property name="margin_top">5</property>
+ <property name="margin_bottom">5</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButtonBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_right">5</property>
+ <property name="margin_top">3</property>
+ <property name="margin_bottom">3</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkToggleButton" id="SaveToggle">
+ <property name="label" translatable="yes">Save</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <signal name="toggled" handler="SaveToggle_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="CloseToggle">
+ <property name="label" translatable="yes">Close</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="margin_left">4</property>
+ <signal name="toggled" handler="CloseToggle_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.cs b/src/Ryujinx/Ui/Windows/DlcWindow.cs
new file mode 100644
index 00000000..b22f1593
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/DlcWindow.cs
@@ -0,0 +1,273 @@
+using Gtk;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Tools.Fs;
+using LibHac.Tools.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.Ui.Widgets;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using GUI = Gtk.Builder.ObjectAttribute;
+
+namespace Ryujinx.Ui.Windows
+{
+ public class DlcWindow : Window
+ {
+ private readonly VirtualFileSystem _virtualFileSystem;
+ private readonly string _titleId;
+ private readonly string _dlcJsonPath;
+ private readonly List<DownloadableContentContainer> _dlcContainerList;
+
+ private static readonly DownloadableContentJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+#pragma warning disable CS0649, IDE0044
+ [GUI] Label _baseTitleInfoLabel;
+ [GUI] TreeView _dlcTreeView;
+ [GUI] TreeSelection _dlcTreeSelection;
+#pragma warning restore CS0649, IDE0044
+
+ public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { }
+
+ private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow"))
+ {
+ builder.Autoconnect(this);
+
+ _titleId = titleId;
+ _virtualFileSystem = virtualFileSystem;
+ _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json");
+ _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]";
+
+ try
+ {
+ _dlcContainerList = JsonHelper.DeserializeFromFile(_dlcJsonPath, SerializerContext.ListDownloadableContentContainer);
+ }
+ catch
+ {
+ _dlcContainerList = new List<DownloadableContentContainer>();
+ }
+
+ _dlcTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string));
+
+ CellRendererToggle enableToggle = new CellRendererToggle();
+ enableToggle.Toggled += (sender, args) =>
+ {
+ _dlcTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path));
+ bool newValue = !(bool)_dlcTreeView.Model.GetValue(treeIter, 0);
+ _dlcTreeView.Model.SetValue(treeIter, 0, newValue);
+
+ if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, treeIter))
+ {
+ do
+ {
+ _dlcTreeView.Model.SetValue(childIter, 0, newValue);
+ }
+ while (_dlcTreeView.Model.IterNext(ref childIter));
+ }
+ };
+
+ _dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0);
+ _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1);
+ _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
+
+ foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
+ {
+ if (File.Exists(dlcContainer.ContainerPath))
+ {
+ // The parent tree item has its own "enabled" check box, but it's the actual
+ // nca entries that store the enabled / disabled state. A bit of a UI inconsistency.
+ // Maybe a tri-state check box would be better, but for now we check the parent
+ // "enabled" box if all child NCAs are enabled. Usually fine since each nsp has only one nca.
+ bool areAllContentPacksEnabled = dlcContainer.DownloadableContentNcaList.TrueForAll((nca) => nca.Enabled);
+ TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.ContainerPath);
+ using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
+ PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
+ _virtualFileSystem.ImportTickets(pfs);
+
+ foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList)
+ {
+ using var ncaFile = new UniqueRef<IFile>();
+
+ pfs.OpenFile(ref ncaFile.Ref, dlcNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.ContainerPath);
+
+ if (nca != null)
+ {
+ ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.FullPath);
+ }
+ }
+ }
+ else
+ {
+ // DLC file moved or renamed. Allow the user to remove it without crashing the whole dialog.
+ TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.ContainerPath}");
+ }
+ }
+ }
+
+ private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
+ {
+ try
+ {
+ return new Nca(_virtualFileSystem.KeySet, ncaStorage);
+ }
+ catch (Exception exception)
+ {
+ GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {containerPath}");
+ }
+
+ return null;
+ }
+
+ private void AddButton_Clicked(object sender, EventArgs args)
+ {
+ FileChooserNative fileChooser = new FileChooserNative("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel")
+ {
+ SelectMultiple = true
+ };
+
+ FileFilter filter = new FileFilter()
+ {
+ Name = "Switch Game DLCs"
+ };
+ filter.AddPattern("*.nsp");
+
+ fileChooser.AddFilter(filter);
+
+ if (fileChooser.Run() == (int)ResponseType.Accept)
+ {
+ foreach (string containerPath in fileChooser.Filenames)
+ {
+ if (!File.Exists(containerPath))
+ {
+ return;
+ }
+
+ using (FileStream containerFile = File.OpenRead(containerPath))
+ {
+ PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
+ bool containsDlc = false;
+
+ _virtualFileSystem.ImportTickets(pfs);
+
+ TreeIter? parentIter = null;
+
+ foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
+ {
+ using var ncaFile = new UniqueRef<IFile>();
+
+ pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath);
+
+ if (nca == null) continue;
+
+ if (nca.Header.ContentType == NcaContentType.PublicData)
+ {
+ if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId)
+ {
+ break;
+ }
+
+ parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath);
+
+ ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
+ containsDlc = true;
+ }
+ }
+
+ if (!containsDlc)
+ {
+ GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
+ }
+ }
+ }
+ }
+
+ fileChooser.Dispose();
+ }
+
+ private void RemoveButton_Clicked(object sender, EventArgs args)
+ {
+ if (_dlcTreeSelection.GetSelected(out ITreeModel treeModel, out TreeIter treeIter))
+ {
+ if (_dlcTreeView.Model.IterParent(out TreeIter parentIter, treeIter) && _dlcTreeView.Model.IterNChildren(parentIter) <= 1)
+ {
+ ((TreeStore)treeModel).Remove(ref parentIter);
+ }
+ else
+ {
+ ((TreeStore)treeModel).Remove(ref treeIter);
+ }
+ }
+ }
+
+ private void RemoveAllButton_Clicked(object sender, EventArgs args)
+ {
+ List<TreeIter> toRemove = new List<TreeIter>();
+
+ if (_dlcTreeView.Model.GetIterFirst(out TreeIter iter))
+ {
+ do
+ {
+ toRemove.Add(iter);
+ }
+ while (_dlcTreeView.Model.IterNext(ref iter));
+ }
+
+ foreach (TreeIter i in toRemove)
+ {
+ TreeIter j = i;
+ ((TreeStore)_dlcTreeView.Model).Remove(ref j);
+ }
+ }
+
+ private void SaveButton_Clicked(object sender, EventArgs args)
+ {
+ _dlcContainerList.Clear();
+
+ if (_dlcTreeView.Model.GetIterFirst(out TreeIter parentIter))
+ {
+ do
+ {
+ if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter))
+ {
+ DownloadableContentContainer dlcContainer = new DownloadableContentContainer
+ {
+ ContainerPath = (string)_dlcTreeView.Model.GetValue(parentIter, 2),
+ DownloadableContentNcaList = new List<DownloadableContentNca>()
+ };
+
+ do
+ {
+ dlcContainer.DownloadableContentNcaList.Add(new DownloadableContentNca
+ {
+ Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0),
+ TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16),
+ FullPath = (string)_dlcTreeView.Model.GetValue(childIter, 2)
+ });
+ }
+ while (_dlcTreeView.Model.IterNext(ref childIter));
+
+ _dlcContainerList.Add(dlcContainer);
+ }
+ }
+ while (_dlcTreeView.Model.IterNext(ref parentIter));
+ }
+
+ JsonHelper.SerializeToFile(_dlcJsonPath, _dlcContainerList, SerializerContext.ListDownloadableContentContainer);
+
+ Dispose();
+ }
+
+ private void CancelButton_Clicked(object sender, EventArgs args)
+ {
+ Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/Windows/DlcWindow.glade b/src/Ryujinx/Ui/Windows/DlcWindow.glade
new file mode 100644
index 00000000..cd0d8674
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/DlcWindow.glade
@@ -0,0 +1,202 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.36.0 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkWindow" id="_dlcWindow">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Ryujinx - DLC Manager</property>
+ <property name="modal">True</property>
+ <property name="window_position">center</property>
+ <property name="default_width">550</property>
+ <property name="default_height">350</property>
+ <child>
+ <object class="GtkBox" id="MainBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="DlcBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="_baseTitleInfoLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="label" translatable="yes">Available DLC</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkViewport">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkTreeView" id="_dlcTreeView">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="headers_clickable">False</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection" id="_dlcTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkButtonBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="layout_style">start</property>
+ <child>
+ <object class="GtkButton" id="_addUpdate">
+ <property name="label" translatable="yes">Add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Adds an update to this list</property>
+ <property name="margin_left">10</property>
+ <signal name="clicked" handler="AddButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="_removeUpdate">
+ <property name="label" translatable="yes">Remove</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Removes the selected update</property>
+ <property name="margin_left">10</property>
+ <signal name="clicked" handler="RemoveButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="_removeAllButton">
+ <property name="label" translatable="yes">Remove All</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Removes the selected update</property>
+ <property name="margin_left">10</property>
+ <signal name="clicked" handler="RemoveAllButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButtonBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="_saveButton">
+ <property name="label" translatable="yes">Save</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">2</property>
+ <property name="margin_bottom">2</property>
+ <signal name="clicked" handler="SaveButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="_cancelButton">
+ <property name="label" translatable="yes">Cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">2</property>
+ <property name="margin_bottom">2</property>
+ <signal name="clicked" handler="CancelButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ </object>
+</interface>
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.cs b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
new file mode 100644
index 00000000..27080bda
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
@@ -0,0 +1,816 @@
+using Gtk;
+using LibHac.Tools.FsSystem;
+using Ryujinx.Audio.Backends.OpenAL;
+using Ryujinx.Audio.Backends.SDL2;
+using Ryujinx.Audio.Backends.SoundIo;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Configuration.Hid;
+using Ryujinx.Common.GraphicsDriver;
+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 Ryujinx.Ui.Helper;
+using Ryujinx.Ui.Widgets;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Net.NetworkInformation;
+using System.Reflection;
+using System.Threading.Tasks;
+using GUI = Gtk.Builder.ObjectAttribute;
+
+namespace Ryujinx.Ui.Windows
+{
+ public class SettingsWindow : Window
+ {
+ private readonly MainWindow _parent;
+ private readonly ListStore _gameDirsBoxStore;
+ private readonly ListStore _audioBackendStore;
+ private readonly TimeZoneContentManager _timeZoneContentManager;
+ private readonly HashSet<string> _validTzRegions;
+
+ private long _systemTimeOffset;
+ private float _previousVolumeLevel;
+ private bool _directoryChanged = false;
+
+#pragma warning disable CS0649, IDE0044
+ [GUI] CheckButton _traceLogToggle;
+ [GUI] CheckButton _errorLogToggle;
+ [GUI] CheckButton _warningLogToggle;
+ [GUI] CheckButton _infoLogToggle;
+ [GUI] CheckButton _stubLogToggle;
+ [GUI] CheckButton _debugLogToggle;
+ [GUI] CheckButton _fileLogToggle;
+ [GUI] CheckButton _guestLogToggle;
+ [GUI] CheckButton _fsAccessLogToggle;
+ [GUI] Adjustment _fsLogSpinAdjustment;
+ [GUI] ComboBoxText _graphicsDebugLevel;
+ [GUI] CheckButton _dockedModeToggle;
+ [GUI] CheckButton _discordToggle;
+ [GUI] CheckButton _checkUpdatesToggle;
+ [GUI] CheckButton _showConfirmExitToggle;
+ [GUI] CheckButton _hideCursorOnIdleToggle;
+ [GUI] CheckButton _vSyncToggle;
+ [GUI] CheckButton _shaderCacheToggle;
+ [GUI] CheckButton _textureRecompressionToggle;
+ [GUI] CheckButton _macroHLEToggle;
+ [GUI] CheckButton _ptcToggle;
+ [GUI] CheckButton _internetToggle;
+ [GUI] CheckButton _fsicToggle;
+ [GUI] RadioButton _mmSoftware;
+ [GUI] RadioButton _mmHost;
+ [GUI] RadioButton _mmHostUnsafe;
+ [GUI] CheckButton _expandRamToggle;
+ [GUI] CheckButton _ignoreToggle;
+ [GUI] CheckButton _directKeyboardAccess;
+ [GUI] CheckButton _directMouseAccess;
+ [GUI] ComboBoxText _systemLanguageSelect;
+ [GUI] ComboBoxText _systemRegionSelect;
+ [GUI] Entry _systemTimeZoneEntry;
+ [GUI] EntryCompletion _systemTimeZoneCompletion;
+ [GUI] Box _audioBackendBox;
+ [GUI] ComboBox _audioBackendSelect;
+ [GUI] Label _audioVolumeLabel;
+ [GUI] Scale _audioVolumeSlider;
+ [GUI] SpinButton _systemTimeYearSpin;
+ [GUI] SpinButton _systemTimeMonthSpin;
+ [GUI] SpinButton _systemTimeDaySpin;
+ [GUI] SpinButton _systemTimeHourSpin;
+ [GUI] SpinButton _systemTimeMinuteSpin;
+ [GUI] Adjustment _systemTimeYearSpinAdjustment;
+ [GUI] Adjustment _systemTimeMonthSpinAdjustment;
+ [GUI] Adjustment _systemTimeDaySpinAdjustment;
+ [GUI] Adjustment _systemTimeHourSpinAdjustment;
+ [GUI] Adjustment _systemTimeMinuteSpinAdjustment;
+ [GUI] ComboBoxText _multiLanSelect;
+ [GUI] CheckButton _custThemeToggle;
+ [GUI] Entry _custThemePath;
+ [GUI] ToggleButton _browseThemePath;
+ [GUI] Label _custThemePathLabel;
+ [GUI] TreeView _gameDirsBox;
+ [GUI] Entry _addGameDirBox;
+ [GUI] ComboBoxText _galThreading;
+ [GUI] Entry _graphicsShadersDumpPath;
+ [GUI] ComboBoxText _anisotropy;
+ [GUI] ComboBoxText _aspectRatio;
+ [GUI] ComboBoxText _antiAliasing;
+ [GUI] ComboBoxText _scalingFilter;
+ [GUI] ComboBoxText _graphicsBackend;
+ [GUI] ComboBoxText _preferredGpu;
+ [GUI] ComboBoxText _resScaleCombo;
+ [GUI] Entry _resScaleText;
+ [GUI] Adjustment _scalingFilterLevel;
+ [GUI] Scale _scalingFilterSlider;
+ [GUI] ToggleButton _configureController1;
+ [GUI] ToggleButton _configureController2;
+ [GUI] ToggleButton _configureController3;
+ [GUI] ToggleButton _configureController4;
+ [GUI] ToggleButton _configureController5;
+ [GUI] ToggleButton _configureController6;
+ [GUI] ToggleButton _configureController7;
+ [GUI] ToggleButton _configureController8;
+ [GUI] ToggleButton _configureControllerH;
+
+#pragma warning restore CS0649, IDE0044
+
+ public SettingsWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(parent, new Builder("Ryujinx.Ui.Windows.SettingsWindow.glade"), virtualFileSystem, contentManager) { }
+
+ private SettingsWindow(MainWindow parent, Builder builder, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : base(builder.GetRawOwnedObject("_settingsWin"))
+ {
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+
+ _parent = parent;
+
+ builder.Autoconnect(this);
+
+ _timeZoneContentManager = new TimeZoneContentManager();
+ _timeZoneContentManager.InitializeInstance(virtualFileSystem, contentManager, IntegrityCheckLevel.None);
+
+ _validTzRegions = new HashSet<string>(_timeZoneContentManager.LocationNameCache.Length, StringComparer.Ordinal); // Zone regions are identifiers. Must match exactly.
+
+ // Bind Events.
+ _configureController1.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player1);
+ _configureController2.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player2);
+ _configureController3.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player3);
+ _configureController4.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player4);
+ _configureController5.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player5);
+ _configureController6.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player6);
+ _configureController7.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player7);
+ _configureController8.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player8);
+ _configureControllerH.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Handheld);
+ _systemTimeZoneEntry.FocusOutEvent += TimeZoneEntry_FocusOut;
+
+ _resScaleCombo.Changed += (sender, args) => _resScaleText.Visible = _resScaleCombo.ActiveId == "-1";
+ _scalingFilter.Changed += (sender, args) => _scalingFilterSlider.Visible = _scalingFilter.ActiveId == "2";
+ _galThreading.Changed += (sender, args) =>
+ {
+ if (_galThreading.ActiveId != ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString())
+ {
+ GtkDialog.CreateInfoDialog("Warning - Backend Threading", "Ryujinx must be restarted after changing this option for it to apply fully. Depending on your platform, you may need to manually disable your driver's own multithreading when using Ryujinx's.");
+ }
+ };
+
+ // Setup Currents.
+ if (ConfigurationState.Instance.Logger.EnableTrace)
+ {
+ _traceLogToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Logger.EnableFileLog)
+ {
+ _fileLogToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Logger.EnableError)
+ {
+ _errorLogToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Logger.EnableWarn)
+ {
+ _warningLogToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Logger.EnableInfo)
+ {
+ _infoLogToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Logger.EnableStub)
+ {
+ _stubLogToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Logger.EnableDebug)
+ {
+ _debugLogToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Logger.EnableGuest)
+ {
+ _guestLogToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Logger.EnableFsAccessLog)
+ {
+ _fsAccessLogToggle.Click();
+ }
+
+ foreach (GraphicsDebugLevel level in Enum.GetValues<GraphicsDebugLevel>())
+ {
+ _graphicsDebugLevel.Append(level.ToString(), level.ToString());
+ }
+
+ _graphicsDebugLevel.SetActiveId(ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value.ToString());
+
+ if (ConfigurationState.Instance.System.EnableDockedMode)
+ {
+ _dockedModeToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.EnableDiscordIntegration)
+ {
+ _discordToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.CheckUpdatesOnStart)
+ {
+ _checkUpdatesToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.ShowConfirmExit)
+ {
+ _showConfirmExitToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.HideCursorOnIdle)
+ {
+ _hideCursorOnIdleToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Graphics.EnableVsync)
+ {
+ _vSyncToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Graphics.EnableShaderCache)
+ {
+ _shaderCacheToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Graphics.EnableTextureRecompression)
+ {
+ _textureRecompressionToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Graphics.EnableMacroHLE)
+ {
+ _macroHLEToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.System.EnablePtc)
+ {
+ _ptcToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.System.EnableInternetAccess)
+ {
+ _internetToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.System.EnableFsIntegrityChecks)
+ {
+ _fsicToggle.Click();
+ }
+
+ switch (ConfigurationState.Instance.System.MemoryManagerMode.Value)
+ {
+ case MemoryManagerMode.SoftwarePageTable:
+ _mmSoftware.Click();
+ break;
+ case MemoryManagerMode.HostMapped:
+ _mmHost.Click();
+ break;
+ case MemoryManagerMode.HostMappedUnsafe:
+ _mmHostUnsafe.Click();
+ break;
+ }
+
+ if (ConfigurationState.Instance.System.ExpandRam)
+ {
+ _expandRamToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.System.IgnoreMissingServices)
+ {
+ _ignoreToggle.Click();
+ }
+
+ if (ConfigurationState.Instance.Hid.EnableKeyboard)
+ {
+ _directKeyboardAccess.Click();
+ }
+
+ if (ConfigurationState.Instance.Hid.EnableMouse)
+ {
+ _directMouseAccess.Click();
+ }
+
+ if (ConfigurationState.Instance.Ui.EnableCustomTheme)
+ {
+ _custThemeToggle.Click();
+ }
+
+ // Custom EntryCompletion Columns. If added to glade, need to override more signals
+ ListStore tzList = new ListStore(typeof(string), typeof(string), typeof(string));
+ _systemTimeZoneCompletion.Model = tzList;
+
+ CellRendererText offsetCol = new CellRendererText();
+ CellRendererText abbrevCol = new CellRendererText();
+
+ _systemTimeZoneCompletion.PackStart(offsetCol, false);
+ _systemTimeZoneCompletion.AddAttribute(offsetCol, "text", 0);
+ _systemTimeZoneCompletion.TextColumn = 1; // Regions Column
+ _systemTimeZoneCompletion.PackStart(abbrevCol, false);
+ _systemTimeZoneCompletion.AddAttribute(abbrevCol, "text", 2);
+
+ int maxLocationLength = 0;
+
+ foreach (var (offset, location, abbr) in _timeZoneContentManager.ParseTzOffsets())
+ {
+ var hours = Math.DivRem(offset, 3600, out int seconds);
+ var minutes = Math.Abs(seconds) / 60;
+
+ var abbr2 = (abbr.StartsWith('+') || abbr.StartsWith('-')) ? string.Empty : abbr;
+
+ tzList.AppendValues($"UTC{hours:+0#;-0#;+00}:{minutes:D2} ", location, abbr2);
+ _validTzRegions.Add(location);
+
+ maxLocationLength = Math.Max(maxLocationLength, location.Length);
+ }
+
+ _systemTimeZoneEntry.WidthChars = Math.Max(20, maxLocationLength + 1); // Ensure minimum Entry width
+ _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(ConfigurationState.Instance.System.TimeZone);
+
+ _systemTimeZoneCompletion.MatchFunc = TimeZoneMatchFunc;
+
+ _systemLanguageSelect.SetActiveId(ConfigurationState.Instance.System.Language.Value.ToString());
+ _systemRegionSelect.SetActiveId(ConfigurationState.Instance.System.Region.Value.ToString());
+ _galThreading.SetActiveId(ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString());
+ _resScaleCombo.SetActiveId(ConfigurationState.Instance.Graphics.ResScale.Value.ToString());
+ _anisotropy.SetActiveId(ConfigurationState.Instance.Graphics.MaxAnisotropy.Value.ToString());
+ _aspectRatio.SetActiveId(((int)ConfigurationState.Instance.Graphics.AspectRatio.Value).ToString());
+ _graphicsBackend.SetActiveId(((int)ConfigurationState.Instance.Graphics.GraphicsBackend.Value).ToString());
+ _antiAliasing.SetActiveId(((int)ConfigurationState.Instance.Graphics.AntiAliasing.Value).ToString());
+ _scalingFilter.SetActiveId(((int)ConfigurationState.Instance.Graphics.ScalingFilter.Value).ToString());
+
+ UpdatePreferredGpuComboBox();
+
+ _graphicsBackend.Changed += (sender, e) => UpdatePreferredGpuComboBox();
+ PopulateNetworkInterfaces();
+ _multiLanSelect.SetActiveId(ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value);
+
+ _custThemePath.Buffer.Text = ConfigurationState.Instance.Ui.CustomThemePath;
+ _resScaleText.Buffer.Text = ConfigurationState.Instance.Graphics.ResScaleCustom.Value.ToString();
+ _scalingFilterLevel.Value = ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value;
+ _resScaleText.Visible = _resScaleCombo.ActiveId == "-1";
+ _scalingFilterSlider.Visible = _scalingFilter.ActiveId == "2";
+ _graphicsShadersDumpPath.Buffer.Text = ConfigurationState.Instance.Graphics.ShadersDumpPath;
+ _fsLogSpinAdjustment.Value = ConfigurationState.Instance.System.FsGlobalAccessLogMode;
+ _systemTimeOffset = ConfigurationState.Instance.System.SystemTimeOffset;
+
+ _gameDirsBox.AppendColumn("", new CellRendererText(), "text", 0);
+ _gameDirsBoxStore = new ListStore(typeof(string));
+ _gameDirsBox.Model = _gameDirsBoxStore;
+
+ foreach (string gameDir in ConfigurationState.Instance.Ui.GameDirs.Value)
+ {
+ _gameDirsBoxStore.AppendValues(gameDir);
+ }
+
+ if (_custThemeToggle.Active == false)
+ {
+ _custThemePath.Sensitive = false;
+ _custThemePathLabel.Sensitive = false;
+ _browseThemePath.Sensitive = false;
+ }
+
+ // Setup system time spinners
+ UpdateSystemTimeSpinners();
+
+ _audioBackendStore = new ListStore(typeof(string), typeof(AudioBackend));
+
+ TreeIter openAlIter = _audioBackendStore.AppendValues("OpenAL", AudioBackend.OpenAl);
+ TreeIter soundIoIter = _audioBackendStore.AppendValues("SoundIO", AudioBackend.SoundIo);
+ TreeIter sdl2Iter = _audioBackendStore.AppendValues("SDL2", AudioBackend.SDL2);
+ TreeIter dummyIter = _audioBackendStore.AppendValues("Dummy", AudioBackend.Dummy);
+
+ _audioBackendSelect = ComboBox.NewWithModelAndEntry(_audioBackendStore);
+ _audioBackendSelect.EntryTextColumn = 0;
+ _audioBackendSelect.Entry.IsEditable = false;
+
+ switch (ConfigurationState.Instance.System.AudioBackend.Value)
+ {
+ case AudioBackend.OpenAl:
+ _audioBackendSelect.SetActiveIter(openAlIter);
+ break;
+ case AudioBackend.SoundIo:
+ _audioBackendSelect.SetActiveIter(soundIoIter);
+ break;
+ case AudioBackend.SDL2:
+ _audioBackendSelect.SetActiveIter(sdl2Iter);
+ break;
+ case AudioBackend.Dummy:
+ _audioBackendSelect.SetActiveIter(dummyIter);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
+ }
+
+ _audioBackendBox.Add(_audioBackendSelect);
+ _audioBackendSelect.Show();
+
+ _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume;
+ _audioVolumeLabel = new Label("Volume: ");
+ _audioVolumeSlider = new Scale(Orientation.Horizontal, 0, 100, 1);
+ _audioVolumeLabel.MarginStart = 10;
+ _audioVolumeSlider.ValuePos = PositionType.Right;
+ _audioVolumeSlider.WidthRequest = 200;
+
+ _audioVolumeSlider.Value = _previousVolumeLevel * 100;
+ _audioVolumeSlider.ValueChanged += VolumeSlider_OnChange;
+ _audioBackendBox.Add(_audioVolumeLabel);
+ _audioBackendBox.Add(_audioVolumeSlider);
+ _audioVolumeLabel.Show();
+ _audioVolumeSlider.Show();
+
+ bool openAlIsSupported = false;
+ bool soundIoIsSupported = false;
+ bool sdl2IsSupported = false;
+
+ Task.Run(() =>
+ {
+ openAlIsSupported = OpenALHardwareDeviceDriver.IsSupported;
+ soundIoIsSupported = !OperatingSystem.IsMacOS() && SoundIoHardwareDeviceDriver.IsSupported;
+ sdl2IsSupported = SDL2HardwareDeviceDriver.IsSupported;
+ });
+
+ // This function runs whenever the dropdown is opened
+ _audioBackendSelect.SetCellDataFunc(_audioBackendSelect.Cells[0], (layout, cell, model, iter) =>
+ {
+ cell.Sensitive = ((AudioBackend)_audioBackendStore.GetValue(iter, 1)) switch
+ {
+ AudioBackend.OpenAl => openAlIsSupported,
+ AudioBackend.SoundIo => soundIoIsSupported,
+ AudioBackend.SDL2 => sdl2IsSupported,
+ AudioBackend.Dummy => true,
+ _ => throw new ArgumentOutOfRangeException()
+ };
+ });
+
+ if (OperatingSystem.IsMacOS())
+ {
+ var store = (_graphicsBackend.Model as ListStore);
+ store.GetIter(out TreeIter openglIter, new TreePath(new int[] {1}));
+ store.Remove(ref openglIter);
+
+ _graphicsBackend.Model = store;
+ }
+ }
+
+ private void UpdatePreferredGpuComboBox()
+ {
+ _preferredGpu.RemoveAll();
+
+ if (Enum.Parse<GraphicsBackend>(_graphicsBackend.ActiveId) == GraphicsBackend.Vulkan)
+ {
+ var devices = VulkanRenderer.GetPhysicalDevices();
+ string preferredGpuIdFromConfig = ConfigurationState.Instance.Graphics.PreferredGpu.Value;
+ string preferredGpuId = preferredGpuIdFromConfig;
+ bool noGpuId = string.IsNullOrEmpty(preferredGpuIdFromConfig);
+
+ foreach (var device in devices)
+ {
+ string dGPU = device.IsDiscrete ? " (dGPU)" : "";
+ _preferredGpu.Append(device.Id, $"{device.Name}{dGPU}");
+
+ // If there's no GPU selected yet, we just pick the first GPU.
+ // If there's a discrete GPU available, we always prefer that over the previous selection,
+ // as it is likely to have better performance and more features.
+ // If the configuration file already has a GPU selection, we always prefer that instead.
+ if (noGpuId && (string.IsNullOrEmpty(preferredGpuId) || device.IsDiscrete))
+ {
+ preferredGpuId = device.Id;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(preferredGpuId))
+ {
+ _preferredGpu.SetActiveId(preferredGpuId);
+ }
+ }
+ }
+
+ private void PopulateNetworkInterfaces()
+ {
+ NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
+
+ foreach (NetworkInterface nif in interfaces)
+ {
+ string guid = nif.Id;
+ string name = nif.Name;
+
+ _multiLanSelect.Append(guid, name);
+ }
+ }
+
+ private void UpdateSystemTimeSpinners()
+ {
+ //Bind system time events
+ _systemTimeYearSpin.ValueChanged -= SystemTimeSpin_ValueChanged;
+ _systemTimeMonthSpin.ValueChanged -= SystemTimeSpin_ValueChanged;
+ _systemTimeDaySpin.ValueChanged -= SystemTimeSpin_ValueChanged;
+ _systemTimeHourSpin.ValueChanged -= SystemTimeSpin_ValueChanged;
+ _systemTimeMinuteSpin.ValueChanged -= SystemTimeSpin_ValueChanged;
+
+ //Apply actual system time + SystemTimeOffset to system time spin buttons
+ DateTime systemTime = DateTime.Now.AddSeconds(_systemTimeOffset);
+
+ _systemTimeYearSpinAdjustment.Value = systemTime.Year;
+ _systemTimeMonthSpinAdjustment.Value = systemTime.Month;
+ _systemTimeDaySpinAdjustment.Value = systemTime.Day;
+ _systemTimeHourSpinAdjustment.Value = systemTime.Hour;
+ _systemTimeMinuteSpinAdjustment.Value = systemTime.Minute;
+
+ //Format spin buttons text to include leading zeros
+ _systemTimeYearSpin.Text = systemTime.Year.ToString("0000");
+ _systemTimeMonthSpin.Text = systemTime.Month.ToString("00");
+ _systemTimeDaySpin.Text = systemTime.Day.ToString("00");
+ _systemTimeHourSpin.Text = systemTime.Hour.ToString("00");
+ _systemTimeMinuteSpin.Text = systemTime.Minute.ToString("00");
+
+ //Bind system time events
+ _systemTimeYearSpin.ValueChanged += SystemTimeSpin_ValueChanged;
+ _systemTimeMonthSpin.ValueChanged += SystemTimeSpin_ValueChanged;
+ _systemTimeDaySpin.ValueChanged += SystemTimeSpin_ValueChanged;
+ _systemTimeHourSpin.ValueChanged += SystemTimeSpin_ValueChanged;
+ _systemTimeMinuteSpin.ValueChanged += SystemTimeSpin_ValueChanged;
+ }
+
+ private void SaveSettings()
+ {
+ if (_directoryChanged)
+ {
+ List<string> gameDirs = new List<string>();
+
+ _gameDirsBoxStore.GetIterFirst(out TreeIter treeIter);
+
+ for (int i = 0; i < _gameDirsBoxStore.IterNChildren(); i++)
+ {
+ gameDirs.Add((string)_gameDirsBoxStore.GetValue(treeIter, 0));
+
+ _gameDirsBoxStore.IterNext(ref treeIter);
+ }
+
+ ConfigurationState.Instance.Ui.GameDirs.Value = gameDirs;
+
+ _directoryChanged = false;
+ }
+
+ if (!float.TryParse(_resScaleText.Buffer.Text, out float resScaleCustom) || resScaleCustom <= 0.0f)
+ {
+ resScaleCustom = 1.0f;
+ }
+
+ if (_validTzRegions.Contains(_systemTimeZoneEntry.Text))
+ {
+ ConfigurationState.Instance.System.TimeZone.Value = _systemTimeZoneEntry.Text;
+ }
+
+ MemoryManagerMode memoryMode = MemoryManagerMode.SoftwarePageTable;
+
+ if (_mmHost.Active)
+ {
+ memoryMode = MemoryManagerMode.HostMapped;
+ }
+
+ if (_mmHostUnsafe.Active)
+ {
+ memoryMode = MemoryManagerMode.HostMappedUnsafe;
+ }
+
+ BackendThreading backendThreading = Enum.Parse<BackendThreading>(_galThreading.ActiveId);
+ if (ConfigurationState.Instance.Graphics.BackendThreading != backendThreading)
+ {
+ DriverUtilities.ToggleOGLThreading(backendThreading == BackendThreading.Off);
+ }
+
+ ConfigurationState.Instance.Logger.EnableError.Value = _errorLogToggle.Active;
+ ConfigurationState.Instance.Logger.EnableTrace.Value = _traceLogToggle.Active;
+ ConfigurationState.Instance.Logger.EnableWarn.Value = _warningLogToggle.Active;
+ ConfigurationState.Instance.Logger.EnableInfo.Value = _infoLogToggle.Active;
+ ConfigurationState.Instance.Logger.EnableStub.Value = _stubLogToggle.Active;
+ ConfigurationState.Instance.Logger.EnableDebug.Value = _debugLogToggle.Active;
+ ConfigurationState.Instance.Logger.EnableGuest.Value = _guestLogToggle.Active;
+ ConfigurationState.Instance.Logger.EnableFsAccessLog.Value = _fsAccessLogToggle.Active;
+ ConfigurationState.Instance.Logger.EnableFileLog.Value = _fileLogToggle.Active;
+ ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value = Enum.Parse<GraphicsDebugLevel>(_graphicsDebugLevel.ActiveId);
+ ConfigurationState.Instance.System.EnableDockedMode.Value = _dockedModeToggle.Active;
+ ConfigurationState.Instance.EnableDiscordIntegration.Value = _discordToggle.Active;
+ ConfigurationState.Instance.CheckUpdatesOnStart.Value = _checkUpdatesToggle.Active;
+ ConfigurationState.Instance.ShowConfirmExit.Value = _showConfirmExitToggle.Active;
+ ConfigurationState.Instance.HideCursorOnIdle.Value = _hideCursorOnIdleToggle.Active;
+ ConfigurationState.Instance.Graphics.EnableVsync.Value = _vSyncToggle.Active;
+ ConfigurationState.Instance.Graphics.EnableShaderCache.Value = _shaderCacheToggle.Active;
+ ConfigurationState.Instance.Graphics.EnableTextureRecompression.Value = _textureRecompressionToggle.Active;
+ ConfigurationState.Instance.Graphics.EnableMacroHLE.Value = _macroHLEToggle.Active;
+ ConfigurationState.Instance.System.EnablePtc.Value = _ptcToggle.Active;
+ ConfigurationState.Instance.System.EnableInternetAccess.Value = _internetToggle.Active;
+ ConfigurationState.Instance.System.EnableFsIntegrityChecks.Value = _fsicToggle.Active;
+ ConfigurationState.Instance.System.MemoryManagerMode.Value = memoryMode;
+ ConfigurationState.Instance.System.ExpandRam.Value = _expandRamToggle.Active;
+ ConfigurationState.Instance.System.IgnoreMissingServices.Value = _ignoreToggle.Active;
+ ConfigurationState.Instance.Hid.EnableKeyboard.Value = _directKeyboardAccess.Active;
+ ConfigurationState.Instance.Hid.EnableMouse.Value = _directMouseAccess.Active;
+ ConfigurationState.Instance.Ui.EnableCustomTheme.Value = _custThemeToggle.Active;
+ ConfigurationState.Instance.System.Language.Value = Enum.Parse<Language>(_systemLanguageSelect.ActiveId);
+ ConfigurationState.Instance.System.Region.Value = Enum.Parse<Common.Configuration.System.Region>(_systemRegionSelect.ActiveId);
+ ConfigurationState.Instance.System.SystemTimeOffset.Value = _systemTimeOffset;
+ ConfigurationState.Instance.Ui.CustomThemePath.Value = _custThemePath.Buffer.Text;
+ ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = _graphicsShadersDumpPath.Buffer.Text;
+ ConfigurationState.Instance.System.FsGlobalAccessLogMode.Value = (int)_fsLogSpinAdjustment.Value;
+ ConfigurationState.Instance.Graphics.MaxAnisotropy.Value = float.Parse(_anisotropy.ActiveId, CultureInfo.InvariantCulture);
+ ConfigurationState.Instance.Graphics.AspectRatio.Value = Enum.Parse<AspectRatio>(_aspectRatio.ActiveId);
+ ConfigurationState.Instance.Graphics.BackendThreading.Value = backendThreading;
+ ConfigurationState.Instance.Graphics.GraphicsBackend.Value = Enum.Parse<GraphicsBackend>(_graphicsBackend.ActiveId);
+ ConfigurationState.Instance.Graphics.PreferredGpu.Value = _preferredGpu.ActiveId;
+ ConfigurationState.Instance.Graphics.ResScale.Value = int.Parse(_resScaleCombo.ActiveId);
+ ConfigurationState.Instance.Graphics.ResScaleCustom.Value = resScaleCustom;
+ ConfigurationState.Instance.System.AudioVolume.Value = (float)_audioVolumeSlider.Value / 100.0f;
+ ConfigurationState.Instance.Graphics.AntiAliasing.Value = Enum.Parse<AntiAliasing>(_antiAliasing.ActiveId);
+ ConfigurationState.Instance.Graphics.ScalingFilter.Value = Enum.Parse<ScalingFilter>(_scalingFilter.ActiveId);
+ ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value = (int)_scalingFilterLevel.Value;
+ ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId;
+
+ _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume.Value;
+
+ if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter))
+ {
+ ConfigurationState.Instance.System.AudioBackend.Value = (AudioBackend)_audioBackendStore.GetValue(activeIter, 1);
+ }
+
+ ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
+ _parent.UpdateGraphicsConfig();
+ ThemeHelper.ApplyTheme();
+ }
+
+ //
+ // Events
+ //
+ private void TimeZoneEntry_FocusOut(object sender, FocusOutEventArgs e)
+ {
+ if (!_validTzRegions.Contains(_systemTimeZoneEntry.Text))
+ {
+ _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(ConfigurationState.Instance.System.TimeZone);
+ }
+ }
+
+ private bool TimeZoneMatchFunc(EntryCompletion compl, string key, TreeIter iter)
+ {
+ key = key.Trim().Replace(' ', '_');
+
+ return ((string)compl.Model.GetValue(iter, 1)).Contains(key, StringComparison.OrdinalIgnoreCase) || // region
+ ((string)compl.Model.GetValue(iter, 2)).StartsWith(key, StringComparison.OrdinalIgnoreCase) || // abbr
+ ((string)compl.Model.GetValue(iter, 0))[3..].StartsWith(key); // offset
+ }
+
+ private void SystemTimeSpin_ValueChanged(object sender, EventArgs e)
+ {
+ int year = _systemTimeYearSpin.ValueAsInt;
+ int month = _systemTimeMonthSpin.ValueAsInt;
+ int day = _systemTimeDaySpin.ValueAsInt;
+ int hour = _systemTimeHourSpin.ValueAsInt;
+ int minute = _systemTimeMinuteSpin.ValueAsInt;
+
+ if (!DateTime.TryParse(year + "-" + month + "-" + day + " " + hour + ":" + minute, out DateTime newTime))
+ {
+ UpdateSystemTimeSpinners();
+
+ return;
+ }
+
+ newTime = newTime.AddSeconds(DateTime.Now.Second).AddMilliseconds(DateTime.Now.Millisecond);
+
+ long systemTimeOffset = (long)Math.Ceiling((newTime - DateTime.Now).TotalMinutes) * 60L;
+
+ if (_systemTimeOffset != systemTimeOffset)
+ {
+ _systemTimeOffset = systemTimeOffset;
+ UpdateSystemTimeSpinners();
+ }
+ }
+
+ private void AddDir_Pressed(object sender, EventArgs args)
+ {
+ if (Directory.Exists(_addGameDirBox.Buffer.Text))
+ {
+ _gameDirsBoxStore.AppendValues(_addGameDirBox.Buffer.Text);
+ }
+ else
+ {
+ FileChooserNative fileChooser = new FileChooserNative("Choose the game directory to add to the list", this, FileChooserAction.SelectFolder, "Add", "Cancel")
+ {
+ SelectMultiple = true
+ };
+
+ if (fileChooser.Run() == (int)ResponseType.Accept)
+ {
+ _directoryChanged = false;
+ foreach (string directory in fileChooser.Filenames)
+ {
+ if (_gameDirsBoxStore.GetIterFirst(out TreeIter treeIter))
+ {
+ do
+ {
+ if (directory.Equals((string)_gameDirsBoxStore.GetValue(treeIter, 0)))
+ {
+ break;
+ }
+ } while (_gameDirsBoxStore.IterNext(ref treeIter));
+ }
+
+ if (!_directoryChanged)
+ {
+ _gameDirsBoxStore.AppendValues(directory);
+ }
+ }
+
+ _directoryChanged = true;
+ }
+
+ fileChooser.Dispose();
+ }
+
+ _addGameDirBox.Buffer.Text = "";
+
+ ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
+ }
+
+ private void RemoveDir_Pressed(object sender, EventArgs args)
+ {
+ TreeSelection selection = _gameDirsBox.Selection;
+
+ if (selection.GetSelected(out TreeIter treeIter))
+ {
+ _gameDirsBoxStore.Remove(ref treeIter);
+
+ _directoryChanged = true;
+ }
+
+ ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
+ }
+
+ private void CustThemeToggle_Activated(object sender, EventArgs args)
+ {
+ _custThemePath.Sensitive = _custThemeToggle.Active;
+ _custThemePathLabel.Sensitive = _custThemeToggle.Active;
+ _browseThemePath.Sensitive = _custThemeToggle.Active;
+ }
+
+ private void BrowseThemeDir_Pressed(object sender, EventArgs args)
+ {
+ using (FileChooserNative fileChooser = new FileChooserNative("Choose the theme to load", this, FileChooserAction.Open, "Select", "Cancel"))
+ {
+ FileFilter filter = new FileFilter()
+ {
+ Name = "Theme Files"
+ };
+ filter.AddPattern("*.css");
+
+ fileChooser.AddFilter(filter);
+
+ if (fileChooser.Run() == (int)ResponseType.Accept)
+ {
+ _custThemePath.Buffer.Text = fileChooser.Filename;
+ }
+ }
+
+ _browseThemePath.SetStateFlags(StateFlags.Normal, true);
+ }
+
+ private void ConfigureController_Pressed(object sender, PlayerIndex playerIndex)
+ {
+ ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
+
+ ControllerWindow controllerWindow = new ControllerWindow(_parent, playerIndex);
+
+ controllerWindow.SetSizeRequest((int)(controllerWindow.DefaultWidth * Program.WindowScaleFactor), (int)(controllerWindow.DefaultHeight * Program.WindowScaleFactor));
+ controllerWindow.Show();
+ }
+
+ private void VolumeSlider_OnChange(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.System.AudioVolume.Value = (float)(_audioVolumeSlider.Value / 100);
+ }
+
+ private void SaveToggle_Activated(object sender, EventArgs args)
+ {
+ SaveSettings();
+ Dispose();
+ }
+
+ private void ApplyToggle_Activated(object sender, EventArgs args)
+ {
+ SaveSettings();
+ }
+
+ private void CloseToggle_Activated(object sender, EventArgs args)
+ {
+ ConfigurationState.Instance.System.AudioVolume.Value = _previousVolumeLevel;
+ Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.glade b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
new file mode 100644
index 00000000..8ae6ea72
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
@@ -0,0 +1,3066 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.40.0 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkAdjustment" id="_fsLogSpinAdjustment">
+ <property name="upper">3</property>
+ <property name="step-increment">1</property>
+ <property name="page-increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="_scalingFilterLevel">
+ <property name="upper">101</property>
+ <property name="step-increment">1</property>
+ <property name="page-increment">5</property>
+ <property name="page-size">1</property>
+ </object>
+ <object class="GtkAdjustment" id="_systemTimeDaySpinAdjustment">
+ <property name="lower">1</property>
+ <property name="upper">31</property>
+ <property name="step-increment">1</property>
+ <property name="page-increment">5</property>
+ </object>
+ <object class="GtkAdjustment" id="_systemTimeHourSpinAdjustment">
+ <property name="upper">23</property>
+ <property name="step-increment">1</property>
+ <property name="page-increment">5</property>
+ </object>
+ <object class="GtkAdjustment" id="_systemTimeMinuteSpinAdjustment">
+ <property name="upper">59</property>
+ <property name="step-increment">1</property>
+ <property name="page-increment">5</property>
+ </object>
+ <object class="GtkAdjustment" id="_systemTimeMonthSpinAdjustment">
+ <property name="lower">1</property>
+ <property name="upper">12</property>
+ <property name="step-increment">1</property>
+ <property name="page-increment">5</property>
+ </object>
+ <object class="GtkAdjustment" id="_systemTimeYearSpinAdjustment">
+ <property name="lower">2000</property>
+ <property name="upper">2060</property>
+ <property name="step-increment">1</property>
+ <property name="page-increment">10</property>
+ </object>
+ <object class="GtkEntryCompletion" id="_systemTimeZoneCompletion">
+ <property name="minimum-key-length">0</property>
+ <property name="inline-completion">True</property>
+ <property name="inline-selection">True</property>
+ </object>
+ <object class="GtkWindow" id="_settingsWin">
+ <property name="can-focus">False</property>
+ <property name="title" translatable="yes">Ryujinx - Settings</property>
+ <property name="modal">True</property>
+ <property name="window-position">center</property>
+ <property name="default-width">650</property>
+ <property name="default-height">650</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="shadow-type">in</property>
+ <child>
+ <object class="GtkViewport">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkNotebook">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <child>
+ <object class="GtkBox" id="TabGeneral">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">10</property>
+ <property name="margin-top">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="CatGeneral">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">General</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="General">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="_discordToggle">
+ <property name="label" translatable="yes">Enable Discord Rich Presence</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Choose whether or not to display Ryujinx on your "currently playing" Discord activity</property>
+ <property name="halign">start</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_checkUpdatesToggle">
+ <property name="label" translatable="yes">Check for Updates on Launch</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="halign">start</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_showConfirmExitToggle">
+ <property name="label" translatable="yes">Show "Confirm Exit" Dialog</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="halign">start</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_hideCursorOnIdleToggle">
+ <property name="label" translatable="yes">Hide Cursor On Idle</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="halign">start</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="CatGameDir">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">Game Directories</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="margin-bottom">10</property>
+ <property name="shadow-type">in</property>
+ <child>
+ <object class="GtkTreeView" id="_gameDirsBox">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="headers-visible">False</property>
+ <property name="headers-clickable">False</property>
+ <child internal-child="selection">
+ <object class="GtkTreeSelection"/>
+ </child>
+ </object>
+ </child>
+ <style>
+ <class name="GameDir"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkEntry" id="_addGameDirBox">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="tooltip-text" translatable="yes">Enter a game directory to add to the list</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_addDir">
+ <property name="label" translatable="yes">Add</property>
+ <property name="width-request">80</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="tooltip-text" translatable="yes"> Add a game directory to the list</property>
+ <property name="margin-left">5</property>
+ <signal name="toggled" handler="AddDir_Pressed" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_removeDir">
+ <property name="label" translatable="yes">Remove</property>
+ <property name="width-request">80</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="tooltip-text" translatable="yes">Remove selected game directory</property>
+ <property name="margin-left">5</property>
+ <signal name="toggled" handler="RemoveDir_Pressed" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="CatThemes">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">Themes</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="_custThemeToggle">
+ <property name="label" translatable="yes">Use Custom Theme</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Enable or disable custom themes in the GUI</property>
+ <property name="halign">start</property>
+ <property name="draw-indicator">True</property>
+ <signal name="toggled" handler="CustThemeToggle_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkLabel" id="_custThemePathLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Path to custom GUI theme</property>
+ <property name="label" translatable="yes">Custom Theme Path:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="_custThemePath">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="tooltip-text" translatable="yes">Path to custom GUI theme</property>
+ <property name="valign">center</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_browseThemePath">
+ <property name="label" translatable="yes">Browse...</property>
+ <property name="width-request">80</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="tooltip-text" translatable="yes">Browse for a custom GUI theme</property>
+ <property name="margin-left">5</property>
+ <signal name="toggled" handler="BrowseThemeDir_Pressed" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">10</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">General</property>
+ </object>
+ <packing>
+ <property name="tab-fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="TabInput">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">10</property>
+ <property name="margin-top">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <child>
+ <object class="GtkCheckButton" id="_dockedModeToggle">
+ <property name="label" translatable="yes">Enable Docked Mode</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Docked mode makes the emulated system behave as a docked Nintendo Switch. This improves graphical fidelity in most games. Conversely, disabling this will make the emulated system behave as a handheld Nintendo Switch, reducing graphics quality.&#13;Configure player 1 controls if planning to use docked mode; configure handheld controls if planning to use handheld mode.&#13;Leave ON if unsure.</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">10</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_directKeyboardAccess">
+ <property name="label" translatable="yes">Direct Keyboard Access</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device.</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="padding">10</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_directMouseAccess">
+ <property name="label" translatable="yes">Direct Mouse Access</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Direct mouse access (HID) support. Provides games access to your mouse as a pointing device.</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="padding">10</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <!-- n-columns=5 n-rows=5 -->
+ <object class="GtkGrid" id="ControllerGrid">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="column-spacing">20</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ <property name="label" translatable="yes">Player 1</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_configureController1">
+ <property name="label" translatable="yes">Configure</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="margin-left">20</property>
+ <property name="margin-right">20</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ <property name="label" translatable="yes">Player 3</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_configureController3">
+ <property name="label" translatable="yes">Configure</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="margin-left">20</property>
+ <property name="margin-right">20</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">4</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ <property name="label" translatable="yes">Player 2</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_configureController2">
+ <property name="label" translatable="yes">Configure</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="margin-left">20</property>
+ <property name="margin-right">20</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ <property name="label" translatable="yes">Handheld</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_configureControllerH">
+ <property name="label" translatable="yes">Configure</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="margin-left">20</property>
+ <property name="margin-right">20</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">4</property>
+ <property name="top-attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ <property name="label" translatable="yes">Player 6</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_configureController6">
+ <property name="label" translatable="yes">Configure</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="margin-left">20</property>
+ <property name="margin-right">20</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">4</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ <property name="label" translatable="yes">Player 5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_configureController5">
+ <property name="label" translatable="yes">Configure</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="margin-left">20</property>
+ <property name="margin-right">20</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ <property name="label" translatable="yes">Player 7</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_configureController7">
+ <property name="label" translatable="yes">Configure</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="margin-left">20</property>
+ <property name="margin-right">20</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ <property name="label" translatable="yes">Player 4</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_configureController4">
+ <property name="label" translatable="yes">Configure</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="margin-left">20</property>
+ <property name="margin-right">20</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ <property name="label" translatable="yes">Player 8</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="_configureController8">
+ <property name="label" translatable="yes">Configure</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <property name="margin-left">20</property>
+ <property name="margin-right">20</property>
+ <property name="margin-top">20</property>
+ <property name="margin-bottom">20</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">3</property>
+ <property name="top-attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">3</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">3</property>
+ <property name="top-attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">1</property>
+ <property name="top-attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">3</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">3</property>
+ <property name="top-attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">4</property>
+ <property name="top-attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">0</property>
+ <property name="top-attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">2</property>
+ <property name="top-attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="left-attach">4</property>
+ <property name="top-attach">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Input</property>
+ </object>
+ <packing>
+ <property name="position">1</property>
+ <property name="tab-fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="TabSystem">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">10</property>
+ <property name="margin-top">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="CatCore">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">Core</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="RegionBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Change System Region</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">System Region:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_systemRegionSelect">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Change System Region</property>
+ <property name="margin-left">5</property>
+ <items>
+ <item id="Japan" translatable="yes">Japan</item>
+ <item id="USA" translatable="yes">USA</item>
+ <item id="Europe" translatable="yes">Europe</item>
+ <item id="Australia" translatable="yes">Australia</item>
+ <item id="China" translatable="yes">China</item>
+ <item id="Korea" translatable="yes">Korea</item>
+ <item id="Taiwan" translatable="yes">Taiwan</item>
+ </items>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="LanguageBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Change System Language</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">System Language:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_systemLanguageSelect">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Change System Language</property>
+ <items>
+ <item id="AmericanEnglish" translatable="yes">American English</item>
+ <item id="BritishEnglish" translatable="yes">British English</item>
+ <item id="CanadianFrench" translatable="yes">Canadian French</item>
+ <item id="Chinese" translatable="yes">Chinese</item>
+ <item id="Dutch" translatable="yes">Dutch</item>
+ <item id="French" translatable="yes">French</item>
+ <item id="German" translatable="yes">German</item>
+ <item id="Italian" translatable="yes">Italian</item>
+ <item id="Japanese" translatable="yes">Japanese</item>
+ <item id="Korean" translatable="yes">Korean</item>
+ <item id="LatinAmericanSpanish" translatable="yes">Latin American Spanish</item>
+ <item id="Portuguese" translatable="yes">Portuguese</item>
+ <item id="Russian" translatable="yes">Russian</item>
+ <item id="SimplifiedChinese" translatable="yes">Simplified Chinese</item>
+ <item id="Spanish" translatable="yes">Spanish</item>
+ <item id="Taiwanese" translatable="yes">Taiwanese</item>
+ <item id="TraditionalChinese" translatable="yes">Traditional Chinese</item>
+ <item id="BrazilianPortuguese" translatable="yes">Brazilian Portuguese</item>
+ </items>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="TimeZoneBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Change System TimeZone</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">System TimeZone:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="_systemTimeZoneEntry">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="tooltip-text" translatable="yes">Change System TimeZone</property>
+ <property name="margin-left">5</property>
+ <property name="completion">_systemTimeZoneCompletion</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="TimeBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Change System Time</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">System Time:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="_systemTimeYearSpin">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="text" translatable="yes">2000</property>
+ <property name="orientation">vertical</property>
+ <property name="adjustment">_systemTimeYearSpinAdjustment</property>
+ <property name="wrap">True</property>
+ <property name="value">2000</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label">-</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="_systemTimeMonthSpin">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="text" translatable="yes">1</property>
+ <property name="orientation">vertical</property>
+ <property name="adjustment">_systemTimeMonthSpinAdjustment</property>
+ <property name="wrap">True</property>
+ <property name="value">1</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label">-</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="_systemTimeDaySpin">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="text" translatable="yes">1</property>
+ <property name="orientation">vertical</property>
+ <property name="adjustment">_systemTimeDaySpinAdjustment</property>
+ <property name="wrap">True</property>
+ <property name="value">1</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="_systemTimeHourSpin">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="text" translatable="yes">0</property>
+ <property name="orientation">vertical</property>
+ <property name="adjustment">_systemTimeHourSpinAdjustment</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label">:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton" id="_systemTimeMinuteSpin">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="text" translatable="yes">0</property>
+ <property name="orientation">vertical</property>
+ <property name="adjustment">_systemTimeMinuteSpinAdjustment</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">8</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_vSyncToggle">
+ <property name="label" translatable="yes">Enable VSync</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck.&#13;Can be toggled in-game with a hotkey of your preference. We recommend doing this if you plan on disabling it.&#13;Leave ON if unsure.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_ptcToggle">
+ <property name="label" translatable="yes">Enable PPTC (Profiled Persistent Translation Cache)</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Saves translated JIT functions so that they do not need to be translated every time the game loads.&#13;Reduces stuttering and significantly speeds up boot times after the first boot of a game.&#13;Leave ON if unsure.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_internetToggle">
+ <property name="label" translatable="yes">Enable Guest Internet Access</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Allows the emulated application to connect to the Internet.&#13;Games with a LAN mode can connect to each other when this is enabled and the systems are connected to the same access point. This includes real consoles as well.&#13;Does NOT allow connecting to Nintendo servers. May cause crashing in certain games that try to connect to the Internet.&#13;Leave OFF if unsure.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_fsicToggle">
+ <property name="label" translatable="yes">Enable FS Integrity Checks</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Checks for corrupt files when booting a game, and if corrupt files are detected, displays a hash error in the log.&#13;Has no impact on performance and is meant to help troubleshooting.&#13;Leave ON if unsure.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">8</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_audioBackendBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Changes the backend used to render audio.&#13;SDL2 is the preferred one, while OpenAL and SoundIO are used as fallbacks. Dummy will have no sound.&#13;Set to SDL2 if unsure.</property>
+ <property name="halign">end</property>
+ <property name="margin-right">5</property>
+ <property name="label" translatable="yes">Audio Backend: </property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="_memoryManagerBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <placeholder/>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Change how guest memory is mapped and accessed. Greatly affects emulated CPU performance.&#13;Set to HOST UNCHECKED if unsure.</property>
+ <property name="halign">end</property>
+ <property name="margin-right">5</property>
+ <property name="label" translatable="yes">Memory Manager Mode: </property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="_mmSoftware">
+ <property name="label" translatable="yes">Software</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Use a software page table for address translation. Highest accuracy but slowest performance.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="_mmHost">
+ <property name="label" translatable="yes">Host (fast)</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Directly map memory in the host address space. Much faster JIT compilation and execution.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ <property name="group">_mmSoftware</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkRadioButton" id="_mmHostUnsafe">
+ <property name="label" translatable="yes">Host Unchecked (fastest, unsafe)</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Directly map memory, but do not mask the address within the guest address space before access. Faster, but at the cost of safety. The guest application can access memory from anywhere in Ryujinx, so only run programs you trust with this mode.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ <property name="group">_mmSoftware</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="CatHacks">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">Hacks</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes"> (may cause instability)</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="_expandRamToggle">
+ <property name="label" translatable="yes">Use alternative memory layout (Developers)</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Utilizes an alternative MemoryMode layout to mimic a Switch development model.&#13;This is only useful for higher-resolution texture packs or 4k resolution mods. Does NOT improve performance.&#13;Leave OFF if unsure.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_ignoreToggle">
+ <property name="label" translatable="yes">Ignore Missing Services</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Ignores unimplemented Horizon OS services. This may help in bypassing crashes when booting certain games.&#13;Leave OFF if unsure.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">System</property>
+ </object>
+ <packing>
+ <property name="position">2</property>
+ <property name="tab-fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="TabGraphics">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="CatFeatures">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">Features</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="FeaturesOptions">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Executes graphics backend commands on a second thread.&#13;Speeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.&#13;Set to AUTO if unsure.</property>
+ <property name="label" translatable="yes">Graphics Backend Multithreading:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_galThreading">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Executes graphics backend commands on a second thread.&#13;Speeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.&#13;Set to AUTO if unsure.</property>
+ <property name="active-id">-1</property>
+ <items>
+ <item id="Auto" translatable="yes">Auto</item>
+ <item id="Off" translatable="yes">Off</item>
+ <item id="On" translatable="yes">On</item>
+ </items>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Graphics Backend to use</property>
+ <property name="label" translatable="yes">Graphics Backend:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_graphicsBackend">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Graphics Backend to use</property>
+ <property name="active-id">-1</property>
+ <items>
+ <item id="0" translatable="yes">Vulkan</item>
+ <item id="1" translatable="yes">OpenGL</item>
+ </items>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Preferred GPU (Vulkan only)</property>
+ <property name="label" translatable="yes">Preferred GPU:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_preferredGpu">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Preferred GPU (Vulkan only)</property>
+ <property name="active-id">-1</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="CatEnhancements">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">Enhancements</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="EnhancementOptions">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="_shaderCacheToggle">
+ <property name="label" translatable="yes">Enable Shader Cache</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Saves a disk shader cache which reduces stuttering in subsequent runs.&#13;Leave ON if unsure.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_textureRecompressionToggle">
+ <property name="label" translatable="yes">Enable Texture Recompression</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Enables or disables Texture Recompression. Reduces VRAM usage at the cost of texture quality, and may also increase stuttering</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_macroHLEToggle">
+ <property name="label" translatable="yes">Enable Macro HLE</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Enables or disables high-level emulation of Macro code. Improves performance but may cause graphical glitches in some games</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Resolution Scale applied to applicable render targets.</property>
+ <property name="label" translatable="yes">Resolution Scale:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_resScaleCombo">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Resolution Scale applied to applicable render targets.</property>
+ <property name="active-id">1</property>
+ <items>
+ <item id="1" translatable="yes">Native (720p/1080p)</item>
+ <item id="2" translatable="yes">2x (1440p/2160p)</item>
+ <item id="3" translatable="yes">3x (2160p/3240p)</item>
+ <item id="4" translatable="yes">4x (2880p/4320p)</item>
+ <item id="-1" translatable="yes">Custom (not recommended)</item>
+ </items>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="_resScaleText">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="tooltip-text" translatable="yes">Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash.</property>
+ <property name="valign">center</property>
+ <property name="caps-lock-warning">False</property>
+ <property name="placeholder-text">1.0</property>
+ <property name="input-purpose">number</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Applies a final effect to the game render</property>
+ <property name="label" translatable="yes">Post Processing Effect:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_antiAliasing">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Applies anti-aliasing to the game render</property>
+ <property name="active-id">1</property>
+ <items>
+ <item id="0" translatable="yes">None</item>
+ <item id="1" translatable="yes">FXAA</item>
+ <item id="2" translatable="yes">SMAA Low</item>
+ <item id="3" translatable="yes">SMAA Medium</item>
+ <item id="4" translatable="yes">SMAA High</item>
+ <item id="5" translatable="yes">SMAA Ultra</item>
+ </items>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="width-request">100</property>
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enables Framebuffer Upscaling</property>
+ <property name="label" translatable="yes">Upscale: </property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_scalingFilter">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enables Framebuffer Upscaling</property>
+ <property name="active-id">1</property>
+ <items>
+ <item id="0" translatable="yes">Bilinear</item>
+ <item id="1" translatable="yes">Nearest</item>
+ <item id="2" translatable="yes">FSR</item>
+ </items>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScale" id="_scalingFilterSlider">
+ <property name="width-request">200</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="margin-start">5</property>
+ <property name="adjustment">_scalingFilterLevel</property>
+ <property name="round-digits">1</property>
+ <property name="value-pos">right</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Level of Anisotropic Filtering (set to Auto to use the value requested by the game)</property>
+ <property name="label" translatable="yes">Anisotropic Filtering:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_anisotropy">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Level of Anisotropic Filtering (set to Auto to use the value requested by the game)</property>
+ <property name="active-id">-1</property>
+ <items>
+ <item id="-1" translatable="yes">Auto</item>
+ <item id="2" translatable="yes">2x</item>
+ <item id="4" translatable="yes">4x</item>
+ <item id="8" translatable="yes">8x</item>
+ <item id="16" translatable="yes">16x</item>
+ </items>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Aspect Ratio applied to the renderer window.</property>
+ <property name="label" translatable="yes">Aspect Ratio:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_aspectRatio">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Aspect Ratio applied to the renderer window.</property>
+ <property name="active-id">1</property>
+ <items>
+ <item id="0" translatable="yes">4:3</item>
+ <item id="1" translatable="yes">16:9</item>
+ <item id="2" translatable="yes">16:10</item>
+ <item id="3" translatable="yes">21:9</item>
+ <item id="4" translatable="yes">32:9</item>
+ <item id="5" translatable="yes">Stretch to Fit Window</item>
+ </items>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="CatDev">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">Developer Options</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="DevOptions">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Graphics Shaders Dump Path</property>
+ <property name="label" translatable="yes">Graphics Shaders Dump Path:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkEntry" id="_graphicsShadersDumpPath">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="tooltip-text" translatable="yes">Graphics Shaders Dump Path</property>
+ <property name="valign">center</property>
+ <property name="caps-lock-warning">False</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Graphics</property>
+ </object>
+ <packing>
+ <property name="position">3</property>
+ <property name="tab-fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="TabLogging">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">10</property>
+ <property name="margin-top">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="CatLogging">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">Logging</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="LogggingOptions">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkCheckButton" id="_fileLogToggle">
+ <property name="label" translatable="yes">Enable Logging to File</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Saves console logging to a log file on disk. Does not affect performance.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_stubLogToggle">
+ <property name="label" translatable="yes">Enable Stub Logs</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Prints stub log messages in the console. Does not affect performance.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_infoLogToggle">
+ <property name="label" translatable="yes">Enable Info Logs</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Prints info log messages in the console. Does not affect performance.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_warningLogToggle">
+ <property name="label" translatable="yes">Enable Warning Logs</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Prints warning log messages in the console. Does not affect performance.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_errorLogToggle">
+ <property name="label" translatable="yes">Enable Error Logs</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Prints error log messages in the console. Does not affect performance.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">6</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_guestLogToggle">
+ <property name="label" translatable="yes">Enable Guest Logs</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Prints guest log messages in the console. Does not affect performance.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">7</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_fsAccessLogToggle">
+ <property name="label" translatable="yes">Enable Fs Access Logs</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Enables FS access log output to the console. Possible modes are 0-3</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">8</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Enables FS access log output to the console. Possible modes are 0-3</property>
+ <property name="label" translatable="yes">Fs Global Access Log Mode:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinButton">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="tooltip-text" translatable="yes">Enables FS access log output to the console. Possible modes are 0-3</property>
+ <property name="text" translatable="yes">0</property>
+ <property name="adjustment">_fsLogSpinAdjustment</property>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">9</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="CatDevLogging">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="margin-top">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Use with care</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">Developer Options (WARNING: Will reduce performance)</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="DevLoggingOptions">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-top">5</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Requires appropriate log levels enabled.</property>
+ <property name="label" translatable="yes">Graphics Backend Log Level</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">22</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_graphicsDebugLevel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">Requires appropriate log levels enabled.</property>
+ <property name="margin-left">5</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">22</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_debugLogToggle">
+ <property name="label" translatable="yes">Enable Debug Logs</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Prints debug log messages in the console.&#13;Only use this if specifically instructed by a staff member, as it will make logs difficult to read and worsen emulator performance.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">21</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkCheckButton" id="_traceLogToggle">
+ <property name="label" translatable="yes">Enable Trace Logs</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">False</property>
+ <property name="tooltip-text" translatable="yes">Prints trace log messages in the console. Does not affect performance.</property>
+ <property name="halign">start</property>
+ <property name="margin-top">5</property>
+ <property name="margin-bottom">5</property>
+ <property name="draw-indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">22</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">22</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">4</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Logging</property>
+ </object>
+ <packing>
+ <property name="position">4</property>
+ <property name="tab-fill">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="TabMultiplayer">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">10</property>
+ <property name="margin-top">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="CatLAN">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin-left">5</property>
+ <property name="margin-right">5</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">LAN Mode</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="LANOptions">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="valign">start</property>
+ <property name="margin-left">10</property>
+ <property name="margin-right">10</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="NetworkInterfaceBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">The network interface used for LAN features</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Network Interface:</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkComboBoxText" id="_multiLanSelect">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="tooltip-text" translatable="yes">The network interface used for LAN features</property>
+ <property name="active-id">0</property>
+ <items>
+ <item id="0" translatable="yes">Default</item>
+ </items>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">5</property>
+ <property name="label" translatable="yes">To use LAN functionality in games, Enable Guest Internet Access must be checked in System.</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="padding">5</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="position">5</property>
+ </packing>
+ </child>
+ <child type="tab">
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="label" translatable="yes">Multiplayer</property>
+ </object>
+ <packing>
+ <property name="position">5</property>
+ <property name="tab-fill">False</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButtonBox" id="_buttonBox">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="margin-right">5</property>
+ <property name="margin-top">3</property>
+ <property name="margin-bottom">3</property>
+ <property name="spacing">5</property>
+ <property name="layout-style">end</property>
+ <child>
+ <object class="GtkToggleButton" id="SaveToggle">
+ <property name="label" translatable="yes">Save</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <signal name="toggled" handler="SaveToggle_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="CloseToggle">
+ <property name="label" translatable="yes">Close</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <signal name="toggled" handler="CloseToggle_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="label" translatable="yes">Apply</property>
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">True</property>
+ <signal name="clicked" handler="ApplyToggle_Activated" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">False</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs
new file mode 100644
index 00000000..c40adc11
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.cs
@@ -0,0 +1,208 @@
+using Gtk;
+using LibHac.Common;
+using LibHac.Fs;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Ns;
+using LibHac.Tools.FsSystem;
+using LibHac.Tools.FsSystem.NcaUtils;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS;
+using Ryujinx.Ui.App.Common;
+using Ryujinx.Ui.Widgets;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using GUI = Gtk.Builder.ObjectAttribute;
+using SpanHelpers = LibHac.Common.SpanHelpers;
+
+namespace Ryujinx.Ui.Windows
+{
+ public class TitleUpdateWindow : Window
+ {
+ private readonly MainWindow _parent;
+ private readonly VirtualFileSystem _virtualFileSystem;
+ private readonly string _titleId;
+ private readonly string _updateJsonPath;
+
+ private TitleUpdateMetadata _titleUpdateWindowData;
+
+ private readonly Dictionary<RadioButton, string> _radioButtonToPathDictionary;
+ private static readonly TitleUpdateMetadataJsonSerializerContext SerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
+#pragma warning disable CS0649, IDE0044
+ [GUI] Label _baseTitleInfoLabel;
+ [GUI] Box _availableUpdatesBox;
+ [GUI] RadioButton _noUpdateRadioButton;
+#pragma warning restore CS0649, IDE0044
+
+ public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { }
+
+ private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
+ {
+ _parent = parent;
+
+ builder.Autoconnect(this);
+
+ _titleId = titleId;
+ _virtualFileSystem = virtualFileSystem;
+ _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json");
+ _radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
+
+ try
+ {
+ _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath, SerializerContext.TitleUpdateMetadata);
+ }
+ catch
+ {
+ _titleUpdateWindowData = new TitleUpdateMetadata
+ {
+ Selected = "",
+ Paths = new List<string>()
+ };
+ }
+
+ _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]";
+
+ foreach (string path in _titleUpdateWindowData.Paths)
+ {
+ AddUpdate(path);
+ }
+
+ if (_titleUpdateWindowData.Selected == "")
+ {
+ _noUpdateRadioButton.Active = true;
+ }
+ else
+ {
+ foreach ((RadioButton update, var _) in _radioButtonToPathDictionary.Where(keyValuePair => keyValuePair.Value == _titleUpdateWindowData.Selected))
+ {
+ update.Active = true;
+ }
+ }
+ }
+
+ private void AddUpdate(string path)
+ {
+ if (File.Exists(path))
+ {
+ using (FileStream file = new FileStream(path, FileMode.Open, FileAccess.Read))
+ {
+ PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
+
+ try
+ {
+ (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0);
+
+ if (controlNca != null && patchNca != null)
+ {
+ ApplicationControlProperty controlData = new ApplicationControlProperty();
+
+ using var nacpFile = new UniqueRef<IFile>();
+
+ controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
+
+ RadioButton radioButton = new RadioButton($"Version {controlData.DisplayVersionString.ToString()} - {path}");
+ radioButton.JoinGroup(_noUpdateRadioButton);
+
+ _availableUpdatesBox.Add(radioButton);
+ _radioButtonToPathDictionary.Add(radioButton, path);
+
+ radioButton.Show();
+ radioButton.Active = true;
+ }
+ else
+ {
+ GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
+ }
+ }
+ catch (Exception exception)
+ {
+ GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}");
+ }
+ }
+ }
+ }
+
+ private void RemoveUpdates(bool removeSelectedOnly = false)
+ {
+ foreach (RadioButton radioButton in _noUpdateRadioButton.Group)
+ {
+ if (radioButton.Label != "No Update" && (!removeSelectedOnly || radioButton.Active))
+ {
+ _availableUpdatesBox.Remove(radioButton);
+ _radioButtonToPathDictionary.Remove(radioButton);
+ radioButton.Dispose();
+ }
+ }
+ }
+
+ private void AddButton_Clicked(object sender, EventArgs args)
+ {
+ using (FileChooserNative fileChooser = new FileChooserNative("Select update files", this, FileChooserAction.Open, "Add", "Cancel"))
+ {
+ fileChooser.SelectMultiple = true;
+
+ FileFilter filter = new FileFilter()
+ {
+ Name = "Switch Game Updates"
+ };
+ filter.AddPattern("*.nsp");
+
+ fileChooser.AddFilter(filter);
+
+ if (fileChooser.Run() == (int)ResponseType.Accept)
+ {
+ foreach (string path in fileChooser.Filenames)
+ {
+ AddUpdate(path);
+ }
+ }
+ }
+ }
+
+ private void RemoveButton_Clicked(object sender, EventArgs args)
+ {
+ RemoveUpdates(true);
+ }
+
+ private void RemoveAllButton_Clicked(object sender, EventArgs args)
+ {
+ RemoveUpdates();
+ }
+
+ private void SaveButton_Clicked(object sender, EventArgs args)
+ {
+ _titleUpdateWindowData.Paths.Clear();
+ _titleUpdateWindowData.Selected = "";
+
+ foreach (string paths in _radioButtonToPathDictionary.Values)
+ {
+ _titleUpdateWindowData.Paths.Add(paths);
+ }
+
+ foreach (RadioButton radioButton in _noUpdateRadioButton.Group)
+ {
+ if (radioButton.Active)
+ {
+ _titleUpdateWindowData.Selected = _radioButtonToPathDictionary.TryGetValue(radioButton, out string updatePath) ? updatePath : "";
+ }
+ }
+
+ JsonHelper.SerializeToFile(_updateJsonPath, _titleUpdateWindowData, SerializerContext.TitleUpdateMetadata);
+
+ _parent.UpdateGameTable();
+
+ Dispose();
+ }
+
+ private void CancelButton_Clicked(object sender, EventArgs args)
+ {
+ Dispose();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Windows/TitleUpdateWindow.glade b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.glade
new file mode 100644
index 00000000..de557471
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/TitleUpdateWindow.glade
@@ -0,0 +1,214 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.36.0 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <object class="GtkWindow" id="_titleUpdateWindow">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Ryujinx - Title Update Manager</property>
+ <property name="modal">True</property>
+ <property name="window_position">center</property>
+ <property name="default_width">550</property>
+ <property name="default_height">250</property>
+ <child>
+ <object class="GtkBox" id="MainBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="UpdatesBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel" id="_baseTitleInfoLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="label" translatable="yes">Available Updates</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="margin_left">10</property>
+ <property name="margin_right">10</property>
+ <property name="shadow_type">in</property>
+ <child>
+ <object class="GtkViewport">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkBox" id="_availableUpdatesBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkRadioButton" id="_noUpdateRadioButton">
+ <property name="label" translatable="yes">No Update</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="active">True</property>
+ <property name="draw_indicator">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkButtonBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="layout_style">start</property>
+ <child>
+ <object class="GtkButton" id="_addUpdate">
+ <property name="label" translatable="yes">Add</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Adds an update to this list</property>
+ <property name="margin_left">10</property>
+ <signal name="clicked" handler="AddButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="_removeUpdate">
+ <property name="label" translatable="yes">Remove</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Removes the selected update</property>
+ <property name="margin_left">10</property>
+ <signal name="clicked" handler="RemoveButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="_removeAllButton">
+ <property name="label" translatable="yes">Remove All</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="tooltip_text" translatable="yes">Removes the selected update</property>
+ <property name="margin_left">10</property>
+ <signal name="clicked" handler="RemoveAllButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButtonBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin_top">10</property>
+ <property name="margin_bottom">10</property>
+ <property name="layout_style">end</property>
+ <child>
+ <object class="GtkButton" id="_saveButton">
+ <property name="label" translatable="yes">Save</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">2</property>
+ <property name="margin_bottom">2</property>
+ <signal name="clicked" handler="SaveButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="_cancelButton">
+ <property name="label" translatable="yes">Cancel</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="margin_right">10</property>
+ <property name="margin_top">2</property>
+ <property name="margin_bottom">2</property>
+ <signal name="clicked" handler="CancelButton_Clicked" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child type="titlebar">
+ <placeholder/>
+ </child>
+ </object>
+</interface>
diff --git a/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs b/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs
new file mode 100644
index 00000000..7c9ae8ba
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.Designer.cs
@@ -0,0 +1,256 @@
+using Gtk;
+using Pango;
+
+namespace Ryujinx.Ui.Windows
+{
+ public partial class UserProfilesManagerWindow : Window
+ {
+ private Box _mainBox;
+ private Label _selectedLabel;
+ private Box _selectedUserBox;
+ private Image _selectedUserImage;
+ private VBox _selectedUserInfoBox;
+ private Entry _selectedUserNameEntry;
+ private Label _selectedUserIdLabel;
+ private VBox _selectedUserButtonsBox;
+ private Button _saveProfileNameButton;
+ private Button _changeProfileImageButton;
+ private Box _usersTreeViewBox;
+ private Label _availableUsersLabel;
+ private ScrolledWindow _usersTreeViewWindow;
+ private ListStore _tableStore;
+ private TreeView _usersTreeView;
+ private Box _bottomBox;
+ private Button _addButton;
+ private Button _deleteButton;
+ private Button _closeButton;
+
+ private void InitializeComponent()
+ {
+
+#pragma warning disable CS0612
+
+ //
+ // UserProfilesManagerWindow
+ //
+ CanFocus = false;
+ Resizable = false;
+ Modal = true;
+ WindowPosition = WindowPosition.Center;
+ DefaultWidth = 620;
+ DefaultHeight = 548;
+ TypeHint = Gdk.WindowTypeHint.Dialog;
+
+ //
+ // _mainBox
+ //
+ _mainBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _selectedLabel
+ //
+ _selectedLabel = new Label("Selected User Profile:")
+ {
+ Margin = 15,
+ Attributes = new AttrList(),
+ Halign = Align.Start
+ };
+ _selectedLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold));
+
+ //
+ // _viewBox
+ //
+ _usersTreeViewBox = new Box(Orientation.Vertical, 0);
+
+ //
+ // _SelectedUserBox
+ //
+ _selectedUserBox = new Box(Orientation.Horizontal, 0)
+ {
+ MarginLeft = 30
+ };
+
+ //
+ // _selectedUserImage
+ //
+ _selectedUserImage = new Image();
+
+ //
+ // _selectedUserInfoBox
+ //
+ _selectedUserInfoBox = new VBox(true, 0);
+
+ //
+ // _selectedUserNameEntry
+ //
+ _selectedUserNameEntry = new Entry("")
+ {
+ MarginLeft = 15,
+ MaxLength = (int)MaxProfileNameLength
+ };
+ _selectedUserNameEntry.KeyReleaseEvent += SelectedUserNameEntry_KeyReleaseEvent;
+
+ //
+ // _selectedUserIdLabel
+ //
+ _selectedUserIdLabel = new Label("")
+ {
+ MarginTop = 15,
+ MarginLeft = 15
+ };
+
+ //
+ // _selectedUserButtonsBox
+ //
+ _selectedUserButtonsBox = new VBox()
+ {
+ MarginRight = 30
+ };
+
+ //
+ // _saveProfileNameButton
+ //
+ _saveProfileNameButton = new Button()
+ {
+ Label = "Save Profile Name",
+ CanFocus = true,
+ ReceivesDefault = true,
+ Sensitive = false
+ };
+ _saveProfileNameButton.Clicked += EditProfileNameButton_Pressed;
+
+ //
+ // _changeProfileImageButton
+ //
+ _changeProfileImageButton = new Button()
+ {
+ Label = "Change Profile Image",
+ CanFocus = true,
+ ReceivesDefault = true,
+ MarginTop = 10
+ };
+ _changeProfileImageButton.Clicked += ChangeProfileImageButton_Pressed;
+
+ //
+ // _availableUsersLabel
+ //
+ _availableUsersLabel = new Label("Available User Profiles:")
+ {
+ Margin = 15,
+ Attributes = new AttrList(),
+ Halign = Align.Start
+ };
+ _availableUsersLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold));
+
+ //
+ // _usersTreeViewWindow
+ //
+ _usersTreeViewWindow = new ScrolledWindow()
+ {
+ ShadowType = ShadowType.In,
+ CanFocus = true,
+ Expand = true,
+ MarginLeft = 30,
+ MarginRight = 30,
+ MarginBottom = 15
+ };
+
+ //
+ // _tableStore
+ //
+ _tableStore = new ListStore(typeof(bool), typeof(Gdk.Pixbuf), typeof(string), typeof(Gdk.RGBA));
+
+ //
+ // _usersTreeView
+ //
+ _usersTreeView = new TreeView(_tableStore)
+ {
+ HoverSelection = true,
+ HeadersVisible = false,
+ };
+ _usersTreeView.RowActivated += UsersTreeView_Activated;
+
+ //
+ // _bottomBox
+ //
+ _bottomBox = new Box(Orientation.Horizontal, 0)
+ {
+ MarginLeft = 30,
+ MarginRight = 30,
+ MarginBottom = 15
+ };
+
+ //
+ // _addButton
+ //
+ _addButton = new Button()
+ {
+ Label = "Add New Profile",
+ CanFocus = true,
+ ReceivesDefault = true,
+ HeightRequest = 35
+ };
+ _addButton.Clicked += AddButton_Pressed;
+
+ //
+ // _deleteButton
+ //
+ _deleteButton = new Button()
+ {
+ Label = "Delete Selected Profile",
+ CanFocus = true,
+ ReceivesDefault = true,
+ HeightRequest = 35,
+ MarginLeft = 10
+ };
+ _deleteButton.Clicked += DeleteButton_Pressed;
+
+ //
+ // _closeButton
+ //
+ _closeButton = new Button()
+ {
+ Label = "Close",
+ CanFocus = true,
+ ReceivesDefault = true,
+ HeightRequest = 35,
+ WidthRequest = 80
+ };
+ _closeButton.Clicked += CloseButton_Pressed;
+
+#pragma warning restore CS0612
+
+ ShowComponent();
+ }
+
+ private void ShowComponent()
+ {
+ _usersTreeViewWindow.Add(_usersTreeView);
+
+ _usersTreeViewBox.Add(_usersTreeViewWindow);
+ _bottomBox.PackStart(_addButton, false, false, 0);
+ _bottomBox.PackStart(_deleteButton, false, false, 0);
+ _bottomBox.PackEnd(_closeButton, false, false, 0);
+
+ _selectedUserInfoBox.Add(_selectedUserNameEntry);
+ _selectedUserInfoBox.Add(_selectedUserIdLabel);
+
+ _selectedUserButtonsBox.Add(_saveProfileNameButton);
+ _selectedUserButtonsBox.Add(_changeProfileImageButton);
+
+ _selectedUserBox.Add(_selectedUserImage);
+ _selectedUserBox.PackStart(_selectedUserInfoBox, false, false, 0);
+ _selectedUserBox.PackEnd(_selectedUserButtonsBox, false, false, 0);
+
+ _mainBox.PackStart(_selectedLabel, false, false, 0);
+ _mainBox.PackStart(_selectedUserBox, false, true, 0);
+ _mainBox.PackStart(_availableUsersLabel, false, false, 0);
+ _mainBox.Add(_usersTreeViewBox);
+ _mainBox.Add(_bottomBox);
+
+ Add(_mainBox);
+
+ ShowAll();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs b/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs
new file mode 100644
index 00000000..a08b5dd1
--- /dev/null
+++ b/src/Ryujinx/Ui/Windows/UserProfilesManagerWindow.cs
@@ -0,0 +1,331 @@
+using Gtk;
+using Ryujinx.Common.Memory;
+using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.Ui.Common.Configuration;
+using Ryujinx.Ui.Widgets;
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.Processing;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Image = SixLabors.ImageSharp.Image;
+using UserId = Ryujinx.HLE.HOS.Services.Account.Acc.UserId;
+
+namespace Ryujinx.Ui.Windows
+{
+ public partial class UserProfilesManagerWindow : Window
+ {
+ private const uint MaxProfileNameLength = 0x20;
+
+ private readonly AccountManager _accountManager;
+ private readonly ContentManager _contentManager;
+
+ private byte[] _bufferImageProfile;
+ private string _tempNewProfileName;
+
+ private Gdk.RGBA _selectedColor;
+
+ private ManualResetEvent _avatarsPreloadingEvent = new ManualResetEvent(false);
+
+ public UserProfilesManagerWindow(AccountManager accountManager, ContentManager contentManager, VirtualFileSystem virtualFileSystem) : base($"Ryujinx {Program.Version} - Manage User Profiles")
+ {
+ Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Ui.Common.Resources.Logo_Ryujinx.png");
+
+ InitializeComponent();
+
+ _selectedColor.Red = 0.212;
+ _selectedColor.Green = 0.843;
+ _selectedColor.Blue = 0.718;
+ _selectedColor.Alpha = 1;
+
+ _accountManager = accountManager;
+ _contentManager = contentManager;
+
+ CellRendererToggle userSelectedToggle = new CellRendererToggle();
+ userSelectedToggle.Toggled += UserSelectedToggle_Toggled;
+
+ // NOTE: Uncomment following line when multiple selection of user profiles is supported.
+ //_usersTreeView.AppendColumn("Selected", userSelectedToggle, "active", 0);
+ _usersTreeView.AppendColumn("User Icon", new CellRendererPixbuf(), "pixbuf", 1);
+ _usersTreeView.AppendColumn("User Info", new CellRendererText(), "text", 2, "background-rgba", 3);
+
+ _tableStore.SetSortColumnId(0, SortType.Descending);
+
+ RefreshList();
+
+ if (_contentManager.GetCurrentFirmwareVersion() != null)
+ {
+ Task.Run(() =>
+ {
+ AvatarWindow.PreloadAvatars(contentManager, virtualFileSystem);
+ _avatarsPreloadingEvent.Set();
+ });
+ }
+ }
+
+ public void RefreshList()
+ {
+ _tableStore.Clear();
+
+ foreach (UserProfile userProfile in _accountManager.GetAllUsers())
+ {
+ _tableStore.AppendValues(userProfile.AccountState == AccountState.Open, new Gdk.Pixbuf(userProfile.Image, 96, 96), $"{userProfile.Name}\n{userProfile.UserId}", Gdk.RGBA.Zero);
+
+ if (userProfile.AccountState == AccountState.Open)
+ {
+ _selectedUserImage.Pixbuf = new Gdk.Pixbuf(userProfile.Image, 96, 96);
+ _selectedUserIdLabel.Text = userProfile.UserId.ToString();
+ _selectedUserNameEntry.Text = userProfile.Name;
+
+ _deleteButton.Sensitive = userProfile.UserId != AccountManager.DefaultUserId;
+
+ _usersTreeView.Model.GetIterFirst(out TreeIter firstIter);
+ _tableStore.SetValue(firstIter, 3, _selectedColor);
+ }
+ }
+ }
+
+ //
+ // Events
+ //
+
+ private void UsersTreeView_Activated(object o, RowActivatedArgs args)
+ {
+ SelectUserTreeView();
+ }
+
+ private void UserSelectedToggle_Toggled(object o, ToggledArgs args)
+ {
+ SelectUserTreeView();
+ }
+
+ private void SelectUserTreeView()
+ {
+ // Get selected item informations.
+ _usersTreeView.Selection.GetSelected(out TreeIter selectedIter);
+
+ Gdk.Pixbuf userPicture = (Gdk.Pixbuf)_tableStore.GetValue(selectedIter, 1);
+
+ string userName = _tableStore.GetValue(selectedIter, 2).ToString().Split("\n")[0];
+ string userId = _tableStore.GetValue(selectedIter, 2).ToString().Split("\n")[1];
+
+ // Unselect the first user.
+ _usersTreeView.Model.GetIterFirst(out TreeIter firstIter);
+ _tableStore.SetValue(firstIter, 0, false);
+ _tableStore.SetValue(firstIter, 3, Gdk.RGBA.Zero);
+
+ // Set new informations.
+ _tableStore.SetValue(selectedIter, 0, true);
+
+ _selectedUserImage.Pixbuf = userPicture;
+ _selectedUserNameEntry.Text = userName;
+ _selectedUserIdLabel.Text = userId;
+ _saveProfileNameButton.Sensitive = false;
+
+ // Open the selected one.
+ _accountManager.OpenUser(new UserId(userId));
+
+ _deleteButton.Sensitive = userId != AccountManager.DefaultUserId.ToString();
+
+ _tableStore.SetValue(selectedIter, 3, _selectedColor);
+ }
+
+ private void SelectedUserNameEntry_KeyReleaseEvent(object o, KeyReleaseEventArgs args)
+ {
+ if (_saveProfileNameButton.Sensitive == false)
+ {
+ _saveProfileNameButton.Sensitive = true;
+ }
+ }
+
+ private void AddButton_Pressed(object sender, EventArgs e)
+ {
+ _tempNewProfileName = GtkDialog.CreateInputDialog(this, "Choose the Profile Name", "Please Enter a Profile Name", MaxProfileNameLength);
+
+ if (_tempNewProfileName != "")
+ {
+ SelectProfileImage(true);
+
+ if (_bufferImageProfile != null)
+ {
+ AddUser();
+ }
+ }
+ }
+
+ private void DeleteButton_Pressed(object sender, EventArgs e)
+ {
+ if (GtkDialog.CreateChoiceDialog("Delete User Profile", "Are you sure you want to delete the profile ?", "Deleting this profile will also delete all associated save data."))
+ {
+ _accountManager.DeleteUser(GetSelectedUserId());
+
+ RefreshList();
+ }
+ }
+
+ private void EditProfileNameButton_Pressed(object sender, EventArgs e)
+ {
+ _saveProfileNameButton.Sensitive = false;
+
+ _accountManager.SetUserName(GetSelectedUserId(), _selectedUserNameEntry.Text);
+
+ RefreshList();
+ }
+
+ private void ProcessProfileImage(byte[] buffer)
+ {
+ using (Image image = Image.Load(buffer))
+ {
+ image.Mutate(x => x.Resize(256, 256));
+
+ using (MemoryStream streamJpg = MemoryStreamManager.Shared.GetStream())
+ {
+ image.SaveAsJpeg(streamJpg);
+
+ _bufferImageProfile = streamJpg.ToArray();
+ }
+ }
+ }
+
+ private void ProfileImageFileChooser()
+ {
+ FileChooserNative fileChooser = new FileChooserNative("Import Custom Profile Image", this, FileChooserAction.Open, "Import", "Cancel")
+ {
+ SelectMultiple = false
+ };
+
+ FileFilter filter = new FileFilter()
+ {
+ Name = "Custom Profile Images"
+ };
+ filter.AddPattern("*.jpg");
+ filter.AddPattern("*.jpeg");
+ filter.AddPattern("*.png");
+ filter.AddPattern("*.bmp");
+
+ fileChooser.AddFilter(filter);
+
+ if (fileChooser.Run() == (int)ResponseType.Accept)
+ {
+ ProcessProfileImage(File.ReadAllBytes(fileChooser.Filename));
+ }
+
+ fileChooser.Dispose();
+ }
+
+ private void SelectProfileImage(bool newUser = false)
+ {
+ if (_contentManager.GetCurrentFirmwareVersion() == null)
+ {
+ ProfileImageFileChooser();
+ }
+ else
+ {
+ Dictionary<int, string> buttons = new Dictionary<int, string>()
+ {
+ { 0, "Import Image File" },
+ { 1, "Select Firmware Avatar" }
+ };
+
+ ResponseType responseDialog = GtkDialog.CreateCustomDialog("Profile Image Selection",
+ "Choose a Profile Image",
+ "You may import a custom profile image, or select an avatar from the system firmware.",
+ buttons, MessageType.Question);
+
+ if (responseDialog == 0)
+ {
+ ProfileImageFileChooser();
+ }
+ else if (responseDialog == (ResponseType)1)
+ {
+ AvatarWindow avatarWindow = new AvatarWindow()
+ {
+ NewUser = newUser
+ };
+
+ avatarWindow.DeleteEvent += AvatarWindow_DeleteEvent;
+
+ avatarWindow.SetSizeRequest((int)(avatarWindow.DefaultWidth * Program.WindowScaleFactor), (int)(avatarWindow.DefaultHeight * Program.WindowScaleFactor));
+ avatarWindow.Show();
+ }
+ }
+ }
+
+ private void ChangeProfileImageButton_Pressed(object sender, EventArgs e)
+ {
+ if (_contentManager.GetCurrentFirmwareVersion() != null)
+ {
+ _avatarsPreloadingEvent.WaitOne();
+ }
+
+ SelectProfileImage();
+
+ if (_bufferImageProfile != null)
+ {
+ SetUserImage();
+ }
+ }
+
+ private void AvatarWindow_DeleteEvent(object sender, DeleteEventArgs args)
+ {
+ _bufferImageProfile = ((AvatarWindow)sender).SelectedProfileImage;
+
+ if (_bufferImageProfile != null)
+ {
+ if (((AvatarWindow)sender).NewUser)
+ {
+ AddUser();
+ }
+ else
+ {
+ SetUserImage();
+ }
+ }
+ }
+
+ private void AddUser()
+ {
+ _accountManager.AddUser(_tempNewProfileName, _bufferImageProfile);
+
+ _bufferImageProfile = null;
+ _tempNewProfileName = "";
+
+ RefreshList();
+ }
+
+ private void SetUserImage()
+ {
+ _accountManager.SetUserImage(GetSelectedUserId(), _bufferImageProfile);
+
+ _bufferImageProfile = null;
+
+ RefreshList();
+ }
+
+ private UserId GetSelectedUserId()
+ {
+ if (_usersTreeView.Model.GetIterFirst(out TreeIter iter))
+ {
+ do
+ {
+ if ((bool)_tableStore.GetValue(iter, 0))
+ {
+ break;
+ }
+ }
+ while (_usersTreeView.Model.IterNext(ref iter));
+ }
+
+ return new UserId(_tableStore.GetValue(iter, 2).ToString().Split("\n")[1]);
+ }
+
+ private void CloseButton_Pressed(object sender, EventArgs e)
+ {
+ Close();
+ }
+ }
+} \ No newline at end of file