gui: Refactoring Part 1 (#1859)

* gui: Refactoring Part 1

* Fix ProfileDialog.glade path

* Fix Application.Quit assert

* Fix TitleUpdateWindow parent

* Fix TitleUpdate selected item

* Remove extra line in TitleUpdateWindow

* Fix empty assign of Enum.TryParse

* Add Patrons list in the About Window

* update about error messages
This commit is contained in:
Ac_K 2021-01-08 09:14:13 +01:00 committed by GitHub
parent 72e94bb089
commit a9cb31e75f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 1979 additions and 2549 deletions

467
Ryujinx/Ui/Windows/AboutWindow.Designer.cs generated Normal file
View file

@ -0,0 +1,467 @@
using Gtk;
using Pango;
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 Box _socialBox;
private EventBox _patreonEventBox;
private Box _patreonBox;
private Image _patreonLogo;
private Label _patreonLabel;
private EventBox _githubEventBox;
private Box _githubBox;
private Image _githubLogo;
private Label _githubLabel;
private Box _discordBox;
private EventBox _discordEventBox;
private Image _discordLogo;
private Label _discordLabel;
private EventBox _twitterEventBox;
private Box _twitterBox;
private Image _twitterLogo;
private Label _twitterLabel;
private Separator _separator;
private Box _rightBox;
private Label _aboutLabel;
private Label _aboutDescriptionLabel;
private Label _createdByLabel;
private TextView _createdByText;
private EventBox _contributorsEventBox;
private Label _contributorsLinkLabel;
private Label _patreonNamesLabel;
private ScrolledWindow _patreonNamesScrolled;
private TextView _patreonNamesText;
private void InitializeComponent()
{
#pragma warning disable CS0612
//
// AboutWindow
//
CanFocus = false;
Resizable = false;
Modal = true;
WindowPosition = WindowPosition.Center;
DefaultWidth = 800;
DefaultHeight = 450;
TypeHint = Gdk.WindowTypeHint.Dialog;
//
// _mainBox
//
_mainBox = new Box(Orientation.Horizontal, 0);
//
// _leftBox
//
_leftBox = new Box(Orientation.Vertical, 0)
{
Margin = 15,
MarginLeft = 30,
MarginRight = 0
};
//
// _logoBox
//
_logoBox = new Box(Orientation.Horizontal, 0);
//
// _ryujinxLogo
//
_ryujinxLogo = new Image(new Gdk.Pixbuf(Assembly.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Ryujinx.png", 100, 100))
{
Margin = 10,
MarginLeft = 15
};
//
// _logoTextBox
//
_logoTextBox = new Box(Orientation.Vertical, 0);
//
// _ryujinxLabel
//
_ryujinxLabel = new Label("Ryujinx")
{
MarginTop = 15,
Justify = Justification.Center,
Attributes = new AttrList()
};
_ryujinxLabel.Attributes.Insert(new Pango.AttrScale(2.7f));
//
// _ryujinxPhoneticLabel
//
_ryujinxPhoneticLabel = new Label("(REE-YOU-JI-NX)")
{
Justify = Justification.Center
};
//
// _ryujinxLink
//
_ryujinxLink = new EventBox()
{
Margin = 5
};
_ryujinxLink.ButtonPressEvent += RyujinxButton_Pressed;
//
// _ryujinxLinkLabel
//
_ryujinxLinkLabel = new Label("www.ryujinx.org")
{
TooltipText = "Click to open the Ryujinx website in your default browser.",
Justify = Justification.Center,
Attributes = new AttrList()
};
_ryujinxLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
//
// _versionLabel
//
_versionLabel = new Label(Program.Version)
{
Expand = true,
Justify = Justification.Center,
Margin = 5
};
//
// _disclaimerLabel
//
_disclaimerLabel = new Label("Ryujinx is not affiliated with Nintendo™,\nor any of its partners, in any way.")
{
Expand = true,
Justify = Justification.Center,
Margin = 5,
Attributes = new AttrList()
};
_disclaimerLabel.Attributes.Insert(new Pango.AttrScale(0.8f));
//
// _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.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Patreon.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.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_GitHub.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.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Discord.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.GetExecutingAssembly(), "Ryujinx.Ui.Resources.Logo_Twitter.png", 30, 30))
{
Margin = 10
};
//
// _twitterLabel
//
_twitterLabel = new Label("Twitter")
{
Justify = Justification.Center
};
//
// _separator
//
_separator = new Separator(Orientation.Vertical)
{
Margin = 15
};
//
// _rightBox
//
_rightBox = new Box(Orientation.Vertical, 0)
{
Margin = 15,
MarginTop = 40
};
//
// _aboutLabel
//
_aboutLabel = new Label("About :")
{
Halign = Align.Start,
Attributes = new AttrList()
};
_aboutLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold));
_aboutLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
//
// _aboutDescriptionLabel
//
_aboutDescriptionLabel = new Label("Ryujinx is an emulator for the Nintendo Switch™.\n" +
"Please support us on Patreon.\n" +
"Get all the latest news on our Twitter or Discord.\n" +
"Developers interested in contributing can find out more on our GitHub or Discord.")
{
Margin = 15,
Halign = Align.Start
};
//
// _createdByLabel
//
_createdByLabel = new Label("Maintained by :")
{
Halign = Align.Start,
Attributes = new AttrList()
};
_createdByLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold));
_createdByLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
//
// _createdByText
//
_createdByText = new TextView()
{
WrapMode = Gtk.WrapMode.Word,
Editable = false,
CursorVisible = false,
Margin = 15,
MarginRight = 30
};
_createdByText.Buffer.Text = "gdkchan, Ac_K, Thog, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, Xpl0itR, GoffyDude, »jD« and more...";
//
// _contributorsEventBox
//
_contributorsEventBox = new EventBox();
_contributorsEventBox.ButtonPressEvent += ContributorsButton_Pressed;
//
// _contributorsLinkLabel
//
_contributorsLinkLabel = new Label("See All Contributors...")
{
TooltipText = "Click to open the Contributors page in your default browser.",
MarginRight = 30,
Halign = Align.End,
Attributes = new AttrList()
};
_contributorsLinkLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
//
// _patreonNamesLabel
//
_patreonNamesLabel = new Label("Supported on Patreon by :")
{
Halign = Align.Start,
Attributes = new AttrList()
};
_patreonNamesLabel.Attributes.Insert(new Pango.AttrWeight(Weight.Bold));
_patreonNamesLabel.Attributes.Insert(new Pango.AttrUnderline(Underline.Single));
//
// _patreonNamesScrolled
//
_patreonNamesScrolled = new ScrolledWindow()
{
Margin = 15,
MarginRight = 30,
Expand = true,
ShadowType = ShadowType.In
};
_patreonNamesScrolled.SetPolicy(PolicyType.Never, PolicyType.Automatic);
//
// _patreonNamesText
//
_patreonNamesText = new TextView()
{
WrapMode = Gtk.WrapMode.Word
};
_patreonNamesText.Buffer.Text = "Loading...";
_patreonNamesText.SetProperty("editable", new GLib.Value(false));
#pragma warning restore CS0612
ShowComponent();
}
private void ShowComponent()
{
_logoBox.Add(_ryujinxLogo);
_ryujinxLink.Add(_ryujinxLinkLabel);
_logoTextBox.Add(_ryujinxLabel);
_logoTextBox.Add(_ryujinxPhoneticLabel);
_logoTextBox.Add(_ryujinxLink);
_logoBox.Add(_logoTextBox);
_patreonBox.Add(_patreonLogo);
_patreonBox.Add(_patreonLabel);
_patreonEventBox.Add(_patreonBox);
_githubBox.Add(_githubLogo);
_githubBox.Add(_githubLabel);
_githubEventBox.Add(_githubBox);
_discordBox.Add(_discordLogo);
_discordBox.Add(_discordLabel);
_discordEventBox.Add(_discordBox);
_twitterBox.Add(_twitterLogo);
_twitterBox.Add(_twitterLabel);
_twitterEventBox.Add(_twitterBox);
_socialBox.Add(_patreonEventBox);
_socialBox.Add(_githubEventBox);
_socialBox.Add(_discordEventBox);
_socialBox.Add(_twitterEventBox);
_leftBox.Add(_logoBox);
_leftBox.Add(_versionLabel);
_leftBox.Add(_disclaimerLabel);
_leftBox.Add(_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();
}
}
}

View file

@ -0,0 +1,73 @@
using Gtk;
using Ryujinx.Common.Utilities;
using Ryujinx.Ui.Helper;
using System.Net.Http;
using System.Net.NetworkInformation;
using System.Threading.Tasks;
namespace Ryujinx.Ui.Windows
{
public partial class AboutWindow : Window
{
public AboutWindow() : base($"Ryujinx {Program.Version} - About")
{
InitializeComponent();
_ = DownloadPatronsJson();
}
private async Task DownloadPatronsJson()
{
if (!NetworkInterface.GetIsNetworkAvailable())
{
_patreonNamesText.Buffer.Text = "Connection Error.";
}
HttpClient httpClient = new HttpClient();
try
{
string patreonJsonString = await httpClient.GetStringAsync("https://patreon.ryujinx.org/");
_patreonNamesText.Buffer.Text = string.Join(", ", JsonHelper.Deserialize<string[]>(patreonJsonString));
}
catch
{
_patreonNamesText.Buffer.Text = "API Error.";
}
}
//
// Events
//
private void RyujinxButton_Pressed(object sender, ButtonPressEventArgs args)
{
OpenHelper.OpenUrl("https://ryujinx.org");
}
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");
}
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,253 @@
using Gtk;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.HLE.FileSystem;
using Ryujinx.Ui.Widgets;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using GUI = Gtk.Builder.ObjectAttribute;
using JsonHelper = Ryujinx.Common.Utilities.JsonHelper;
namespace Ryujinx.Ui.Windows
{
public class DlcWindow : Window
{
private readonly VirtualFileSystem _virtualFileSystem;
private readonly string _titleId;
private readonly string _dlcJsonPath;
private readonly List<DlcContainer> _dlcContainerList;
#pragma warning disable CS0649, IDE0044
[GUI] Label _baseTitleInfoLabel;
[GUI] TreeView _dlcTreeView;
[GUI] TreeSelection _dlcTreeSelection;
#pragma warning restore CS0649, IDE0044
public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { }
private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetObject("_dlcWindow").Handle)
{
builder.Autoconnect(this);
_titleId = titleId;
_virtualFileSystem = virtualFileSystem;
_dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json");
_baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]";
try
{
_dlcContainerList = JsonHelper.DeserializeFromFile<List<DlcContainer>>(_dlcJsonPath);
}
catch
{
_dlcContainerList = new List<DlcContainer>();
}
_dlcTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string));
CellRendererToggle enableToggle = new CellRendererToggle();
enableToggle.Toggled += (sender, args) =>
{
_dlcTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path));
bool newValue = !(bool)_dlcTreeView.Model.GetValue(treeIter, 0);
_dlcTreeView.Model.SetValue(treeIter, 0, newValue);
if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, treeIter))
{
do
{
_dlcTreeView.Model.SetValue(childIter, 0, newValue);
}
while (_dlcTreeView.Model.IterNext(ref childIter));
}
};
_dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0);
_dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1);
_dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
foreach (DlcContainer dlcContainer in _dlcContainerList)
{
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", dlcContainer.Path);
using FileStream containerFile = File.OpenRead(dlcContainer.Path);
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
_virtualFileSystem.ImportTickets(pfs);
foreach (DlcNca dlcNca in dlcContainer.DlcNcaList)
{
pfs.OpenFile(out IFile ncaFile, dlcNca.Path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryCreateNca(ncaFile.AsStorage(), dlcContainer.Path);
if (nca != null)
{
((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.Path);
}
}
}
}
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
{
try
{
return new Nca(_virtualFileSystem.KeySet, ncaStorage);
}
catch (Exception exception)
{
GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {containerPath}");
}
return null;
}
private void AddButton_Clicked(object sender, EventArgs args)
{
FileChooserDialog fileChooser = new FileChooserDialog("Select DLC files", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept)
{
SelectMultiple = true,
Filter = new FileFilter()
};
fileChooser.SetPosition(WindowPosition.Center);
fileChooser.Filter.AddPattern("*.nsp");
if (fileChooser.Run() == (int)ResponseType.Accept)
{
foreach (string containerPath in fileChooser.Filenames)
{
if (!File.Exists(containerPath))
{
return;
}
using (FileStream containerFile = File.OpenRead(containerPath))
{
PartitionFileSystem pfs = new PartitionFileSystem(containerFile.AsStorage());
bool containsDlc = false;
_virtualFileSystem.ImportTickets(pfs);
TreeIter? parentIter = null;
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
pfs.OpenFile(out IFile ncaFile, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryCreateNca(ncaFile.AsStorage(), containerPath);
if (nca == null) continue;
if (nca.Header.ContentType == NcaContentType.PublicData)
{
if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId)
{
break;
}
parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath);
((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
containsDlc = true;
}
}
if (!containsDlc)
{
GtkDialog.CreateErrorDialog("The specified file does not contain a DLC for the selected title!");
}
}
}
}
fileChooser.Dispose();
}
private void RemoveButton_Clicked(object sender, EventArgs args)
{
if (_dlcTreeSelection.GetSelected(out ITreeModel treeModel, out TreeIter treeIter))
{
if (_dlcTreeView.Model.IterParent(out TreeIter parentIter, treeIter) && _dlcTreeView.Model.IterNChildren(parentIter) <= 1)
{
((TreeStore)treeModel).Remove(ref parentIter);
}
else
{
((TreeStore)treeModel).Remove(ref treeIter);
}
}
}
private void RemoveAllButton_Clicked(object sender, EventArgs args)
{
List<TreeIter> toRemove = new List<TreeIter>();
if (_dlcTreeView.Model.GetIterFirst(out TreeIter iter))
{
do
{
toRemove.Add(iter);
}
while (_dlcTreeView.Model.IterNext(ref iter));
}
foreach (TreeIter i in toRemove)
{
TreeIter j = i;
((TreeStore)_dlcTreeView.Model).Remove(ref j);
}
}
private void SaveButton_Clicked(object sender, EventArgs args)
{
_dlcContainerList.Clear();
if (_dlcTreeView.Model.GetIterFirst(out TreeIter parentIter))
{
do
{
if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, parentIter))
{
DlcContainer dlcContainer = new DlcContainer
{
Path = (string)_dlcTreeView.Model.GetValue(parentIter, 2),
DlcNcaList = new List<DlcNca>()
};
do
{
dlcContainer.DlcNcaList.Add(new DlcNca
{
Enabled = (bool)_dlcTreeView.Model.GetValue(childIter, 0),
TitleId = Convert.ToUInt64(_dlcTreeView.Model.GetValue(childIter, 1).ToString(), 16),
Path = (string)_dlcTreeView.Model.GetValue(childIter, 2)
});
}
while (_dlcTreeView.Model.IterNext(ref childIter));
_dlcContainerList.Add(dlcContainer);
}
}
while (_dlcTreeView.Model.IterNext(ref parentIter));
}
using (FileStream dlcJsonStream = File.Create(_dlcJsonPath, 4096, FileOptions.WriteThrough))
{
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_dlcContainerList, true)));
}
Dispose();
}
private void CancelButton_Clicked(object sender, EventArgs args)
{
Dispose();
}
}
}

