diff options
Diffstat (limited to 'src/Ryujinx.Gtk3/UI/Windows')
17 files changed, 10845 insertions, 0 deletions
diff --git a/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs b/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs new file mode 100644 index 00000000..fd912ef9 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.Designer.cs @@ -0,0 +1,511 @@ +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(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.cs new file mode 100644 index 00000000..f4bb533c --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/AboutWindow.cs @@ -0,0 +1,85 @@ +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"); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs b/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs new file mode 100644 index 00000000..3bf73318 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.Designer.cs @@ -0,0 +1,190 @@ +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(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.cs new file mode 100644 index 00000000..d8c0b0c0 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/AmiiboWindow.cs @@ -0,0 +1,438 @@ +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); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs new file mode 100644 index 00000000..7cddc362 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs @@ -0,0 +1,291 @@ +using Gtk; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ncm; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.Common.Configuration; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Image = SixLabors.ImageSharp.Image; + +namespace Ryujinx.UI.Windows +{ + public class AvatarWindow : Window + { + public byte[] SelectedProfileImage; + public bool NewUser; + + private static 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; + + Image avatarImage = Image.LoadPixelData<Rgba32>(DecompressYaz0(stream), 256, 256); + + avatarImage.SaveAsPng(streamPng); + + _avatarDict.Add(item.FullPath, streamPng.ToArray()); + } + } + } + } + + private void ProcessAvatars() + { + _listStore.Clear(); + + foreach (var avatar in _avatarDict) + { + _listStore.AppendValues(avatar.Key, new Gdk.Pixbuf(ProcessImage(avatar.Value), 96, 96)); + } + + _iconView.SelectPath(new TreePath(new[] { 0 })); + } + + private byte[] ProcessImage(byte[] data) + { + using MemoryStream streamJpg = MemoryStreamManager.Shared.GetStream(); + + Image avatarImage = Image.Load(data, new PngDecoder()); + + avatarImage.Mutate(x => x.BackgroundColor(new Rgba32( + (byte)(_backgroundColor.Red * 255), + (byte)(_backgroundColor.Green * 255), + (byte)(_backgroundColor.Blue * 255), + (byte)(_backgroundColor.Alpha * 255) + ))); + avatarImage.SaveAsJpeg(streamJpg); + + return streamJpg.ToArray(); + } + + private void CloseButton_Pressed(object sender, EventArgs e) + { + SelectedProfileImage = null; + + Close(); + } + + private void IconView_SelectionChanged(object sender, EventArgs e) + { + if (_iconView.SelectedItems.Length > 0) + { + _listStore.GetIter(out TreeIter iter, _iconView.SelectedItems[0]); + + SelectedProfileImage = ProcessImage(_avatarDict[(string)_listStore.GetValue(iter, 0)]); + } + } + + private void SetBackgroungColorButton_Pressed(object sender, EventArgs e) + { + using ColorChooserDialog colorChooserDialog = new("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.Read(input, 0, input.Length); + + long inputOffset = 0; + + byte[] output = new byte[decodedLength]; + long outputOffset = 0; + + ushort mask = 0; + byte header = 0; + + while (outputOffset < decodedLength) + { + if ((mask >>= 1) == 0) + { + header = input[inputOffset++]; + mask = 0x80; + } + + if ((header & mask) > 0) + { + if (outputOffset == output.Length) + { + break; + } + + output[outputOffset++] = input[inputOffset++]; + } + else + { + byte byte1 = input[inputOffset++]; + byte byte2 = input[inputOffset++]; + + int dist = ((byte1 & 0xF) << 8) | byte2; + int position = (int)outputOffset - (dist + 1); + + int length = byte1 >> 4; + if (length == 0) + { + length = input[inputOffset++] + 0x12; + } + else + { + length += 2; + } + + while (length-- > 0) + { + output[outputOffset++] = output[position++]; + } + } + } + + return output; + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs new file mode 100644 index 00000000..96ed0723 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs @@ -0,0 +1,156 @@ +using Gtk; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS; +using Ryujinx.UI.App.Common; +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); + _baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]"; + _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, 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(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.glade new file mode 100644 index 00000000..9a165f1a --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.glade @@ -0,0 +1,150 @@ +<?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> diff --git a/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs new file mode 100644 index 00000000..6712657f --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.cs @@ -0,0 +1,1230 @@ +using Gtk; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Configuration.Hid; +using Ryujinx.Common.Configuration.Hid.Controller; +using Ryujinx.Common.Configuration.Hid.Controller.Motion; +using Ryujinx.Common.Configuration.Hid.Keyboard; +using Ryujinx.Common.Logging; +using Ryujinx.Common.Utilities; +using Ryujinx.Input; +using Ryujinx.Input.Assigner; +using Ryujinx.Input.GTK3; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text.Json; +using System.Threading; +using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId; +using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId; +using GUI = Gtk.Builder.ObjectAttribute; +using Key = Ryujinx.Common.Configuration.Hid.Key; + +namespace Ryujinx.UI.Windows +{ + public class ControllerWindow : Window + { + private readonly PlayerIndex _playerIndex; + private readonly InputConfig _inputConfig; + + private bool _isWaitingForInput; + +#pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier + [GUI] Adjustment _controllerStrongRumble; + [GUI] Adjustment _controllerWeakRumble; + [GUI] Adjustment _controllerDeadzoneLeft; + [GUI] Adjustment _controllerDeadzoneRight; + [GUI] Adjustment _controllerRangeLeft; + [GUI] Adjustment _controllerRangeRight; + [GUI] Adjustment _controllerTriggerThreshold; + [GUI] Adjustment _slotNumber; + [GUI] Adjustment _altSlotNumber; + [GUI] Adjustment _sensitivity; + [GUI] Adjustment _gyroDeadzone; + [GUI] CheckButton _enableMotion; + [GUI] CheckButton _enableCemuHook; + [GUI] CheckButton _mirrorInput; + [GUI] Entry _dsuServerHost; + [GUI] Entry _dsuServerPort; + [GUI] ComboBoxText _inputDevice; + [GUI] ComboBoxText _profile; + [GUI] Box _settingsBox; + [GUI] Box _motionAltBox; + [GUI] Box _motionBox; + [GUI] Box _dsuServerHostBox; + [GUI] Box _dsuServerPortBox; + [GUI] Box _motionControllerSlot; + [GUI] Grid _leftStickKeyboard; + [GUI] Grid _leftStickController; + [GUI] Box _deadZoneLeftBox; + [GUI] Box _rangeLeftBox; + [GUI] Grid _rightStickKeyboard; + [GUI] Grid _rightStickController; + [GUI] Box _deadZoneRightBox; + [GUI] Box _rangeRightBox; + [GUI] Grid _leftSideTriggerBox; + [GUI] Grid _rightSideTriggerBox; + [GUI] Box _triggerThresholdBox; + [GUI] ComboBoxText _controllerType; + [GUI] ToggleButton _lStick; + [GUI] CheckButton _invertLStickX; + [GUI] CheckButton _invertLStickY; + [GUI] CheckButton _rotateL90CW; + [GUI] ToggleButton _lStickUp; + [GUI] ToggleButton _lStickDown; + [GUI] ToggleButton _lStickLeft; + [GUI] ToggleButton _lStickRight; + [GUI] ToggleButton _lStickButton; + [GUI] ToggleButton _dpadUp; + [GUI] ToggleButton _dpadDown; + [GUI] ToggleButton _dpadLeft; + [GUI] ToggleButton _dpadRight; + [GUI] ToggleButton _minus; + [GUI] ToggleButton _l; + [GUI] ToggleButton _zL; + [GUI] ToggleButton _rStick; + [GUI] CheckButton _invertRStickX; + [GUI] CheckButton _invertRStickY; + [GUI] CheckButton _rotateR90CW; + [GUI] ToggleButton _rStickUp; + [GUI] ToggleButton _rStickDown; + [GUI] ToggleButton _rStickLeft; + [GUI] ToggleButton _rStickRight; + [GUI] ToggleButton _rStickButton; + [GUI] ToggleButton _a; + [GUI] ToggleButton _b; + [GUI] ToggleButton _x; + [GUI] ToggleButton _y; + [GUI] ToggleButton _plus; + [GUI] ToggleButton _r; + [GUI] ToggleButton _zR; + [GUI] ToggleButton _lSl; + [GUI] ToggleButton _lSr; + [GUI] ToggleButton _rSl; + [GUI] ToggleButton _rSr; + [GUI] Image _controllerImage; + [GUI] CheckButton _enableRumble; + [GUI] Box _rumbleBox; +#pragma warning restore CS0649, IDE0044 + + private readonly MainWindow _mainWindow; + private readonly IGamepadDriver _gtk3KeyboardDriver; + private IGamepad _selectedGamepad; + private bool _mousePressed; + private bool _middleMousePressed; + + private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + + public ControllerWindow(MainWindow mainWindow, PlayerIndex controllerId) : this(mainWindow, new Builder("Ryujinx.Gtk3.UI.Windows.ControllerWindow.glade"), controllerId) { } + + private ControllerWindow(MainWindow mainWindow, Builder builder, PlayerIndex controllerId) : base(builder.GetRawOwnedObject("_controllerWin")) + { + _mainWindow = mainWindow; + _selectedGamepad = null; + + // NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused... + _gtk3KeyboardDriver = new GTK3KeyboardDriver(this); + + Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png"); + + builder.Autoconnect(this); + + _playerIndex = controllerId; + _inputConfig = ConfigurationState.Instance.Hid.InputConfig.Value.Find(inputConfig => inputConfig.PlayerIndex == _playerIndex); + + Title = $"Ryujinx - Controller Settings - {_playerIndex}"; + + if (_playerIndex == PlayerIndex.Handheld) + { + _controllerType.Append(ControllerType.Handheld.ToString(), "Handheld"); + _controllerType.Sensitive = false; + } + else + { + _controllerType.Append(ControllerType.ProController.ToString(), "Pro Controller"); + _controllerType.Append(ControllerType.JoyconPair.ToString(), "Joycon Pair"); + _controllerType.Append(ControllerType.JoyconLeft.ToString(), "Joycon Left"); + _controllerType.Append(ControllerType.JoyconRight.ToString(), "Joycon Right"); + } + + _controllerType.Active = 0; // Set initial value to first in list. + + // Bind Events. + _lStick.Clicked += ButtonForStick_Pressed; + _lStickUp.Clicked += Button_Pressed; + _lStickDown.Clicked += Button_Pressed; + _lStickLeft.Clicked += Button_Pressed; + _lStickRight.Clicked += Button_Pressed; + _lStickButton.Clicked += Button_Pressed; + _dpadUp.Clicked += Button_Pressed; + _dpadDown.Clicked += Button_Pressed; + _dpadLeft.Clicked += Button_Pressed; + _dpadRight.Clicked += Button_Pressed; + _minus.Clicked += Button_Pressed; + _l.Clicked += Button_Pressed; + _zL.Clicked += Button_Pressed; + _lSl.Clicked += Button_Pressed; + _lSr.Clicked += Button_Pressed; + _rStick.Clicked += ButtonForStick_Pressed; + _rStickUp.Clicked += Button_Pressed; + _rStickDown.Clicked += Button_Pressed; + _rStickLeft.Clicked += Button_Pressed; + _rStickRight.Clicked += Button_Pressed; + _rStickButton.Clicked += Button_Pressed; + _a.Clicked += Button_Pressed; + _b.Clicked += Button_Pressed; + _x.Clicked += Button_Pressed; + _y.Clicked += Button_Pressed; + _plus.Clicked += Button_Pressed; + _r.Clicked += Button_Pressed; + _zR.Clicked += Button_Pressed; + _rSl.Clicked += Button_Pressed; + _rSr.Clicked += Button_Pressed; + _enableCemuHook.Clicked += CemuHookCheckButtonPressed; + + // Setup current values. + UpdateInputDeviceList(); + SetAvailableOptions(); + + ClearValues(); + if (_inputDevice.ActiveId != null) + { + SetCurrentValues(); + } + + mainWindow.InputManager.GamepadDriver.OnGamepadConnected += HandleOnGamepadConnected; + mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected; + + _mainWindow.RendererWidget?.NpadManager.BlockInputUpdates(); + } + + private void CemuHookCheckButtonPressed(object sender, EventArgs e) + { + UpdateCemuHookSpecificFieldsVisibility(); + } + + private void HandleOnGamepadDisconnected(string id) + { + Application.Invoke(delegate + { + UpdateInputDeviceList(); + }); + } + + private void HandleOnGamepadConnected(string id) + { + Application.Invoke(delegate + { + UpdateInputDeviceList(); + }); + } + + protected override void OnDestroyed() + { + _mainWindow.InputManager.GamepadDriver.OnGamepadConnected -= HandleOnGamepadConnected; + _mainWindow.InputManager.GamepadDriver.OnGamepadDisconnected -= HandleOnGamepadDisconnected; + + _mainWindow.RendererWidget?.NpadManager.UnblockInputUpdates(); + + _selectedGamepad?.Dispose(); + + _gtk3KeyboardDriver.Dispose(); + } + + private static string GetShortGamepadName(string str) + { + const string ShrinkChars = "..."; + const int MaxSize = 50; + + if (str.Length > MaxSize) + { + return $"{str.AsSpan(0, MaxSize - ShrinkChars.Length)}{ShrinkChars}"; + } + + return str; + } + + private void UpdateInputDeviceList() + { + _inputDevice.RemoveAll(); + _inputDevice.Append("disabled", "Disabled"); + _inputDevice.SetActiveId("disabled"); + + foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds) + { + IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); + + if (gamepad != null) + { + _inputDevice.Append($"keyboard/{id}", GetShortGamepadName($"{gamepad.Name} ({id})")); + + gamepad.Dispose(); + } + } + + foreach (string id in _mainWindow.InputManager.GamepadDriver.GamepadsIds) + { + IGamepad gamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id); + + if (gamepad != null) + { + _inputDevice.Append($"controller/{id}", GetShortGamepadName($"{gamepad.Name} ({id})")); + + gamepad.Dispose(); + } + } + + switch (_inputConfig) + { + case StandardKeyboardInputConfig keyboard: + _inputDevice.SetActiveId($"keyboard/{keyboard.Id}"); + break; + case StandardControllerInputConfig controller: + _inputDevice.SetActiveId($"controller/{controller.Id}"); + break; + } + } + + private void UpdateCemuHookSpecificFieldsVisibility() + { + if (_enableCemuHook.Active) + { + _dsuServerHostBox.Show(); + _dsuServerPortBox.Show(); + _motionControllerSlot.Show(); + _motionAltBox.Show(); + _mirrorInput.Show(); + } + else + { + _dsuServerHostBox.Hide(); + _dsuServerPortBox.Hide(); + _motionControllerSlot.Hide(); + _motionAltBox.Hide(); + _mirrorInput.Hide(); + } + } + + private void SetAvailableOptions() + { + if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("keyboard")) + { + ShowAll(); + _leftStickController.Hide(); + _rightStickController.Hide(); + _deadZoneLeftBox.Hide(); + _deadZoneRightBox.Hide(); + _rangeLeftBox.Hide(); + _rangeRightBox.Hide(); + _triggerThresholdBox.Hide(); + _motionBox.Hide(); + _rumbleBox.Hide(); + } + else if (_inputDevice.ActiveId != null && _inputDevice.ActiveId.StartsWith("controller")) + { + ShowAll(); + _leftStickKeyboard.Hide(); + _rightStickKeyboard.Hide(); + + UpdateCemuHookSpecificFieldsVisibility(); + } + else + { + _settingsBox.Hide(); + } + + ClearValues(); + } + + private void SetCurrentValues() + { + SetControllerSpecificFields(); + + SetProfiles(); + + if (_inputDevice.ActiveId.StartsWith("keyboard") && _inputConfig is StandardKeyboardInputConfig) + { + SetValues(_inputConfig); + } + else if (_inputDevice.ActiveId.StartsWith("controller") && _inputConfig is StandardControllerInputConfig) + { + SetValues(_inputConfig); + } + } + + private void SetControllerSpecificFields() + { + _leftSideTriggerBox.Hide(); + _rightSideTriggerBox.Hide(); + _motionAltBox.Hide(); + + switch (_controllerType.ActiveId) + { + case "JoyconLeft": + _leftSideTriggerBox.Show(); + break; + case "JoyconRight": + _rightSideTriggerBox.Show(); + break; + case "JoyconPair": + _motionAltBox.Show(); + break; + } + + if (!OperatingSystem.IsMacOS()) + { + _controllerImage.Pixbuf = _controllerType.ActiveId switch + { + "ProController" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Controller_ProCon.svg", 400, 400), + "JoyconLeft" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Controller_JoyConLeft.svg", 400, 500), + "JoyconRight" => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Controller_JoyConRight.svg", 400, 500), + _ => new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Controller_JoyConPair.svg", 400, 500), + }; + } + } + + private void ClearValues() + { + _lStick.Label = "Unbound"; + _lStickUp.Label = "Unbound"; + _lStickDown.Label = "Unbound"; + _lStickLeft.Label = "Unbound"; + _lStickRight.Label = "Unbound"; + _lStickButton.Label = "Unbound"; + _dpadUp.Label = "Unbound"; + _dpadDown.Label = "Unbound"; + _dpadLeft.Label = "Unbound"; + _dpadRight.Label = "Unbound"; + _minus.Label = "Unbound"; + _l.Label = "Unbound"; + _zL.Label = "Unbound"; + _lSl.Label = "Unbound"; + _lSr.Label = "Unbound"; + _rStick.Label = "Unbound"; + _rStickUp.Label = "Unbound"; + _rStickDown.Label = "Unbound"; + _rStickLeft.Label = "Unbound"; + _rStickRight.Label = "Unbound"; + _rStickButton.Label = "Unbound"; + _a.Label = "Unbound"; + _b.Label = "Unbound"; + _x.Label = "Unbound"; + _y.Label = "Unbound"; + _plus.Label = "Unbound"; + _r.Label = "Unbound"; + _zR.Label = "Unbound"; + _rSl.Label = "Unbound"; + _rSr.Label = "Unbound"; + _controllerStrongRumble.Value = 1; + _controllerWeakRumble.Value = 1; + _controllerDeadzoneLeft.Value = 0; + _controllerDeadzoneRight.Value = 0; + _controllerRangeLeft.Value = 1; + _controllerRangeRight.Value = 1; + _controllerTriggerThreshold.Value = 0; + _mirrorInput.Active = false; + _enableMotion.Active = false; + _enableCemuHook.Active = false; + _slotNumber.Value = 0; + _altSlotNumber.Value = 0; + _sensitivity.Value = 100; + _gyroDeadzone.Value = 1; + _dsuServerHost.Buffer.Text = ""; + _dsuServerPort.Buffer.Text = ""; + _enableRumble.Active = false; + } + + private void SetValues(InputConfig config) + { + switch (config) + { + case StandardKeyboardInputConfig keyboardConfig: + if (!_controllerType.SetActiveId(keyboardConfig.ControllerType.ToString())) + { + _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld + ? ControllerType.Handheld.ToString() + : ControllerType.ProController.ToString()); + } + + _lStickUp.Label = keyboardConfig.LeftJoyconStick.StickUp.ToString(); + _lStickDown.Label = keyboardConfig.LeftJoyconStick.StickDown.ToString(); + _lStickLeft.Label = keyboardConfig.LeftJoyconStick.StickLeft.ToString(); + _lStickRight.Label = keyboardConfig.LeftJoyconStick.StickRight.ToString(); + _lStickButton.Label = keyboardConfig.LeftJoyconStick.StickButton.ToString(); + _dpadUp.Label = keyboardConfig.LeftJoycon.DpadUp.ToString(); + _dpadDown.Label = keyboardConfig.LeftJoycon.DpadDown.ToString(); + _dpadLeft.Label = keyboardConfig.LeftJoycon.DpadLeft.ToString(); + _dpadRight.Label = keyboardConfig.LeftJoycon.DpadRight.ToString(); + _minus.Label = keyboardConfig.LeftJoycon.ButtonMinus.ToString(); + _l.Label = keyboardConfig.LeftJoycon.ButtonL.ToString(); + _zL.Label = keyboardConfig.LeftJoycon.ButtonZl.ToString(); + _lSl.Label = keyboardConfig.LeftJoycon.ButtonSl.ToString(); + _lSr.Label = keyboardConfig.LeftJoycon.ButtonSr.ToString(); + _rStickUp.Label = keyboardConfig.RightJoyconStick.StickUp.ToString(); + _rStickDown.Label = keyboardConfig.RightJoyconStick.StickDown.ToString(); + _rStickLeft.Label = keyboardConfig.RightJoyconStick.StickLeft.ToString(); + _rStickRight.Label = keyboardConfig.RightJoyconStick.StickRight.ToString(); + _rStickButton.Label = keyboardConfig.RightJoyconStick.StickButton.ToString(); + _a.Label = keyboardConfig.RightJoycon.ButtonA.ToString(); + _b.Label = keyboardConfig.RightJoycon.ButtonB.ToString(); + _x.Label = keyboardConfig.RightJoycon.ButtonX.ToString(); + _y.Label = keyboardConfig.RightJoycon.ButtonY.ToString(); + _plus.Label = keyboardConfig.RightJoycon.ButtonPlus.ToString(); + _r.Label = keyboardConfig.RightJoycon.ButtonR.ToString(); + _zR.Label = keyboardConfig.RightJoycon.ButtonZr.ToString(); + _rSl.Label = keyboardConfig.RightJoycon.ButtonSl.ToString(); + _rSr.Label = keyboardConfig.RightJoycon.ButtonSr.ToString(); + break; + + case StandardControllerInputConfig controllerConfig: + if (!_controllerType.SetActiveId(controllerConfig.ControllerType.ToString())) + { + _controllerType.SetActiveId(_playerIndex == PlayerIndex.Handheld + ? ControllerType.Handheld.ToString() + : ControllerType.ProController.ToString()); + } + + _lStick.Label = controllerConfig.LeftJoyconStick.Joystick.ToString(); + _invertLStickX.Active = controllerConfig.LeftJoyconStick.InvertStickX; + _invertLStickY.Active = controllerConfig.LeftJoyconStick.InvertStickY; + _rotateL90CW.Active = controllerConfig.LeftJoyconStick.Rotate90CW; + _lStickButton.Label = controllerConfig.LeftJoyconStick.StickButton.ToString(); + _dpadUp.Label = controllerConfig.LeftJoycon.DpadUp.ToString(); + _dpadDown.Label = controllerConfig.LeftJoycon.DpadDown.ToString(); + _dpadLeft.Label = controllerConfig.LeftJoycon.DpadLeft.ToString(); + _dpadRight.Label = controllerConfig.LeftJoycon.DpadRight.ToString(); + _minus.Label = controllerConfig.LeftJoycon.ButtonMinus.ToString(); + _l.Label = controllerConfig.LeftJoycon.ButtonL.ToString(); + _zL.Label = controllerConfig.LeftJoycon.ButtonZl.ToString(); + _lSl.Label = controllerConfig.LeftJoycon.ButtonSl.ToString(); + _lSr.Label = controllerConfig.LeftJoycon.ButtonSr.ToString(); + _rStick.Label = controllerConfig.RightJoyconStick.Joystick.ToString(); + _invertRStickX.Active = controllerConfig.RightJoyconStick.InvertStickX; + _invertRStickY.Active = controllerConfig.RightJoyconStick.InvertStickY; + _rotateR90CW.Active = controllerConfig.RightJoyconStick.Rotate90CW; + _rStickButton.Label = controllerConfig.RightJoyconStick.StickButton.ToString(); + _a.Label = controllerConfig.RightJoycon.ButtonA.ToString(); + _b.Label = controllerConfig.RightJoycon.ButtonB.ToString(); + _x.Label = controllerConfig.RightJoycon.ButtonX.ToString(); + _y.Label = controllerConfig.RightJoycon.ButtonY.ToString(); + _plus.Label = controllerConfig.RightJoycon.ButtonPlus.ToString(); + _r.Label = controllerConfig.RightJoycon.ButtonR.ToString(); + _zR.Label = controllerConfig.RightJoycon.ButtonZr.ToString(); + _rSl.Label = controllerConfig.RightJoycon.ButtonSl.ToString(); + _rSr.Label = controllerConfig.RightJoycon.ButtonSr.ToString(); + _controllerStrongRumble.Value = controllerConfig.Rumble.StrongRumble; + _controllerWeakRumble.Value = controllerConfig.Rumble.WeakRumble; + _enableRumble.Active = controllerConfig.Rumble.EnableRumble; + _controllerDeadzoneLeft.Value = controllerConfig.DeadzoneLeft; + _controllerDeadzoneRight.Value = controllerConfig.DeadzoneRight; + _controllerRangeLeft.Value = controllerConfig.RangeLeft; + _controllerRangeRight.Value = controllerConfig.RangeRight; + _controllerTriggerThreshold.Value = controllerConfig.TriggerThreshold; + _sensitivity.Value = controllerConfig.Motion.Sensitivity; + _gyroDeadzone.Value = controllerConfig.Motion.GyroDeadzone; + _enableMotion.Active = controllerConfig.Motion.EnableMotion; + _enableCemuHook.Active = controllerConfig.Motion.MotionBackend == MotionInputBackendType.CemuHook; + + // If both stick ranges are 0 (usually indicative of an outdated profile load) then both sticks will be set to 1.0. + if (_controllerRangeLeft.Value <= 0.0 && _controllerRangeRight.Value <= 0.0) + { + _controllerRangeLeft.Value = 1.0; + _controllerRangeRight.Value = 1.0; + + Logger.Info?.Print(LogClass.Application, $"{config.PlayerIndex} stick range reset. Save the profile now to update your configuration"); + } + + if (controllerConfig.Motion is CemuHookMotionConfigController cemuHookMotionConfig) + { + _slotNumber.Value = cemuHookMotionConfig.Slot; + _altSlotNumber.Value = cemuHookMotionConfig.AltSlot; + _mirrorInput.Active = cemuHookMotionConfig.MirrorInput; + _dsuServerHost.Buffer.Text = cemuHookMotionConfig.DsuServerHost; + _dsuServerPort.Buffer.Text = cemuHookMotionConfig.DsuServerPort.ToString(); + } + + break; + } + } + + private InputConfig GetValues() + { + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { +#pragma warning disable CA1806, IDE0055 // Disable formatting + Enum.TryParse(_lStickUp.Label, out Key lStickUp); + Enum.TryParse(_lStickDown.Label, out Key lStickDown); + Enum.TryParse(_lStickLeft.Label, out Key lStickLeft); + Enum.TryParse(_lStickRight.Label, out Key lStickRight); + Enum.TryParse(_lStickButton.Label, out Key lStickButton); + Enum.TryParse(_dpadUp.Label, out Key lDPadUp); + Enum.TryParse(_dpadDown.Label, out Key lDPadDown); + Enum.TryParse(_dpadLeft.Label, out Key lDPadLeft); + Enum.TryParse(_dpadRight.Label, out Key lDPadRight); + Enum.TryParse(_minus.Label, out Key lButtonMinus); + Enum.TryParse(_l.Label, out Key lButtonL); + Enum.TryParse(_zL.Label, out Key lButtonZl); + Enum.TryParse(_lSl.Label, out Key lButtonSl); + Enum.TryParse(_lSr.Label, out Key lButtonSr); + + Enum.TryParse(_rStickUp.Label, out Key rStickUp); + Enum.TryParse(_rStickDown.Label, out Key rStickDown); + Enum.TryParse(_rStickLeft.Label, out Key rStickLeft); + Enum.TryParse(_rStickRight.Label, out Key rStickRight); + Enum.TryParse(_rStickButton.Label, out Key rStickButton); + Enum.TryParse(_a.Label, out Key rButtonA); + Enum.TryParse(_b.Label, out Key rButtonB); + Enum.TryParse(_x.Label, out Key rButtonX); + Enum.TryParse(_y.Label, out Key rButtonY); + Enum.TryParse(_plus.Label, out Key rButtonPlus); + Enum.TryParse(_r.Label, out Key rButtonR); + Enum.TryParse(_zR.Label, out Key rButtonZr); + Enum.TryParse(_rSl.Label, out Key rButtonSl); + Enum.TryParse(_rSr.Label, out Key rButtonSr); +#pragma warning restore CA1806, IDE0055 + + return new StandardKeyboardInputConfig + { + Backend = InputBackendType.WindowKeyboard, + Version = InputConfig.CurrentVersion, + Id = _inputDevice.ActiveId.Split("/")[1], + ControllerType = Enum.Parse<ControllerType>(_controllerType.ActiveId), + PlayerIndex = _playerIndex, + LeftJoycon = new LeftJoyconCommonConfig<Key> + { + ButtonMinus = lButtonMinus, + ButtonL = lButtonL, + ButtonZl = lButtonZl, + ButtonSl = lButtonSl, + ButtonSr = lButtonSr, + DpadUp = lDPadUp, + DpadDown = lDPadDown, + DpadLeft = lDPadLeft, + DpadRight = lDPadRight, + }, + LeftJoyconStick = new JoyconConfigKeyboardStick<Key> + { + StickUp = lStickUp, + StickDown = lStickDown, + StickLeft = lStickLeft, + StickRight = lStickRight, + StickButton = lStickButton, + }, + RightJoycon = new RightJoyconCommonConfig<Key> + { + ButtonA = rButtonA, + ButtonB = rButtonB, + ButtonX = rButtonX, + ButtonY = rButtonY, + ButtonPlus = rButtonPlus, + ButtonR = rButtonR, + ButtonZr = rButtonZr, + ButtonSl = rButtonSl, + ButtonSr = rButtonSr, + }, + RightJoyconStick = new JoyconConfigKeyboardStick<Key> + { + StickUp = rStickUp, + StickDown = rStickDown, + StickLeft = rStickLeft, + StickRight = rStickRight, + StickButton = rStickButton, + }, + }; + } + + if (_inputDevice.ActiveId.StartsWith("controller")) + { +#pragma warning disable CA1806, IDE0055 // Disable formatting + Enum.TryParse(_lStick.Label, out ConfigStickInputId lStick); + Enum.TryParse(_lStickButton.Label, out ConfigGamepadInputId lStickButton); + Enum.TryParse(_minus.Label, out ConfigGamepadInputId lButtonMinus); + Enum.TryParse(_l.Label, out ConfigGamepadInputId lButtonL); + Enum.TryParse(_zL.Label, out ConfigGamepadInputId lButtonZl); + Enum.TryParse(_lSl.Label, out ConfigGamepadInputId lButtonSl); + Enum.TryParse(_lSr.Label, out ConfigGamepadInputId lButtonSr); + Enum.TryParse(_dpadUp.Label, out ConfigGamepadInputId lDPadUp); + Enum.TryParse(_dpadDown.Label, out ConfigGamepadInputId lDPadDown); + Enum.TryParse(_dpadLeft.Label, out ConfigGamepadInputId lDPadLeft); + Enum.TryParse(_dpadRight.Label, out ConfigGamepadInputId lDPadRight); + + Enum.TryParse(_rStick.Label, out ConfigStickInputId rStick); + Enum.TryParse(_rStickButton.Label, out ConfigGamepadInputId rStickButton); + Enum.TryParse(_a.Label, out ConfigGamepadInputId rButtonA); + Enum.TryParse(_b.Label, out ConfigGamepadInputId rButtonB); + Enum.TryParse(_x.Label, out ConfigGamepadInputId rButtonX); + Enum.TryParse(_y.Label, out ConfigGamepadInputId rButtonY); + Enum.TryParse(_plus.Label, out ConfigGamepadInputId rButtonPlus); + Enum.TryParse(_r.Label, out ConfigGamepadInputId rButtonR); + Enum.TryParse(_zR.Label, out ConfigGamepadInputId rButtonZr); + Enum.TryParse(_rSl.Label, out ConfigGamepadInputId rButtonSl); + Enum.TryParse(_rSr.Label, out ConfigGamepadInputId rButtonSr); + + int.TryParse(_dsuServerPort.Buffer.Text, out int port); +#pragma warning restore CA1806, IDE0055 + + MotionConfigController motionConfig; + + if (_enableCemuHook.Active) + { + motionConfig = new CemuHookMotionConfigController + { + MotionBackend = MotionInputBackendType.CemuHook, + EnableMotion = _enableMotion.Active, + Sensitivity = (int)_sensitivity.Value, + GyroDeadzone = _gyroDeadzone.Value, + MirrorInput = _mirrorInput.Active, + Slot = (int)_slotNumber.Value, + AltSlot = (int)_altSlotNumber.Value, + DsuServerHost = _dsuServerHost.Buffer.Text, + DsuServerPort = port, + }; + } + else + { + motionConfig = new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + EnableMotion = _enableMotion.Active, + Sensitivity = (int)_sensitivity.Value, + GyroDeadzone = _gyroDeadzone.Value, + }; + } + + return new StandardControllerInputConfig + { + Backend = InputBackendType.GamepadSDL2, + Version = InputConfig.CurrentVersion, + Id = _inputDevice.ActiveId.Split("/")[1].Split(" ")[0], + ControllerType = Enum.Parse<ControllerType>(_controllerType.ActiveId), + PlayerIndex = _playerIndex, + DeadzoneLeft = (float)_controllerDeadzoneLeft.Value, + DeadzoneRight = (float)_controllerDeadzoneRight.Value, + RangeLeft = (float)_controllerRangeLeft.Value, + RangeRight = (float)_controllerRangeRight.Value, + TriggerThreshold = (float)_controllerTriggerThreshold.Value, + LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId> + { + ButtonMinus = lButtonMinus, + ButtonL = lButtonL, + ButtonZl = lButtonZl, + ButtonSl = lButtonSl, + ButtonSr = lButtonSr, + DpadUp = lDPadUp, + DpadDown = lDPadDown, + DpadLeft = lDPadLeft, + DpadRight = lDPadRight, + }, + LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId> + { + InvertStickX = _invertLStickX.Active, + Joystick = lStick, + InvertStickY = _invertLStickY.Active, + StickButton = lStickButton, + Rotate90CW = _rotateL90CW.Active, + }, + RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId> + { + ButtonA = rButtonA, + ButtonB = rButtonB, + ButtonX = rButtonX, + ButtonY = rButtonY, + ButtonPlus = rButtonPlus, + ButtonR = rButtonR, + ButtonZr = rButtonZr, + ButtonSl = rButtonSl, + ButtonSr = rButtonSr, + }, + RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId> + { + InvertStickX = _invertRStickX.Active, + Joystick = rStick, + InvertStickY = _invertRStickY.Active, + StickButton = rStickButton, + Rotate90CW = _rotateR90CW.Active, + }, + Motion = motionConfig, + Rumble = new RumbleConfigController + { + StrongRumble = (float)_controllerStrongRumble.Value, + WeakRumble = (float)_controllerWeakRumble.Value, + EnableRumble = _enableRumble.Active, + }, + }; + } + + if (!_inputDevice.ActiveId.StartsWith("disabled")) + { + GtkDialog.CreateErrorDialog("Invalid data detected in one or more fields; the configuration was not saved."); + } + + return null; + } + + private string GetProfileBasePath() + { + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + return System.IO.Path.Combine(AppDataManager.ProfilesDirPath, "keyboard"); + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + return System.IO.Path.Combine(AppDataManager.ProfilesDirPath, "controller"); + } + + return AppDataManager.ProfilesDirPath; + } + + // + // Events + // + private void InputDevice_Changed(object sender, EventArgs args) + { + SetAvailableOptions(); + SetControllerSpecificFields(); + + _selectedGamepad?.Dispose(); + _selectedGamepad = null; + + if (_inputDevice.ActiveId != null) + { + SetProfiles(); + + string id = GetCurrentGamepadId(); + + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + if (_inputConfig is StandardKeyboardInputConfig) + { + SetValues(_inputConfig); + } + + if (_mainWindow.InputManager.KeyboardDriver is GTK3KeyboardDriver) + { + // NOTE: To get input in this window, we need to bind a custom keyboard driver instead of using the InputManager one as the main window isn't focused... + _selectedGamepad = _gtk3KeyboardDriver.GetGamepad(id); + } + else + { + _selectedGamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id); + } + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + if (_inputConfig is StandardControllerInputConfig) + { + SetValues(_inputConfig); + } + + _selectedGamepad = _mainWindow.InputManager.GamepadDriver.GetGamepad(id); + } + } + } + + private string GetCurrentGamepadId() + { + if (_inputDevice.ActiveId == null || _inputDevice.ActiveId == "disabled") + { + return null; + } + + return _inputDevice.ActiveId.Split("/")[1].Split(" ")[0]; + } + + private void Controller_Changed(object sender, EventArgs args) + { + SetControllerSpecificFields(); + } + + private IButtonAssigner CreateButtonAssigner(bool forStick) + { + IButtonAssigner assigner; + + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + assigner = new KeyboardKeyAssigner((IKeyboard)_selectedGamepad); + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + assigner = new GamepadButtonAssigner(_selectedGamepad, (float)_controllerTriggerThreshold.Value, forStick); + } + else + { + throw new Exception("Controller not supported"); + } + + return assigner; + } + + private void HandleButtonPressed(ToggleButton button, bool forStick) + { + if (_isWaitingForInput) + { + button.Active = false; + + return; + } + + _mousePressed = false; + + ButtonPressEvent += MouseClick; + + IButtonAssigner assigner = CreateButtonAssigner(forStick); + + _isWaitingForInput = true; + + // Open GTK3 keyboard for cancel operations + IKeyboard keyboard = (IKeyboard)_gtk3KeyboardDriver.GetGamepad("0"); + + Thread inputThread = new(() => + { + assigner.Initialize(); + + while (true) + { + Thread.Sleep(10); + assigner.ReadInput(); + + if (_mousePressed || keyboard.IsPressed(Ryujinx.Input.Key.Escape) || assigner.HasAnyButtonPressed() || assigner.ShouldCancel()) + { + break; + } + } + + string pressedButton = assigner.GetPressedButton(); + + Application.Invoke(delegate + { + if (_middleMousePressed) + { + button.Label = "Unbound"; + } + else if (pressedButton != "") + { + button.Label = pressedButton; + } + + _middleMousePressed = false; + + ButtonPressEvent -= MouseClick; + keyboard.Dispose(); + + button.Active = false; + _isWaitingForInput = false; + }); + }) + { + Name = "GUI.InputThread", + IsBackground = true, + }; + inputThread.Start(); + } + + private void Button_Pressed(object sender, EventArgs args) + { + HandleButtonPressed((ToggleButton)sender, false); + } + + private void ButtonForStick_Pressed(object sender, EventArgs args) + { + HandleButtonPressed((ToggleButton)sender, true); + } + + private void MouseClick(object sender, ButtonPressEventArgs args) + { + _mousePressed = true; + _middleMousePressed = args.Event.Button == 2; + } + + private void SetProfiles() + { + _profile.RemoveAll(); + + string basePath = GetProfileBasePath(); + + if (!Directory.Exists(basePath)) + { + Directory.CreateDirectory(basePath); + } + + if (_inputDevice.ActiveId == null || _inputDevice.ActiveId.Equals("disabled")) + { + _profile.Append("default", "None"); + } + else + { + _profile.Append("default", "Default"); + + foreach (string profile in Directory.GetFiles(basePath, "*.*", SearchOption.AllDirectories)) + { + _profile.Append(System.IO.Path.GetFileName(profile), System.IO.Path.GetFileNameWithoutExtension(profile)); + } + } + + _profile.SetActiveId("default"); + } + + private void ProfileLoad_Activated(object sender, EventArgs args) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == null) + { + return; + } + + InputConfig config = null; + int pos = _profile.Active; + + if (_profile.ActiveId == "default") + { + if (_inputDevice.ActiveId.StartsWith("keyboard")) + { + config = new StandardKeyboardInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.WindowKeyboard, + Id = null, + ControllerType = ControllerType.ProController, + LeftJoycon = new LeftJoyconCommonConfig<Key> + { + DpadUp = Key.Up, + DpadDown = Key.Down, + DpadLeft = Key.Left, + DpadRight = Key.Right, + ButtonMinus = Key.Minus, + ButtonL = Key.E, + ButtonZl = Key.Q, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + + LeftJoyconStick = new JoyconConfigKeyboardStick<Key> + { + StickUp = Key.W, + StickDown = Key.S, + StickLeft = Key.A, + StickRight = Key.D, + StickButton = Key.F, + }, + + RightJoycon = new RightJoyconCommonConfig<Key> + { + ButtonA = Key.Z, + ButtonB = Key.X, + ButtonX = Key.C, + ButtonY = Key.V, + ButtonPlus = Key.Plus, + ButtonR = Key.U, + ButtonZr = Key.O, + ButtonSl = Key.Unbound, + ButtonSr = Key.Unbound, + }, + + RightJoyconStick = new JoyconConfigKeyboardStick<Key> + { + StickUp = Key.I, + StickDown = Key.K, + StickLeft = Key.J, + StickRight = Key.L, + StickButton = Key.H, + }, + }; + } + else if (_inputDevice.ActiveId.StartsWith("controller")) + { + bool isNintendoStyle = _inputDevice.ActiveText.Contains("Nintendo"); + + config = new StandardControllerInputConfig + { + Version = InputConfig.CurrentVersion, + Backend = InputBackendType.GamepadSDL2, + Id = null, + ControllerType = ControllerType.JoyconPair, + DeadzoneLeft = 0.1f, + DeadzoneRight = 0.1f, + RangeLeft = 1.0f, + RangeRight = 1.0f, + TriggerThreshold = 0.5f, + LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId> + { + DpadUp = ConfigGamepadInputId.DpadUp, + DpadDown = ConfigGamepadInputId.DpadDown, + DpadLeft = ConfigGamepadInputId.DpadLeft, + DpadRight = ConfigGamepadInputId.DpadRight, + ButtonMinus = ConfigGamepadInputId.Minus, + ButtonL = ConfigGamepadInputId.LeftShoulder, + ButtonZl = ConfigGamepadInputId.LeftTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound, + }, + + LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId> + { + Joystick = ConfigStickInputId.Left, + StickButton = ConfigGamepadInputId.LeftStick, + InvertStickX = false, + InvertStickY = false, + Rotate90CW = false, + }, + + RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId> + { + ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B, + ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A, + ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y, + ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X, + ButtonPlus = ConfigGamepadInputId.Plus, + ButtonR = ConfigGamepadInputId.RightShoulder, + ButtonZr = ConfigGamepadInputId.RightTrigger, + ButtonSl = ConfigGamepadInputId.Unbound, + ButtonSr = ConfigGamepadInputId.Unbound, + }, + + RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId> + { + Joystick = ConfigStickInputId.Right, + StickButton = ConfigGamepadInputId.RightStick, + InvertStickX = false, + InvertStickY = false, + Rotate90CW = false, + }, + + Motion = new StandardMotionConfigController + { + MotionBackend = MotionInputBackendType.GamepadDriver, + EnableMotion = true, + Sensitivity = 100, + GyroDeadzone = 1, + }, + Rumble = new RumbleConfigController + { + StrongRumble = 1f, + WeakRumble = 1f, + EnableRumble = false, + }, + }; + } + } + else + { + string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId); + + if (!File.Exists(path)) + { + if (pos >= 0) + { + _profile.Remove(pos); + } + + return; + } + + try + { + config = JsonHelper.DeserializeFromFile(path, _serializerContext.InputConfig); + } + catch (JsonException) { } + } + + SetValues(config); + } + + private void ProfileAdd_Activated(object sender, EventArgs args) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + if (_inputDevice.ActiveId == "disabled") + { + return; + } + + InputConfig inputConfig = GetValues(); + ProfileDialog profileDialog = new(); + + if (inputConfig == null) + { + return; + } + + if (profileDialog.Run() == (int)ResponseType.Ok) + { + string path = System.IO.Path.Combine(GetProfileBasePath(), profileDialog.FileName); + string jsonString = JsonHelper.Serialize(inputConfig, _serializerContext.InputConfig); + + File.WriteAllText(path, jsonString); + } + + profileDialog.Dispose(); + + SetProfiles(); + } + + private void ProfileRemove_Activated(object sender, EventArgs args) + { + ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true); + + if (_inputDevice.ActiveId == "disabled" || _profile.ActiveId == "default" || _profile.ActiveId == null) + { + return; + } + + MessageDialog confirmDialog = GtkDialog.CreateConfirmationDialog("Deleting Profile", "This action is irreversible, are you sure you want to continue?"); + + if (confirmDialog.Run() == (int)ResponseType.Yes) + { + string path = System.IO.Path.Combine(GetProfileBasePath(), _profile.ActiveId); + + if (File.Exists(path)) + { + File.Delete(path); + } + + SetProfiles(); + } + } + + private void SaveToggle_Activated(object sender, EventArgs args) + { + InputConfig inputConfig = GetValues(); + + var newConfig = new List<InputConfig>(); + newConfig.AddRange(ConfigurationState.Instance.Hid.InputConfig.Value); + + if (_inputConfig == null && inputConfig != null) + { + newConfig.Add(inputConfig); + } + else + { + if (_inputDevice.ActiveId == "disabled") + { + newConfig.Remove(_inputConfig); + } + else if (inputConfig != null) + { + int index = newConfig.IndexOf(_inputConfig); + + newConfig[index] = inputConfig; + } + } + + _mainWindow.RendererWidget?.NpadManager.ReloadConfiguration(newConfig, ConfigurationState.Instance.Hid.EnableKeyboard, ConfigurationState.Instance.Hid.EnableMouse); + + // Atomically replace and signal input change. + // NOTE: Do not modify InputConfig.Value directly as other code depends on the on-change event. + ConfigurationState.Instance.Hid.InputConfig.Value = newConfig; + + ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath); + + Dispose(); + } + + private void CloseToggle_Activated(object sender, EventArgs args) + { + Dispose(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.glade new file mode 100644 index 00000000..e433f5cc --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/ControllerWindow.glade @@ -0,0 +1,2241 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.22.1 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkAdjustment" id="_altSlotNumber"> + <property name="upper">4</property> + <property name="step_increment">1</property> + <property name="page_increment">4</property> + </object> + <object class="GtkAdjustment" id="_controllerStrongRumble"> + <property name="lower">0.1</property> + <property name="upper">10</property> + <property name="value">1.0</property> + <property name="step_increment">0.1</property> + <property name="page_increment">1.0</property> + </object> + <object class="GtkAdjustment" id="_controllerWeakRumble"> + <property name="lower">0.1</property> + <property name="upper">10</property> + <property name="value">1.0</property> + <property name="step_increment">0.1</property> + <property name="page_increment">1.0</property> + </object> + <object class="GtkAdjustment" id="_controllerDeadzoneLeft"> + <property name="upper">1</property> + <property name="value">0.050000000000000003</property> + <property name="step_increment">0.01</property> + <property name="page_increment">0.10000000000000001</property> + </object> + <object class="GtkAdjustment" id="_controllerDeadzoneRight"> + <property name="upper">1</property> + <property name="value">0.050000000000000003</property> + <property name="step_increment">0.01</property> + <property name="page_increment">0.10000000000000001</property> + </object> + <object class="GtkAdjustment" id="_controllerRangeLeft"> + <property name="upper">2</property> + <property name="value">1.000000000000000003</property> + <property name="step_increment">0.01</property> + <property name="page_increment">0.10000000000000001</property> + </object> + <object class="GtkAdjustment" id="_controllerRangeRight"> + <property name="upper">2</property> + <property name="value">1.000000000000000003</property> + <property name="step_increment">0.01</property> + <property name="page_increment">0.10000000000000001</property> + </object> + <object class="GtkAdjustment" id="_controllerTriggerThreshold"> + <property name="upper">1</property> + <property name="value">0.5</property> + <property name="step_increment">0.01</property> + <property name="page_increment">0.10000000000000001</property> + </object> + <object class="GtkAdjustment" id="_gyroDeadzone"> + <property name="upper">100</property> + <property name="value">0.01</property> + <property name="step_increment">0.01</property> + <property name="page_increment">0.10000000000000001</property> + <property name="page_size">0.10000000000000001</property> + </object> + <object class="GtkAdjustment" id="_sensitivity"> + <property name="upper">1000</property> + <property name="value">100</property> + <property name="step_increment">1</property> + <property name="page_increment">4</property> + </object> + <object class="GtkAdjustment" id="_slotNumber"> + <property name="upper">4</property> + <property name="step_increment">1</property> + <property name="page_increment">4</property> + </object> + <object class="GtkWindow" id="_controllerWin"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Ryujinx - Controller Settings</property> + <property name="modal">True</property> + <property name="window_position">center</property> + <property name="default_width">1200</property> + <property name="default_height">720</property> + <child type="titlebar"> + <placeholder/> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="HeadBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <property name="margin_top">10</property> + <property name="margin_bottom">10</property> + <child> + <object class="GtkBox" id="DeviceBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_right">5</property> + <property name="label" translatable="yes">Input Device</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_inputDevice"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="active">0</property> + <property name="active_id">disabled</property> + <items> + <item id="disabled" translatable="yes">Disabled</item> + </items> + <signal name="changed" handler="InputDevice_Changed" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="ControllerTypeBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">20</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The controller's type</property> + <property name="halign">center</property> + <property name="margin_right">5</property> + <property name="label" translatable="yes">Controller Type:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_controllerType"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="tooltip_text" translatable="yes">The controller's type</property> + <property name="active">0</property> + <signal name="changed" handler="Controller_Changed" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="ProfileBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">20</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_right">5</property> + <property name="label" translatable="yes">Profile:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_profile"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_right">5</property> + <property name="active">0</property> + <property name="active_id">default</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton"> + <property name="label" translatable="yes">Load</property> + <property name="width_request">60</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="margin_right">5</property> + <signal name="toggled" handler="ProfileLoad_Activated" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkToggleButton"> + <property name="label" translatable="yes">Add</property> + <property name="width_request">60</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="margin_right">5</property> + <signal name="toggled" handler="ProfileAdd_Activated" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkToggleButton"> + <property name="label" translatable="yes">Remove</property> + <property name="width_request">60</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="toggled" handler="ProfileRemove_Activated" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_settingsBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <property name="margin_top">5</property> + <child> + <object class="GtkBox" id="ButtonsBox"> + <property name="width_request">156</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + <property name="label" translatable="yes">Buttons</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">A</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">B</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">X</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Y</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_a"> + <property name="label" translatable="yes"> </property> + <property name="width_request">70</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_b"> + <property name="label" translatable="yes"> </property> + <property name="width_request">70</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_x"> + <property name="label" translatable="yes"> </property> + <property name="width_request">70</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_y"> + <property name="label" translatable="yes"> </property> + <property name="width_request">70</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">+</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">4</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">-</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_minus"> + <property name="label" translatable="yes"> </property> + <property name="width_request">70</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">5</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_plus"> + <property name="label" translatable="yes"> </property> + <property name="width_request">70</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="LeftStickBox"> + <property name="width_request">160</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <property name="margin_right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + <property name="label" translatable="yes">Left Stick</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_bottom">5</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">LStick Button</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_lStickButton"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="_leftStickKeyboard"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkToggleButton" id="_lStickDown"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_lStickUp"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_lStickLeft"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_lStickRight"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">LStick Down</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">LStick Up</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">LStick Right</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">LStick Left</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="_leftStickController"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">LStick</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_lStick"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_invertLStickX"> + <property name="label" translatable="yes">Invert Stick X</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_invertLStickY"> + <property name="label" translatable="yes">Invert Stick Y</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_rotateL90CW"> + <property name="label" translatable="yes">Rotate 90° Clockwise</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_deadZoneLeftBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Deadzone Left</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScale"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">_controllerDeadzoneLeft</property> + <property name="round_digits">2</property> + <property name="digits">2</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_rangeLeftBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Range Left</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScale"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">_controllerRangeLeft</property> + <property name="round_digits">2</property> + <property name="digits">2</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox" id="TriggerBox"> + <property name="width_request">150</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <property name="margin_right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + <property name="label" translatable="yes">Triggers</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">L</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">R</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_l"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_r"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">ZL</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">ZR</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_zL"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_zR"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="_leftSideTriggerBox"> + <property name="name">_sideTriggerBox</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">5</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Left SL</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Left SR</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_lSl"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_lSr"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="_rightSideTriggerBox"> + <property name="name">_sideTriggerBox</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">5</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Right SL</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Right SR</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_rSl"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_rSr"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_triggerThresholdBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="margin_left">10</property> + <property name="label" translatable="yes">Trigger Threshold</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScale"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">_controllerTriggerThreshold</property> + <property name="round_digits">2</property> + <property name="digits">2</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <child> + <object class="GtkBox" id="DPadBox"> + <property name="width_request">156</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_right">10</property> + <property name="margin_top">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + <property name="label" translatable="yes">Directional Pad</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Dpad Up</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Dpad Down</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Dpad Left</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Dpad Right</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_dpadUp"> + <property name="label" translatable="yes"> </property> + <property name="width_request">70</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_dpadDown"> + <property name="label" translatable="yes"> </property> + <property name="width_request">70</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_dpadLeft"> + <property name="label" translatable="yes"> </property> + <property name="width_request">70</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_dpadRight"> + <property name="label" translatable="yes"> </property> + <property name="width_request">70</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_rumbleBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="margin_bottom">5</property> + <property name="label" translatable="yes">Rumble</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_enableRumble"> + <property name="label" translatable="yes">Enable</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_StrongMultiBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Strong rumble multiplier</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScale"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">_controllerStrongRumble</property> + <property name="round_digits">1</property> + <property name="digits">1</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_WeakMultiBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Weak rumble multiplier</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScale"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">_controllerWeakRumble</property> + <property name="round_digits">1</property> + <property name="digits">1</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="RightStickBox"> + <property name="width_request">160</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <property name="margin_right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + <property name="label" translatable="yes">Right Stick</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkGrid"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_bottom">5</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">RStick Button</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_rStickButton"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="_rightStickKeyboard"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">RStick Up</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">RStick Down</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">RStick Left</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">RStick Right</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">3</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_rStickUp"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_rStickDown"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_rStickLeft"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">2</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_rStickRight"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkGrid" id="_rightStickController"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="row_spacing">3</property> + <property name="column_spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="width_request">80</property> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">RStick</property> + <property name="xalign">0</property> + </object> + <packing> + <property name="left_attach">0</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_rStick"> + <property name="label" translatable="yes"> </property> + <property name="width_request">65</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + </object> + <packing> + <property name="left_attach">1</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_invertRStickX"> + <property name="label" translatable="yes">Invert Stick X</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_invertRStickY"> + <property name="label" translatable="yes">Invert Stick Y</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_rotateR90CW"> + <property name="label" translatable="yes">Rotate 90° Clockwise</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="left_attach">2</property> + <property name="top_attach">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_deadZoneRightBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Deadzone Right</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScale"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">_controllerDeadzoneRight</property> + <property name="round_digits">2</property> + <property name="digits">2</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_rangeRightBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Range Right</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScale"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">_controllerRangeRight</property> + <property name="round_digits">2</property> + <property name="digits">2</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can_focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_motionBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <property name="margin_right">10</property> + <property name="orientation">vertical</property> + <property name="spacing">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + <property name="label" translatable="yes">Motion</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_enableMotion"> + <property name="label" translatable="yes">Enable Motion Controls</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_enableCemuHook"> + <property name="label" translatable="yes">Use CemuHook compatible motion</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_motionControllerSlot"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_right">17</property> + <property name="label" translatable="yes">Controller Slot</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="_slot"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="margin_left">10</property> + <property name="adjustment">_slotNumber</property> + <property name="climb_rate">1</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_right">5</property> + <property name="label" translatable="yes">Gyro Sensitivity %</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="text" translatable="yes">0</property> + <property name="adjustment">_sensitivity</property> + <property name="climb_rate">1</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_motionAltBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="_mirrorInput"> + <property name="label" translatable="yes">Mirror Input</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">10</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Right JoyCon Slot</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="_slotRight"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="text" translatable="yes">0</property> + <property name="adjustment">_altSlotNumber</property> + <property name="climb_rate">1</property> + <property name="snap_to_ticks">True</property> + <property name="numeric">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_dsuServerHostBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">30</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Server Host</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="_dsuServerHost"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">6</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_dsuServerPortBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="spacing">30</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="label" translatable="yes">Server Port</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="_dsuServerPort"> + <property name="visible">True</property> + <property name="can_focus">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">7</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="halign">start</property> + <property name="label" translatable="yes">Gyro Deadzone</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">8</property> + </packing> + </child> + <child> + <object class="GtkScale"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="adjustment">_gyroDeadzone</property> + <property name="round_digits">2</property> + <property name="digits">2</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">9</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkImage" id="_controllerImage"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <property name="margin_right">20</property> + <property name="margin_top">5</property> + <property name="margin_bottom">5</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButtonBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_right">5</property> + <property name="margin_top">3</property> + <property name="margin_bottom">3</property> + <property name="layout_style">end</property> + <child> + <object class="GtkToggleButton" id="SaveToggle"> + <property name="label" translatable="yes">Save</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <signal name="toggled" handler="SaveToggle_Activated" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="CloseToggle"> + <property name="label" translatable="yes">Close</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="margin_left">4</property> + <signal name="toggled" handler="CloseToggle_Activated" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs new file mode 100644 index 00000000..388f1108 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs @@ -0,0 +1,280 @@ +using Gtk; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Tools.Fs; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using GUI = Gtk.Builder.ObjectAttribute; + +namespace Ryujinx.UI.Windows +{ + public class DlcWindow : Window + { + private readonly VirtualFileSystem _virtualFileSystem; + private readonly string _titleId; + private readonly string _dlcJsonPath; + private readonly List<DownloadableContentContainer> _dlcContainerList; + + private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + +#pragma warning disable CS0649, IDE0044 // 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 titleId, string titleName) : this(new Builder("Ryujinx.Gtk3.UI.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { } + + private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow")) + { + builder.Autoconnect(this); + + _titleId = titleId; + _virtualFileSystem = virtualFileSystem; + _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json"); + _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]"; + + try + { + _dlcContainerList = JsonHelper.DeserializeFromFile(_dlcJsonPath, _serializerContext.ListDownloadableContentContainer); + } + catch + { + _dlcContainerList = new List<DownloadableContentContainer>(); + } + + _dlcTreeView.Model = new TreeStore(typeof(bool), typeof(string), typeof(string)); + + CellRendererToggle enableToggle = new(); + enableToggle.Toggled += (sender, args) => + { + _dlcTreeView.Model.GetIter(out TreeIter treeIter, new TreePath(args.Path)); + bool newValue = !(bool)_dlcTreeView.Model.GetValue(treeIter, 0); + _dlcTreeView.Model.SetValue(treeIter, 0, newValue); + + if (_dlcTreeView.Model.IterChildren(out TreeIter childIter, treeIter)) + { + do + { + _dlcTreeView.Model.SetValue(childIter, 0, newValue); + } + while (_dlcTreeView.Model.IterNext(ref childIter)); + } + }; + + _dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0); + _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1); + _dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2); + + foreach (DownloadableContentContainer dlcContainer in _dlcContainerList) + { + if (File.Exists(dlcContainer.ContainerPath)) + { + // The parent tree item has its own "enabled" check box, but it's the actual + // nca entries that store the enabled / disabled state. A bit of a UI inconsistency. + // Maybe a tri-state check box would be better, but for now we check the parent + // "enabled" box if all child NCAs are enabled. Usually fine since each nsp has only one nca. + bool areAllContentPacksEnabled = dlcContainer.DownloadableContentNcaList.TrueForAll((nca) => nca.Enabled); + TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.ContainerPath); + + using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath); + + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + + _virtualFileSystem.ImportTickets(pfs); + + foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList) + { + using var ncaFile = new UniqueRef<IFile>(); + + pfs.OpenFile(ref ncaFile.Ref, dlcNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.ContainerPath); + + if (nca != null) + { + ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter, dlcNca.Enabled, nca.Header.TitleId.ToString("X16"), dlcNca.FullPath); + } + } + } + else + { + // DLC file moved or renamed. Allow the user to remove it without crashing the whole dialog. + TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.ContainerPath}"); + } + } + } + + private Nca TryCreateNca(IStorage ncaStorage, string containerPath) + { + try + { + return new Nca(_virtualFileSystem.KeySet, ncaStorage); + } + catch (Exception exception) + { + GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {containerPath}"); + } + + return null; + } + + private void AddButton_Clicked(object sender, EventArgs args) + { + FileChooserNative fileChooser = new("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) + { + if (!File.Exists(containerPath)) + { + return; + } + + using FileStream containerFile = File.OpenRead(containerPath); + + PartitionFileSystem pfs = new(); + pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure(); + bool containsDlc = false; + + _virtualFileSystem.ImportTickets(pfs); + + TreeIter? parentIter = null; + + foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) + { + using var ncaFile = new UniqueRef<IFile>(); + + pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure(); + + Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath); + + if (nca == null) + { + continue; + } + + if (nca.Header.ContentType == NcaContentType.PublicData) + { + if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId) + { + break; + } + + parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath); + + ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath); + containsDlc = true; + } + } + + if (!containsDlc) + { + GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!"); + } + } + } + + fileChooser.Dispose(); + } + + private void RemoveButton_Clicked(object sender, EventArgs args) + { + if (_dlcTreeSelection.GetSelected(out ITreeModel treeModel, out TreeIter treeIter)) + { + if (_dlcTreeView.Model.IterParent(out TreeIter parentIter, treeIter) && _dlcTreeView.Model.IterNChildren(parentIter) <= 1) + { + ((TreeStore)treeModel).Remove(ref parentIter); + } + else + { + ((TreeStore)treeModel).Remove(ref treeIter); + } + } + } + + private void RemoveAllButton_Clicked(object sender, EventArgs args) + { + List<TreeIter> toRemove = new(); + + 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(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.glade new file mode 100644 index 00000000..bdb0e647 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.glade @@ -0,0 +1,202 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.36.0 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkWindow" id="_dlcWindow"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Ryujinx - DLC Manager</property> + <property name="modal">True</property> + <property name="window_position">center</property> + <property name="default_width">550</property> + <property name="default_height">350</property> + <child> + <object class="GtkBox" id="MainBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="DlcBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="_baseTitleInfoLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <property name="margin_right">10</property> + <property name="margin_top">10</property> + <property name="margin_bottom">10</property> + <property name="label" translatable="yes">Available DLC</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="margin_left">10</property> + <property name="margin_right">10</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkTreeView" id="_dlcTreeView"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="headers_clickable">False</property> + <child internal-child="selection"> + <object class="GtkTreeSelection" id="_dlcTreeSelection"/> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkButtonBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="margin_bottom">10</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="_addUpdate"> + <property name="label" translatable="yes">Add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Adds 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> diff --git a/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs new file mode 100644 index 00000000..dc467c0f --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.cs @@ -0,0 +1,847 @@ +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(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade new file mode 100644 index 00000000..f0dbd6b6 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/SettingsWindow.glade @@ -0,0 +1,3221 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.40.0 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkAdjustment" id="_fsLogSpinAdjustment"> + <property name="upper">3</property> + <property name="step-increment">1</property> + <property name="page-increment">10</property> + </object> + <object class="GtkAdjustment" id="_scalingFilterLevel"> + <property name="upper">101</property> + <property name="step-increment">1</property> + <property name="page-increment">5</property> + <property name="page-size">1</property> + </object> + <object class="GtkAdjustment" id="_systemTimeDaySpinAdjustment"> + <property name="lower">1</property> + <property name="upper">31</property> + <property name="step-increment">1</property> + <property name="page-increment">5</property> + </object> + <object class="GtkAdjustment" id="_systemTimeHourSpinAdjustment"> + <property name="upper">23</property> + <property name="step-increment">1</property> + <property name="page-increment">5</property> + </object> + <object class="GtkAdjustment" id="_systemTimeMinuteSpinAdjustment"> + <property name="upper">59</property> + <property name="step-increment">1</property> + <property name="page-increment">5</property> + </object> + <object class="GtkAdjustment" id="_systemTimeMonthSpinAdjustment"> + <property name="lower">1</property> + <property name="upper">12</property> + <property name="step-increment">1</property> + <property name="page-increment">5</property> + </object> + <object class="GtkAdjustment" id="_systemTimeYearSpinAdjustment"> + <property name="lower">2000</property> + <property name="upper">2060</property> + <property name="step-increment">1</property> + <property name="page-increment">10</property> + </object> + <object class="GtkEntryCompletion" id="_systemTimeZoneCompletion"> + <property name="minimum-key-length">0</property> + <property name="inline-completion">True</property> + <property name="inline-selection">True</property> + </object> + <object class="GtkWindow" id="_settingsWin"> + <property name="can-focus">False</property> + <property name="title" translatable="yes">Ryujinx - Settings</property> + <property name="modal">True</property> + <property name="window-position">center</property> + <property name="default-width">650</property> + <property name="default-height">650</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="shadow-type">in</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkNotebook"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <child> + <object class="GtkBox" id="TabGeneral"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">10</property> + <property name="margin-top">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="CatGeneral"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">General</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="General"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="_discordToggle"> + <property name="label" translatable="yes">Enable Discord Rich Presence</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Choose whether or not to display Ryujinx on your "currently playing" Discord activity</property> + <property name="halign">start</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_checkUpdatesToggle"> + <property name="label" translatable="yes">Check for Updates on Launch</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="halign">start</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_showConfirmExitToggle"> + <property name="label" translatable="yes">Show "Confirm Exit" Dialog</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="halign">start</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_hideCursorBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">end</property> + <property name="label" translatable="yes">Hide Cursor:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="_hideCursorNever"> + <property name="label" translatable="yes">Never</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</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">3</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="_hideCursorOnIdle"> + <property name="label" translatable="yes">On Idle</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + <property name="group">_hideCursorNever</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="_hideCursorAlways"> + <property name="label" translatable="yes">Always</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + <property name="group">_hideCursorNever</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="CatGameDir"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">Game Directories</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="margin-bottom">10</property> + <property name="shadow-type">in</property> + <child> + <object class="GtkTreeView" id="_gameDirsBox"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="headers-visible">False</property> + <property name="headers-clickable">False</property> + <child internal-child="selection"> + <object class="GtkTreeSelection"/> + </child> + </object> + </child> + <style> + <class name="GameDir"/> + </style> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkEntry" id="_addGameDirBox"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="tooltip-text" translatable="yes">Enter a game directory to add to the list</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_addDir"> + <property name="label" translatable="yes">Add</property> + <property name="width-request">80</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="tooltip-text" translatable="yes"> Add a game directory to the list</property> + <property name="margin-left">5</property> + <signal name="toggled" handler="AddDir_Pressed" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_removeDir"> + <property name="label" translatable="yes">Remove</property> + <property name="width-request">80</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="tooltip-text" translatable="yes">Remove selected game directory</property> + <property name="margin-left">5</property> + <signal name="toggled" handler="RemoveDir_Pressed" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkBox" id="CatThemes"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">Themes</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="_custThemeToggle"> + <property name="label" translatable="yes">Use Custom Theme</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Enable or disable custom themes in the GUI</property> + <property name="halign">start</property> + <property name="draw-indicator">True</property> + <signal name="toggled" handler="CustThemeToggle_Activated" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel" id="_custThemePathLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Path to custom GUI theme</property> + <property name="label" translatable="yes">Custom Theme Path:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="_custThemePath"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="tooltip-text" translatable="yes">Path to custom GUI theme</property> + <property name="valign">center</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_browseThemePath"> + <property name="label" translatable="yes">Browse...</property> + <property name="width-request">80</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="tooltip-text" translatable="yes">Browse for a custom GUI theme</property> + <property name="margin-left">5</property> + <signal name="toggled" handler="BrowseThemeDir_Pressed" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">10</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">6</property> + </packing> + </child> + </object> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">General</property> + </object> + <packing> + <property name="tab-fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="TabInput"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">10</property> + <property name="margin-top">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <child> + <object class="GtkCheckButton" id="_dockedModeToggle"> + <property name="label" translatable="yes">Enable Docked Mode</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Docked mode makes the emulated system behave as a docked Nintendo Switch. This improves graphical fidelity in most games. Conversely, disabling this will make the emulated system behave as a handheld Nintendo Switch, reducing graphics quality. Configure player 1 controls if planning to use docked mode; configure handheld controls if planning to use handheld mode. Leave ON if unsure.</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">10</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_directKeyboardAccess"> + <property name="label" translatable="yes">Direct Keyboard Access</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device.</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">10</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_directMouseAccess"> + <property name="label" translatable="yes">Direct Mouse Access</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Direct mouse access (HID) support. Provides games access to your mouse as a pointing device.</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="padding">10</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <!-- n-columns=5 n-rows=5 --> + <object class="GtkGrid" id="ControllerGrid"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">center</property> + <property name="valign">center</property> + <property name="column-spacing">20</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + <property name="label" translatable="yes">Player 1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_configureController1"> + <property name="label" translatable="yes">Configure</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="margin-left">20</property> + <property name="margin-right">20</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + <property name="label" translatable="yes">Player 3</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_configureController3"> + <property name="label" translatable="yes">Configure</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="margin-left">20</property> + <property name="margin-right">20</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">4</property> + <property name="top-attach">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + <property name="label" translatable="yes">Player 2</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_configureController2"> + <property name="label" translatable="yes">Configure</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="margin-left">20</property> + <property name="margin-right">20</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + <property name="label" translatable="yes">Handheld</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_configureControllerH"> + <property name="label" translatable="yes">Configure</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="margin-left">20</property> + <property name="margin-right">20</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">4</property> + <property name="top-attach">4</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + <property name="label" translatable="yes">Player 6</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_configureController6"> + <property name="label" translatable="yes">Configure</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="margin-left">20</property> + <property name="margin-right">20</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">4</property> + <property name="top-attach">2</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + <property name="label" translatable="yes">Player 5</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_configureController5"> + <property name="label" translatable="yes">Configure</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="margin-left">20</property> + <property name="margin-right">20</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">2</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + <property name="label" translatable="yes">Player 7</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_configureController7"> + <property name="label" translatable="yes">Configure</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="margin-left">20</property> + <property name="margin-right">20</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">4</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + <property name="label" translatable="yes">Player 4</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_configureController4"> + <property name="label" translatable="yes">Configure</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="margin-left">20</property> + <property name="margin-right">20</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">2</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + <property name="label" translatable="yes">Player 8</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="_configureController8"> + <property name="label" translatable="yes">Configure</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <property name="margin-left">20</property> + <property name="margin-right">20</property> + <property name="margin-top">20</property> + <property name="margin-bottom">20</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">4</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">3</property> + <property name="top-attach">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">3</property> + <property name="top-attach">2</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">3</property> + <property name="top-attach">4</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">2</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">4</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">1</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">1</property> + <property name="top-attach">3</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">3</property> + <property name="top-attach">1</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">3</property> + <property name="top-attach">3</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">1</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">1</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">4</property> + <property name="top-attach">1</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">0</property> + <property name="top-attach">3</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">2</property> + <property name="top-attach">3</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="left-attach">4</property> + <property name="top-attach">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="position">1</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Input</property> + </object> + <packing> + <property name="position">1</property> + <property name="tab-fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="TabSystem"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">10</property> + <property name="margin-top">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="CatCore"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">Core</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="RegionBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Change System Region</property> + <property name="halign">end</property> + <property name="label" translatable="yes">System Region:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_systemRegionSelect"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Change System Region</property> + <property name="margin-left">5</property> + <items> + <item id="Japan" translatable="yes">Japan</item> + <item id="USA" translatable="yes">USA</item> + <item id="Europe" translatable="yes">Europe</item> + <item id="Australia" translatable="yes">Australia</item> + <item id="China" translatable="yes">China</item> + <item id="Korea" translatable="yes">Korea</item> + <item id="Taiwan" translatable="yes">Taiwan</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="LanguageBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Change System Language</property> + <property name="halign">end</property> + <property name="label" translatable="yes">System Language:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_systemLanguageSelect"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Change System Language</property> + <items> + <item id="AmericanEnglish" translatable="yes">American English</item> + <item id="BritishEnglish" translatable="yes">British English</item> + <item id="CanadianFrench" translatable="yes">Canadian French</item> + <item id="Chinese" translatable="yes">Chinese</item> + <item id="Dutch" translatable="yes">Dutch</item> + <item id="French" translatable="yes">French</item> + <item id="German" translatable="yes">German</item> + <item id="Italian" translatable="yes">Italian</item> + <item id="Japanese" translatable="yes">Japanese</item> + <item id="Korean" translatable="yes">Korean</item> + <item id="LatinAmericanSpanish" translatable="yes">Latin American Spanish</item> + <item id="Portuguese" translatable="yes">Portuguese</item> + <item id="Russian" translatable="yes">Russian</item> + <item id="SimplifiedChinese" translatable="yes">Simplified Chinese</item> + <item id="Spanish" translatable="yes">Spanish</item> + <item id="Taiwanese" translatable="yes">Taiwanese</item> + <item id="TraditionalChinese" translatable="yes">Traditional Chinese</item> + <item id="BrazilianPortuguese" translatable="yes">Brazilian Portuguese</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="TimeZoneBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Change System TimeZone</property> + <property name="halign">end</property> + <property name="label" translatable="yes">System TimeZone:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="_systemTimeZoneEntry"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="tooltip-text" translatable="yes">Change System TimeZone</property> + <property name="margin-left">5</property> + <property name="completion">_systemTimeZoneCompletion</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="TimeBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Change System Time</property> + <property name="halign">end</property> + <property name="label" translatable="yes">System Time:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="_systemTimeYearSpin"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="text" translatable="yes">2000</property> + <property name="orientation">vertical</property> + <property name="adjustment">_systemTimeYearSpinAdjustment</property> + <property name="wrap">True</property> + <property name="value">2000</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">end</property> + <property name="label">-</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="_systemTimeMonthSpin"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="text" translatable="yes">1</property> + <property name="orientation">vertical</property> + <property name="adjustment">_systemTimeMonthSpinAdjustment</property> + <property name="wrap">True</property> + <property name="value">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">end</property> + <property name="label">-</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="_systemTimeDaySpin"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="text" translatable="yes">1</property> + <property name="orientation">vertical</property> + <property name="adjustment">_systemTimeDaySpinAdjustment</property> + <property name="wrap">True</property> + <property name="value">1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="_systemTimeHourSpin"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="text" translatable="yes">0</property> + <property name="orientation">vertical</property> + <property name="adjustment">_systemTimeHourSpinAdjustment</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">6</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">end</property> + <property name="label">:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">7</property> + </packing> + </child> + <child> + <object class="GtkSpinButton" id="_systemTimeMinuteSpin"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="text" translatable="yes">0</property> + <property name="orientation">vertical</property> + <property name="adjustment">_systemTimeMinuteSpinAdjustment</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">8</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_vSyncToggle"> + <property name="label" translatable="yes">Enable VSync</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck. Can be toggled in-game with a hotkey of your preference. We recommend doing this if you plan on disabling it. Leave ON if unsure.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_ptcToggle"> + <property name="label" translatable="yes">Enable PPTC (Profiled Persistent Translation Cache)</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Saves translated JIT functions so that they do not need to be translated every time the game loads. Reduces stuttering and significantly speeds up boot times after the first boot of a game. Leave ON if unsure.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">6</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_internetToggle"> + <property name="label" translatable="yes">Enable Guest Internet Access</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Allows the emulated application to connect to the Internet. Games with a LAN mode can connect to each other when this is enabled and the systems are connected to the same access point. This includes real consoles as well. Does NOT allow connecting to Nintendo servers. May cause crashing in certain games that try to connect to the Internet. Leave OFF if unsure.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">7</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_fsicToggle"> + <property name="label" translatable="yes">Enable FS Integrity Checks</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Checks for corrupt files when booting a game, and if corrupt files are detected, displays a hash error in the log. Has no impact on performance and is meant to help troubleshooting. Leave ON if unsure.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">8</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_audioBackendBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Changes the backend used to render audio. SDL2 is the preferred one, while OpenAL and SoundIO are used as fallbacks. Dummy will have no sound. Set to SDL2 if unsure.</property> + <property name="halign">end</property> + <property name="margin-right">5</property> + <property name="label" translatable="yes">Audio Backend: </property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox" id="_memoryManagerBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <placeholder/> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Change how guest memory is mapped and accessed. Greatly affects emulated CPU performance. Set to HOST UNCHECKED if unsure.</property> + <property name="halign">end</property> + <property name="margin-right">5</property> + <property name="label" translatable="yes">Memory Manager Mode: </property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="_mmSoftware"> + <property name="label" translatable="yes">Software</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Use a software page table for address translation. Highest accuracy but slowest performance.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="_mmHost"> + <property name="label" translatable="yes">Host (fast)</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Directly map memory in the host address space. Much faster JIT compilation and execution.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + <property name="group">_mmSoftware</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkRadioButton" id="_mmHostUnsafe"> + <property name="label" translatable="yes">Host Unchecked (fastest, unsafe)</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Directly map memory, but do not mask the address within the guest address space before access. Faster, but at the cost of safety. The guest application can access memory from anywhere in Ryujinx, so only run programs you trust with this mode.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + <property name="group">_mmSoftware</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox" id="CatHacks"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">Hacks</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes"> (may cause instability)</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="_expandRamToggle"> + <property name="label" translatable="yes">Use alternative memory layout (Developers)</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Utilizes an alternative MemoryMode layout to mimic a Switch development model. This is only useful for higher-resolution texture packs or 4k resolution mods. Does NOT improve performance. Leave OFF if unsure.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_ignoreToggle"> + <property name="label" translatable="yes">Ignore Missing Services</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Ignores unimplemented Horizon OS services. This may help in bypassing crashes when booting certain games. Leave OFF if unsure.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="position">2</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">end</property> + <property name="label" translatable="yes">System</property> + </object> + <packing> + <property name="position">2</property> + <property name="tab-fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="TabGraphics"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="CatFeatures"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">Features</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="FeaturesOptions"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Executes graphics backend commands on a second thread. Speeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading. Set to AUTO if unsure.</property> + <property name="label" translatable="yes">Graphics Backend Multithreading:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_galThreading"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Executes graphics backend commands on a second thread. Speeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading. Set to AUTO if unsure.</property> + <property name="active-id">-1</property> + <items> + <item id="Auto" translatable="yes">Auto</item> + <item id="Off" translatable="yes">Off</item> + <item id="On" translatable="yes">On</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Graphics Backend to use</property> + <property name="label" translatable="yes">Graphics Backend:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_graphicsBackend"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Graphics Backend to use</property> + <property name="active-id">-1</property> + <items> + <item id="0" translatable="yes">Vulkan</item> + <item id="1" translatable="yes">OpenGL</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Preferred GPU (Vulkan only)</property> + <property name="label" translatable="yes">Preferred GPU:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_preferredGpu"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Preferred GPU (Vulkan only)</property> + <property name="active-id">-1</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="CatEnhancements"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">Enhancements</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="EnhancementOptions"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="_shaderCacheToggle"> + <property name="label" translatable="yes">Enable Shader Cache</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Saves a disk shader cache which reduces stuttering in subsequent runs. Leave ON if unsure.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_textureRecompressionToggle"> + <property name="label" translatable="yes">Enable Texture Recompression</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Enables or disables Texture Recompression. Reduces VRAM usage at the cost of texture quality, and may also increase stuttering</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_macroHLEToggle"> + <property name="label" translatable="yes">Enable Macro HLE</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Enables or disables high-level emulation of Macro code. Improves performance but may cause graphical glitches in some games</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Resolution Scale applied to applicable render targets.</property> + <property name="label" translatable="yes">Resolution Scale:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_resScaleCombo"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Resolution Scale applied to applicable render targets.</property> + <property name="active-id">1</property> + <items> + <item id="1" translatable="yes">Native (720p/1080p)</item> + <item id="2" translatable="yes">2x (1440p/2160p)</item> + <item id="3" translatable="yes">3x (2160p/3240p)</item> + <item id="4" translatable="yes">4x (2880p/4320p)</item> + <item id="-1" translatable="yes">Custom (not recommended)</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="_resScaleText"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="tooltip-text" translatable="yes">Floating point resolution scale, such as 1.5. Non-integral scales are more likely to cause issues or crash.</property> + <property name="valign">center</property> + <property name="caps-lock-warning">False</property> + <property name="placeholder-text">1.0</property> + <property name="input-purpose">number</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Applies a final effect to the game render</property> + <property name="label" translatable="yes">Post Processing Effect:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_antiAliasing"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Applies anti-aliasing to the game render</property> + <property name="active-id">1</property> + <items> + <item id="0" translatable="yes">None</item> + <item id="1" translatable="yes">FXAA</item> + <item id="2" translatable="yes">SMAA Low</item> + <item id="3" translatable="yes">SMAA Medium</item> + <item id="4" translatable="yes">SMAA High</item> + <item id="5" translatable="yes">SMAA Ultra</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="width-request">100</property> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Enables Framebuffer Upscaling</property> + <property name="label" translatable="yes">Upscale: </property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_scalingFilter"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Enables Framebuffer Upscaling</property> + <property name="active-id">1</property> + <items> + <item id="0" translatable="yes">Bilinear</item> + <item id="1" translatable="yes">Nearest</item> + <item id="2" translatable="yes">FSR</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkScale" id="_scalingFilterSlider"> + <property name="width-request">200</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="margin-start">5</property> + <property name="adjustment">_scalingFilterLevel</property> + <property name="round-digits">1</property> + <property name="value-pos">right</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Level of Anisotropic Filtering (set to Auto to use the value requested by the game)</property> + <property name="label" translatable="yes">Anisotropic Filtering:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_anisotropy"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Level of Anisotropic Filtering (set to Auto to use the value requested by the game)</property> + <property name="active-id">-1</property> + <items> + <item id="-1" translatable="yes">Auto</item> + <item id="2" translatable="yes">2x</item> + <item id="4" translatable="yes">4x</item> + <item id="8" translatable="yes">8x</item> + <item id="16" translatable="yes">16x</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">6</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Aspect Ratio applied to the renderer window.</property> + <property name="label" translatable="yes">Aspect Ratio:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_aspectRatio"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Aspect Ratio applied to the renderer window.</property> + <property name="active-id">1</property> + <items> + <item id="0" translatable="yes">4:3</item> + <item id="1" translatable="yes">16:9</item> + <item id="2" translatable="yes">16:10</item> + <item id="3" translatable="yes">21:9</item> + <item id="4" translatable="yes">32:9</item> + <item id="5" translatable="yes">Stretch to Fit Window</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">7</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">2</property> + </packing> + </child> + <child> + <object class="GtkSeparator"> + <property name="visible">True</property> + <property name="can-focus">False</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkBox" id="CatDev"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">Developer Options</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="DevOptions"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Graphics Shaders Dump Path</property> + <property name="label" translatable="yes">Graphics Shaders Dump Path:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkEntry" id="_graphicsShadersDumpPath"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="tooltip-text" translatable="yes">Graphics Shaders Dump Path</property> + <property name="valign">center</property> + <property name="caps-lock-warning">False</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">4</property> + </packing> + </child> + </object> + <packing> + <property name="position">3</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Graphics</property> + </object> + <packing> + <property name="position">3</property> + <property name="tab-fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="TabLogging"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">10</property> + <property name="margin-top">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="CatLogging"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">Logging</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="LogggingOptions"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkCheckButton" id="_fileLogToggle"> + <property name="label" translatable="yes">Enable Logging to File</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Saves console logging to a log file on disk. Does not affect performance.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_stubLogToggle"> + <property name="label" translatable="yes">Enable Stub Logs</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Prints stub log messages in the console. Does not affect performance.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_infoLogToggle"> + <property name="label" translatable="yes">Enable Info Logs</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Prints info log messages in the console. Does not affect performance.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">4</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_warningLogToggle"> + <property name="label" translatable="yes">Enable Warning Logs</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Prints warning log messages in the console. Does not affect performance.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_errorLogToggle"> + <property name="label" translatable="yes">Enable Error Logs</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Prints error log messages in the console. Does not affect performance.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">6</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_guestLogToggle"> + <property name="label" translatable="yes">Enable Guest Logs</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Prints guest log messages in the console. Does not affect performance.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">7</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_fsAccessLogToggle"> + <property name="label" translatable="yes">Enable Fs Access Logs</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Enables FS access log output to the console. Possible modes are 0-3</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">8</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Enables FS access log output to the console. Possible modes are 0-3</property> + <property name="label" translatable="yes">Fs Global Access Log Mode:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkSpinButton"> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="tooltip-text" translatable="yes">Enables FS access log output to the console. Possible modes are 0-3</property> + <property name="text" translatable="yes">0</property> + <property name="adjustment">_fsLogSpinAdjustment</property> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">9</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="CatDevLogging"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="margin-top">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Use with care</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">Developer Options (WARNING: Will reduce performance)</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="DevLoggingOptions"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-top">5</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Requires appropriate log levels enabled.</property> + <property name="label" translatable="yes">Graphics Backend Log Level</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">22</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_graphicsDebugLevel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Requires appropriate log levels enabled.</property> + <property name="margin-left">5</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">22</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_debugLogToggle"> + <property name="label" translatable="yes">Enable Debug Logs</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Prints debug log messages in the console. Only use this if specifically instructed by a staff member, as it will make logs difficult to read and worsen emulator performance.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">21</property> + </packing> + </child> + <child> + <object class="GtkCheckButton" id="_traceLogToggle"> + <property name="label" translatable="yes">Enable Trace Logs</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">False</property> + <property name="tooltip-text" translatable="yes">Prints trace log messages in the console. Does not affect performance.</property> + <property name="halign">start</property> + <property name="margin-top">5</property> + <property name="margin-bottom">5</property> + <property name="draw-indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">22</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">22</property> + </packing> + </child> + </object> + <packing> + <property name="position">4</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Logging</property> + </object> + <packing> + <property name="position">4</property> + <property name="tab-fill">False</property> + </packing> + </child> + <child> + <object class="GtkBox" id="TabMultiplayer"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-left">5</property> + <property name="margin-right">10</property> + <property name="margin-top">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="CatMultiplayer"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">Multiplayer</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="MultiplayerOptions"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="ModeBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Change Multiplayer Mode</property> + <property name="halign">end</property> + <property name="label" translatable="yes">Mode:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_multiModeSelect"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">Change Multiplayer Mode</property> + <property name="active-id">Disabled</property> + <items> + <item id="Disabled" translatable="yes">Disabled</item> + <item id="LdnMitm" translatable="yes">ldn_mitm</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">3</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="CatLAN"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + <property name="margin-left">5</property> + <property name="margin-right">5</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">LAN Mode</property> + <attributes> + <attribute name="weight" value="bold"/> + </attributes> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox" id="LANOptions"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="valign">start</property> + <property name="margin-left">10</property> + <property name="margin-right">10</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="NetworkInterfaceBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">The network interface used for LAN/LDN features</property> + <property name="halign">end</property> + <property name="label" translatable="yes">Network Interface:</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkComboBoxText" id="_multiLanSelect"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="tooltip-text" translatable="yes">The network interface used for LAN/LDN features</property> + <property name="active-id">0</property> + <items> + <item id="0" translatable="yes">Default</item> + </items> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="halign">start</property> + <property name="margin-bottom">5</property> + <property name="label" translatable="yes">To use LAN functionality in games, Enable Guest Internet Access must be checked in System.</property> + <property name="wrap">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="padding">5</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="position">5</property> + </packing> + </child> + <child type="tab"> + <object class="GtkLabel"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="label" translatable="yes">Multiplayer</property> + </object> + <packing> + <property name="position">5</property> + <property name="tab-fill">False</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButtonBox" id="_buttonBox"> + <property name="visible">True</property> + <property name="can-focus">False</property> + <property name="margin-right">5</property> + <property name="margin-top">3</property> + <property name="margin-bottom">3</property> + <property name="spacing">5</property> + <property name="layout-style">end</property> + <child> + <object class="GtkToggleButton" id="SaveToggle"> + <property name="label" translatable="yes">Save</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <signal name="toggled" handler="SaveToggle_Activated" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkToggleButton" id="CloseToggle"> + <property name="label" translatable="yes">Close</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <signal name="toggled" handler="CloseToggle_Activated" swapped="no"/> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton"> + <property name="label" translatable="yes">Apply</property> + <property name="visible">True</property> + <property name="can-focus">True</property> + <property name="receives-default">True</property> + <signal name="clicked" handler="ApplyToggle_Activated" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">2</property> + </packing> + </child> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">False</property> + <property name="position">1</property> + </packing> + </child> + </object> + </child> + </object> +</interface> diff --git a/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs new file mode 100644 index 00000000..74b2330e --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs @@ -0,0 +1,206 @@ +using Gtk; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Fsa; +using LibHac.FsSystem; +using LibHac.Ns; +using LibHac.Tools.FsSystem; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Utilities; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.App.Common; +using Ryujinx.UI.Widgets; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using GUI = Gtk.Builder.ObjectAttribute; +using SpanHelpers = LibHac.Common.SpanHelpers; + +namespace Ryujinx.UI.Windows +{ + public class TitleUpdateWindow : Window + { + private readonly MainWindow _parent; + private readonly VirtualFileSystem _virtualFileSystem; + private readonly string _titleId; + private readonly string _updateJsonPath; + + private TitleUpdateMetadata _titleUpdateWindowData; + + private readonly Dictionary<RadioButton, string> _radioButtonToPathDictionary; + private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); + +#pragma warning disable CS0649, IDE0044 // 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, string titleId, string titleName) : this(new Builder("Ryujinx.Gtk3.UI.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { } + + private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow")) + { + _parent = parent; + + builder.Autoconnect(this); + + _titleId = titleId; + _virtualFileSystem = virtualFileSystem; + _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json"); + _radioButtonToPathDictionary = new Dictionary<RadioButton, string>(); + + try + { + _titleUpdateWindowData = JsonHelper.DeserializeFromFile(_updateJsonPath, _serializerContext.TitleUpdateMetadata); + } + catch + { + _titleUpdateWindowData = new TitleUpdateMetadata + { + Selected = "", + Paths = new List<string>(), + }; + } + + _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]"; + + foreach (string path in _titleUpdateWindowData.Paths) + { + AddUpdate(path); + } + + if (_titleUpdateWindowData.Selected == "") + { + _noUpdateRadioButton.Active = true; + } + else + { + foreach ((RadioButton update, var _) in _radioButtonToPathDictionary.Where(keyValuePair => keyValuePair.Value == _titleUpdateWindowData.Selected)) + { + update.Active = true; + } + } + } + + private void AddUpdate(string path) + { + if (File.Exists(path)) + { + using FileStream file = new(path, FileMode.Open, FileAccess.Read); + + PartitionFileSystem nsp = new(); + nsp.Initialize(file.AsStorage()).ThrowIfFailure(); + + try + { + (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0); + + 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(); + + RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}"); + radioButton.JoinGroup(_noUpdateRadioButton); + + _availableUpdatesBox.Add(radioButton); + _radioButtonToPathDictionary.Add(radioButton, path); + + radioButton.Show(); + radioButton.Active = true; + } + else + { + GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!"); + } + } + catch (Exception exception) + { + GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}"); + } + } + } + + private void RemoveUpdates(bool removeSelectedOnly = false) + { + foreach (RadioButton radioButton in _noUpdateRadioButton.Group) + { + if (radioButton.Label != "No Update" && (!removeSelectedOnly || radioButton.Active)) + { + _availableUpdatesBox.Remove(radioButton); + _radioButtonToPathDictionary.Remove(radioButton); + radioButton.Dispose(); + } + } + } + + private void AddButton_Clicked(object sender, EventArgs args) + { + using FileChooserNative fileChooser = new("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(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.glade b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.glade new file mode 100644 index 00000000..cfbac86d --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.glade @@ -0,0 +1,214 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- Generated with glade 3.36.0 --> +<interface> + <requires lib="gtk+" version="3.20"/> + <object class="GtkWindow" id="_titleUpdateWindow"> + <property name="can_focus">False</property> + <property name="title" translatable="yes">Ryujinx - Title Update Manager</property> + <property name="modal">True</property> + <property name="window_position">center</property> + <property name="default_width">550</property> + <property name="default_height">250</property> + <child> + <object class="GtkBox" id="MainBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkBox" id="UpdatesBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkLabel" id="_baseTitleInfoLabel"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_left">10</property> + <property name="margin_right">10</property> + <property name="margin_top">10</property> + <property name="margin_bottom">10</property> + <property name="label" translatable="yes">Available Updates</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkScrolledWindow"> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="margin_left">10</property> + <property name="margin_right">10</property> + <property name="shadow_type">in</property> + <child> + <object class="GtkViewport"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkBox" id="_availableUpdatesBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="orientation">vertical</property> + <child> + <object class="GtkRadioButton" id="_noUpdateRadioButton"> + <property name="label" translatable="yes">No Update</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">False</property> + <property name="active">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + </object> + </child> + </object> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <child> + <object class="GtkButtonBox"> + <property name="visible">True</property> + <property name="can_focus">False</property> + <property name="margin_top">10</property> + <property name="margin_bottom">10</property> + <property name="layout_style">start</property> + <child> + <object class="GtkButton" id="_addUpdate"> + <property name="label" translatable="yes">Add</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Adds an update to this list</property> + <property name="margin_left">10</property> + <signal name="clicked" handler="AddButton_Clicked" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">0</property> + </packing> + </child> + <child> + <object class="GtkButton" id="_removeUpdate"> + <property name="label" translatable="yes">Remove</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Removes the selected update</property> + <property name="margin_left">10</property> + <signal name="clicked" handler="RemoveButton_Clicked" swapped="no"/> + </object> + <packing> + <property name="expand">True</property> + <property name="fill">True</property> + <property name="position">1</property> + </packing> + </child> + <child> + <object class="GtkButton" id="_removeAllButton"> + <property name="label" translatable="yes">Remove All</property> + <property name="visible">True</property> + <property name="can_focus">True</property> + <property name="receives_default">True</property> + <property name="tooltip_text" translatable="yes">Removes 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> diff --git a/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.Designer.cs b/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.Designer.cs new file mode 100644 index 00000000..bc5a18f9 --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.Designer.cs @@ -0,0 +1,255 @@ +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(); + } + } +} diff --git a/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.cs new file mode 100644 index 00000000..d1e5fa9f --- /dev/null +++ b/src/Ryujinx.Gtk3/UI/Windows/UserProfilesManagerWindow.cs @@ -0,0 +1,328 @@ +using Gtk; +using Ryujinx.Common.Memory; +using Ryujinx.HLE.FileSystem; +using Ryujinx.HLE.HOS.Services.Account.Acc; +using Ryujinx.UI.Common.Configuration; +using Ryujinx.UI.Widgets; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Image = SixLabors.ImageSharp.Image; + +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 Image image = Image.Load(buffer); + + image.Mutate(x => x.Resize(256, 256)); + + using MemoryStream streamJpg = MemoryStreamManager.Shared.GetStream(); + + image.SaveAsJpeg(streamJpg); + + _bufferImageProfile = streamJpg.ToArray(); + } + + private void ProfileImageFileChooser() + { + FileChooserNative fileChooser = new("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(); + } + } +} |
