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 }