View file

@ -0,0 +1,202 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkWindow" id="_dlcWindow">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Ryujinx - DLC Manager</property>
<property name="modal">True</property>
<property name="window_position">center</property>
<property name="default_width">550</property>
<property name="default_height">350</property>
<child>
<object class="GtkBox" id="MainBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="DlcBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="_baseTitleInfoLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="label" translatable="yes">Available DLC</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkTreeView" id="_dlcTreeView">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_clickable">False</property>
<child internal-child="selection">
<object class="GtkTreeSelection" id="_dlcTreeSelection"/>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="layout_style">start</property>
<child>
<object class="GtkButton" id="_addUpdate">
<property name="label" translatable="yes">Add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Adds an update to this list</property>
<property name="margin_left">10</property>
<signal name="clicked" handler="AddButton_Clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_removeUpdate">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Removes the selected update</property>
<property name="margin_left">10</property>
<signal name="clicked" handler="RemoveButton_Clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_removeAllButton">
<property name="label" translatable="yes">Remove All</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Removes the selected update</property>
<property name="margin_left">10</property>
<signal name="clicked" handler="RemoveAllButton_Clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="_saveButton">
<property name="label" translatable="yes">Save</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="margin_right">10</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<signal name="clicked" handler="SaveButton_Clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_cancelButton">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="margin_right">10</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<signal name="clicked" handler="CancelButton_Clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<placeholder/>
</child>
</object>
</interface>

View file

@ -0,0 +1,575 @@
using Gtk;
using Ryujinx.Audio;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Configuration;
using Ryujinx.Configuration.System;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Time.TimeZone;
using Ryujinx.Ui.Helper;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
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;
#pragma warning disable CS0649, IDE0044
[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 _vSyncToggle;
[GUI] CheckButton _shaderCacheToggle;
[GUI] CheckButton _ptcToggle;
[GUI] CheckButton _fsicToggle;
[GUI] CheckButton _ignoreToggle;
[GUI] CheckButton _directKeyboardAccess;
[GUI] ComboBoxText _systemLanguageSelect;
[GUI] ComboBoxText _systemRegionSelect;
[GUI] Entry _systemTimeZoneEntry;
[GUI] EntryCompletion _systemTimeZoneCompletion;
[GUI] Box _audioBackendBox;
[GUI] ComboBox _audioBackendSelect;
[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] CheckButton _custThemeToggle;
[GUI] Entry _custThemePath;
[GUI] ToggleButton _browseThemePath;
[GUI] Label _custThemePathLabel;
[GUI] TreeView _gameDirsBox;
[GUI] Entry _addGameDirBox;
[GUI] Entry _graphicsShadersDumpPath;
[GUI] ComboBoxText _anisotropy;
[GUI] ComboBoxText _aspectRatio;
[GUI] ComboBoxText _resScaleCombo;
[GUI] Entry _resScaleText;
[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, HLE.FileSystem.Content.ContentManager contentManager) : this(parent, new Builder("Ryujinx.Ui.Windows.SettingsWindow.glade"), virtualFileSystem, contentManager) { }
private SettingsWindow(MainWindow parent, Builder builder, VirtualFileSystem virtualFileSystem, HLE.FileSystem.Content.ContentManager contentManager) : base(builder.GetObject("_settingsWin").Handle)
{
_parent = parent;
builder.Autoconnect(this);
_timeZoneContentManager = new TimeZoneContentManager();
_timeZoneContentManager.InitializeInstance(virtualFileSystem, contentManager, LibHac.FsSystem.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";
// Setup Currents.
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(typeof(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.Graphics.EnableVsync)
{
_vSyncToggle.Click();
}
if (ConfigurationState.Instance.Graphics.EnableShaderCache)
{
_shaderCacheToggle.Click();
}
if (ConfigurationState.Instance.System.EnablePtc)
{
_ptcToggle.Click();
}
if (ConfigurationState.Instance.System.EnableFsIntegrityChecks)
{
_fsicToggle.Click();
}
if (ConfigurationState.Instance.System.IgnoreMissingServices)
{
_ignoreToggle.Click();
}
if (ConfigurationState.Instance.Hid.EnableKeyboard)
{
_directKeyboardAccess.Click();
}
if (ConfigurationState.Instance.Ui.EnableCustomTheme)
{
_custThemeToggle.Click();
}
// Custom EntryCompletion Columns. If added to glade, need to override more signals
ListStore tzList = new ListStore(typeof(string), typeof(string), typeof(string));
_systemTimeZoneCompletion.Model = tzList;
CellRendererText offsetCol = new CellRendererText();
CellRendererText abbrevCol = new CellRendererText();
_systemTimeZoneCompletion.PackStart(offsetCol, false);
_systemTimeZoneCompletion.AddAttribute(offsetCol, "text", 0);
_systemTimeZoneCompletion.TextColumn = 1; // Regions Column
_systemTimeZoneCompletion.PackStart(abbrevCol, false);
_systemTimeZoneCompletion.AddAttribute(abbrevCol, "text", 2);
int maxLocationLength = 0;
foreach (var (offset, location, abbr) in _timeZoneContentManager.ParseTzOffsets())
{
var hours = Math.DivRem(offset, 3600, out int seconds);
var minutes = Math.Abs(seconds) / 60;
var abbr2 = (abbr.StartsWith('+') || abbr.StartsWith('-')) ? string.Empty : abbr;
tzList.AppendValues($"UTC{hours:+0#;-0#;+00}:{minutes:D2} ", location, abbr2);
_validTzRegions.Add(location);
maxLocationLength = Math.Max(maxLocationLength, location.Length);
}
_systemTimeZoneEntry.WidthChars = Math.Max(20, maxLocationLength + 1); // Ensure minimum Entry width
_systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName();
_systemTimeZoneCompletion.MatchFunc = TimeZoneMatchFunc;
_systemLanguageSelect.SetActiveId(ConfigurationState.Instance.System.Language.Value.ToString());
_systemRegionSelect.SetActiveId(ConfigurationState.Instance.System.Region.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());
_custThemePath.Buffer.Text = ConfigurationState.Instance.Ui.CustomThemePath;
_resScaleText.Buffer.Text = ConfigurationState.Instance.Graphics.ResScaleCustom.Value.ToString();
_resScaleText.Visible = _resScaleCombo.ActiveId == "-1";
_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 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.Dummy:
_audioBackendSelect.SetActiveIter(dummyIter);
break;
default:
throw new ArgumentOutOfRangeException();
}
_audioBackendBox.Add(_audioBackendSelect);
_audioBackendSelect.Show();
bool openAlIsSupported = false;
bool soundIoIsSupported = false;
Task.Run(() =>
{
openAlIsSupported = OpenALAudioOut.IsSupported;
soundIoIsSupported = SoundIoAudioOut.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.Dummy => true,
_ => throw new ArgumentOutOfRangeException()
};
});
}
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()
{
List<string> gameDirs = new List<string>();
_gameDirsBoxStore.GetIterFirst(out TreeIter treeIter);
for (int i = 0; i < _gameDirsBoxStore.IterNChildren(); i++)
{
gameDirs.Add((string)_gameDirsBoxStore.GetValue(treeIter, 0));
_gameDirsBoxStore.IterNext(ref treeIter);
}
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;
}
ConfigurationState.Instance.Logger.EnableError.Value = _errorLogToggle.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.Graphics.EnableVsync.Value = _vSyncToggle.Active;
ConfigurationState.Instance.Graphics.EnableShaderCache.Value = _shaderCacheToggle.Active;
ConfigurationState.Instance.System.EnablePtc.Value = _ptcToggle.Active;
ConfigurationState.Instance.System.EnableFsIntegrityChecks.Value = _fsicToggle.Active;
ConfigurationState.Instance.System.IgnoreMissingServices.Value = _ignoreToggle.Active;
ConfigurationState.Instance.Hid.EnableKeyboard.Value = _directKeyboardAccess.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<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.Ui.GameDirs.Value = gameDirs;
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.ResScale.Value = int.Parse(_resScaleCombo.ActiveId);
ConfigurationState.Instance.Graphics.ResScaleCustom.Value = resScaleCustom;
if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter))
{
ConfigurationState.Instance.System.AudioBackend.Value = (AudioBackend)_audioBackendStore.GetValue(activeIter, 1);
}
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
_parent.UpdateGraphicsConfig();
ThemeHelper.ApplyTheme();
}
//
// Events
//
private void TimeZoneEntry_FocusOut(object sender, FocusOutEventArgs e)
{
if (!_validTzRegions.Contains(_systemTimeZoneEntry.Text))
{
_systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName();
}
}
private bool TimeZoneMatchFunc(EntryCompletion compl, string key, TreeIter iter)
{
key = key.Trim().Replace(' ', '_');
return ((string)compl.Model.GetValue(iter, 1)).Contains(key, StringComparison.OrdinalIgnoreCase) || // region
((string)compl.Model.GetValue(iter, 2)).StartsWith(key, StringComparison.OrdinalIgnoreCase) || // abbr
((string)compl.Model.GetValue(iter, 0))[3..].StartsWith(key); // offset
}
private void SystemTimeSpin_ValueChanged(object sender, EventArgs e)
{
int year = _systemTimeYearSpin.ValueAsInt;
int month = _systemTimeMonthSpin.ValueAsInt;
int day = _systemTimeDaySpin.ValueAsInt;
int hour = _systemTimeHourSpin.ValueAsInt;
int minute = _systemTimeMinuteSpin.ValueAsInt;
if (!DateTime.TryParse(year + "-" + month + "-" + day + " " + hour + ":" + minute, out DateTime newTime))
{
UpdateSystemTimeSpinners();
return;
}
newTime = newTime.AddSeconds(DateTime.Now.Second).AddMilliseconds(DateTime.Now.Millisecond);
long systemTimeOffset = (long)Math.Ceiling((newTime - DateTime.Now).TotalMinutes) * 60L;
if (_systemTimeOffset != systemTimeOffset)
{
_systemTimeOffset = systemTimeOffset;
UpdateSystemTimeSpinners();
}
}
private void AddDir_Pressed(object sender, EventArgs args)
{
if (Directory.Exists(_addGameDirBox.Buffer.Text))
{
_gameDirsBoxStore.AppendValues(_addGameDirBox.Buffer.Text);
}
else
{
FileChooserDialog fileChooser = new FileChooserDialog("Choose the game directory to add to the list", this, FileChooserAction.SelectFolder, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept)
{
SelectMultiple = true
};
if (fileChooser.Run() == (int)ResponseType.Accept)
{
foreach (string directory in fileChooser.Filenames)
{
bool directoryAdded = false;
if (_gameDirsBoxStore.GetIterFirst(out TreeIter treeIter))
{
do
{
if (directory.Equals((string)_gameDirsBoxStore.GetValue(treeIter, 0)))
{
directoryAdded = true;
break;
}
} while(_gameDirsBoxStore.IterNext(ref treeIter));
}
if (!directoryAdded)
{
_gameDirsBoxStore.AppendValues(directory);
}
}
}
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);
}
((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 (FileChooserDialog fileChooser = new FileChooserDialog("Choose the theme to load", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Select", ResponseType.Accept))
{
fileChooser.Filter = new FileFilter();
fileChooser.Filter.AddPattern("*.css");
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);
new ControllerWindow(playerIndex).Show();
}
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)
{
Dispose();
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,202 @@
using Gtk;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
using LibHac.FsSystem.NcaUtils;
using LibHac.Ns;
using Ryujinx.Common.Configuration;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.Ui.Widgets;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using GUI = Gtk.Builder.ObjectAttribute;
using JsonHelper = Ryujinx.Common.Utilities.JsonHelper;
namespace Ryujinx.Ui.Windows
{
public class TitleUpdateWindow : Window
{
private readonly MainWindow _parent;
private readonly VirtualFileSystem _virtualFileSystem;
private readonly string _titleId;
private readonly string _updateJsonPath;
private TitleUpdateMetadata _titleUpdateWindowData;
private readonly Dictionary<RadioButton, string> _radioButtonToPathDictionary;
#pragma warning disable CS0649, IDE0044
[GUI] Label _baseTitleInfoLabel;
[GUI] Box _availableUpdatesBox;
[GUI] RadioButton _noUpdateRadioButton;
#pragma warning restore CS0649, IDE0044
public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Ui.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { }
private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetObject("_titleUpdateWindow").Handle)
{
_parent = parent;
builder.Autoconnect(this);
_titleId = titleId;
_virtualFileSystem = virtualFileSystem;
_updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json");
_radioButtonToPathDictionary = new Dictionary<RadioButton, string>();
try
{
_titleUpdateWindowData = JsonHelper.DeserializeFromFile<TitleUpdateMetadata>(_updateJsonPath);
}
catch
{
_titleUpdateWindowData = new TitleUpdateMetadata
{
Selected = "",
Paths = new List<string>()
};
}
_baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]";
foreach (string path in _titleUpdateWindowData.Paths)
{
AddUpdate(path);
}
if (_titleUpdateWindowData.Selected == "")
{
_noUpdateRadioButton.Active = true;
}
else
{
foreach ((RadioButton update, var _) in _radioButtonToPathDictionary.Where(keyValuePair => keyValuePair.Value == _titleUpdateWindowData.Selected))
{
update.Active = true;
}
}
}
private void AddUpdate(string path)
{
if (File.Exists(path))
{
using (FileStream file = new FileStream(path, FileMode.Open, FileAccess.Read))
{
PartitionFileSystem nsp = new PartitionFileSystem(file.AsStorage());
try
{
(Nca patchNca, Nca controlNca) = ApplicationLoader.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0);
if (controlNca != null && patchNca != null)
{
ApplicationControlProperty controlData = new ApplicationControlProperty();
controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(out IFile nacpFile, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
nacpFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
RadioButton radioButton = new RadioButton($"Version {controlData.DisplayVersion.ToString()} - {path}");
radioButton.JoinGroup(_noUpdateRadioButton);
_availableUpdatesBox.Add(radioButton);
_radioButtonToPathDictionary.Add(radioButton, path);
radioButton.Show();
radioButton.Active = true;
}
else
{
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
}
}
catch (Exception exception)
{
GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}");
}
}
}
}
private void RemoveUpdates(bool removeSelectedOnly = false)
{
foreach (RadioButton radioButton in _noUpdateRadioButton.Group)
{
if (radioButton.Label != "No Update" && (!removeSelectedOnly || radioButton.Active))
{
_availableUpdatesBox.Remove(radioButton);
_radioButtonToPathDictionary.Remove(radioButton);
radioButton.Dispose();
}
}
}
private void AddButton_Clicked(object sender, EventArgs args)
{
using (FileChooserDialog fileChooser = new FileChooserDialog("Select update files", this, FileChooserAction.Open, "Cancel", ResponseType.Cancel, "Add", ResponseType.Accept))
{
fileChooser.SelectMultiple = true;
fileChooser.SetPosition(WindowPosition.Center);
fileChooser.Filter = new FileFilter();
fileChooser.Filter.AddPattern("*.nsp");
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 : "";
}
}
using (FileStream dlcJsonStream = File.Create(_updateJsonPath, 4096, FileOptions.WriteThrough))
{
dlcJsonStream.Write(Encoding.UTF8.GetBytes(JsonHelper.Serialize(_titleUpdateWindowData, true)));
}
_parent.UpdateGameTable();
Dispose();
}
private void CancelButton_Clicked(object sender, EventArgs args)
{
Dispose();
}
}
}

View file

@ -0,0 +1,214 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.36.0 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkWindow" id="_titleUpdateWindow">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Ryujinx - Title Update Manager</property>
<property name="modal">True</property>
<property name="window_position">center</property>
<property name="default_width">550</property>
<property name="default_height">250</property>
<child>
<object class="GtkBox" id="MainBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="UpdatesBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel" id="_baseTitleInfoLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="label" translatable="yes">Available Updates</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="margin_left">10</property>
<property name="margin_right">10</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkViewport">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="_availableUpdatesBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkRadioButton" id="_noUpdateRadioButton">
<property name="label" translatable="yes">No Update</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="layout_style">start</property>
<child>
<object class="GtkButton" id="_addUpdate">
<property name="label" translatable="yes">Add</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Adds an update to this list</property>
<property name="margin_left">10</property>
<signal name="clicked" handler="AddButton_Clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_removeUpdate">
<property name="label" translatable="yes">Remove</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Removes the selected update</property>
<property name="margin_left">10</property>
<signal name="clicked" handler="RemoveButton_Clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_removeAllButton">
<property name="label" translatable="yes">Remove All</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Removes the selected update</property>
<property name="margin_left">10</property>
<signal name="clicked" handler="RemoveAllButton_Clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="_saveButton">
<property name="label" translatable="yes">Save</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="margin_right">10</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<signal name="clicked" handler="SaveButton_Clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="_cancelButton">
<property name="label" translatable="yes">Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="margin_right">10</property>
<property name="margin_top">2</property>
<property name="margin_bottom">2</property>
<signal name="clicked" handler="CancelButton_Clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<placeholder/>
</child>
</object>
</interface>