/ src / Ryujinx.Gtk3 / UI / Windows / SettingsWindow.cs
SettingsWindow.cs
  1  using Gtk;
  2  using LibHac.Tools.FsSystem;
  3  using Ryujinx.Audio.Backends.OpenAL;
  4  using Ryujinx.Audio.Backends.SDL2;
  5  using Ryujinx.Audio.Backends.SoundIo;
  6  using Ryujinx.Common.Configuration;
  7  using Ryujinx.Common.Configuration.Hid;
  8  using Ryujinx.Common.Configuration.Multiplayer;
  9  using Ryujinx.Common.GraphicsDriver;
 10  using Ryujinx.HLE.FileSystem;
 11  using Ryujinx.HLE.HOS.Services.Time.TimeZone;
 12  using Ryujinx.UI.Common.Configuration;
 13  using Ryujinx.UI.Common.Configuration.System;
 14  using Ryujinx.UI.Helper;
 15  using Ryujinx.UI.Widgets;
 16  using System;
 17  using System.Collections.Generic;
 18  using System.Globalization;
 19  using System.IO;
 20  using System.Net.NetworkInformation;
 21  using System.Reflection;
 22  using System.Text.RegularExpressions;
 23  using System.Threading.Tasks;
 24  using GUI = Gtk.Builder.ObjectAttribute;
 25  
 26  namespace Ryujinx.UI.Windows
 27  {
 28      public class SettingsWindow : Window
 29      {
 30          private readonly MainWindow _parent;
 31          private readonly ListStore _gameDirsBoxStore;
 32          private readonly ListStore _audioBackendStore;
 33          private readonly TimeZoneContentManager _timeZoneContentManager;
 34          private readonly HashSet<string> _validTzRegions;
 35  
 36          private long _systemTimeOffset;
 37          private float _previousVolumeLevel;
 38          private bool _directoryChanged = false;
 39  
 40  #pragma warning disable CS0649, IDE0044 // Field is never assigned to, Add readonly modifier
 41          [GUI] CheckButton _traceLogToggle;
 42          [GUI] CheckButton _errorLogToggle;
 43          [GUI] CheckButton _warningLogToggle;
 44          [GUI] CheckButton _infoLogToggle;
 45          [GUI] CheckButton _stubLogToggle;
 46          [GUI] CheckButton _debugLogToggle;
 47          [GUI] CheckButton _fileLogToggle;
 48          [GUI] CheckButton _guestLogToggle;
 49          [GUI] CheckButton _fsAccessLogToggle;
 50          [GUI] Adjustment _fsLogSpinAdjustment;
 51          [GUI] ComboBoxText _graphicsDebugLevel;
 52          [GUI] CheckButton _dockedModeToggle;
 53          [GUI] CheckButton _discordToggle;
 54          [GUI] CheckButton _checkUpdatesToggle;
 55          [GUI] CheckButton _showConfirmExitToggle;
 56          [GUI] RadioButton _hideCursorNever;
 57          [GUI] RadioButton _hideCursorOnIdle;
 58          [GUI] RadioButton _hideCursorAlways;
 59          [GUI] CheckButton _vSyncToggle;
 60          [GUI] CheckButton _shaderCacheToggle;
 61          [GUI] CheckButton _textureRecompressionToggle;
 62          [GUI] CheckButton _macroHLEToggle;
 63          [GUI] CheckButton _ptcToggle;
 64          [GUI] CheckButton _internetToggle;
 65          [GUI] CheckButton _fsicToggle;
 66          [GUI] RadioButton _mmSoftware;
 67          [GUI] RadioButton _mmHost;
 68          [GUI] RadioButton _mmHostUnsafe;
 69          [GUI] CheckButton _expandRamToggle;
 70          [GUI] CheckButton _ignoreToggle;
 71          [GUI] CheckButton _directKeyboardAccess;
 72          [GUI] CheckButton _directMouseAccess;
 73          [GUI] ComboBoxText _systemLanguageSelect;
 74          [GUI] ComboBoxText _systemRegionSelect;
 75          [GUI] Entry _systemTimeZoneEntry;
 76          [GUI] EntryCompletion _systemTimeZoneCompletion;
 77          [GUI] Box _audioBackendBox;
 78          [GUI] ComboBox _audioBackendSelect;
 79          [GUI] Label _audioVolumeLabel;
 80          [GUI] Scale _audioVolumeSlider;
 81          [GUI] SpinButton _systemTimeYearSpin;
 82          [GUI] SpinButton _systemTimeMonthSpin;
 83          [GUI] SpinButton _systemTimeDaySpin;
 84          [GUI] SpinButton _systemTimeHourSpin;
 85          [GUI] SpinButton _systemTimeMinuteSpin;
 86          [GUI] Adjustment _systemTimeYearSpinAdjustment;
 87          [GUI] Adjustment _systemTimeMonthSpinAdjustment;
 88          [GUI] Adjustment _systemTimeDaySpinAdjustment;
 89          [GUI] Adjustment _systemTimeHourSpinAdjustment;
 90          [GUI] Adjustment _systemTimeMinuteSpinAdjustment;
 91          [GUI] ComboBoxText _multiLanSelect;
 92          [GUI] ComboBoxText _multiModeSelect;
 93          [GUI] CheckButton _custThemeToggle;
 94          [GUI] Entry _custThemePath;
 95          [GUI] ToggleButton _browseThemePath;
 96          [GUI] Label _custThemePathLabel;
 97          [GUI] TreeView _gameDirsBox;
 98          [GUI] Entry _addGameDirBox;
 99          [GUI] ComboBoxText _galThreading;
100          [GUI] Entry _graphicsShadersDumpPath;
101          [GUI] ComboBoxText _anisotropy;
102          [GUI] ComboBoxText _aspectRatio;
103          [GUI] ComboBoxText _antiAliasing;
104          [GUI] ComboBoxText _scalingFilter;
105          [GUI] ComboBoxText _graphicsBackend;
106          [GUI] ComboBoxText _preferredGpu;
107          [GUI] ComboBoxText _resScaleCombo;
108          [GUI] Entry _resScaleText;
109          [GUI] Adjustment _scalingFilterLevel;
110          [GUI] Scale _scalingFilterSlider;
111          [GUI] ToggleButton _configureController1;
112          [GUI] ToggleButton _configureController2;
113          [GUI] ToggleButton _configureController3;
114          [GUI] ToggleButton _configureController4;
115          [GUI] ToggleButton _configureController5;
116          [GUI] ToggleButton _configureController6;
117          [GUI] ToggleButton _configureController7;
118          [GUI] ToggleButton _configureController8;
119          [GUI] ToggleButton _configureControllerH;
120  
121  #pragma warning restore CS0649, IDE0044
122  
123          public SettingsWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this(parent, new Builder("Ryujinx.Gtk3.UI.Windows.SettingsWindow.glade"), virtualFileSystem, contentManager) { }
124  
125          private SettingsWindow(MainWindow parent, Builder builder, VirtualFileSystem virtualFileSystem, ContentManager contentManager) : base(builder.GetRawOwnedObject("_settingsWin"))
126          {
127              Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.UI.Common.Resources.Logo_Ryujinx.png");
128  
129              _parent = parent;
130  
131              builder.Autoconnect(this);
132  
133              _timeZoneContentManager = new TimeZoneContentManager();
134              _timeZoneContentManager.InitializeInstance(virtualFileSystem, contentManager, IntegrityCheckLevel.None);
135  
136              _validTzRegions = new HashSet<string>(_timeZoneContentManager.LocationNameCache.Length, StringComparer.Ordinal); // Zone regions are identifiers. Must match exactly.
137  
138              // Bind Events.
139              _configureController1.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player1);
140              _configureController2.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player2);
141              _configureController3.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player3);
142              _configureController4.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player4);
143              _configureController5.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player5);
144              _configureController6.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player6);
145              _configureController7.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player7);
146              _configureController8.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Player8);
147              _configureControllerH.Pressed += (sender, args) => ConfigureController_Pressed(sender, PlayerIndex.Handheld);
148              _systemTimeZoneEntry.FocusOutEvent += TimeZoneEntry_FocusOut;
149  
150              _resScaleCombo.Changed += (sender, args) => _resScaleText.Visible = _resScaleCombo.ActiveId == "-1";
151              _scalingFilter.Changed += (sender, args) => _scalingFilterSlider.Visible = _scalingFilter.ActiveId == "2";
152              _galThreading.Changed += (sender, args) =>
153              {
154                  if (_galThreading.ActiveId != ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString())
155                  {
156                      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.");
157                  }
158              };
159  
160              // Setup Currents.
161              if (ConfigurationState.Instance.Logger.EnableTrace)
162              {
163                  _traceLogToggle.Click();
164              }
165  
166              if (ConfigurationState.Instance.Logger.EnableFileLog)
167              {
168                  _fileLogToggle.Click();
169              }
170  
171              if (ConfigurationState.Instance.Logger.EnableError)
172              {
173                  _errorLogToggle.Click();
174              }
175  
176              if (ConfigurationState.Instance.Logger.EnableWarn)
177              {
178                  _warningLogToggle.Click();
179              }
180  
181              if (ConfigurationState.Instance.Logger.EnableInfo)
182              {
183                  _infoLogToggle.Click();
184              }
185  
186              if (ConfigurationState.Instance.Logger.EnableStub)
187              {
188                  _stubLogToggle.Click();
189              }
190  
191              if (ConfigurationState.Instance.Logger.EnableDebug)
192              {
193                  _debugLogToggle.Click();
194              }
195  
196              if (ConfigurationState.Instance.Logger.EnableGuest)
197              {
198                  _guestLogToggle.Click();
199              }
200  
201              if (ConfigurationState.Instance.Logger.EnableFsAccessLog)
202              {
203                  _fsAccessLogToggle.Click();
204              }
205  
206              foreach (GraphicsDebugLevel level in Enum.GetValues<GraphicsDebugLevel>())
207              {
208                  _graphicsDebugLevel.Append(level.ToString(), level.ToString());
209              }
210  
211              _graphicsDebugLevel.SetActiveId(ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value.ToString());
212  
213              if (ConfigurationState.Instance.System.EnableDockedMode)
214              {
215                  _dockedModeToggle.Click();
216              }
217  
218              if (ConfigurationState.Instance.EnableDiscordIntegration)
219              {
220                  _discordToggle.Click();
221              }
222  
223              if (ConfigurationState.Instance.CheckUpdatesOnStart)
224              {
225                  _checkUpdatesToggle.Click();
226              }
227  
228              if (ConfigurationState.Instance.ShowConfirmExit)
229              {
230                  _showConfirmExitToggle.Click();
231              }
232  
233              switch (ConfigurationState.Instance.HideCursor.Value)
234              {
235                  case HideCursorMode.Never:
236                      _hideCursorNever.Click();
237                      break;
238                  case HideCursorMode.OnIdle:
239                      _hideCursorOnIdle.Click();
240                      break;
241                  case HideCursorMode.Always:
242                      _hideCursorAlways.Click();
243                      break;
244              }
245  
246              if (ConfigurationState.Instance.Graphics.EnableVsync)
247              {
248                  _vSyncToggle.Click();
249              }
250  
251              if (ConfigurationState.Instance.Graphics.EnableShaderCache)
252              {
253                  _shaderCacheToggle.Click();
254              }
255  
256              if (ConfigurationState.Instance.Graphics.EnableTextureRecompression)
257              {
258                  _textureRecompressionToggle.Click();
259              }
260  
261              if (ConfigurationState.Instance.Graphics.EnableMacroHLE)
262              {
263                  _macroHLEToggle.Click();
264              }
265  
266              if (ConfigurationState.Instance.System.EnablePtc)
267              {
268                  _ptcToggle.Click();
269              }
270  
271              if (ConfigurationState.Instance.System.EnableInternetAccess)
272              {
273                  _internetToggle.Click();
274              }
275  
276              if (ConfigurationState.Instance.System.EnableFsIntegrityChecks)
277              {
278                  _fsicToggle.Click();
279              }
280  
281              switch (ConfigurationState.Instance.System.MemoryManagerMode.Value)
282              {
283                  case MemoryManagerMode.SoftwarePageTable:
284                      _mmSoftware.Click();
285                      break;
286                  case MemoryManagerMode.HostMapped:
287                      _mmHost.Click();
288                      break;
289                  case MemoryManagerMode.HostMappedUnsafe:
290                      _mmHostUnsafe.Click();
291                      break;
292              }
293  
294              if (ConfigurationState.Instance.System.ExpandRam)
295              {
296                  _expandRamToggle.Click();
297              }
298  
299              if (ConfigurationState.Instance.System.IgnoreMissingServices)
300              {
301                  _ignoreToggle.Click();
302              }
303  
304              if (ConfigurationState.Instance.Hid.EnableKeyboard)
305              {
306                  _directKeyboardAccess.Click();
307              }
308  
309              if (ConfigurationState.Instance.Hid.EnableMouse)
310              {
311                  _directMouseAccess.Click();
312              }
313  
314              if (ConfigurationState.Instance.UI.EnableCustomTheme)
315              {
316                  _custThemeToggle.Click();
317              }
318  
319              // Custom EntryCompletion Columns. If added to glade, need to override more signals
320              ListStore tzList = new(typeof(string), typeof(string), typeof(string));
321              _systemTimeZoneCompletion.Model = tzList;
322  
323              CellRendererText offsetCol = new();
324              CellRendererText abbrevCol = new();
325  
326              _systemTimeZoneCompletion.PackStart(offsetCol, false);
327              _systemTimeZoneCompletion.AddAttribute(offsetCol, "text", 0);
328              _systemTimeZoneCompletion.TextColumn = 1; // Regions Column
329              _systemTimeZoneCompletion.PackStart(abbrevCol, false);
330              _systemTimeZoneCompletion.AddAttribute(abbrevCol, "text", 2);
331  
332              int maxLocationLength = 0;
333  
334              foreach (var (offset, location, abbr) in _timeZoneContentManager.ParseTzOffsets())
335              {
336                  var hours = Math.DivRem(offset, 3600, out int seconds);
337                  var minutes = Math.Abs(seconds) / 60;
338  
339                  var abbr2 = (abbr.StartsWith('+') || abbr.StartsWith('-')) ? string.Empty : abbr;
340  
341                  tzList.AppendValues($"UTC{hours:+0#;-0#;+00}:{minutes:D2} ", location, abbr2);
342                  _validTzRegions.Add(location);
343  
344                  maxLocationLength = Math.Max(maxLocationLength, location.Length);
345              }
346  
347              _systemTimeZoneEntry.WidthChars = Math.Max(20, maxLocationLength + 1); // Ensure minimum Entry width
348              _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(ConfigurationState.Instance.System.TimeZone);
349  
350              _systemTimeZoneCompletion.MatchFunc = TimeZoneMatchFunc;
351  
352              _systemLanguageSelect.SetActiveId(ConfigurationState.Instance.System.Language.Value.ToString());
353              _systemRegionSelect.SetActiveId(ConfigurationState.Instance.System.Region.Value.ToString());
354              _galThreading.SetActiveId(ConfigurationState.Instance.Graphics.BackendThreading.Value.ToString());
355              _resScaleCombo.SetActiveId(ConfigurationState.Instance.Graphics.ResScale.Value.ToString());
356              _anisotropy.SetActiveId(ConfigurationState.Instance.Graphics.MaxAnisotropy.Value.ToString());
357              _aspectRatio.SetActiveId(((int)ConfigurationState.Instance.Graphics.AspectRatio.Value).ToString());
358              _graphicsBackend.SetActiveId(((int)ConfigurationState.Instance.Graphics.GraphicsBackend.Value).ToString());
359              _antiAliasing.SetActiveId(((int)ConfigurationState.Instance.Graphics.AntiAliasing.Value).ToString());
360              _scalingFilter.SetActiveId(((int)ConfigurationState.Instance.Graphics.ScalingFilter.Value).ToString());
361  
362              UpdatePreferredGpuComboBox();
363  
364              _graphicsBackend.Changed += (sender, e) => UpdatePreferredGpuComboBox();
365              PopulateNetworkInterfaces();
366              _multiLanSelect.SetActiveId(ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value);
367              _multiModeSelect.SetActiveId(ConfigurationState.Instance.Multiplayer.Mode.Value.ToString());
368  
369              _custThemePath.Buffer.Text = ConfigurationState.Instance.UI.CustomThemePath;
370              _resScaleText.Buffer.Text = ConfigurationState.Instance.Graphics.ResScaleCustom.Value.ToString();
371              _scalingFilterLevel.Value = ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value;
372              _resScaleText.Visible = _resScaleCombo.ActiveId == "-1";
373              _scalingFilterSlider.Visible = _scalingFilter.ActiveId == "2";
374              _graphicsShadersDumpPath.Buffer.Text = ConfigurationState.Instance.Graphics.ShadersDumpPath;
375              _fsLogSpinAdjustment.Value = ConfigurationState.Instance.System.FsGlobalAccessLogMode;
376              _systemTimeOffset = ConfigurationState.Instance.System.SystemTimeOffset;
377  
378              _gameDirsBox.AppendColumn("", new CellRendererText(), "text", 0);
379              _gameDirsBoxStore = new ListStore(typeof(string));
380              _gameDirsBox.Model = _gameDirsBoxStore;
381  
382              foreach (string gameDir in ConfigurationState.Instance.UI.GameDirs.Value)
383              {
384                  _gameDirsBoxStore.AppendValues(gameDir);
385              }
386  
387              if (_custThemeToggle.Active == false)
388              {
389                  _custThemePath.Sensitive = false;
390                  _custThemePathLabel.Sensitive = false;
391                  _browseThemePath.Sensitive = false;
392              }
393  
394              // Setup system time spinners
395              UpdateSystemTimeSpinners();
396  
397              _audioBackendStore = new ListStore(typeof(string), typeof(AudioBackend));
398  
399              TreeIter openAlIter = _audioBackendStore.AppendValues("OpenAL", AudioBackend.OpenAl);
400              TreeIter soundIoIter = _audioBackendStore.AppendValues("SoundIO", AudioBackend.SoundIo);
401              TreeIter sdl2Iter = _audioBackendStore.AppendValues("SDL2", AudioBackend.SDL2);
402              TreeIter dummyIter = _audioBackendStore.AppendValues("Dummy", AudioBackend.Dummy);
403  
404              _audioBackendSelect = ComboBox.NewWithModelAndEntry(_audioBackendStore);
405              _audioBackendSelect.EntryTextColumn = 0;
406              _audioBackendSelect.Entry.IsEditable = false;
407  
408              switch (ConfigurationState.Instance.System.AudioBackend.Value)
409              {
410                  case AudioBackend.OpenAl:
411                      _audioBackendSelect.SetActiveIter(openAlIter);
412                      break;
413                  case AudioBackend.SoundIo:
414                      _audioBackendSelect.SetActiveIter(soundIoIter);
415                      break;
416                  case AudioBackend.SDL2:
417                      _audioBackendSelect.SetActiveIter(sdl2Iter);
418                      break;
419                  case AudioBackend.Dummy:
420                      _audioBackendSelect.SetActiveIter(dummyIter);
421                      break;
422                  default:
423                      throw new InvalidOperationException($"{nameof(ConfigurationState.Instance.System.AudioBackend)} contains an invalid value: {ConfigurationState.Instance.System.AudioBackend.Value}");
424              }
425  
426              _audioBackendBox.Add(_audioBackendSelect);
427              _audioBackendSelect.Show();
428  
429              _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume;
430              _audioVolumeLabel = new Label("Volume: ");
431              _audioVolumeSlider = new Scale(Orientation.Horizontal, 0, 100, 1);
432              _audioVolumeLabel.MarginStart = 10;
433              _audioVolumeSlider.ValuePos = PositionType.Right;
434              _audioVolumeSlider.WidthRequest = 200;
435  
436              _audioVolumeSlider.Value = _previousVolumeLevel * 100;
437              _audioVolumeSlider.ValueChanged += VolumeSlider_OnChange;
438              _audioBackendBox.Add(_audioVolumeLabel);
439              _audioBackendBox.Add(_audioVolumeSlider);
440              _audioVolumeLabel.Show();
441              _audioVolumeSlider.Show();
442  
443              bool openAlIsSupported = false;
444              bool soundIoIsSupported = false;
445              bool sdl2IsSupported = false;
446  
447              Task.Run(() =>
448              {
449                  openAlIsSupported = OpenALHardwareDeviceDriver.IsSupported;
450                  soundIoIsSupported = !OperatingSystem.IsMacOS() && SoundIoHardwareDeviceDriver.IsSupported;
451                  sdl2IsSupported = SDL2HardwareDeviceDriver.IsSupported;
452              });
453  
454              // This function runs whenever the dropdown is opened
455              _audioBackendSelect.SetCellDataFunc(_audioBackendSelect.Cells[0], (layout, cell, model, iter) =>
456              {
457                  cell.Sensitive = ((AudioBackend)_audioBackendStore.GetValue(iter, 1)) switch
458                  {
459                      AudioBackend.OpenAl => openAlIsSupported,
460                      AudioBackend.SoundIo => soundIoIsSupported,
461                      AudioBackend.SDL2 => sdl2IsSupported,
462                      AudioBackend.Dummy => true,
463                      _ => throw new InvalidOperationException($"{nameof(_audioBackendStore)} contains an invalid value for iteration {iter}: {_audioBackendStore.GetValue(iter, 1)}"),
464                  };
465              });
466  
467              if (OperatingSystem.IsMacOS())
468              {
469                  var store = (_graphicsBackend.Model as ListStore);
470                  store.GetIter(out TreeIter openglIter, new TreePath(new[] { 1 }));
471                  store.Remove(ref openglIter);
472  
473                  _graphicsBackend.Model = store;
474              }
475          }
476  
477          private void UpdatePreferredGpuComboBox()
478          {
479              _preferredGpu.RemoveAll();
480  
481              if (Enum.Parse<GraphicsBackend>(_graphicsBackend.ActiveId) == GraphicsBackend.Vulkan)
482              {
483                  var devices = Graphics.Vulkan.VulkanRenderer.GetPhysicalDevices();
484                  string preferredGpuIdFromConfig = ConfigurationState.Instance.Graphics.PreferredGpu.Value;
485                  string preferredGpuId = preferredGpuIdFromConfig;
486                  bool noGpuId = string.IsNullOrEmpty(preferredGpuIdFromConfig);
487  
488                  foreach (var device in devices)
489                  {
490                      string dGpu = device.IsDiscrete ? " (dGPU)" : "";
491                      _preferredGpu.Append(device.Id, $"{device.Name}{dGpu}");
492  
493                      // If there's no GPU selected yet, we just pick the first GPU.
494                      // If there's a discrete GPU available, we always prefer that over the previous selection,
495                      // as it is likely to have better performance and more features.
496                      // If the configuration file already has a GPU selection, we always prefer that instead.
497                      if (noGpuId && (string.IsNullOrEmpty(preferredGpuId) || device.IsDiscrete))
498                      {
499                          preferredGpuId = device.Id;
500                      }
501                  }
502  
503                  if (!string.IsNullOrEmpty(preferredGpuId))
504                  {
505                      _preferredGpu.SetActiveId(preferredGpuId);
506                  }
507              }
508          }
509  
510          private void PopulateNetworkInterfaces()
511          {
512              NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
513  
514              foreach (NetworkInterface nif in interfaces)
515              {
516                  string guid = nif.Id;
517                  string name = nif.Name;
518  
519                  _multiLanSelect.Append(guid, name);
520              }
521          }
522  
523          private void UpdateSystemTimeSpinners()
524          {
525              //Bind system time events
526              _systemTimeYearSpin.ValueChanged -= SystemTimeSpin_ValueChanged;
527              _systemTimeMonthSpin.ValueChanged -= SystemTimeSpin_ValueChanged;
528              _systemTimeDaySpin.ValueChanged -= SystemTimeSpin_ValueChanged;
529              _systemTimeHourSpin.ValueChanged -= SystemTimeSpin_ValueChanged;
530              _systemTimeMinuteSpin.ValueChanged -= SystemTimeSpin_ValueChanged;
531  
532              //Apply actual system time + SystemTimeOffset to system time spin buttons
533              DateTime systemTime = DateTime.Now.AddSeconds(_systemTimeOffset);
534  
535              _systemTimeYearSpinAdjustment.Value = systemTime.Year;
536              _systemTimeMonthSpinAdjustment.Value = systemTime.Month;
537              _systemTimeDaySpinAdjustment.Value = systemTime.Day;
538              _systemTimeHourSpinAdjustment.Value = systemTime.Hour;
539              _systemTimeMinuteSpinAdjustment.Value = systemTime.Minute;
540  
541              //Format spin buttons text to include leading zeros
542              _systemTimeYearSpin.Text = systemTime.Year.ToString("0000");
543              _systemTimeMonthSpin.Text = systemTime.Month.ToString("00");
544              _systemTimeDaySpin.Text = systemTime.Day.ToString("00");
545              _systemTimeHourSpin.Text = systemTime.Hour.ToString("00");
546              _systemTimeMinuteSpin.Text = systemTime.Minute.ToString("00");
547  
548              //Bind system time events
549              _systemTimeYearSpin.ValueChanged += SystemTimeSpin_ValueChanged;
550              _systemTimeMonthSpin.ValueChanged += SystemTimeSpin_ValueChanged;
551              _systemTimeDaySpin.ValueChanged += SystemTimeSpin_ValueChanged;
552              _systemTimeHourSpin.ValueChanged += SystemTimeSpin_ValueChanged;
553              _systemTimeMinuteSpin.ValueChanged += SystemTimeSpin_ValueChanged;
554          }
555  
556          private void SaveSettings()
557          {
558              if (_directoryChanged)
559              {
560                  List<string> gameDirs = new();
561  
562                  _gameDirsBoxStore.GetIterFirst(out TreeIter treeIter);
563  
564                  for (int i = 0; i < _gameDirsBoxStore.IterNChildren(); i++)
565                  {
566                      gameDirs.Add((string)_gameDirsBoxStore.GetValue(treeIter, 0));
567  
568                      _gameDirsBoxStore.IterNext(ref treeIter);
569                  }
570  
571                  ConfigurationState.Instance.UI.GameDirs.Value = gameDirs;
572  
573                  _directoryChanged = false;
574              }
575  
576              HideCursorMode hideCursor = HideCursorMode.Never;
577  
578              if (_hideCursorOnIdle.Active)
579              {
580                  hideCursor = HideCursorMode.OnIdle;
581              }
582  
583              if (_hideCursorAlways.Active)
584              {
585                  hideCursor = HideCursorMode.Always;
586              }
587  
588              if (!float.TryParse(_resScaleText.Buffer.Text, out float resScaleCustom) || resScaleCustom <= 0.0f)
589              {
590                  resScaleCustom = 1.0f;
591              }
592  
593              if (_validTzRegions.Contains(_systemTimeZoneEntry.Text))
594              {
595                  ConfigurationState.Instance.System.TimeZone.Value = _systemTimeZoneEntry.Text;
596              }
597  
598              MemoryManagerMode memoryMode = MemoryManagerMode.SoftwarePageTable;
599  
600              if (_mmHost.Active)
601              {
602                  memoryMode = MemoryManagerMode.HostMapped;
603              }
604  
605              if (_mmHostUnsafe.Active)
606              {
607                  memoryMode = MemoryManagerMode.HostMappedUnsafe;
608              }
609  
610              BackendThreading backendThreading = Enum.Parse<BackendThreading>(_galThreading.ActiveId);
611              if (ConfigurationState.Instance.Graphics.BackendThreading != backendThreading)
612              {
613                  DriverUtilities.ToggleOGLThreading(backendThreading == BackendThreading.Off);
614              }
615  
616              ConfigurationState.Instance.Logger.EnableError.Value = _errorLogToggle.Active;
617              ConfigurationState.Instance.Logger.EnableTrace.Value = _traceLogToggle.Active;
618              ConfigurationState.Instance.Logger.EnableWarn.Value = _warningLogToggle.Active;
619              ConfigurationState.Instance.Logger.EnableInfo.Value = _infoLogToggle.Active;
620              ConfigurationState.Instance.Logger.EnableStub.Value = _stubLogToggle.Active;
621              ConfigurationState.Instance.Logger.EnableDebug.Value = _debugLogToggle.Active;
622              ConfigurationState.Instance.Logger.EnableGuest.Value = _guestLogToggle.Active;
623              ConfigurationState.Instance.Logger.EnableFsAccessLog.Value = _fsAccessLogToggle.Active;
624              ConfigurationState.Instance.Logger.EnableFileLog.Value = _fileLogToggle.Active;
625              ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value = Enum.Parse<GraphicsDebugLevel>(_graphicsDebugLevel.ActiveId);
626              ConfigurationState.Instance.System.EnableDockedMode.Value = _dockedModeToggle.Active;
627              ConfigurationState.Instance.EnableDiscordIntegration.Value = _discordToggle.Active;
628              ConfigurationState.Instance.CheckUpdatesOnStart.Value = _checkUpdatesToggle.Active;
629              ConfigurationState.Instance.ShowConfirmExit.Value = _showConfirmExitToggle.Active;
630              ConfigurationState.Instance.HideCursor.Value = hideCursor;
631              ConfigurationState.Instance.Graphics.EnableVsync.Value = _vSyncToggle.Active;
632              ConfigurationState.Instance.Graphics.EnableShaderCache.Value = _shaderCacheToggle.Active;
633              ConfigurationState.Instance.Graphics.EnableTextureRecompression.Value = _textureRecompressionToggle.Active;
634              ConfigurationState.Instance.Graphics.EnableMacroHLE.Value = _macroHLEToggle.Active;
635              ConfigurationState.Instance.System.EnablePtc.Value = _ptcToggle.Active;
636              ConfigurationState.Instance.System.EnableInternetAccess.Value = _internetToggle.Active;
637              ConfigurationState.Instance.System.EnableFsIntegrityChecks.Value = _fsicToggle.Active;
638              ConfigurationState.Instance.System.MemoryManagerMode.Value = memoryMode;
639              ConfigurationState.Instance.System.ExpandRam.Value = _expandRamToggle.Active;
640              ConfigurationState.Instance.System.IgnoreMissingServices.Value = _ignoreToggle.Active;
641              ConfigurationState.Instance.Hid.EnableKeyboard.Value = _directKeyboardAccess.Active;
642              ConfigurationState.Instance.Hid.EnableMouse.Value = _directMouseAccess.Active;
643              ConfigurationState.Instance.UI.EnableCustomTheme.Value = _custThemeToggle.Active;
644              ConfigurationState.Instance.System.Language.Value = Enum.Parse<Language>(_systemLanguageSelect.ActiveId);
645              ConfigurationState.Instance.System.Region.Value = Enum.Parse<Common.Configuration.System.Region>(_systemRegionSelect.ActiveId);
646              ConfigurationState.Instance.System.SystemTimeOffset.Value = _systemTimeOffset;
647              ConfigurationState.Instance.UI.CustomThemePath.Value = _custThemePath.Buffer.Text;
648              ConfigurationState.Instance.Graphics.ShadersDumpPath.Value = _graphicsShadersDumpPath.Buffer.Text;
649              ConfigurationState.Instance.System.FsGlobalAccessLogMode.Value = (int)_fsLogSpinAdjustment.Value;
650              ConfigurationState.Instance.Graphics.MaxAnisotropy.Value = float.Parse(_anisotropy.ActiveId, CultureInfo.InvariantCulture);
651              ConfigurationState.Instance.Graphics.AspectRatio.Value = Enum.Parse<AspectRatio>(_aspectRatio.ActiveId);
652              ConfigurationState.Instance.Graphics.BackendThreading.Value = backendThreading;
653              ConfigurationState.Instance.Graphics.GraphicsBackend.Value = Enum.Parse<GraphicsBackend>(_graphicsBackend.ActiveId);
654              ConfigurationState.Instance.Graphics.PreferredGpu.Value = _preferredGpu.ActiveId;
655              ConfigurationState.Instance.Graphics.ResScale.Value = int.Parse(_resScaleCombo.ActiveId);
656              ConfigurationState.Instance.Graphics.ResScaleCustom.Value = resScaleCustom;
657              ConfigurationState.Instance.System.AudioVolume.Value = (float)_audioVolumeSlider.Value / 100.0f;
658              ConfigurationState.Instance.Graphics.AntiAliasing.Value = Enum.Parse<AntiAliasing>(_antiAliasing.ActiveId);
659              ConfigurationState.Instance.Graphics.ScalingFilter.Value = Enum.Parse<ScalingFilter>(_scalingFilter.ActiveId);
660              ConfigurationState.Instance.Graphics.ScalingFilterLevel.Value = (int)_scalingFilterLevel.Value;
661              ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId;
662  
663              _previousVolumeLevel = ConfigurationState.Instance.System.AudioVolume.Value;
664  
665              ConfigurationState.Instance.Multiplayer.Mode.Value = Enum.Parse<MultiplayerMode>(_multiModeSelect.ActiveId);
666              ConfigurationState.Instance.Multiplayer.LanInterfaceId.Value = _multiLanSelect.ActiveId;
667  
668              if (_audioBackendSelect.GetActiveIter(out TreeIter activeIter))
669              {
670                  ConfigurationState.Instance.System.AudioBackend.Value = (AudioBackend)_audioBackendStore.GetValue(activeIter, 1);
671              }
672  
673              ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
674  
675              _parent.UpdateInternetAccess();
676              MainWindow.UpdateGraphicsConfig();
677              ThemeHelper.ApplyTheme();
678          }
679  
680          //
681          // Events
682          //
683          private void TimeZoneEntry_FocusOut(object sender, FocusOutEventArgs e)
684          {
685              if (!_validTzRegions.Contains(_systemTimeZoneEntry.Text))
686              {
687                  _systemTimeZoneEntry.Text = _timeZoneContentManager.SanityCheckDeviceLocationName(ConfigurationState.Instance.System.TimeZone);
688              }
689          }
690  
691          private bool TimeZoneMatchFunc(EntryCompletion compl, string key, TreeIter iter)
692          {
693              key = key.Trim().Replace(' ', '_');
694  
695              return ((string)compl.Model.GetValue(iter, 1)).Contains(key, StringComparison.OrdinalIgnoreCase) || // region
696                     ((string)compl.Model.GetValue(iter, 2)).StartsWith(key, StringComparison.OrdinalIgnoreCase) || // abbr
697                     ((string)compl.Model.GetValue(iter, 0))[3..].StartsWith(key); // offset
698          }
699  
700          private void SystemTimeSpin_ValueChanged(object sender, EventArgs e)
701          {
702              int year = _systemTimeYearSpin.ValueAsInt;
703              int month = _systemTimeMonthSpin.ValueAsInt;
704              int day = _systemTimeDaySpin.ValueAsInt;
705              int hour = _systemTimeHourSpin.ValueAsInt;
706              int minute = _systemTimeMinuteSpin.ValueAsInt;
707  
708              if (!DateTime.TryParse(year + "-" + month + "-" + day + " " + hour + ":" + minute, out DateTime newTime))
709              {
710                  UpdateSystemTimeSpinners();
711  
712                  return;
713              }
714  
715              newTime = newTime.AddSeconds(DateTime.Now.Second).AddMilliseconds(DateTime.Now.Millisecond);
716  
717              long systemTimeOffset = (long)Math.Ceiling((newTime - DateTime.Now).TotalMinutes) * 60L;
718  
719              if (_systemTimeOffset != systemTimeOffset)
720              {
721                  _systemTimeOffset = systemTimeOffset;
722                  UpdateSystemTimeSpinners();
723              }
724          }
725  
726          private void AddDir_Pressed(object sender, EventArgs args)
727          {
728              if (Directory.Exists(_addGameDirBox.Buffer.Text))
729              {
730                  _gameDirsBoxStore.AppendValues(_addGameDirBox.Buffer.Text);
731                  _directoryChanged = true;
732              }
733              else
734              {
735                  FileChooserNative fileChooser = new("Choose the game directory to add to the list", this, FileChooserAction.SelectFolder, "Add", "Cancel")
736                  {
737                      SelectMultiple = true,
738                  };
739  
740                  if (fileChooser.Run() == (int)ResponseType.Accept)
741                  {
742                      _directoryChanged = false;
743                      foreach (string directory in fileChooser.Filenames)
744                      {
745                          if (_gameDirsBoxStore.GetIterFirst(out TreeIter treeIter))
746                          {
747                              do
748                              {
749                                  if (directory.Equals((string)_gameDirsBoxStore.GetValue(treeIter, 0)))
750                                  {
751                                      break;
752                                  }
753                              } while (_gameDirsBoxStore.IterNext(ref treeIter));
754                          }
755  
756                          if (!_directoryChanged)
757                          {
758                              _gameDirsBoxStore.AppendValues(directory);
759                          }
760                      }
761  
762                      _directoryChanged = true;
763                  }
764  
765                  fileChooser.Dispose();
766              }
767  
768              _addGameDirBox.Buffer.Text = "";
769  
770              ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
771          }
772  
773          private void RemoveDir_Pressed(object sender, EventArgs args)
774          {
775              TreeSelection selection = _gameDirsBox.Selection;
776  
777              if (selection.GetSelected(out TreeIter treeIter))
778              {
779                  _gameDirsBoxStore.Remove(ref treeIter);
780  
781                  _directoryChanged = true;
782              }
783  
784              ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
785          }
786  
787          private void CustThemeToggle_Activated(object sender, EventArgs args)
788          {
789              _custThemePath.Sensitive = _custThemeToggle.Active;
790              _custThemePathLabel.Sensitive = _custThemeToggle.Active;
791              _browseThemePath.Sensitive = _custThemeToggle.Active;
792          }
793  
794          private void BrowseThemeDir_Pressed(object sender, EventArgs args)
795          {
796              using (FileChooserNative fileChooser = new("Choose the theme to load", this, FileChooserAction.Open, "Select", "Cancel"))
797              {
798                  FileFilter filter = new()
799                  {
800                      Name = "Theme Files",
801                  };
802                  filter.AddPattern("*.css");
803  
804                  fileChooser.AddFilter(filter);
805  
806                  if (fileChooser.Run() == (int)ResponseType.Accept)
807                  {
808                      _custThemePath.Buffer.Text = fileChooser.Filename;
809                  }
810              }
811  
812              _browseThemePath.SetStateFlags(StateFlags.Normal, true);
813          }
814  
815          private void ConfigureController_Pressed(object sender, PlayerIndex playerIndex)
816          {
817              ((ToggleButton)sender).SetStateFlags(StateFlags.Normal, true);
818  
819              ControllerWindow controllerWindow = new(_parent, playerIndex);
820  
821              controllerWindow.SetSizeRequest((int)(controllerWindow.DefaultWidth * Program.WindowScaleFactor), (int)(controllerWindow.DefaultHeight * Program.WindowScaleFactor));
822              controllerWindow.Show();
823          }
824  
825          private void VolumeSlider_OnChange(object sender, EventArgs args)
826          {
827              ConfigurationState.Instance.System.AudioVolume.Value = (float)(_audioVolumeSlider.Value / 100);
828          }
829  
830          private void SaveToggle_Activated(object sender, EventArgs args)
831          {
832              SaveSettings();
833              Dispose();
834          }
835  
836          private void ApplyToggle_Activated(object sender, EventArgs args)
837          {
838              SaveSettings();
839          }
840  
841          private void CloseToggle_Activated(object sender, EventArgs args)
842          {
843              ConfigurationState.Instance.System.AudioVolume.Value = _previousVolumeLevel;
844              Dispose();
845          }
846      }
847  }