mirror of
https://github.com/GreemDev/Ryujinx.git
synced 2024-12-22 23:55:47 +00:00
You see, when I said remove GTK3, what I actually meant was remove references to it. Here's removing GTK3.
This commit is contained in:
parent
12358182aa
commit
9a1863c752
53 changed files with 0 additions and 19339 deletions
|
@ -1,205 +0,0 @@
|
|||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
|
||||
|
||||
namespace Ryujinx.Input.GTK3
|
||||
{
|
||||
public class GTK3Keyboard : IKeyboard
|
||||
{
|
||||
private class ButtonMappingEntry
|
||||
{
|
||||
public readonly GamepadButtonInputId To;
|
||||
public readonly Key From;
|
||||
|
||||
public ButtonMappingEntry(GamepadButtonInputId to, Key from)
|
||||
{
|
||||
To = to;
|
||||
From = from;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly object _userMappingLock = new();
|
||||
|
||||
private readonly GTK3KeyboardDriver _driver;
|
||||
private StandardKeyboardInputConfig _configuration;
|
||||
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
|
||||
|
||||
public GTK3Keyboard(GTK3KeyboardDriver driver, string id, string name)
|
||||
{
|
||||
_driver = driver;
|
||||
Id = id;
|
||||
Name = name;
|
||||
_buttonsUserMapping = new List<ButtonMappingEntry>();
|
||||
}
|
||||
|
||||
private bool HasConfiguration => _configuration != null;
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public bool IsConnected => true;
|
||||
|
||||
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// No operations
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public KeyboardStateSnapshot GetKeyboardStateSnapshot()
|
||||
{
|
||||
return IKeyboard.GetStateSnapshot(this);
|
||||
}
|
||||
|
||||
private static float ConvertRawStickValue(short value)
|
||||
{
|
||||
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
|
||||
|
||||
return value * ConvertRate;
|
||||
}
|
||||
|
||||
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
|
||||
{
|
||||
short stickX = 0;
|
||||
short stickY = 0;
|
||||
|
||||
if (snapshot.IsPressed((Key)stickConfig.StickUp))
|
||||
{
|
||||
stickY += 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed((Key)stickConfig.StickDown))
|
||||
{
|
||||
stickY -= 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed((Key)stickConfig.StickRight))
|
||||
{
|
||||
stickX += 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
|
||||
{
|
||||
stickX -= 1;
|
||||
}
|
||||
|
||||
OpenTK.Mathematics.Vector2 stick = new(stickX, stickY);
|
||||
|
||||
stick.NormalizeFast();
|
||||
|
||||
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetMappedStateSnapshot()
|
||||
{
|
||||
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
|
||||
GamepadStateSnapshot result = default;
|
||||
|
||||
lock (_userMappingLock)
|
||||
{
|
||||
if (!HasConfiguration)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
|
||||
{
|
||||
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not touch state of button already pressed
|
||||
if (!result.IsPressed(entry.To))
|
||||
{
|
||||
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
|
||||
}
|
||||
}
|
||||
|
||||
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
||||
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
||||
|
||||
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
||||
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetStateSnapshot()
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public (float, float) GetStick(StickInputId inputId)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public bool IsPressed(GamepadButtonInputId inputId)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public bool IsPressed(Key key)
|
||||
{
|
||||
return _driver.IsPressed(key);
|
||||
}
|
||||
|
||||
public void SetConfiguration(InputConfig configuration)
|
||||
{
|
||||
lock (_userMappingLock)
|
||||
{
|
||||
_configuration = (StandardKeyboardInputConfig)configuration;
|
||||
|
||||
_buttonsUserMapping.Clear();
|
||||
|
||||
// Then left joycon
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
|
||||
|
||||
// Finally right joycon
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
|
||||
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetTriggerThreshold(float triggerThreshold)
|
||||
{
|
||||
// No operations
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
// No operations
|
||||
}
|
||||
|
||||
public Vector3 GetMotionData(MotionInputId inputId)
|
||||
{
|
||||
// No operations
|
||||
|
||||
return Vector3.Zero;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
using Gdk;
|
||||
using Gtk;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GtkKey = Gdk.Key;
|
||||
|
||||
namespace Ryujinx.Input.GTK3
|
||||
{
|
||||
public class GTK3KeyboardDriver : IGamepadDriver
|
||||
{
|
||||
private readonly Widget _widget;
|
||||
private readonly HashSet<GtkKey> _pressedKeys;
|
||||
|
||||
public GTK3KeyboardDriver(Widget widget)
|
||||
{
|
||||
_widget = widget;
|
||||
_pressedKeys = new HashSet<GtkKey>();
|
||||
|
||||
_widget.KeyPressEvent += OnKeyPress;
|
||||
_widget.KeyReleaseEvent += OnKeyRelease;
|
||||
}
|
||||
|
||||
public string DriverName => "GTK3";
|
||||
|
||||
private static readonly string[] _keyboardIdentifers = new string[1] { "0" };
|
||||
|
||||
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
|
||||
|
||||
public event Action<string> OnGamepadConnected
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event Action<string> OnGamepadDisconnected
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_widget.KeyPressEvent -= OnKeyPress;
|
||||
_widget.KeyReleaseEvent -= OnKeyRelease;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
[GLib.ConnectBefore]
|
||||
protected void OnKeyPress(object sender, KeyPressEventArgs args)
|
||||
{
|
||||
GtkKey key = (GtkKey)Keyval.ToLower((uint)args.Event.Key);
|
||||
|
||||
_pressedKeys.Add(key);
|
||||
}
|
||||
|
||||
[GLib.ConnectBefore]
|
||||
protected void OnKeyRelease(object sender, KeyReleaseEventArgs args)
|
||||
{
|
||||
GtkKey key = (GtkKey)Keyval.ToLower((uint)args.Event.Key);
|
||||
|
||||
_pressedKeys.Remove(key);
|
||||
}
|
||||
|
||||
internal bool IsPressed(Key key)
|
||||
{
|
||||
if (key == Key.Unbound || key == Key.Unknown)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
GtkKey nativeKey = GTK3MappingHelper.ToGtkKey(key);
|
||||
|
||||
return _pressedKeys.Contains(nativeKey);
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_pressedKeys.Clear();
|
||||
}
|
||||
|
||||
public IGamepad GetGamepad(string id)
|
||||
{
|
||||
if (!_keyboardIdentifers[0].Equals(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new GTK3Keyboard(this, _keyboardIdentifers[0], "All keyboards");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using GtkKey = Gdk.Key;
|
||||
|
||||
namespace Ryujinx.Input.GTK3
|
||||
{
|
||||
public static class GTK3MappingHelper
|
||||
{
|
||||
private static readonly GtkKey[] _keyMapping = new GtkKey[(int)Key.Count]
|
||||
{
|
||||
// NOTE: invalid
|
||||
GtkKey.blank,
|
||||
|
||||
GtkKey.Shift_L,
|
||||
GtkKey.Shift_R,
|
||||
GtkKey.Control_L,
|
||||
GtkKey.Control_R,
|
||||
GtkKey.Alt_L,
|
||||
GtkKey.Alt_R,
|
||||
GtkKey.Super_L,
|
||||
GtkKey.Super_R,
|
||||
GtkKey.Menu,
|
||||
GtkKey.F1,
|
||||
GtkKey.F2,
|
||||
GtkKey.F3,
|
||||
GtkKey.F4,
|
||||
GtkKey.F5,
|
||||
GtkKey.F6,
|
||||
GtkKey.F7,
|
||||
GtkKey.F8,
|
||||
GtkKey.F9,
|
||||
GtkKey.F10,
|
||||
GtkKey.F11,
|
||||
GtkKey.F12,
|
||||
GtkKey.F13,
|
||||
GtkKey.F14,
|
||||
GtkKey.F15,
|
||||
GtkKey.F16,
|
||||
GtkKey.F17,
|
||||
GtkKey.F18,
|
||||
GtkKey.F19,
|
||||
GtkKey.F20,
|
||||
GtkKey.F21,
|
||||
GtkKey.F22,
|
||||
GtkKey.F23,
|
||||
GtkKey.F24,
|
||||
GtkKey.F25,
|
||||
GtkKey.F26,
|
||||
GtkKey.F27,
|
||||
GtkKey.F28,
|
||||
GtkKey.F29,
|
||||
GtkKey.F30,
|
||||
GtkKey.F31,
|
||||
GtkKey.F32,
|
||||
GtkKey.F33,
|
||||
GtkKey.F34,
|
||||
GtkKey.F35,
|
||||
GtkKey.Up,
|
||||
GtkKey.Down,
|
||||
GtkKey.Left,
|
||||
GtkKey.Right,
|
||||
GtkKey.Return,
|
||||
GtkKey.Escape,
|
||||
GtkKey.space,
|
||||
GtkKey.Tab,
|
||||
GtkKey.BackSpace,
|
||||
GtkKey.Insert,
|
||||
GtkKey.Delete,
|
||||
GtkKey.Page_Up,
|
||||
GtkKey.Page_Down,
|
||||
GtkKey.Home,
|
||||
GtkKey.End,
|
||||
GtkKey.Caps_Lock,
|
||||
GtkKey.Scroll_Lock,
|
||||
GtkKey.Print,
|
||||
GtkKey.Pause,
|
||||
GtkKey.Num_Lock,
|
||||
GtkKey.Clear,
|
||||
GtkKey.KP_0,
|
||||
GtkKey.KP_1,
|
||||
GtkKey.KP_2,
|
||||
GtkKey.KP_3,
|
||||
GtkKey.KP_4,
|
||||
GtkKey.KP_5,
|
||||
GtkKey.KP_6,
|
||||
GtkKey.KP_7,
|
||||
GtkKey.KP_8,
|
||||
GtkKey.KP_9,
|
||||
GtkKey.KP_Divide,
|
||||
GtkKey.KP_Multiply,
|
||||
GtkKey.KP_Subtract,
|
||||
GtkKey.KP_Add,
|
||||
GtkKey.KP_Decimal,
|
||||
GtkKey.KP_Enter,
|
||||
GtkKey.a,
|
||||
GtkKey.b,
|
||||
GtkKey.c,
|
||||
GtkKey.d,
|
||||
GtkKey.e,
|
||||
GtkKey.f,
|
||||
GtkKey.g,
|
||||
GtkKey.h,
|
||||
GtkKey.i,
|
||||
GtkKey.j,
|
||||
GtkKey.k,
|
||||
GtkKey.l,
|
||||
GtkKey.m,
|
||||
GtkKey.n,
|
||||
GtkKey.o,
|
||||
GtkKey.p,
|
||||
GtkKey.q,
|
||||
GtkKey.r,
|
||||
GtkKey.s,
|
||||
GtkKey.t,
|
||||
GtkKey.u,
|
||||
GtkKey.v,
|
||||
GtkKey.w,
|
||||
GtkKey.x,
|
||||
GtkKey.y,
|
||||
GtkKey.z,
|
||||
GtkKey.Key_0,
|
||||
GtkKey.Key_1,
|
||||
GtkKey.Key_2,
|
||||
GtkKey.Key_3,
|
||||
GtkKey.Key_4,
|
||||
GtkKey.Key_5,
|
||||
GtkKey.Key_6,
|
||||
GtkKey.Key_7,
|
||||
GtkKey.Key_8,
|
||||
GtkKey.Key_9,
|
||||
GtkKey.grave,
|
||||
GtkKey.grave,
|
||||
GtkKey.minus,
|
||||
GtkKey.plus,
|
||||
GtkKey.bracketleft,
|
||||
GtkKey.bracketright,
|
||||
GtkKey.semicolon,
|
||||
GtkKey.quoteright,
|
||||
GtkKey.comma,
|
||||
GtkKey.period,
|
||||
GtkKey.slash,
|
||||
GtkKey.backslash,
|
||||
|
||||
// NOTE: invalid
|
||||
GtkKey.blank,
|
||||
};
|
||||
|
||||
private static readonly Dictionary<GtkKey, Key> _gtkKeyMapping;
|
||||
|
||||
static GTK3MappingHelper()
|
||||
{
|
||||
var inputKeys = Enum.GetValues<Key>().SkipLast(1);
|
||||
|
||||
// GtkKey is not contiguous and quite large, so use a dictionary instead of an array.
|
||||
_gtkKeyMapping = new Dictionary<GtkKey, Key>();
|
||||
|
||||
foreach (var key in inputKeys)
|
||||
{
|
||||
var index = ToGtkKey(key);
|
||||
_gtkKeyMapping[index] = key;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static GtkKey ToGtkKey(Key key)
|
||||
{
|
||||
return _keyMapping[(int)key];
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Key ToInputKey(GtkKey key)
|
||||
{
|
||||
return _gtkKeyMapping.GetValueOrDefault(key, Key.Unknown);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
using Ryujinx.Common.Configuration.Hid;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ryujinx.Input.GTK3
|
||||
{
|
||||
public class GTK3Mouse : IMouse
|
||||
{
|
||||
private GTK3MouseDriver _driver;
|
||||
|
||||
public GamepadFeaturesFlag Features => throw new NotImplementedException();
|
||||
|
||||
public string Id => "0";
|
||||
|
||||
public string Name => "GTKMouse";
|
||||
|
||||
public bool IsConnected => true;
|
||||
|
||||
public bool[] Buttons => _driver.PressedButtons;
|
||||
|
||||
public GTK3Mouse(GTK3MouseDriver driver)
|
||||
{
|
||||
_driver = driver;
|
||||
}
|
||||
|
||||
public Size ClientSize => _driver.GetClientSize();
|
||||
|
||||
public Vector2 GetPosition()
|
||||
{
|
||||
return _driver.CurrentPosition;
|
||||
}
|
||||
|
||||
public Vector2 GetScroll()
|
||||
{
|
||||
return _driver.Scroll;
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetMappedStateSnapshot()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public Vector3 GetMotionData(MotionInputId inputId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public GamepadStateSnapshot GetStateSnapshot()
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public (float, float) GetStick(StickInputId inputId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool IsButtonPressed(MouseButton button)
|
||||
{
|
||||
return _driver.IsButtonPressed(button);
|
||||
}
|
||||
|
||||
public bool IsPressed(GamepadButtonInputId inputId)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetConfiguration(InputConfig configuration)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void SetTriggerThreshold(float triggerThreshold)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
_driver = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
using Gdk;
|
||||
using Gtk;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using Size = System.Drawing.Size;
|
||||
|
||||
namespace Ryujinx.Input.GTK3
|
||||
{
|
||||
public class GTK3MouseDriver : IGamepadDriver
|
||||
{
|
||||
private Widget _widget;
|
||||
private bool _isDisposed;
|
||||
|
||||
public bool[] PressedButtons { get; }
|
||||
|
||||
public Vector2 CurrentPosition { get; private set; }
|
||||
public Vector2 Scroll { get; private set; }
|
||||
|
||||
public GTK3MouseDriver(Widget parent)
|
||||
{
|
||||
_widget = parent;
|
||||
|
||||
_widget.MotionNotifyEvent += Parent_MotionNotifyEvent;
|
||||
_widget.ButtonPressEvent += Parent_ButtonPressEvent;
|
||||
_widget.ButtonReleaseEvent += Parent_ButtonReleaseEvent;
|
||||
_widget.ScrollEvent += Parent_ScrollEvent;
|
||||
|
||||
PressedButtons = new bool[(int)MouseButton.Count];
|
||||
}
|
||||
|
||||
|
||||
[GLib.ConnectBefore]
|
||||
private void Parent_ScrollEvent(object o, ScrollEventArgs args)
|
||||
{
|
||||
Scroll = new Vector2((float)args.Event.X, (float)args.Event.Y);
|
||||
}
|
||||
|
||||
[GLib.ConnectBefore]
|
||||
private void Parent_ButtonReleaseEvent(object o, ButtonReleaseEventArgs args)
|
||||
{
|
||||
PressedButtons[args.Event.Button - 1] = false;
|
||||
}
|
||||
|
||||
[GLib.ConnectBefore]
|
||||
private void Parent_ButtonPressEvent(object o, ButtonPressEventArgs args)
|
||||
{
|
||||
PressedButtons[args.Event.Button - 1] = true;
|
||||
}
|
||||
|
||||
[GLib.ConnectBefore]
|
||||
private void Parent_MotionNotifyEvent(object o, MotionNotifyEventArgs args)
|
||||
{
|
||||
if (args.Event.Device.InputSource == InputSource.Mouse)
|
||||
{
|
||||
CurrentPosition = new Vector2((float)args.Event.X, (float)args.Event.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsButtonPressed(MouseButton button)
|
||||
{
|
||||
return PressedButtons[(int)button];
|
||||
}
|
||||
|
||||
public Size GetClientSize()
|
||||
{
|
||||
return new Size(_widget.AllocatedWidth, _widget.AllocatedHeight);
|
||||
}
|
||||
|
||||
public string DriverName => "GTK3";
|
||||
|
||||
public event Action<string> OnGamepadConnected
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public event Action<string> OnGamepadDisconnected
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public ReadOnlySpan<string> GamepadsIds => new[] { "0" };
|
||||
|
||||
public IGamepad GetGamepad(string id)
|
||||
{
|
||||
return new GTK3Mouse(this);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
|
||||
_isDisposed = true;
|
||||
|
||||
_widget.MotionNotifyEvent -= Parent_MotionNotifyEvent;
|
||||
_widget.ButtonPressEvent -= Parent_ButtonPressEvent;
|
||||
_widget.ButtonReleaseEvent -= Parent_ButtonReleaseEvent;
|
||||
|
||||
_widget = null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
using Gdk;
|
||||
using Gtk;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.UI;
|
||||
using Ryujinx.UI.Common.Configuration;
|
||||
using Ryujinx.UI.Common.Helper;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Ryujinx.Modules
|
||||
{
|
||||
public class UpdateDialog : Gtk.Window
|
||||
{
|
||||
#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier
|
||||
[Builder.Object] public Label MainText;
|
||||
[Builder.Object] public Label SecondaryText;
|
||||
[Builder.Object] public LevelBar ProgressBar;
|
||||
[Builder.Object] public Button YesButton;
|
||||
[Builder.Object] public Button NoButton;
|
||||
#pragma warning restore CS0649, IDE0044
|
||||
|
||||
private readonly MainWindow _mainWindow;
|
||||
private readonly string _buildUrl;
|
||||
private bool _restartQuery;
|
||||
|
||||
public UpdateDialog(MainWindow mainWindow, Version newVersion, string buildUrl) : this(new Builder("Ryujinx.Gtk3.Modules.Updater.UpdateDialog.glade"), mainWindow, newVersion, buildUrl) { }
|
||||
|
||||
private UpdateDialog(Builder builder, MainWindow mainWindow, Version newVersion, string buildUrl) : base(builder.GetRawOwnedObject("UpdateDialog"))
|
||||
{
|
||||
builder.Autoconnect(this);
|
||||
|
||||
_mainWindow = mainWindow;
|
||||
_buildUrl = buildUrl;
|
||||
|
||||
Icon = new Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Gtk3.UI.Common.Resources.Logo_Ryujinx.png");
|
||||
MainText.Text = "Do you want to update Ryujinx to the latest version?";
|
||||
SecondaryText.Text = $"{Program.Version} -> {newVersion}";
|
||||
|
||||
ProgressBar.Hide();
|
||||
|
||||
YesButton.Clicked += YesButton_Clicked;
|
||||
NoButton.Clicked += NoButton_Clicked;
|
||||
}
|
||||
|
||||
private void YesButton_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
if (_restartQuery)
|
||||
{
|
||||
string ryuName = OperatingSystem.IsWindows() ? "Ryujinx.exe" : "Ryujinx";
|
||||
|
||||
ProcessStartInfo processStart = new(ryuName)
|
||||
{
|
||||
UseShellExecute = true,
|
||||
WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory
|
||||
};
|
||||
|
||||
foreach (string argument in CommandLineState.Arguments)
|
||||
{
|
||||
processStart.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
Process.Start(processStart);
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Window.Functions = _mainWindow.Window.Functions = WMFunction.All & WMFunction.Close;
|
||||
_mainWindow.ExitMenuItem.Sensitive = false;
|
||||
|
||||
YesButton.Hide();
|
||||
NoButton.Hide();
|
||||
ProgressBar.Show();
|
||||
|
||||
SecondaryText.Text = "";
|
||||
_restartQuery = true;
|
||||
|
||||
Updater.UpdateRyujinx(this, _buildUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private void NoButton_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
Updater.Running = false;
|
||||
_mainWindow.Window.Functions = WMFunction.All;
|
||||
|
||||
_mainWindow.ExitMenuItem.Sensitive = true;
|
||||
_mainWindow.UpdateMenuItem.Sensitive = true;
|
||||
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.22.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.20"/>
|
||||
<object class="GtkWindow" id="UpdateDialog">
|
||||
<property name="can_focus">False</property>
|
||||
<property name="title" translatable="yes">Ryujinx - Updater</property>
|
||||
<property name="resizable">False</property>
|
||||
<property name="window_position">center</property>
|
||||
<property name="default_width">400</property>
|
||||
<property name="default_height">130</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="MainBox">
|
||||
<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="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="BodyBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="MainText">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
<attributes>
|
||||
<attribute name="weight" value="bold"/>
|
||||
<attribute name="size" value="10000"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="SecondaryText">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<property name="margin_top">5</property>
|
||||
<property name="margin_bottom">5</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLevelBar" id="ProgressBar">
|
||||
<property name="height_request">20</property>
|
||||
<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="max_value">100</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">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="ButtonBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">False</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="YesButton">
|
||||
<property name="label" translatable="yes">Yes</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="margin_right">5</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">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="NoButton">
|
||||
<property name="label" translatable="yes">No</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can_focus">True</property>
|
||||
<property name="receives_default">True</property>
|
||||
<property name="margin_left">5</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">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
|
@ -1,622 +0,0 @@
|
|||
using Gtk;
|
||||
using ICSharpCode.SharpZipLib.GZip;
|
||||
using ICSharpCode.SharpZipLib.Tar;
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.UI;
|
||||
using Ryujinx.UI.Common.Models.Github;
|
||||
using Ryujinx.UI.Widgets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Modules
|
||||
{
|
||||
public static class Updater
|
||||
{
|
||||
private const string GitHubApiUrl = "https://api.github.com";
|
||||
private const int ConnectionCount = 4;
|
||||
|
||||
internal static bool Running;
|
||||
|
||||
private static readonly string _homeDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||
private static readonly string _updateDir = Path.Combine(Path.GetTempPath(), "Ryujinx", "update");
|
||||
private static readonly string _updatePublishDir = Path.Combine(_updateDir, "publish");
|
||||
|
||||
private static string _buildVer;
|
||||
private static string _platformExt;
|
||||
private static string _buildUrl;
|
||||
private static long _buildSize;
|
||||
|
||||
private static readonly GithubReleasesJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
// On Windows, GtkSharp.Dependencies adds these extra dirs that must be cleaned during updates.
|
||||
private static readonly string[] _windowsDependencyDirs = { "bin", "etc", "lib", "share" };
|
||||
|
||||
private static HttpClient ConstructHttpClient()
|
||||
{
|
||||
HttpClient result = new();
|
||||
|
||||
// Required by GitHub to interact with APIs.
|
||||
result.DefaultRequestHeaders.Add("User-Agent", "Ryujinx-Updater/1.0.0");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static async Task BeginParse(MainWindow mainWindow, bool showVersionUpToDate)
|
||||
{
|
||||
if (Running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Running = true;
|
||||
mainWindow.UpdateMenuItem.Sensitive = false;
|
||||
|
||||
int artifactIndex = -1;
|
||||
|
||||
// Detect current platform
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
_platformExt = "osx_x64.zip";
|
||||
artifactIndex = 1;
|
||||
}
|
||||
else if (OperatingSystem.IsWindows())
|
||||
{
|
||||
_platformExt = "win_x64.zip";
|
||||
artifactIndex = 2;
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
var arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "arm64" : "x64";
|
||||
_platformExt = $"linux_{arch}.tar.gz";
|
||||
artifactIndex = 0;
|
||||
}
|
||||
|
||||
if (artifactIndex == -1)
|
||||
{
|
||||
GtkDialog.CreateErrorDialog("Your platform is not supported!");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Version newVersion;
|
||||
Version currentVersion;
|
||||
|
||||
try
|
||||
{
|
||||
currentVersion = Version.Parse(Program.Version);
|
||||
}
|
||||
catch
|
||||
{
|
||||
GtkDialog.CreateWarningDialog("Failed to convert the current Ryujinx version.", "Cancelling Update!");
|
||||
Logger.Error?.Print(LogClass.Application, "Failed to convert the current Ryujinx version!");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Get latest version number from GitHub API
|
||||
try
|
||||
{
|
||||
using HttpClient jsonClient = ConstructHttpClient();
|
||||
string buildInfoUrl = $"{GitHubApiUrl}/repos/{ReleaseInformation.ReleaseChannelOwner}/{ReleaseInformation.ReleaseChannelRepo}/releases/latest";
|
||||
|
||||
// Fetch latest build information
|
||||
string fetchedJson = await jsonClient.GetStringAsync(buildInfoUrl);
|
||||
var fetched = JsonHelper.Deserialize(fetchedJson, _serializerContext.GithubReleasesJsonResponse);
|
||||
_buildVer = fetched.Name;
|
||||
|
||||
foreach (var asset in fetched.Assets)
|
||||
{
|
||||
if (asset.Name.StartsWith("gtk-ryujinx") && asset.Name.EndsWith(_platformExt))
|
||||
{
|
||||
_buildUrl = asset.BrowserDownloadUrl;
|
||||
|
||||
if (asset.State != "uploaded")
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (_buildUrl == null)
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, exception.Message);
|
||||
GtkDialog.CreateErrorDialog("An error occurred when trying to get release information from GitHub Release. This can be caused if a new release is being compiled by GitHub Actions. Try again in a few minutes.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
newVersion = Version.Parse(_buildVer);
|
||||
}
|
||||
catch
|
||||
{
|
||||
GtkDialog.CreateWarningDialog("Failed to convert the received Ryujinx version from GitHub Release.", "Cancelling Update!");
|
||||
Logger.Error?.Print(LogClass.Application, "Failed to convert the received Ryujinx version from GitHub Release!");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (newVersion <= currentVersion)
|
||||
{
|
||||
if (showVersionUpToDate)
|
||||
{
|
||||
GtkDialog.CreateUpdaterInfoDialog("You are already using the latest version of Ryujinx!", "");
|
||||
}
|
||||
|
||||
Running = false;
|
||||
mainWindow.UpdateMenuItem.Sensitive = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch build size information to learn chunk sizes.
|
||||
using HttpClient buildSizeClient = ConstructHttpClient();
|
||||
try
|
||||
{
|
||||
buildSizeClient.DefaultRequestHeaders.Add("Range", "bytes=0-0");
|
||||
|
||||
HttpResponseMessage message = await buildSizeClient.GetAsync(new Uri(_buildUrl), HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
_buildSize = message.Content.Headers.ContentRange.Length.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, ex.Message);
|
||||
Logger.Warning?.Print(LogClass.Application, "Couldn't determine build size for update, using single-threaded updater");
|
||||
|
||||
_buildSize = -1;
|
||||
}
|
||||
|
||||
// Show a message asking the user if they want to update
|
||||
UpdateDialog updateDialog = new(mainWindow, newVersion, _buildUrl);
|
||||
updateDialog.Show();
|
||||
}
|
||||
|
||||
public static void UpdateRyujinx(UpdateDialog updateDialog, string downloadUrl)
|
||||
{
|
||||
// Empty update dir, although it shouldn't ever have anything inside it
|
||||
if (Directory.Exists(_updateDir))
|
||||
{
|
||||
Directory.Delete(_updateDir, true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(_updateDir);
|
||||
|
||||
string updateFile = Path.Combine(_updateDir, "update.bin");
|
||||
|
||||
// Download the update .zip
|
||||
updateDialog.MainText.Text = "Downloading Update...";
|
||||
updateDialog.ProgressBar.Value = 0;
|
||||
updateDialog.ProgressBar.MaxValue = 100;
|
||||
|
||||
if (_buildSize >= 0)
|
||||
{
|
||||
DoUpdateWithMultipleThreads(updateDialog, downloadUrl, updateFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DoUpdateWithMultipleThreads(UpdateDialog updateDialog, string downloadUrl, string updateFile)
|
||||
{
|
||||
// Multi-Threaded Updater
|
||||
long chunkSize = _buildSize / ConnectionCount;
|
||||
long remainderChunk = _buildSize % ConnectionCount;
|
||||
|
||||
int completedRequests = 0;
|
||||
int totalProgressPercentage = 0;
|
||||
int[] progressPercentage = new int[ConnectionCount];
|
||||
|
||||
List<byte[]> list = new(ConnectionCount);
|
||||
List<WebClient> webClients = new(ConnectionCount);
|
||||
|
||||
for (int i = 0; i < ConnectionCount; i++)
|
||||
{
|
||||
list.Add(Array.Empty<byte>());
|
||||
}
|
||||
|
||||
for (int i = 0; i < ConnectionCount; i++)
|
||||
{
|
||||
#pragma warning disable SYSLIB0014
|
||||
// TODO: WebClient is obsolete and need to be replaced with a more complex logic using HttpClient.
|
||||
using WebClient client = new();
|
||||
#pragma warning restore SYSLIB0014
|
||||
webClients.Add(client);
|
||||
|
||||
if (i == ConnectionCount - 1)
|
||||
{
|
||||
client.Headers.Add("Range", $"bytes={chunkSize * i}-{(chunkSize * (i + 1) - 1) + remainderChunk}");
|
||||
}
|
||||
else
|
||||
{
|
||||
client.Headers.Add("Range", $"bytes={chunkSize * i}-{chunkSize * (i + 1) - 1}");
|
||||
}
|
||||
|
||||
client.DownloadProgressChanged += (_, args) =>
|
||||
{
|
||||
int index = (int)args.UserState;
|
||||
|
||||
Interlocked.Add(ref totalProgressPercentage, -1 * progressPercentage[index]);
|
||||
Interlocked.Exchange(ref progressPercentage[index], args.ProgressPercentage);
|
||||
Interlocked.Add(ref totalProgressPercentage, args.ProgressPercentage);
|
||||
|
||||
updateDialog.ProgressBar.Value = totalProgressPercentage / ConnectionCount;
|
||||
};
|
||||
|
||||
client.DownloadDataCompleted += (_, args) =>
|
||||
{
|
||||
int index = (int)args.UserState;
|
||||
|
||||
if (args.Cancelled)
|
||||
{
|
||||
webClients[index].Dispose();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
list[index] = args.Result;
|
||||
Interlocked.Increment(ref completedRequests);
|
||||
|
||||
if (Equals(completedRequests, ConnectionCount))
|
||||
{
|
||||
byte[] mergedFileBytes = new byte[_buildSize];
|
||||
for (int connectionIndex = 0, destinationOffset = 0; connectionIndex < ConnectionCount; connectionIndex++)
|
||||
{
|
||||
Array.Copy(list[connectionIndex], 0, mergedFileBytes, destinationOffset, list[connectionIndex].Length);
|
||||
destinationOffset += list[connectionIndex].Length;
|
||||
}
|
||||
|
||||
File.WriteAllBytes(updateFile, mergedFileBytes);
|
||||
|
||||
try
|
||||
{
|
||||
InstallUpdate(updateDialog, updateFile);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, e.Message);
|
||||
Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
|
||||
|
||||
DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
client.DownloadDataAsync(new Uri(downloadUrl), i);
|
||||
}
|
||||
catch (WebException ex)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, ex.Message);
|
||||
Logger.Warning?.Print(LogClass.Application, "Multi-Threaded update failed, falling back to single-threaded updater.");
|
||||
|
||||
foreach (WebClient webClient in webClients)
|
||||
{
|
||||
webClient.CancelAsync();
|
||||
}
|
||||
|
||||
DoUpdateWithSingleThread(updateDialog, downloadUrl, updateFile);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DoUpdateWithSingleThreadWorker(UpdateDialog updateDialog, string downloadUrl, string updateFile)
|
||||
{
|
||||
using HttpClient client = new();
|
||||
// We do not want to timeout while downloading
|
||||
client.Timeout = TimeSpan.FromDays(1);
|
||||
|
||||
using HttpResponseMessage response = client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead).Result;
|
||||
using Stream remoteFileStream = response.Content.ReadAsStreamAsync().Result;
|
||||
using Stream updateFileStream = File.Open(updateFile, FileMode.Create);
|
||||
|
||||
long totalBytes = response.Content.Headers.ContentLength.Value;
|
||||
long byteWritten = 0;
|
||||
|
||||
byte[] buffer = new byte[32 * 1024];
|
||||
|
||||
while (true)
|
||||
{
|
||||
int readSize = remoteFileStream.Read(buffer);
|
||||
|
||||
if (readSize == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
byteWritten += readSize;
|
||||
|
||||
updateDialog.ProgressBar.Value = ((double)byteWritten / totalBytes) * 100;
|
||||
updateFileStream.Write(buffer, 0, readSize);
|
||||
}
|
||||
|
||||
InstallUpdate(updateDialog, updateFile);
|
||||
}
|
||||
|
||||
private static void DoUpdateWithSingleThread(UpdateDialog updateDialog, string downloadUrl, string updateFile)
|
||||
{
|
||||
Thread worker = new(() => DoUpdateWithSingleThreadWorker(updateDialog, downloadUrl, updateFile))
|
||||
{
|
||||
Name = "Updater.SingleThreadWorker",
|
||||
};
|
||||
worker.Start();
|
||||
}
|
||||
|
||||
private static async void InstallUpdate(UpdateDialog updateDialog, string updateFile)
|
||||
{
|
||||
// Extract Update
|
||||
updateDialog.MainText.Text = "Extracting Update...";
|
||||
updateDialog.ProgressBar.Value = 0;
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
using Stream inStream = File.OpenRead(updateFile);
|
||||
using Stream gzipStream = new GZipInputStream(inStream);
|
||||
using TarInputStream tarStream = new(gzipStream, Encoding.ASCII);
|
||||
updateDialog.ProgressBar.MaxValue = inStream.Length;
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
TarEntry tarEntry;
|
||||
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
while ((tarEntry = tarStream.GetNextEntry()) != null)
|
||||
{
|
||||
if (tarEntry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string outPath = Path.Combine(_updateDir, tarEntry.Name);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||
|
||||
using FileStream outStream = File.OpenWrite(outPath);
|
||||
tarStream.CopyEntryContents(outStream);
|
||||
|
||||
File.SetUnixFileMode(outPath, (UnixFileMode)tarEntry.TarHeader.Mode);
|
||||
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(tarEntry.ModTime, DateTimeKind.Utc));
|
||||
|
||||
TarEntry entry = tarEntry;
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
updateDialog.ProgressBar.Value += entry.Size;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
updateDialog.ProgressBar.Value = inStream.Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
using Stream inStream = File.OpenRead(updateFile);
|
||||
using ZipFile zipFile = new(inStream);
|
||||
updateDialog.ProgressBar.MaxValue = zipFile.Count;
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach (ZipEntry zipEntry in zipFile)
|
||||
{
|
||||
if (zipEntry.IsDirectory)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string outPath = Path.Combine(_updateDir, zipEntry.Name);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outPath));
|
||||
|
||||
using Stream zipStream = zipFile.GetInputStream(zipEntry);
|
||||
using FileStream outStream = File.OpenWrite(outPath);
|
||||
zipStream.CopyTo(outStream);
|
||||
|
||||
File.SetLastWriteTime(outPath, DateTime.SpecifyKind(zipEntry.DateTime, DateTimeKind.Utc));
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
updateDialog.ProgressBar.Value++;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete downloaded zip
|
||||
File.Delete(updateFile);
|
||||
|
||||
List<string> allFiles = EnumerateFilesToDelete().ToList();
|
||||
|
||||
updateDialog.MainText.Text = "Renaming Old Files...";
|
||||
updateDialog.ProgressBar.Value = 0;
|
||||
updateDialog.ProgressBar.MaxValue = allFiles.Count;
|
||||
|
||||
// Replace old files
|
||||
await Task.Run(() =>
|
||||
{
|
||||
foreach (string file in allFiles)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Move(file, file + ".ryuold");
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
updateDialog.ProgressBar.Value++;
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, "Updater was unable to rename file: " + file);
|
||||
}
|
||||
}
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
updateDialog.MainText.Text = "Adding New Files...";
|
||||
updateDialog.ProgressBar.Value = 0;
|
||||
updateDialog.ProgressBar.MaxValue = Directory.GetFiles(_updatePublishDir, "*", SearchOption.AllDirectories).Length;
|
||||
});
|
||||
|
||||
MoveAllFilesOver(_updatePublishDir, _homeDir, updateDialog);
|
||||
});
|
||||
|
||||
Directory.Delete(_updateDir, true);
|
||||
|
||||
updateDialog.MainText.Text = "Update Complete!";
|
||||
updateDialog.SecondaryText.Text = "Do you want to restart Ryujinx now?";
|
||||
updateDialog.Modal = true;
|
||||
|
||||
updateDialog.ProgressBar.Hide();
|
||||
updateDialog.YesButton.Show();
|
||||
updateDialog.NoButton.Show();
|
||||
}
|
||||
|
||||
public static bool CanUpdate(bool showWarnings)
|
||||
{
|
||||
#if !DISABLE_UPDATER
|
||||
if (!NetworkInterface.GetIsNetworkAvailable())
|
||||
{
|
||||
if (showWarnings)
|
||||
{
|
||||
GtkDialog.CreateWarningDialog("You are not connected to the Internet!", "Please verify that you have a working Internet connection!");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Program.Version.Contains("dirty") || !ReleaseInformation.IsValid)
|
||||
{
|
||||
if (showWarnings)
|
||||
{
|
||||
GtkDialog.CreateWarningDialog("You cannot update a Dirty build of Ryujinx!", "Please download Ryujinx at https://ryujinx.org/ if you are looking for a supported version.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
#else
|
||||
if (showWarnings)
|
||||
{
|
||||
if (ReleaseInformation.IsFlatHubBuild)
|
||||
{
|
||||
GtkDialog.CreateWarningDialog("Updater Disabled!", "Please update Ryujinx via FlatHub.");
|
||||
}
|
||||
else
|
||||
{
|
||||
GtkDialog.CreateWarningDialog("Updater Disabled!", "Please download Ryujinx at https://ryujinx.org/ if you are looking for a supported version.");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// NOTE: This method should always reflect the latest build layout.
|
||||
private static IEnumerable<string> EnumerateFilesToDelete()
|
||||
{
|
||||
var files = Directory.EnumerateFiles(_homeDir); // All files directly in base dir.
|
||||
|
||||
// Determine and exclude user files only when the updater is running, not when cleaning old files
|
||||
if (Running)
|
||||
{
|
||||
// Compare the loose files in base directory against the loose files from the incoming update, and store foreign ones in a user list.
|
||||
var oldFiles = Directory.EnumerateFiles(_homeDir, "*", SearchOption.TopDirectoryOnly).Select(Path.GetFileName);
|
||||
var newFiles = Directory.EnumerateFiles(_updatePublishDir, "*", SearchOption.TopDirectoryOnly).Select(Path.GetFileName);
|
||||
var userFiles = oldFiles.Except(newFiles).Select(filename => Path.Combine(_homeDir, filename));
|
||||
|
||||
// Remove user files from the paths in files.
|
||||
files = files.Except(userFiles);
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
foreach (string dir in _windowsDependencyDirs)
|
||||
{
|
||||
string dirPath = Path.Combine(_homeDir, dir);
|
||||
if (Directory.Exists(dirPath))
|
||||
{
|
||||
files = files.Concat(Directory.EnumerateFiles(dirPath, "*", SearchOption.AllDirectories));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files.Where(f => !new FileInfo(f).Attributes.HasFlag(FileAttributes.Hidden | FileAttributes.System));
|
||||
}
|
||||
|
||||
private static void MoveAllFilesOver(string root, string dest, UpdateDialog dialog)
|
||||
{
|
||||
foreach (string directory in Directory.GetDirectories(root))
|
||||
{
|
||||
string dirName = Path.GetFileName(directory);
|
||||
|
||||
if (!Directory.Exists(Path.Combine(dest, dirName)))
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(dest, dirName));
|
||||
}
|
||||
|
||||
MoveAllFilesOver(directory, Path.Combine(dest, dirName), dialog);
|
||||
}
|
||||
|
||||
foreach (string file in Directory.GetFiles(root))
|
||||
{
|
||||
File.Move(file, Path.Combine(dest, Path.GetFileName(file)), true);
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
dialog.ProgressBar.Value++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void CleanupUpdate()
|
||||
{
|
||||
foreach (string file in EnumerateFilesToDelete())
|
||||
{
|
||||
if (Path.GetExtension(file).EndsWith(".ryuold"))
|
||||
{
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,403 +0,0 @@
|
|||
using Gtk;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.SystemInterop;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Graphics.Vulkan.MoltenVK;
|
||||
using Ryujinx.Modules;
|
||||
using Ryujinx.SDL2.Common;
|
||||
using Ryujinx.UI;
|
||||
using Ryujinx.UI.App.Common;
|
||||
using Ryujinx.UI.Common;
|
||||
using Ryujinx.UI.Common.Configuration;
|
||||
using Ryujinx.UI.Common.Helper;
|
||||
using Ryujinx.UI.Common.SystemInfo;
|
||||
using Ryujinx.UI.Widgets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx
|
||||
{
|
||||
partial class Program
|
||||
{
|
||||
public static double WindowScaleFactor { get; private set; }
|
||||
|
||||
public static string Version { get; private set; }
|
||||
|
||||
public static string ConfigurationPath { get; set; }
|
||||
|
||||
public static string CommandLineProfile { get; set; }
|
||||
|
||||
private const string X11LibraryName = "libX11";
|
||||
|
||||
[LibraryImport(X11LibraryName)]
|
||||
private static partial int XInitThreads();
|
||||
|
||||
[LibraryImport("user32.dll", SetLastError = true)]
|
||||
public static partial int MessageBoxA(IntPtr hWnd, [MarshalAs(UnmanagedType.LPStr)] string text, [MarshalAs(UnmanagedType.LPStr)] string caption, uint type);
|
||||
|
||||
private const uint MbIconWarning = 0x30;
|
||||
|
||||
static Program()
|
||||
{
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
NativeLibrary.SetDllImportResolver(typeof(Program).Assembly, (name, assembly, path) =>
|
||||
{
|
||||
if (name != X11LibraryName)
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
if (!NativeLibrary.TryLoad("libX11.so.6", assembly, path, out IntPtr result))
|
||||
{
|
||||
if (!NativeLibrary.TryLoad("libX11.so", assembly, path, out result))
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
Version = ReleaseInformation.Version;
|
||||
|
||||
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
|
||||
{
|
||||
MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 1803 and newer.\n", $"Ryujinx {Version}", MbIconWarning);
|
||||
}
|
||||
|
||||
// Parse arguments
|
||||
CommandLineState.ParseArguments(args);
|
||||
|
||||
// Hook unhandled exception and process exit events.
|
||||
GLib.ExceptionManager.UnhandledException += (GLib.UnhandledExceptionArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
|
||||
AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => ProcessUnhandledException(e.ExceptionObject as Exception, e.IsTerminating);
|
||||
AppDomain.CurrentDomain.ProcessExit += (object sender, EventArgs e) => Exit();
|
||||
|
||||
// Make process DPI aware for proper window sizing on high-res screens.
|
||||
ForceDpiAware.Windows();
|
||||
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
|
||||
|
||||
// Delete backup files after updating.
|
||||
Task.Run(Updater.CleanupUpdate);
|
||||
|
||||
Console.Title = $"Ryujinx Console {Version}";
|
||||
|
||||
// NOTE: GTK3 doesn't init X11 in a multi threaded way.
|
||||
// This ends up causing race condition and abort of XCB when a context is created by SPB (even if SPB do call XInitThreads).
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
if (XInitThreads() == 0)
|
||||
{
|
||||
throw new NotSupportedException("Failed to initialize multi-threading support.");
|
||||
}
|
||||
|
||||
OsUtils.SetEnvironmentVariableNoCaching("GDK_BACKEND", "x11");
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
MVKInitialization.InitializeResolver();
|
||||
|
||||
string baseDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
|
||||
string resourcesDataDir;
|
||||
|
||||
if (Path.GetFileName(baseDirectory) == "MacOS")
|
||||
{
|
||||
resourcesDataDir = Path.Combine(Directory.GetParent(baseDirectory).FullName, "Resources");
|
||||
}
|
||||
else
|
||||
{
|
||||
resourcesDataDir = baseDirectory;
|
||||
}
|
||||
|
||||
// On macOS, GTK3 needs XDG_DATA_DIRS to be set, otherwise it will try searching for "gschemas.compiled" in system directories.
|
||||
OsUtils.SetEnvironmentVariableNoCaching("XDG_DATA_DIRS", Path.Combine(resourcesDataDir, "share"));
|
||||
|
||||
// On macOS, GTK3 needs GDK_PIXBUF_MODULE_FILE to be set, otherwise it will try searching for "loaders.cache" in system directories.
|
||||
OsUtils.SetEnvironmentVariableNoCaching("GDK_PIXBUF_MODULE_FILE", Path.Combine(resourcesDataDir, "lib", "gdk-pixbuf-2.0", "2.10.0", "loaders.cache"));
|
||||
|
||||
OsUtils.SetEnvironmentVariableNoCaching("GTK_IM_MODULE_FILE", Path.Combine(resourcesDataDir, "lib", "gtk-3.0", "3.0.0", "immodules.cache"));
|
||||
}
|
||||
|
||||
string systemPath = Environment.GetEnvironmentVariable("Path", EnvironmentVariableTarget.Machine);
|
||||
Environment.SetEnvironmentVariable("Path", $"{Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "bin")};{systemPath}");
|
||||
|
||||
// Setup base data directory.
|
||||
AppDataManager.Initialize(CommandLineState.BaseDirPathArg);
|
||||
|
||||
// Initialize the configuration.
|
||||
ConfigurationState.Initialize();
|
||||
|
||||
// Initialize the logger system.
|
||||
LoggerModule.Initialize();
|
||||
|
||||
// Initialize Discord integration.
|
||||
DiscordIntegrationModule.Initialize();
|
||||
|
||||
// Initialize SDL2 driver
|
||||
SDL2Driver.MainThreadDispatcher = action =>
|
||||
{
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
action();
|
||||
});
|
||||
};
|
||||
|
||||
string localConfigurationPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, ReleaseInformation.ConfigName);
|
||||
string appDataConfigurationPath = Path.Combine(AppDataManager.BaseDirPath, ReleaseInformation.ConfigName);
|
||||
|
||||
// Now load the configuration as the other subsystems are now registered
|
||||
ConfigurationPath = File.Exists(localConfigurationPath)
|
||||
? localConfigurationPath
|
||||
: File.Exists(appDataConfigurationPath)
|
||||
? appDataConfigurationPath
|
||||
: null;
|
||||
|
||||
if (ConfigurationPath == null)
|
||||
{
|
||||
// No configuration, we load the default values and save it to disk
|
||||
ConfigurationPath = appDataConfigurationPath;
|
||||
Logger.Notice.Print(LogClass.Application, $"No configuration file found. Saving default configuration to: {ConfigurationPath}");
|
||||
|
||||
ConfigurationState.Instance.LoadDefault();
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Loading configuration from: {ConfigurationPath}");
|
||||
|
||||
if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat))
|
||||
{
|
||||
ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location: {ConfigurationPath}");
|
||||
|
||||
ConfigurationState.Instance.LoadDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if graphics backend was overridden.
|
||||
if (CommandLineState.OverrideGraphicsBackend != null)
|
||||
{
|
||||
if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
|
||||
{
|
||||
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.OpenGl;
|
||||
}
|
||||
else if (CommandLineState.OverrideGraphicsBackend.ToLower() == "vulkan")
|
||||
{
|
||||
ConfigurationState.Instance.Graphics.GraphicsBackend.Value = GraphicsBackend.Vulkan;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if HideCursor was overridden.
|
||||
if (CommandLineState.OverrideHideCursor is not null)
|
||||
{
|
||||
ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
|
||||
{
|
||||
"never" => HideCursorMode.Never,
|
||||
"onidle" => HideCursorMode.OnIdle,
|
||||
"always" => HideCursorMode.Always,
|
||||
_ => ConfigurationState.Instance.HideCursor.Value,
|
||||
};
|
||||
}
|
||||
|
||||
// Check if docked mode was overridden.
|
||||
if (CommandLineState.OverrideDockedMode.HasValue)
|
||||
{
|
||||
ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
|
||||
}
|
||||
|
||||
// Logging system information.
|
||||
PrintSystemInfo();
|
||||
|
||||
// Enable OGL multithreading on the driver, and some other flags.
|
||||
BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading;
|
||||
DriverUtilities.InitDriverConfig(threadingMode == BackendThreading.Off);
|
||||
|
||||
// Initialize Gtk.
|
||||
Application.Init();
|
||||
|
||||
// Check if keys exists.
|
||||
bool hasSystemProdKeys = File.Exists(Path.Combine(AppDataManager.KeysDirPath, "prod.keys"));
|
||||
bool hasCommonProdKeys = AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile && File.Exists(Path.Combine(AppDataManager.KeysDirPathUser, "prod.keys"));
|
||||
if (!hasSystemProdKeys && !hasCommonProdKeys)
|
||||
{
|
||||
UserErrorDialog.CreateUserErrorDialog(UserError.NoKeys);
|
||||
}
|
||||
|
||||
// Show the main window UI.
|
||||
MainWindow mainWindow = new();
|
||||
mainWindow.Show();
|
||||
|
||||
// Load the game table if no application was requested by the command line
|
||||
if (CommandLineState.LaunchPathArg == null)
|
||||
{
|
||||
mainWindow.UpdateGameTable();
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
int currentVmMaxMapCount = LinuxHelper.VmMaxMapCount;
|
||||
|
||||
if (LinuxHelper.VmMaxMapCount < LinuxHelper.RecommendedVmMaxMapCount)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"The value of vm.max_map_count is lower than {LinuxHelper.RecommendedVmMaxMapCount}. ({currentVmMaxMapCount})");
|
||||
|
||||
if (LinuxHelper.PkExecPath is not null)
|
||||
{
|
||||
var buttonTexts = new Dictionary<int, string>()
|
||||
{
|
||||
{ 0, "Yes, until the next restart" },
|
||||
{ 1, "Yes, permanently" },
|
||||
{ 2, "No" },
|
||||
};
|
||||
|
||||
ResponseType response = GtkDialog.CreateCustomDialog(
|
||||
"Ryujinx - Low limit for memory mappings detected",
|
||||
$"Would you like to increase the value of vm.max_map_count to {LinuxHelper.RecommendedVmMaxMapCount}?",
|
||||
"Some games might try to create more memory mappings than currently allowed. " +
|
||||
"Ryujinx will crash as soon as this limit gets exceeded.",
|
||||
buttonTexts,
|
||||
MessageType.Question);
|
||||
|
||||
int rc;
|
||||
|
||||
switch ((int)response)
|
||||
{
|
||||
case 0:
|
||||
rc = LinuxHelper.RunPkExec($"echo {LinuxHelper.RecommendedVmMaxMapCount} > {LinuxHelper.VmMaxMapCountPath}");
|
||||
if (rc == 0)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount} until the next restart.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Unable to change vm.max_map_count. Process exited with code: {rc}");
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
rc = LinuxHelper.RunPkExec($"echo \"vm.max_map_count = {LinuxHelper.RecommendedVmMaxMapCount}\" > {LinuxHelper.SysCtlConfigPath} && sysctl -p {LinuxHelper.SysCtlConfigPath}");
|
||||
if (rc == 0)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, $"vm.max_map_count set to {LinuxHelper.VmMaxMapCount}. Written to config: {LinuxHelper.SysCtlConfigPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Unable to write new value for vm.max_map_count to config. Process exited with code: {rc}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GtkDialog.CreateWarningDialog(
|
||||
"Max amount of memory mappings is lower than recommended.",
|
||||
$"The current value of vm.max_map_count ({currentVmMaxMapCount}) is lower than {LinuxHelper.RecommendedVmMaxMapCount}." +
|
||||
"Some games might try to create more memory mappings than currently allowed. " +
|
||||
"Ryujinx will crash as soon as this limit gets exceeded.\n\n" +
|
||||
"You might want to either manually increase the limit or install pkexec, which allows Ryujinx to assist with that.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (CommandLineState.LaunchPathArg != null)
|
||||
{
|
||||
if (mainWindow.ApplicationLibrary.TryGetApplicationsFromFile(CommandLineState.LaunchPathArg, out List<ApplicationData> applications))
|
||||
{
|
||||
ApplicationData applicationData;
|
||||
|
||||
if (CommandLineState.LaunchApplicationId != null)
|
||||
{
|
||||
applicationData = applications.Find(application => application.IdString == CommandLineState.LaunchApplicationId);
|
||||
|
||||
if (applicationData != null)
|
||||
{
|
||||
mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Couldn't find requested application id '{CommandLineState.LaunchApplicationId}' in '{CommandLineState.LaunchPathArg}'.");
|
||||
UserErrorDialog.CreateUserErrorDialog(UserError.ApplicationNotFound);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
applicationData = applications[0];
|
||||
mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Couldn't find any application in '{CommandLineState.LaunchPathArg}'.");
|
||||
UserErrorDialog.CreateUserErrorDialog(UserError.ApplicationNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))
|
||||
{
|
||||
Updater.BeginParse(mainWindow, false).ContinueWith(task =>
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}");
|
||||
}, TaskContinuationOptions.OnlyOnFaulted);
|
||||
}
|
||||
|
||||
Application.Run();
|
||||
}
|
||||
|
||||
private static void PrintSystemInfo()
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Ryujinx Version: {Version}");
|
||||
SystemInfo.Gather().Print();
|
||||
|
||||
var enabledLogs = Logger.GetEnabledLevels();
|
||||
Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogs.Count == 0 ? "<None>" : string.Join(", ", enabledLogs))}");
|
||||
|
||||
if (AppDataManager.Mode == AppDataManager.LaunchMode.Custom)
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Launch Mode: Custom Path {AppDataManager.BaseDirPath}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Launch Mode: {AppDataManager.Mode}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ProcessUnhandledException(Exception ex, bool isTerminating)
|
||||
{
|
||||
string message = $"Unhandled exception caught: {ex}";
|
||||
|
||||
Logger.Error?.PrintMsg(LogClass.Application, message);
|
||||
|
||||
if (Logger.Error == null)
|
||||
{
|
||||
Logger.Notice.PrintMsg(LogClass.Application, message);
|
||||
}
|
||||
|
||||
if (isTerminating)
|
||||
{
|
||||
Exit();
|
||||
}
|
||||
}
|
||||
|
||||
public static void Exit()
|
||||
{
|
||||
DiscordIntegrationModule.Exit();
|
||||
|
||||
Logger.Shutdown();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Version>1.0.0-dirty</Version>
|
||||
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
|
||||
<!-- As we already provide GTK3 on Windows via GtkSharp.Dependencies this is redundant. -->
|
||||
<SkipGtkInstall>true</SkipGtkInstall>
|
||||
<TieredPGO>true</TieredPGO>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' != ''">
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<TrimmerSingleWarn>false</TrimmerSingleWarn>
|
||||
<PublishTrimmed>true</PublishTrimmed>
|
||||
<TrimMode>partial</TrimMode>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Ryujinx.GtkSharp" />
|
||||
<PackageReference Include="GtkSharp.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
|
||||
<PackageReference Include="GtkSharp.Dependencies.osx" Condition="'$(RuntimeIdentifier)' == 'osx-x64' OR '$(RuntimeIdentifier)' == 'osx-arm64'" />
|
||||
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" />
|
||||
<PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
|
||||
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64'" />
|
||||
<PackageReference Include="OpenTK.Core" />
|
||||
<PackageReference Include="OpenTK.Graphics" />
|
||||
<PackageReference Include="SPB" />
|
||||
<PackageReference Include="SharpZipLib" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
|
||||
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.UI.Common\Ryujinx.UI.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\distribution\windows\alsoft.ini" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64'">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<TargetPath>alsoft.ini</TargetPath>
|
||||
</Content>
|
||||
<Content Include="..\..\distribution\legal\THIRDPARTY.md">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<TargetPath>THIRDPARTY.md</TargetPath>
|
||||
</Content>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<TargetPath>LICENSE.txt</TargetPath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="'$(RuntimeIdentifier)' == 'linux-x64' OR '$(RuntimeIdentifier)' == 'linux-arm64' OR ('$(RuntimeIdentifier)' == '' AND $([MSBuild]::IsOSPlatform('Linux')))">
|
||||
<Content Include="..\..\distribution\linux\Ryujinx.sh">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\distribution\linux\mime\Ryujinx.xml">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<TargetPath>mime\Ryujinx.xml</TargetPath>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Due to .net core 3.1 embedded resource loading -->
|
||||
<PropertyGroup>
|
||||
<EmbeddedResourceUseDependentUponConvention>false</EmbeddedResourceUseDependentUponConvention>
|
||||
<ApplicationIcon>Ryujinx.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="UI\MainWindow.glade" />
|
||||
<None Remove="UI\Widgets\ProfileDialog.glade" />
|
||||
<None Remove="UI\Windows\CheatWindow.glade" />
|
||||
<None Remove="UI\Windows\ControllerWindow.glade" />
|
||||
<None Remove="UI\Windows\DlcWindow.glade" />
|
||||
<None Remove="UI\Windows\SettingsWindow.glade" />
|
||||
<None Remove="UI\Windows\TitleUpdateWindow.glade" />
|
||||
<None Remove="Modules\Updater\UpdateDialog.glade" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="UI\MainWindow.glade" />
|
||||
<EmbeddedResource Include="UI\Widgets\ProfileDialog.glade" />
|
||||
<EmbeddedResource Include="UI\Windows\CheatWindow.glade" />
|
||||
<EmbeddedResource Include="UI\Windows\ControllerWindow.glade" />
|
||||
<EmbeddedResource Include="UI\Windows\DlcWindow.glade" />
|
||||
<EmbeddedResource Include="UI\Windows\SettingsWindow.glade" />
|
||||
<EmbeddedResource Include="UI\Windows\TitleUpdateWindow.glade" />
|
||||
<EmbeddedResource Include="Modules\Updater\UpdateDialog.glade" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
Binary file not shown.
Before Width: | Height: | Size: 106 KiB |
|
@ -1,31 +0,0 @@
|
|||
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.Gtk3.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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
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();
|
||||
private readonly RawInputToTextEntry _inputToTextEntry = new();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
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;
|
||||
|
||||
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(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(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);
|
||||
swkbdDialog.SetInputValidation(args.KeyboardMode);
|
||||
|
||||
((MainWindow)_parent).RendererWidget.NpadManager.BlockInputUpdates();
|
||||
|
||||
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();
|
||||
((MainWindow)_parent).RendererWidget.NpadManager.UnblockInputUpdates();
|
||||
|
||||
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(false);
|
||||
|
||||
bool showDetails = false;
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
try
|
||||
{
|
||||
ErrorAppletDialog msgDialog = new(_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;
|
||||
}
|
||||
|
||||
public IDynamicTextInputHandler CreateDynamicTextInputHandler()
|
||||
{
|
||||
return new GtkDynamicTextInputHandler(_parent);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
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.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();
|
||||
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 static 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
using Gtk;
|
||||
using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.UI.Applet
|
||||
{
|
||||
public class SwkbdAppletDialog : MessageDialog
|
||||
{
|
||||
private int _inputMin;
|
||||
private int _inputMax;
|
||||
#pragma warning disable IDE0052 // Remove unread private member
|
||||
private KeyboardMode _mode;
|
||||
#pragma warning restore IDE0052
|
||||
|
||||
private string _validationInfoText = "";
|
||||
|
||||
private Predicate<int> _checkLength = _ => true;
|
||||
private Predicate<string> _checkInput = _ => true;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private void ApplyValidationInfo()
|
||||
{
|
||||
_validationInfo.Visible = !string.IsNullOrEmpty(_validationInfoText);
|
||||
_validationInfo.Markup = _validationInfoText;
|
||||
}
|
||||
|
||||
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 = _ => true;
|
||||
}
|
||||
else if (_inputMin > 0 && _inputMax == int.MaxValue)
|
||||
{
|
||||
_validationInfoText = $"<i>Must be at least {_inputMin} characters long.</i> ";
|
||||
|
||||
_checkLength = length => _inputMin <= length;
|
||||
}
|
||||
else
|
||||
{
|
||||
_validationInfoText = $"<i>Must be {_inputMin}-{_inputMax} characters long.</i> ";
|
||||
|
||||
_checkLength = length => _inputMin <= length && length <= _inputMax;
|
||||
}
|
||||
|
||||
ApplyValidationInfo();
|
||||
OnInputChanged(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public void SetInputValidation(KeyboardMode mode)
|
||||
{
|
||||
_mode = mode;
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case KeyboardMode.Numeric:
|
||||
_validationInfoText += "<i>Must be 0-9 or '.' only.</i>";
|
||||
_checkInput = text => text.All(NumericCharacterValidation.IsNumeric);
|
||||
break;
|
||||
case KeyboardMode.Alphabet:
|
||||
_validationInfoText += "<i>Must be non CJK-characters only.</i>";
|
||||
_checkInput = text => text.All(value => !CJKCharacterValidation.IsCJK(value));
|
||||
break;
|
||||
case KeyboardMode.ASCII:
|
||||
_validationInfoText += "<i>Must be ASCII text only.</i>";
|
||||
_checkInput = text => text.All(char.IsAscii);
|
||||
break;
|
||||
default:
|
||||
_checkInput = _ => true;
|
||||
break;
|
||||
}
|
||||
|
||||
ApplyValidationInfo();
|
||||
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) && _checkInput(InputEntry.Text);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
||||
using StickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
||||
|
||||
namespace Ryujinx.UI.Helper
|
||||
{
|
||||
public static class ButtonHelper
|
||||
{
|
||||
private static readonly Dictionary<Key, string> _keysMap = new()
|
||||
{
|
||||
{ Key.Unknown, "Unknown" },
|
||||
{ Key.ShiftLeft, "ShiftLeft" },
|
||||
{ Key.ShiftRight, "ShiftRight" },
|
||||
{ Key.ControlLeft, "CtrlLeft" },
|
||||
{ Key.ControlRight, "CtrlRight" },
|
||||
{ Key.AltLeft, OperatingSystem.IsMacOS() ? "OptLeft" : "AltLeft" },
|
||||
{ Key.AltRight, OperatingSystem.IsMacOS() ? "OptRight" : "AltRight" },
|
||||
{ Key.WinLeft, OperatingSystem.IsMacOS() ? "CmdLeft" : "WinLeft" },
|
||||
{ Key.WinRight, OperatingSystem.IsMacOS() ? "CmdRight" : "WinRight" },
|
||||
{ Key.Up, "Up" },
|
||||
{ Key.Down, "Down" },
|
||||
{ Key.Left, "Left" },
|
||||
{ Key.Right, "Right" },
|
||||
{ Key.Enter, "Enter" },
|
||||
{ Key.Escape, "Escape" },
|
||||
{ Key.Space, "Space" },
|
||||
{ Key.Tab, "Tab" },
|
||||
{ Key.BackSpace, "Backspace" },
|
||||
{ Key.Insert, "Insert" },
|
||||
{ Key.Delete, "Delete" },
|
||||
{ Key.PageUp, "PageUp" },
|
||||
{ Key.PageDown, "PageDown" },
|
||||
{ Key.Home, "Home" },
|
||||
{ Key.End, "End" },
|
||||
{ Key.CapsLock, "CapsLock" },
|
||||
{ Key.ScrollLock, "ScrollLock" },
|
||||
{ Key.PrintScreen, "PrintScreen" },
|
||||
{ Key.Pause, "Pause" },
|
||||
{ Key.NumLock, "NumLock" },
|
||||
{ Key.Clear, "Clear" },
|
||||
{ Key.Keypad0, "Keypad0" },
|
||||
{ Key.Keypad1, "Keypad1" },
|
||||
{ Key.Keypad2, "Keypad2" },
|
||||
{ Key.Keypad3, "Keypad3" },
|
||||
{ Key.Keypad4, "Keypad4" },
|
||||
{ Key.Keypad5, "Keypad5" },
|
||||
{ Key.Keypad6, "Keypad6" },
|
||||
{ Key.Keypad7, "Keypad7" },
|
||||
{ Key.Keypad8, "Keypad8" },
|
||||
{ Key.Keypad9, "Keypad9" },
|
||||
{ Key.KeypadDivide, "KeypadDivide" },
|
||||
{ Key.KeypadMultiply, "KeypadMultiply" },
|
||||
{ Key.KeypadSubtract, "KeypadSubtract" },
|
||||
{ Key.KeypadAdd, "KeypadAdd" },
|
||||
{ Key.KeypadDecimal, "KeypadDecimal" },
|
||||
{ Key.KeypadEnter, "KeypadEnter" },
|
||||
{ Key.Number0, "0" },
|
||||
{ Key.Number1, "1" },
|
||||
{ Key.Number2, "2" },
|
||||
{ Key.Number3, "3" },
|
||||
{ Key.Number4, "4" },
|
||||
{ Key.Number5, "5" },
|
||||
{ Key.Number6, "6" },
|
||||
{ Key.Number7, "7" },
|
||||
{ Key.Number8, "8" },
|
||||
{ Key.Number9, "9" },
|
||||
{ Key.Tilde, "~" },
|
||||
{ Key.Grave, "`" },
|
||||
{ Key.Minus, "-" },
|
||||
{ Key.Plus, "+" },
|
||||
{ Key.BracketLeft, "[" },
|
||||
{ Key.BracketRight, "]" },
|
||||
{ Key.Semicolon, ";" },
|
||||
{ Key.Quote, "'" },
|
||||
{ Key.Comma, "," },
|
||||
{ Key.Period, "." },
|
||||
{ Key.Slash, "/" },
|
||||
{ Key.BackSlash, "\\" },
|
||||
{ Key.Unbound, "Unbound" },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<GamepadInputId, string> _gamepadInputIdMap = new()
|
||||
{
|
||||
{ GamepadInputId.LeftStick, "LeftStick" },
|
||||
{ GamepadInputId.RightStick, "RightStick" },
|
||||
{ GamepadInputId.LeftShoulder, "LeftShoulder" },
|
||||
{ GamepadInputId.RightShoulder, "RightShoulder" },
|
||||
{ GamepadInputId.LeftTrigger, "LeftTrigger" },
|
||||
{ GamepadInputId.RightTrigger, "RightTrigger" },
|
||||
{ GamepadInputId.DpadUp, "DpadUp" },
|
||||
{ GamepadInputId.DpadDown, "DpadDown" },
|
||||
{ GamepadInputId.DpadLeft, "DpadLeft" },
|
||||
{ GamepadInputId.DpadRight, "DpadRight" },
|
||||
{ GamepadInputId.Minus, "Minus" },
|
||||
{ GamepadInputId.Plus, "Plus" },
|
||||
{ GamepadInputId.Guide, "Guide" },
|
||||
{ GamepadInputId.Misc1, "Misc1" },
|
||||
{ GamepadInputId.Paddle1, "Paddle1" },
|
||||
{ GamepadInputId.Paddle2, "Paddle2" },
|
||||
{ GamepadInputId.Paddle3, "Paddle3" },
|
||||
{ GamepadInputId.Paddle4, "Paddle4" },
|
||||
{ GamepadInputId.Touchpad, "Touchpad" },
|
||||
{ GamepadInputId.SingleLeftTrigger0, "SingleLeftTrigger0" },
|
||||
{ GamepadInputId.SingleRightTrigger0, "SingleRightTrigger0" },
|
||||
{ GamepadInputId.SingleLeftTrigger1, "SingleLeftTrigger1" },
|
||||
{ GamepadInputId.SingleRightTrigger1, "SingleRightTrigger1" },
|
||||
{ GamepadInputId.Unbound, "Unbound" },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<StickInputId, string> _stickInputIdMap = new()
|
||||
{
|
||||
{ StickInputId.Left, "StickLeft" },
|
||||
{ StickInputId.Right, "StickRight" },
|
||||
{ StickInputId.Unbound, "Unbound" },
|
||||
};
|
||||
|
||||
public static string ToString(Button button)
|
||||
{
|
||||
string keyString = "";
|
||||
|
||||
switch (button.Type)
|
||||
{
|
||||
case ButtonType.Key:
|
||||
var key = button.AsHidType<Key>();
|
||||
|
||||
if (!_keysMap.TryGetValue(button.AsHidType<Key>(), out keyString))
|
||||
{
|
||||
keyString = key.ToString();
|
||||
}
|
||||
|
||||
break;
|
||||
case ButtonType.GamepadButtonInputId:
|
||||
var gamepadButton = button.AsHidType<GamepadInputId>();
|
||||
|
||||
if (!_gamepadInputIdMap.TryGetValue(button.AsHidType<GamepadInputId>(), out keyString))
|
||||
{
|
||||
keyString = gamepadButton.ToString();
|
||||
}
|
||||
|
||||
break;
|
||||
case ButtonType.StickId:
|
||||
var stickInput = button.AsHidType<StickInputId>();
|
||||
|
||||
if (!_stickInputIdMap.TryGetValue(button.AsHidType<StickInputId>(), out keyString))
|
||||
{
|
||||
keyString = stickInput.ToString();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return keyString;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
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 readonly 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(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);
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
using Gtk;
|
||||
using Ryujinx.UI.Common.Helper;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.UI.Helper
|
||||
{
|
||||
static class SortHelper
|
||||
{
|
||||
public static int TimePlayedSort(ITreeModel model, TreeIter a, TreeIter b)
|
||||
{
|
||||
TimeSpan aTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(a, 5).ToString());
|
||||
TimeSpan bTimeSpan = ValueFormatUtils.ParseTimeSpan(model.GetValue(b, 5).ToString());
|
||||
|
||||
return TimeSpan.Compare(aTimeSpan, bTimeSpan);
|
||||
}
|
||||
|
||||
public static int LastPlayedSort(ITreeModel model, TreeIter a, TreeIter b)
|
||||
{
|
||||
DateTime aDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(a, 6).ToString());
|
||||
DateTime bDateTime = ValueFormatUtils.ParseDateTime(model.GetValue(b, 6).ToString());
|
||||
|
||||
return DateTime.Compare(aDateTime, bDateTime);
|
||||
}
|
||||
|
||||
public static int FileSizeSort(ITreeModel model, TreeIter a, TreeIter b)
|
||||
{
|
||||
long aSize = ValueFormatUtils.ParseFileSize(model.GetValue(a, 8).ToString());
|
||||
long bSize = ValueFormatUtils.ParseFileSize(model.GetValue(b, 8).ToString());
|
||||
|
||||
return aSize.CompareTo(bSize);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
using Gtk;
|
||||
using Ryujinx.Common;
|
||||
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.LoadFromPath(ConfigurationState.Instance.UI.CustomThemePath);
|
||||
|
||||
StyleContext.AddProviderForScreen(Gdk.Screen.Default, cssProvider, 800);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"The \"custom_theme_path\" section in \"{ReleaseInformation.ConfigName}\" 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,142 +0,0 @@
|
|||
using OpenTK.Graphics.OpenGL;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Input.HLE;
|
||||
using SPB.Graphics;
|
||||
using SPB.Graphics.Exceptions;
|
||||
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 OpenGLRenderer : RendererWidgetBase
|
||||
{
|
||||
private readonly GraphicsDebugLevel _glLogLevel;
|
||||
|
||||
private bool _initializedOpenGL;
|
||||
|
||||
private OpenGLContextBase _openGLContext;
|
||||
private SwappableNativeWindowBase _nativeWindow;
|
||||
|
||||
public OpenGLRenderer(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.
|
||||
((Graphics.OpenGL.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 (ContextException e)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.UI, $"Failed to bind OpenGL context: {e}");
|
||||
}
|
||||
|
||||
Device?.DisposeGpu();
|
||||
NpadManager.Dispose();
|
||||
|
||||
// Unbind context and destroy everything.
|
||||
try
|
||||
{
|
||||
_openGLContext?.MakeCurrent(null);
|
||||
}
|
||||
catch (ContextException e)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.UI, $"Failed to unbind OpenGL context: {e}");
|
||||
}
|
||||
|
||||
_openGLContext?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
using SPB.Graphics;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.UI
|
||||
{
|
||||
public class OpenToolkitBindingsContext : OpenTK.IBindingsContext
|
||||
{
|
||||
private readonly IBindingsContext _bindingContext;
|
||||
|
||||
public OpenToolkitBindingsContext(IBindingsContext bindingsContext)
|
||||
{
|
||||
_bindingContext = bindingsContext;
|
||||
}
|
||||
|
||||
public IntPtr GetProcAddress(string procName)
|
||||
{
|
||||
return _bindingContext.GetProcAddress(procName);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,813 +0,0 @@
|
|||
using Gdk;
|
||||
using Gtk;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
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.Common.Helper;
|
||||
using Ryujinx.UI.Widgets;
|
||||
using SkiaSharp;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Key = Ryujinx.Input.Key;
|
||||
using ScalingFilter = Ryujinx.Graphics.GAL.ScalingFilter;
|
||||
using Switch = Ryujinx.HLE.Switch;
|
||||
|
||||
namespace Ryujinx.UI
|
||||
{
|
||||
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 ManualResetEvent _gpuDoneEvent;
|
||||
|
||||
private readonly CancellationTokenSource _gpuCancellationTokenSource;
|
||||
|
||||
// Hide Cursor
|
||||
const int CursorHideIdleTime = 5; // seconds
|
||||
private static readonly Cursor _invisibleCursor = new(Display.Default, CursorType.BlankCursor);
|
||||
private long _lastCursorMoveTime;
|
||||
private HideCursorMode _hideCursorMode;
|
||||
private readonly InputManager _inputManager;
|
||||
private readonly IKeyboard _keyboardInterface;
|
||||
private readonly GraphicsDebugLevel _glLogLevel;
|
||||
private string _gpuBackendName;
|
||||
private string _gpuDriverName;
|
||||
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);
|
||||
_gpuDoneEvent = new ManualResetEvent(false);
|
||||
|
||||
_gpuCancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
_hideCursorMode = ConfigurationState.Instance.HideCursor;
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
|
||||
ConfigurationState.Instance.HideCursor.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 GetGpuDriverName()
|
||||
{
|
||||
return Renderer.GetHardwareInfo().GpuDriver;
|
||||
}
|
||||
|
||||
private void HideCursorStateChanged(object sender, ReactiveEventArgs<HideCursorMode> state)
|
||||
{
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
_hideCursorMode = state.NewValue;
|
||||
|
||||
switch (_hideCursorMode)
|
||||
{
|
||||
case HideCursorMode.Never:
|
||||
Window.Cursor = null;
|
||||
break;
|
||||
case HideCursorMode.OnIdle:
|
||||
_lastCursorMoveTime = Stopwatch.GetTimestamp();
|
||||
break;
|
||||
case HideCursorMode.Always:
|
||||
Window.Cursor = _invisibleCursor;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(state));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void Renderer_Destroyed(object sender, EventArgs e)
|
||||
{
|
||||
ConfigurationState.Instance.HideCursor.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 (_hideCursorMode == HideCursorMode.OnIdle)
|
||||
{
|
||||
_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 (_isMouseInClient)
|
||||
{
|
||||
if (ConfigurationState.Instance.Hid.EnableMouse.Value)
|
||||
{
|
||||
Window.Cursor = _invisibleCursor;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (_hideCursorMode)
|
||||
{
|
||||
case HideCursorMode.OnIdle:
|
||||
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
|
||||
Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null;
|
||||
break;
|
||||
case HideCursorMode.Always:
|
||||
Window.Cursor = _invisibleCursor;
|
||||
break;
|
||||
case HideCursorMode.Never:
|
||||
Window.Cursor = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
string applicationName = Device.Processes.ActiveApplication.Name;
|
||||
string sanitizedApplicationName = FileSystemUtils.SanitizeFileName(applicationName);
|
||||
DateTime currentTime = DateTime.Now;
|
||||
|
||||
string filename = $"{sanitizedApplicationName}_{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 or AppDataManager.LaunchMode.Custom => 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;
|
||||
}
|
||||
|
||||
var colorType = e.IsBgra ? SKColorType.Bgra8888 : SKColorType.Rgba8888;
|
||||
using var image = new SKBitmap(new SKImageInfo(e.Width, e.Height, colorType, SKAlphaType.Premul));
|
||||
|
||||
Marshal.Copy(e.Data, 0, image.GetPixels(), e.Data.Length);
|
||||
using var surface = SKSurface.Create(image.Info);
|
||||
var canvas = surface.Canvas;
|
||||
|
||||
if (e.FlipX || e.FlipY)
|
||||
{
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
float scaleX = e.FlipX ? -1 : 1;
|
||||
float scaleY = e.FlipY ? -1 : 1;
|
||||
|
||||
var matrix = SKMatrix.CreateScale(scaleX, scaleY, image.Width / 2f, image.Height / 2f);
|
||||
|
||||
canvas.SetMatrix(matrix);
|
||||
}
|
||||
canvas.DrawBitmap(image, new SKPoint());
|
||||
|
||||
surface.Flush();
|
||||
using var snapshot = surface.Snapshot();
|
||||
using var encoded = snapshot.Encode(SKEncodedImageFormat.Png, 80);
|
||||
using var file = File.OpenWrite(path);
|
||||
encoded.SaveTo(file);
|
||||
|
||||
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();
|
||||
_gpuDriverName = GetGpuDriverName();
|
||||
|
||||
Device.Gpu.Renderer.RunLoop(() =>
|
||||
{
|
||||
Device.Gpu.SetGpuThread();
|
||||
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
|
||||
|
||||
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: {_gpuDriverName}"));
|
||||
|
||||
_ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure all commands in the run loop are fully executed before leaving the loop.
|
||||
if (Device.Gpu.Renderer is ThreadedRenderer threaded)
|
||||
{
|
||||
threaded.FlushThreadedCommands();
|
||||
}
|
||||
|
||||
_gpuDoneEvent.Set();
|
||||
});
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
_chrono.Restart();
|
||||
|
||||
_isActive = true;
|
||||
|
||||
Gtk.Window parent = Toplevel as Gtk.Window;
|
||||
|
||||
Application.Invoke(delegate
|
||||
{
|
||||
parent.Present();
|
||||
|
||||
var activeProcess = Device.Processes.ActiveApplication;
|
||||
|
||||
parent.Title = TitleHelper.ActiveApplicationTitle(activeProcess, Program.Version);
|
||||
});
|
||||
|
||||
Thread renderLoopThread = new(Render)
|
||||
{
|
||||
Name = "GUI.RenderLoop",
|
||||
};
|
||||
renderLoopThread.Start();
|
||||
|
||||
Thread nvidiaStutterWorkaround = null;
|
||||
if (Renderer is Graphics.OpenGL.OpenGLRenderer)
|
||||
{
|
||||
nvidiaStutterWorkaround = new Thread(NvidiaStutterWorkaround)
|
||||
{
|
||||
Name = "GUI.NvidiaStutterWorkaround",
|
||||
};
|
||||
nvidiaStutterWorkaround.Start();
|
||||
}
|
||||
|
||||
MainLoop();
|
||||
|
||||
// NOTE: The render loop is allowed to stay alive until the renderer itself is disposed, as it may handle resource dispose.
|
||||
// We only need to wait for all commands submitted during the main gpu loop to be processed.
|
||||
_gpuDoneEvent.WaitOne();
|
||||
_gpuDoneEvent.Dispose();
|
||||
nvidiaStutterWorkaround?.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 NvidiaStutterWorkaround()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
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 readonly OpenGLContextBase _context;
|
||||
private readonly 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 bool HasContext() => _context.IsCurrent;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
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 VulkanRenderer : RendererWidgetBase
|
||||
{
|
||||
public NativeWindowBase NativeWindow { get; private set; }
|
||||
private UpdateBoundsCallbackDelegate _updateBoundsCallback;
|
||||
|
||||
public VulkanRenderer(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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,233 +0,0 @@
|
|||
using Gtk;
|
||||
using System;
|
||||
|
||||
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 MenuItem _createShortcutMenuItem;
|
||||
|
||||
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;
|
||||
|
||||
//
|
||||
// _createShortcutMenuItem
|
||||
//
|
||||
_createShortcutMenuItem = new MenuItem("Create Application Shortcut")
|
||||
{
|
||||
TooltipText = OperatingSystem.IsMacOS() ? "Create a shortcut in macOS's Applications folder that launches the selected Application" : "Create a Desktop Shortcut that launches the selected Application."
|
||||
};
|
||||
_createShortcutMenuItem.Activated += CreateShortcut_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(_createShortcutMenuItem);
|
||||
Add(new SeparatorMenuItem());
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,634 +0,0 @@
|
|||
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;
|
||||
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.HLE.Loaders.Processes.Extensions;
|
||||
using Ryujinx.HLE.Utilities;
|
||||
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.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 ApplicationData _applicationData;
|
||||
|
||||
private MessageDialog _dialog;
|
||||
private bool _cancel;
|
||||
|
||||
public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, ApplicationData applicationData)
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_accountManager = accountManager;
|
||||
_horizonClient = horizonClient;
|
||||
_applicationData = applicationData;
|
||||
|
||||
if (!_applicationData.ControlHolder.ByteSpan.IsZeros())
|
||||
{
|
||||
_openSaveUserDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.UserAccountSaveDataSize > 0;
|
||||
_openSaveDeviceDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.DeviceSaveDataSize > 0;
|
||||
_openSaveBcatDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
_openSaveUserDirMenuItem.Sensitive = false;
|
||||
_openSaveDeviceDirMenuItem.Sensitive = false;
|
||||
_openSaveBcatDirMenuItem.Sensitive = false;
|
||||
}
|
||||
|
||||
string fileExt = System.IO.Path.GetExtension(_applicationData.Path).ToLower();
|
||||
bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
|
||||
|
||||
_extractRomFsMenuItem.Sensitive = hasNca;
|
||||
_extractExeFsMenuItem.Sensitive = hasNca;
|
||||
_extractLogoMenuItem.Sensitive = hasNca;
|
||||
|
||||
_createShortcutMenuItem.Sensitive = !ReleaseInformation.IsFlatHubBuild;
|
||||
|
||||
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((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(_applicationData.Name, _applicationData.Id, _applicationData.ControlHolder, 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("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(() =>
|
||||
{
|
||||
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.Gtk3.UI.Common.Resources.Logo_Ryujinx.png"),
|
||||
SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_applicationData.Path)}...",
|
||||
WindowPosition = WindowPosition.Center,
|
||||
};
|
||||
|
||||
int dialogResponse = _dialog.Run();
|
||||
if (dialogResponse == (int)ResponseType.Cancel || dialogResponse == (int)ResponseType.DeleteEvent)
|
||||
{
|
||||
_cancel = true;
|
||||
_dialog.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
using FileStream file = new(_applicationData.Path, FileMode.Open, FileAccess.Read);
|
||||
|
||||
Nca mainNca = null;
|
||||
Nca patchNca = null;
|
||||
|
||||
if ((System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".nsp") ||
|
||||
(System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".pfs0") ||
|
||||
(System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".xci"))
|
||||
{
|
||||
IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(_applicationData.Path, _virtualFileSystem);
|
||||
|
||||
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(_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(_applicationData.Path).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;
|
||||
}
|
||||
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None;
|
||||
|
||||
(Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _);
|
||||
|
||||
if (updatePatchNca != null)
|
||||
{
|
||||
patchNca = updatePatchNca;
|
||||
}
|
||||
|
||||
int index = Nca.GetSectionIndexFromType(ncaSectionType, mainNca.Header.ContentType);
|
||||
|
||||
bool sectionExistsInPatch = false;
|
||||
|
||||
if (patchNca != null)
|
||||
{
|
||||
sectionExistsInPatch = patchNca.CanOpenSection(index);
|
||||
}
|
||||
|
||||
IFileSystem ncaFileSystem = sectionExistsInPatch ? 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(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());
|
||||
})
|
||||
{
|
||||
Name = "GUI.NcaSectionExtractorThread",
|
||||
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 static 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(_applicationData.Id, saveType: default, userId, saveDataId: default, index: default);
|
||||
|
||||
OpenSaveDir(in saveDataFilter);
|
||||
}
|
||||
|
||||
private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
var saveDataFilter = SaveDataFilter.Make(_applicationData.Id, SaveDataType.Device, userId: default, saveDataId: default, index: default);
|
||||
|
||||
OpenSaveDir(in saveDataFilter);
|
||||
}
|
||||
|
||||
private void OpenSaveBcatDir_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
var saveDataFilter = SaveDataFilter.Make(_applicationData.Id, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
|
||||
|
||||
OpenSaveDir(in saveDataFilter);
|
||||
}
|
||||
|
||||
private void ManageTitleUpdates_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
new TitleUpdateWindow(_parent, _virtualFileSystem, _applicationData).Show();
|
||||
}
|
||||
|
||||
private void ManageDlc_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
new DlcWindow(_virtualFileSystem, _applicationData.IdBaseString, _applicationData).Show();
|
||||
}
|
||||
|
||||
private void ManageCheats_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
new CheatWindow(_virtualFileSystem, _applicationData.Id, _applicationData.Name, _applicationData.Path).Show();
|
||||
}
|
||||
|
||||
private void OpenTitleModDir_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
string modsBasePath = ModLoader.GetModsBasePath();
|
||||
string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, _applicationData.IdString);
|
||||
|
||||
OpenHelper.OpenFolder(titleModsPath);
|
||||
}
|
||||
|
||||
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
string sdModsBasePath = ModLoader.GetSdModsBasePath();
|
||||
string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, _applicationData.IdString);
|
||||
|
||||
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, _applicationData.IdString, "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, _applicationData.IdString, "cache", "shader");
|
||||
|
||||
if (!Directory.Exists(shaderCacheDir))
|
||||
{
|
||||
Directory.CreateDirectory(shaderCacheDir);
|
||||
}
|
||||
|
||||
OpenHelper.OpenFolder(shaderCacheDir);
|
||||
}
|
||||
|
||||
private void PurgePtcCache_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "cpu", "0"));
|
||||
DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "cpu", "1"));
|
||||
|
||||
MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n<b>{_applicationData.Name}</b>\n\nAre you sure you want to proceed?");
|
||||
|
||||
List<FileInfo> cacheFiles = new();
|
||||
|
||||
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(System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "shader"));
|
||||
|
||||
using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n<b>{_applicationData.Name}</b>\n\nAre you sure you want to proceed?");
|
||||
|
||||
List<DirectoryInfo> oldCacheDirectories = new();
|
||||
List<FileInfo> newCacheFiles = new();
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateShortcut_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None;
|
||||
byte[] appIcon = new ApplicationLibrary(_virtualFileSystem, checkLevel).GetApplicationIcon(_applicationData.Path, ConfigurationState.Instance.System.Language, _applicationData.Id);
|
||||
ShortcutHelper.CreateAppShortcut(_applicationData.Path, _applicationData.Name, _applicationData.IdString, appIcon);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,114 +0,0 @@
|
|||
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(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(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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
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()
|
||||
{
|
||||
Text = mainText,
|
||||
};
|
||||
|
||||
InputEntry = new Entry
|
||||
{
|
||||
MaxLength = (int)inputMax,
|
||||
};
|
||||
|
||||
Label inputMaxTextLabel = new()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
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 // Field is never assigned to, Add readonly modifier
|
||||
[GUI] Entry _profileEntry;
|
||||
[GUI] Label _errorMessage;
|
||||
#pragma warning restore CS0649, IDE0044
|
||||
|
||||
public ProfileDialog() : this(new Builder("Ryujinx.Gtk3.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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,124 +0,0 @@
|
|||
<?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>
|
|
@ -1,27 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
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 static string GetErrorCode(UserError error)
|
||||
{
|
||||
return $"RYU-{(uint)error:X4}";
|
||||
}
|
||||
|
||||
private static 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 static 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();
|
||||
}
|
||||
}
|
||||
}
|
511
src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs
generated
511
src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs
generated
|
@ -1,511 +0,0 @@
|
|||
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 EventBox _changelogEventBox;
|
||||
private Label _changelogLinkLabel;
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
|
||||
//
|
||||
// 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,
|
||||
MarginStart = 30,
|
||||
MarginEnd = 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,
|
||||
MarginStart = 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,
|
||||
};
|
||||
|
||||
//
|
||||
// _changelogEventBox
|
||||
//
|
||||
_changelogEventBox = new EventBox();
|
||||
_changelogEventBox.ButtonPressEvent += ChangelogButton_Pressed;
|
||||
|
||||
//
|
||||
// _changelogLinkLabel
|
||||
//
|
||||
_changelogLinkLabel = new Label("View Changelog on GitHub")
|
||||
{
|
||||
TooltipText = "Click to open the changelog for this version in your default browser.",
|
||||
Justify = Justification.Center,
|
||||
Attributes = new AttrList(),
|
||||
};
|
||||
_changelogLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
|
||||
|
||||
//
|
||||
// _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,
|
||||
MarginEnd = 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.",
|
||||
MarginEnd = 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,
|
||||
MarginEnd = 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));
|
||||
|
||||
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);
|
||||
|
||||
_changelogEventBox.Add(_changelogLinkLabel);
|
||||
|
||||
_leftBox.Add(_logoBox);
|
||||
_leftBox.Add(_versionLabel);
|
||||
_leftBox.Add(_changelogEventBox);
|
||||
_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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,85 +0,0 @@
|
|||
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();
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
private void ChangelogButton_Pressed(object sender, ButtonPressEventArgs args)
|
||||
{
|
||||
OpenHelper.OpenUrl("https://github.com/Ryujinx/Ryujinx/wiki/Changelog#ryujinx-changelog");
|
||||
}
|
||||
}
|
||||
}
|
190
src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs
generated
190
src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs
generated
|
@ -1,190 +0,0 @@
|
|||
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()
|
||||
{
|
||||
//
|
||||
// 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,
|
||||
MarginStart = 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,
|
||||
MarginStart = 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,
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,438 +0,0 @@
|
|||
using Gdk;
|
||||
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.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Window = Gtk.Window;
|
||||
|
||||
namespace Ryujinx.UI.Windows
|
||||
{
|
||||
public partial class AmiiboWindow : Window
|
||||
{
|
||||
private const string DefaultJson = "{ \"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 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 Pixbuf(_amiiboLogoBytes);
|
||||
|
||||
_scanButton.Sensitive = false;
|
||||
_randomUuidCheckBox.Sensitive = false;
|
||||
|
||||
_ = LoadContentAsync();
|
||||
}
|
||||
|
||||
private static bool TryGetAmiiboJson(string json, out AmiiboJson amiiboJson)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json))
|
||||
{
|
||||
amiiboJson = JsonHelper.Deserialize(DefaultJson, _serializerContext.AmiiboJson);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
amiiboJson = JsonHelper.Deserialize(json, _serializerContext.AmiiboJson);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (JsonException exception)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Unable to deserialize amiibo data: {exception}");
|
||||
amiiboJson = JsonHelper.Deserialize(DefaultJson, _serializerContext.AmiiboJson);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<AmiiboJson> GetMostRecentAmiiboListOrDefaultJson()
|
||||
{
|
||||
bool localIsValid = false;
|
||||
bool remoteIsValid = false;
|
||||
AmiiboJson amiiboJson = new();
|
||||
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_amiiboJsonPath))
|
||||
{
|
||||
localIsValid = TryGetAmiiboJson(await File.ReadAllTextAsync(_amiiboJsonPath), out amiiboJson);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Unable to read data from '{_amiiboJsonPath}': {exception}");
|
||||
}
|
||||
|
||||
if (!localIsValid || await NeedsUpdate(amiiboJson.LastUpdated))
|
||||
{
|
||||
remoteIsValid = TryGetAmiiboJson(await DownloadAmiiboJson(), out amiiboJson);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (!(localIsValid || remoteIsValid))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Couldn't get valid amiibo data: {exception}");
|
||||
|
||||
// Neither local or remote files are valid JSON, close window.
|
||||
ShowInfoDialog();
|
||||
Close();
|
||||
}
|
||||
else if (!remoteIsValid)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Couldn't update amiibo data: {exception}");
|
||||
|
||||
// Only the local file is valid, the local one should be used
|
||||
// but the user should be warned.
|
||||
ShowInfoDialog();
|
||||
}
|
||||
}
|
||||
|
||||
return amiiboJson;
|
||||
}
|
||||
|
||||
private async Task LoadContentAsync()
|
||||
{
|
||||
AmiiboJson amiiboJson = await GetMostRecentAmiiboListOrDefaultJson();
|
||||
|
||||
_amiiboList = amiiboJson.Amiibo.OrderBy(amiibo => amiibo.AmiiboSeries).ToList();
|
||||
|
||||
if (LastScannedAmiiboShowAll)
|
||||
{
|
||||
_showAllCheckBox.Click();
|
||||
}
|
||||
|
||||
ParseAmiiboData();
|
||||
|
||||
_showAllCheckBox.Clicked += ShowAllCheckBox_Clicked;
|
||||
}
|
||||
|
||||
private void ParseAmiiboData()
|
||||
{
|
||||
List<string> comboxItemList = new();
|
||||
|
||||
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.Find(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;
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Unable to check for amiibo data updates: {exception}");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<string> DownloadAmiiboJson()
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response = await _httpClient.GetAsync("https://amiibo.ryujinx.org/");
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string amiiboJsonString = await response.Content.ReadAsStringAsync();
|
||||
|
||||
try
|
||||
{
|
||||
using FileStream dlcJsonStream = File.Create(_amiiboJsonPath, 4096, FileOptions.WriteThrough);
|
||||
dlcJsonStream.Write(Encoding.UTF8.GetBytes(amiiboJsonString));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Application, $"Couldn't write amiibo data to file '{_amiiboJsonPath}: {exception}'");
|
||||
}
|
||||
|
||||
return amiiboJsonString;
|
||||
}
|
||||
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to download amiibo data. Response status code: {response.StatusCode}");
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to request amiibo data: {exception}");
|
||||
}
|
||||
|
||||
GtkDialog.CreateInfoDialog("Amiibo API", "An error occured while fetching information from the API.");
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task UpdateAmiiboPreview(string imageUrl)
|
||||
{
|
||||
HttpResponseMessage response = await _httpClient.GetAsync(imageUrl);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
Pixbuf amiiboPreview = new(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, InterpType.Bilinear);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to get amiibo preview. Response status code: {response.StatusCode}");
|
||||
}
|
||||
}
|
||||
|
||||
private static 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();
|
||||
|
||||
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 Pixbuf(_amiiboLogoBytes);
|
||||
|
||||
string imageUrl = _amiiboList.Find(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 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,298 +0,0 @@
|
|||
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 SkiaSharp;
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.UI.Windows
|
||||
{
|
||||
public class AvatarWindow : Window
|
||||
{
|
||||
public byte[] SelectedProfileImage;
|
||||
public bool NewUser;
|
||||
|
||||
private static readonly Dictionary<string, byte[]> _avatarDict = new();
|
||||
|
||||
private readonly ListStore _listStore;
|
||||
private readonly IconView _iconView;
|
||||
private readonly 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()
|
||||
{
|
||||
ShadowType = ShadowType.EtchedIn,
|
||||
};
|
||||
scrolledWindow.SetPolicy(PolicyType.Automatic, PolicyType.Automatic);
|
||||
|
||||
Box hbox = new(Orientation.Horizontal, 0);
|
||||
|
||||
Button chooseButton = new()
|
||||
{
|
||||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
ItemWidth = 64,
|
||||
ItemPadding = 10,
|
||||
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(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;
|
||||
|
||||
using var avatarImage = new SKBitmap(new SKImageInfo(256, 256, SKColorType.Rgba8888));
|
||||
var data = DecompressYaz0(stream);
|
||||
Marshal.Copy(data, 0, avatarImage.GetPixels(), data.Length);
|
||||
|
||||
avatarImage.Encode(streamPng, SKEncodedImageFormat.Png, 80);
|
||||
|
||||
_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[] { 0 }));
|
||||
}
|
||||
|
||||
private byte[] ProcessImage(byte[] data)
|
||||
{
|
||||
using MemoryStream streamJpg = MemoryStreamManager.Shared.GetStream();
|
||||
|
||||
using var avatarImage = SKBitmap.Decode(data);
|
||||
using var surface = SKSurface.Create(avatarImage.Info);
|
||||
|
||||
var background = new SKColor(
|
||||
(byte)(_backgroundColor.Red * 255),
|
||||
(byte)(_backgroundColor.Green * 255),
|
||||
(byte)(_backgroundColor.Blue * 255),
|
||||
(byte)(_backgroundColor.Alpha * 255)
|
||||
);
|
||||
var canvas = surface.Canvas;
|
||||
canvas.Clear(background);
|
||||
canvas.DrawBitmap(avatarImage, new SKPoint());
|
||||
|
||||
surface.Flush();
|
||||
using var snapshot = surface.Snapshot();
|
||||
using var encoded = snapshot.Encode(SKEncodedImageFormat.Jpeg, 80);
|
||||
encoded.SaveTo(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("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(stream);
|
||||
|
||||
reader.ReadInt32(); // Magic
|
||||
|
||||
uint decodedLength = BinaryPrimitives.ReverseEndianness(reader.ReadUInt32());
|
||||
|
||||
reader.ReadInt64(); // Padding
|
||||
|
||||
byte[] input = new byte[stream.Length - stream.Position];
|
||||
stream.ReadExactly(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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
using Gtk;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
using Ryujinx.UI.App.Common;
|
||||
using Ryujinx.UI.Common.Configuration;
|
||||
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 // Field is never assigned to, Add readonly modifier
|
||||
[GUI] Label _baseTitleInfoLabel;
|
||||
[GUI] TextView _buildIdTextView;
|
||||
[GUI] TreeView _cheatTreeView;
|
||||
[GUI] Button _saveButton;
|
||||
#pragma warning restore CS0649, IDE0044
|
||||
|
||||
public CheatWindow(VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : this(new Builder("Ryujinx.Gtk3.UI.Windows.CheatWindow.glade"), virtualFileSystem, titleId, titleName, titlePath) { }
|
||||
|
||||
private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow"))
|
||||
{
|
||||
builder.Autoconnect(this);
|
||||
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None;
|
||||
|
||||
_baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
|
||||
_buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath)}";
|
||||
|
||||
string modsBasePath = ModLoader.GetModsBasePath();
|
||||
string titleModsPath = ModLoader.GetApplicationDir(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();
|
||||
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 = Array.Empty<string>();
|
||||
|
||||
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[1..^7];
|
||||
((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();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
<?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="GtkTextView" id="_buildIdTextView">
|
||||
<property name="visible">True</property>
|
||||
<property name="margin_top">10</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin_bottom">10</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="cursor_visible">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</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">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">
|
||||
<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>
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,288 +0,0 @@
|
|||
using Gtk;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
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.HLE.Loaders.Processes.Extensions;
|
||||
using Ryujinx.HLE.Utilities;
|
||||
using Ryujinx.UI.App.Common;
|
||||
using Ryujinx.UI.Widgets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using GUI = Gtk.Builder.ObjectAttribute;
|
||||
|
||||
namespace Ryujinx.UI.Windows
|
||||
{
|
||||
public class DlcWindow : Window
|
||||
{
|
||||
private readonly VirtualFileSystem _virtualFileSystem;
|
||||
private readonly string _applicationIdBase;
|
||||
private readonly string _dlcJsonPath;
|
||||
private readonly List<DownloadableContentContainer> _dlcContainerList;
|
||||
|
||||
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
|
||||
|
||||
#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier
|
||||
[GUI] Label _baseTitleInfoLabel;
|
||||
[GUI] TreeView _dlcTreeView;
|
||||
[GUI] TreeSelection _dlcTreeSelection;
|
||||
#pragma warning restore CS0649, IDE0044
|
||||
|
||||
public DlcWindow(VirtualFileSystem virtualFileSystem, string applicationIdBase, ApplicationData applicationData) : this(new Builder("Ryujinx.Gtk3.UI.Windows.DlcWindow.glade"), virtualFileSystem, applicationIdBase, applicationData) { }
|
||||
|
||||
private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string applicationIdBase, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_dlcWindow"))
|
||||
{
|
||||
builder.Autoconnect(this);
|
||||
|
||||
_applicationIdBase = applicationIdBase;
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationIdBase, "dlc.json");
|
||||
_baseTitleInfoLabel.Text = $"DLC Available for {applicationData.Name} [{applicationIdBase.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();
|
||||
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("ApplicationId", 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 IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(dlcContainer.ContainerPath, _virtualFileSystem, false);
|
||||
|
||||
if (partitionFileSystem == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList)
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
partitionFileSystem.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}");
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: Try to load downloadable contents from PFS last to preserve enabled state.
|
||||
AddDlc(applicationData.Path, true);
|
||||
}
|
||||
|
||||
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 AddDlc(string path, bool ignoreNotFound = false)
|
||||
{
|
||||
if (!File.Exists(path) || _dlcContainerList.Any(x => x.ContainerPath == path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
|
||||
|
||||
bool containsDlc = false;
|
||||
|
||||
TreeIter? parentIter = null;
|
||||
|
||||
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
|
||||
{
|
||||
using var ncaFile = new UniqueRef<IFile>();
|
||||
|
||||
partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
|
||||
|
||||
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
|
||||
|
||||
if (nca == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nca.Header.ContentType == NcaContentType.PublicData)
|
||||
{
|
||||
if (nca.GetProgramIdBase() != ulong.Parse(_applicationIdBase, NumberStyles.HexNumber))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", path);
|
||||
|
||||
((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
|
||||
containsDlc = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!containsDlc && !ignoreNotFound)
|
||||
{
|
||||
GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
|
||||
}
|
||||
}
|
||||
|
||||
private void AddButton_Clicked(object sender, EventArgs args)
|
||||
{
|
||||
FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel")
|
||||
{
|
||||
SelectMultiple = true,
|
||||
};
|
||||
|
||||
FileFilter filter = new()
|
||||
{
|
||||
Name = "Switch Game DLCs",
|
||||
};
|
||||
filter.AddPattern("*.nsp");
|
||||
|
||||
fileChooser.AddFilter(filter);
|
||||
|
||||
if (fileChooser.Run() == (int)ResponseType.Accept)
|
||||
{
|
||||
foreach (string containerPath in fileChooser.Filenames)
|
||||
{
|
||||
AddDlc(containerPath);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
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()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,202 +0,0 @@
|
|||
<?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 a DLC 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 DLC</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 all DLCs</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>
|
|
@ -1,847 +0,0 @@
|
|||
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.Configuration.Multiplayer;
|
||||
using Ryujinx.Common.GraphicsDriver;
|
||||
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.Text.RegularExpressions;
|
||||
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 // Field is never assigned to, Add readonly modifier
|
||||
[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] RadioButton _hideCursorNever;
|
||||
[GUI] RadioButton _hideCursorOnIdle;
|
||||
[GUI] RadioButton _hideCursorAlways;
|
||||
[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] ComboBoxText _multiModeSelect;
|
||||
[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.Gtk3.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();
|
||||
}
|
||||
|
||||
switch (ConfigurationState.Instance.HideCursor.Value)
|
||||
{
|
||||
case HideCursorMode.Never:
|
||||
_hideCursorNever.Click();
|
||||
break;
|
||||
case HideCursorMode.OnIdle:
|
||||
_hideCursorOnIdle.Click();
|
||||
break;
|
||||
case HideCursorMode.Always:
|
||||
_hideCursorAlways.Click();
|
||||
break;
|
||||
}
|
||||
|
||||
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(typeof(string), typeof(string), typeof(string));
|
||||
_systemTimeZoneCompletion.Model = tzList;
|
||||
|
||||
CellRendererText offsetCol = new();
|
||||
CellRendererText abbrevCol = new();
|
||||
|
||||
_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);
|
||||
_multiModeSelect.SetActiveId(ConfigurationState.Instance.Multiplayer.Mode.Value.ToString());
|
||||
|
||||
_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 InvalidOperationException($"{nameof(ConfigurationState.Instance.System.AudioBackend)} contains an invalid value: {ConfigurationState.Instance.System.AudioBackend.Value}");
|
||||
}
|
||||
|
||||
_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 InvalidOperationException($"{nameof(_audioBackendStore)} contains an invalid value for iteration {iter}: {_audioBackendStore.GetValue(iter, 1)}"),
|
||||
};
|
||||
});
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
var store = (_graphicsBackend.Model as ListStore);
|
||||
store.GetIter(out TreeIter openglIter, new TreePath(new[] { 1 }));
|
||||
store.Remove(ref openglIter);
|
||||
|
||||
_graphicsBackend.Model = store;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePreferredGpuComboBox()
|
||||
{
|
||||
_preferredGpu.RemoveAll();
|
||||
|
||||
if (Enum.Parse<GraphicsBackend>(_graphicsBackend.ActiveId) == GraphicsBackend.Vulkan)
|
||||
{
|
||||
var devices = Graphics.Vulkan.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();
|
||||
|
||||
_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;
|
||||
}
|
||||
|
||||
HideCursorMode hideCursor = HideCursorMode.Never;
|
||||
|
||||
if (_hideCursorOnIdle.Active)
|
||||
{
|
||||
hideCursor = HideCursorMode.OnIdle;
|
||||
}
|
||||
|
||||
if (_hideCursorAlways.Active)
|
||||
{
|
||||
hideCursor = HideCursorMode.Always;
|
||||
}
|
||||
|
||||
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.HideCursor.Value = hideCursor;
|
||||
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;
|
||||
|
||||
ConfigurationState.Instance.Multiplayer.Mode.Value = Enum.Parse<MultiplayerMode>(_multiModeSelect.ActiveId);
|
||||
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId;
|
||||
|
||||
if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter))
|
||||
{
|
||||
ConfigurationState.Instance.System.AudioBackend.Value = (AudioBackend)_audioBackendStore.GetValue(activeIter, 1);
|
||||
}
|
||||
|
||||
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
|
||||
|
||||
_parent.UpdateInternetAccess();
|
||||
MainWindow.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);
|
||||
_directoryChanged = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
FileChooserNative fileChooser = new("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("Choose the theme to load", this, FileChooserAction.Open, "Select", "Cancel"))
|
||||
{
|
||||
FileFilter filter = new()
|
||||
{
|
||||
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(_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();
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -1,234 +0,0 @@
|
|||
using Gtk;
|
||||
using LibHac.Common;
|
||||
using LibHac.Fs;
|
||||
using LibHac.Fs.Fsa;
|
||||
using LibHac.Ncm;
|
||||
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.Loaders.Processes.Extensions;
|
||||
using Ryujinx.HLE.Utilities;
|
||||
using Ryujinx.UI.App.Common;
|
||||
using Ryujinx.UI.Common.Configuration;
|
||||
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 ApplicationData _applicationData;
|
||||
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 // Field is never assigned to, Add readonly modifier
|
||||
[GUI] Label _baseTitleInfoLabel;
|
||||
[GUI] Box _availableUpdatesBox;
|
||||
[GUI] RadioButton _noUpdateRadioButton;
|
||||
#pragma warning restore CS0649, IDE0044
|
||||
|
||||
public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : this(new Builder("Ryujinx.Gtk3.UI.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, applicationData) { }
|
||||
|
||||
private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
|
||||
{
|
||||
_parent = parent;
|
||||
|
||||
builder.Autoconnect(this);
|
||||
|
||||
_applicationData = applicationData;
|
||||
_virtualFileSystem = virtualFileSystem;
|
||||
_updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, applicationData.IdBaseString, "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 {applicationData.Name} [{applicationData.IdBaseString}]";
|
||||
|
||||
// Try to get updates from PFS first
|
||||
AddUpdate(_applicationData.Path, true);
|
||||
|
||||
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, bool ignoreNotFound = false)
|
||||
{
|
||||
if (!File.Exists(path) || _radioButtonToPathDictionary.ContainsValue(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
|
||||
? IntegrityCheckLevel.ErrorOnInvalid
|
||||
: IntegrityCheckLevel.None;
|
||||
|
||||
try
|
||||
{
|
||||
using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
|
||||
|
||||
Dictionary<ulong, ContentMetaData> updates = pfs.GetContentData(ContentMetaType.Patch, _virtualFileSystem, checkLevel);
|
||||
|
||||
Nca patchNca = null;
|
||||
Nca controlNca = null;
|
||||
|
||||
if (updates.TryGetValue(_applicationData.Id, out ContentMetaData update))
|
||||
{
|
||||
patchNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Program);
|
||||
controlNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Control);
|
||||
}
|
||||
|
||||
if (controlNca != null && patchNca != null)
|
||||
{
|
||||
ApplicationControlProperty controlData = new();
|
||||
|
||||
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();
|
||||
|
||||
string radioLabel = $"Version {controlData.DisplayVersionString.ToString()} - {path}";
|
||||
|
||||
if (System.IO.Path.GetExtension(path).ToLower() == ".xci")
|
||||
{
|
||||
radioLabel = "Bundled: " + radioLabel;
|
||||
}
|
||||
|
||||
RadioButton radioButton = new(radioLabel);
|
||||
radioButton.JoinGroup(_noUpdateRadioButton);
|
||||
|
||||
_availableUpdatesBox.Add(radioButton);
|
||||
_radioButtonToPathDictionary.Add(radioButton, path);
|
||||
|
||||
radioButton.Show();
|
||||
radioButton.Active = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ignoreNotFound)
|
||||
{
|
||||
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("Select update files", this, FileChooserAction.Open, "Add", "Cancel");
|
||||
|
||||
fileChooser.SelectMultiple = true;
|
||||
|
||||
FileFilter filter = new()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,214 +0,0 @@
|
|||
<?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 all the updates</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>
|
|
@ -1,255 +0,0 @@
|
|||
using Gtk;
|
||||
using Pango;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.UI.Windows
|
||||
{
|
||||
public partial class UserProfilesManagerWindow : Window
|
||||
{
|
||||
private Box _mainBox;
|
||||
private Label _selectedLabel;
|
||||
private Box _selectedUserBox;
|
||||
private Image _selectedUserImage;
|
||||
private Box _selectedUserInfoBox;
|
||||
private Entry _selectedUserNameEntry;
|
||||
private Label _selectedUserIdLabel;
|
||||
private Box _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()
|
||||
{
|
||||
//
|
||||
// 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)
|
||||
{
|
||||
MarginStart = 30,
|
||||
};
|
||||
|
||||
//
|
||||
// _selectedUserImage
|
||||
//
|
||||
_selectedUserImage = new Image();
|
||||
|
||||
//
|
||||
// _selectedUserInfoBox
|
||||
//
|
||||
_selectedUserInfoBox = new Box(Orientation.Vertical, 0)
|
||||
{
|
||||
Homogeneous = true,
|
||||
};
|
||||
|
||||
//
|
||||
// _selectedUserNameEntry
|
||||
//
|
||||
_selectedUserNameEntry = new Entry("")
|
||||
{
|
||||
MarginStart = 15,
|
||||
MaxLength = (int)MaxProfileNameLength,
|
||||
};
|
||||
_selectedUserNameEntry.KeyReleaseEvent += SelectedUserNameEntry_KeyReleaseEvent;
|
||||
|
||||
//
|
||||
// _selectedUserIdLabel
|
||||
//
|
||||
_selectedUserIdLabel = new Label("")
|
||||
{
|
||||
MarginTop = 15,
|
||||
MarginStart = 15,
|
||||
};
|
||||
|
||||
//
|
||||
// _selectedUserButtonsBox
|
||||
//
|
||||
_selectedUserButtonsBox = new Box(Orientation.Vertical, 0)
|
||||
{
|
||||
MarginEnd = 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,
|
||||
MarginStart = 30,
|
||||
MarginEnd = 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)
|
||||
{
|
||||
MarginStart = 30,
|
||||
MarginEnd = 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,
|
||||
MarginStart = 10,
|
||||
};
|
||||
_deleteButton.Clicked += DeleteButton_Pressed;
|
||||
|
||||
//
|
||||
// _closeButton
|
||||
//
|
||||
_closeButton = new Button()
|
||||
{
|
||||
Label = "Close",
|
||||
CanFocus = true,
|
||||
ReceivesDefault = true,
|
||||
HeightRequest = 35,
|
||||
WidthRequest = 80,
|
||||
};
|
||||
_closeButton.Clicked += CloseButton_Pressed;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,326 +0,0 @@
|
|||
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 SkiaSharp;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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 readonly ManualResetEvent _avatarsPreloadingEvent = new(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();
|
||||
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 var image = SKBitmap.Decode(buffer);
|
||||
|
||||
image.Resize(new SKImageInfo(256, 256), SKFilterQuality.High);
|
||||
|
||||
using MemoryStream streamJpg = MemoryStreamManager.Shared.GetStream();
|
||||
|
||||
image.Encode(streamJpg, SKEncodedImageFormat.Jpeg, 80);
|
||||
|
||||
_bufferImageProfile = streamJpg.ToArray();
|
||||
}
|
||||
|
||||
private void ProfileImageFileChooser()
|
||||
{
|
||||
FileChooserNative fileChooser = new("Import Custom Profile Image", this, FileChooserAction.Open, "Import", "Cancel")
|
||||
{
|
||||
SelectMultiple = false,
|
||||
};
|
||||
|
||||
FileFilter filter = new()
|
||||
{
|
||||
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()
|
||||
{
|
||||
{ 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()
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue