From feb61d62dce4c9eb5557fba2bae46b0646959e2f Mon Sep 17 00:00:00 2001 From: Pawel Osmolski Date: Mon, 12 Oct 2026 12:13:44 +0100 Subject: [PATCH] Improve Settings navigation, UI consistency and diagnostics --- DS4Windows/App.xaml.cs | 1 + DS4Windows/DS4Control/ControlService.cs | 15 +- .../DS4Control/DTOXml/AppSettingsDTO.cs | 9 + DS4Windows/DS4Control/DriverDiagnostics.cs | 105 ++ DS4Windows/DS4Control/ScpUtil.cs | 7 + .../Viiper/ViiperStartupDiagnostics.cs | 19 + DS4Windows/DS4Forms/AutoProfiles.xaml | 10 +- DS4Windows/DS4Forms/LanguagePackControl.xaml | 22 +- .../DS4Forms/LanguagePackControl.xaml.cs | 7 +- DS4Windows/DS4Forms/MainWindow.xaml | 958 +++++++++--------- DS4Windows/DS4Forms/MainWindow.xaml.cs | 86 +- DS4Windows/DS4Forms/SettingsNavigator.cs | 130 +++ .../DS4Forms/Themes/BridgeShellStyles.xaml | 180 +++- DS4Windows/DS4Forms/Themes/DarkTheme.xaml | 15 +- DS4Windows/DS4Forms/Themes/DefaultTheme.xaml | 18 +- .../ViewModels/LanguagePackViewModel.cs | 4 +- .../DS4Forms/ViewModels/LogViewModel.cs | 1 + DS4Windows/InstallationModeInfo.cs | 39 + DS4Windows/LogWriter.cs | 23 +- DS4Windows/SettingsSectionInfo.cs | 24 + DS4WindowsTests/LogDiagnosticsTests.cs | 34 + DS4WindowsTests/SettingsEnhancementTests.cs | 58 ++ DS4WindowsTests/SettingsNavigationTests.cs | 148 +++ .../ViiperStartupDiagnosticsTests.cs | 56 + docs/images/settings-navigation.png | Bin 0 -> 150247 bytes docs/settings-navigation.md | 15 + 26 files changed, 1448 insertions(+), 536 deletions(-) create mode 100644 DS4Windows/DS4Control/DriverDiagnostics.cs create mode 100644 DS4Windows/DS4Control/Viiper/ViiperStartupDiagnostics.cs create mode 100644 DS4Windows/DS4Forms/SettingsNavigator.cs create mode 100644 DS4Windows/InstallationModeInfo.cs create mode 100644 DS4Windows/SettingsSectionInfo.cs create mode 100644 DS4WindowsTests/LogDiagnosticsTests.cs create mode 100644 DS4WindowsTests/SettingsEnhancementTests.cs create mode 100644 DS4WindowsTests/SettingsNavigationTests.cs create mode 100644 DS4WindowsTests/ViiperStartupDiagnosticsTests.cs create mode 100644 docs/images/settings-navigation.png create mode 100644 docs/settings-navigation.md diff --git a/DS4Windows/App.xaml.cs b/DS4Windows/App.xaml.cs index dacfcfb5..47b0fdd7 100644 --- a/DS4Windows/App.xaml.cs +++ b/DS4Windows/App.xaml.cs @@ -416,6 +416,7 @@ private void ApplicationStartupCore(object sender, StartupEventArgs e) logger.Info($"DS4Windows version {version}"); logger.Info($"DS4Windows exe file: {DS4Windows.Global.exeFileName}"); logger.Info($"DS4Windows Assembly Architecture: {(Environment.Is64BitProcess ? "x64" : "x86")}"); + logger.Info(DS4Windows.InstallationModeInfo.Read()); foreach (string line in DS4Windows.WindowsVersionInfo.Read().LogLines) logger.Info(line); logger.Info($"System Architecture: {(Environment.Is64BitOperatingSystem ? "x64" : "x86")}"); diff --git a/DS4Windows/DS4Control/ControlService.cs b/DS4Windows/DS4Control/ControlService.cs index 5593c8a5..70e9c09d 100644 --- a/DS4Windows/DS4Control/ControlService.cs +++ b/DS4Windows/DS4Control/ControlService.cs @@ -1012,7 +1012,7 @@ private void DS4Devices_RequestElevation(RequestElevationArgs args) LogDebug($"{ExeName} was not added to HidHide: {rejection}", warning: true); return; } - LogDebug("HidHide control device found"); + LogDebug("HidHide control device found; driver " + DriverDiagnostics.Describe("HidHide")); lock (hidHideDriverMutationLock) { using (HidHideAPIDevice hidHideDevice = new HidHideAPIDevice()) @@ -2866,7 +2866,18 @@ private bool StartCore(bool showlog) } LogDebug($"Using output KB+M handler: {Global.outputKBMHandler.GetFullDisplayName()}"); - LogDebug("VIIPER virtual-controller backend ready"); + try + { + var backendStatus = ViiperSetupManager.GetStatus(); + foreach (string line in ViiperStartupDiagnostics.Describe(backendStatus)) + LogDebug(line, warning: !backendStatus.Ready); + LogDebug("USB/IP virtual host driver (usbip2_ude): " + DriverDiagnostics.Describe("usbip2_ude")); + LogDebug("USB/IP filter driver (usbip2_filter): " + DriverDiagnostics.Describe("usbip2_filter")); + } + catch (Exception error) + { + LogDebug("VIIPER/USB/IP diagnostics unavailable: " + error.Message, warning: true); + } DS4Devices.isExclusiveMode = getUseExclusiveMode(); //Re-enable Exclusive Mode diff --git a/DS4Windows/DS4Control/DTOXml/AppSettingsDTO.cs b/DS4Windows/DS4Control/DTOXml/AppSettingsDTO.cs index 32576259..a9e09cc4 100644 --- a/DS4Windows/DS4Control/DTOXml/AppSettingsDTO.cs +++ b/DS4Windows/DS4Control/DTOXml/AppSettingsDTO.cs @@ -448,6 +448,13 @@ public string CloseMinimizesString } } + private string lastSettingsSection = "General"; + public string LastSettingsSection + { + get => lastSettingsSection; + set => lastSettingsSection = SettingsSectionInfo.Normalize(value); + } + // Default UseLang to empty string. Not null public string UseLang { @@ -939,6 +946,7 @@ public void MapFrom(BackingStore source) PreferredViiperPath = source.preferredViiperPath; CloseMinimizes = source.closeMini; UseLang = source.useLang; + LastSettingsSection = source.lastSettingsSection; DownloadLang = source.downloadLang; FlashWhenLate = source.flashWhenLate; FlashWhenLateAt = source.flashWhenLateAt; @@ -1052,6 +1060,7 @@ public void MapTo(BackingStore destination) string.Empty; destination.closeMini = CloseMinimizes; destination.useLang = UseLang; + destination.lastSettingsSection = LastSettingsSection; destination.downloadLang = DownloadLang; destination.flashWhenLate = FlashWhenLate; destination.flashWhenLateAt = FlashWhenLateAt; diff --git a/DS4Windows/DS4Control/DriverDiagnostics.cs b/DS4Windows/DS4Control/DriverDiagnostics.cs new file mode 100644 index 00000000..cd2cad8a --- /dev/null +++ b/DS4Windows/DS4Control/DriverDiagnostics.cs @@ -0,0 +1,105 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.Win32; + +namespace DS4Windows +{ + internal static class DriverDiagnostics + { + internal static string Describe(string serviceName) + { + string version; + try + { + using var key = Registry.LocalMachine.OpenSubKey( + @"SYSTEM\CurrentControlSet\Services\" + serviceName); + if (key == null) return "not installed"; + string path = ViiperSetupManager.ResolveSystemDriverPath( + key.GetValue("ImagePath", null, RegistryValueOptions.DoNotExpandEnvironmentNames) as string); + version = !string.IsNullOrEmpty(path) && File.Exists(path) + ? FileVersionInfo.GetVersionInfo(path).FileVersion : null; + if (!string.IsNullOrWhiteSpace(version)) + version = "file version " + version; + else if (!string.IsNullOrEmpty(path) && File.Exists(path)) + { + // usbip-win2's SYS projects have no VERSIONINFO resource. + // Read only the INF beside the registered driver image, + // not an unrelated/staged package or the app version. + string inf = Path.ChangeExtension(path, ".inf"); + string packageVersion = File.Exists(inf) ? ReadInfVersion(File.ReadLines(inf)) : null; + if (packageVersion != null) version = "package version " + packageVersion + " (INF)"; + } + } + catch (Exception error) + { + return "version/status unavailable (" + error.Message + ")"; + } + return $"{(string.IsNullOrWhiteSpace(version) ? "version metadata unavailable" : version)}; {ReadState(serviceName)}"; + } + + internal static string ReadInfVersion(System.Collections.Generic.IEnumerable lines) + { + bool inVersion = false; + foreach (string raw in lines) + { + string line = raw.Split(';')[0].Trim(); + if (line.StartsWith("[", StringComparison.Ordinal)) + { + inVersion = line.Equals("[Version]", StringComparison.OrdinalIgnoreCase); + continue; + } + int equals = line.IndexOf('='); + if (!inVersion || equals < 0 || + !line.Substring(0, equals).Trim().Equals("DriverVer", StringComparison.OrdinalIgnoreCase)) continue; + string[] parts = line.Substring(equals + 1).Split(','); + if (parts.Length == 2 && Version.TryParse(parts[1].Trim(), out var version) && version.Revision >= 0) + return version.ToString(); + } + return null; + } + + private static string ReadState(string serviceName) + { + IntPtr manager = IntPtr.Zero, service = IntPtr.Zero; + try + { + manager = OpenSCManager(null, null, 0x0001); // SC_MANAGER_CONNECT + if (manager == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error()); + service = OpenService(manager, serviceName, 0x0004); // SERVICE_QUERY_STATUS + if (service == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error()); + if (!QueryServiceStatus(service, out var status)) throw new Win32Exception(Marshal.GetLastWin32Error()); + return status.CurrentState switch + { + 1 => "stopped", 2 => "starting", 3 => "stopping", 4 => "running", + 5 => "continuing", 6 => "pausing", 7 => "paused", _ => "state unknown" + }; + } + catch (Exception error) { return "status unavailable (" + error.Message + ")"; } + finally + { + if (service != IntPtr.Zero) CloseServiceHandle(service); + if (manager != IntPtr.Zero) CloseServiceHandle(manager); + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct ServiceStatus + { + public uint ServiceType, CurrentState, ControlsAccepted, Win32ExitCode, + ServiceSpecificExitCode, CheckPoint, WaitHint; + } + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr OpenSCManager(string machineName, string databaseName, uint access); + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr OpenService(IntPtr manager, string serviceName, uint access); + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool QueryServiceStatus(IntPtr service, out ServiceStatus status); + [DllImport("advapi32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseServiceHandle(IntPtr handle); + } +} diff --git a/DS4Windows/DS4Control/ScpUtil.cs b/DS4Windows/DS4Control/ScpUtil.cs index e51b9e87..a574a5de 100644 --- a/DS4Windows/DS4Control/ScpUtil.cs +++ b/DS4Windows/DS4Control/ScpUtil.cs @@ -1923,6 +1923,12 @@ public static int FormLocationY get { return m_Config.formLocationY; } } + public static string LastSettingsSection + { + get => SettingsSectionInfo.Normalize(m_Config.lastSettingsSection); + set => m_Config.lastSettingsSection = SettingsSectionInfo.Normalize(value); + } + public static string UseLang { set { m_Config.useLang = value; } @@ -5022,6 +5028,7 @@ public void setSZOutCurveMode(int index, int value) new Dictionary(), new Dictionary(), new Dictionary(), new Dictionary(), new Dictionary(), new Dictionary() }; public string useLang = ""; + public string lastSettingsSection = "General"; public bool downloadLang = true; public TrayIconChoice useIconChoice; public const bool DEFAULT_FLASH_WHEN_LATE = true; diff --git a/DS4Windows/DS4Control/Viiper/ViiperStartupDiagnostics.cs b/DS4Windows/DS4Control/Viiper/ViiperStartupDiagnostics.cs new file mode 100644 index 00000000..9f168189 --- /dev/null +++ b/DS4Windows/DS4Control/Viiper/ViiperStartupDiagnostics.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace DS4Windows +{ + internal static class ViiperStartupDiagnostics + { + internal static IEnumerable Describe(ViiperPrerequisiteStatus status) + { + yield return $"USB/IP (usbip-win2): installed version {status.UsbipVersion ?? "unknown or missing"}; required {ViiperSetupManager.RequiredUsbipVersion}"; + yield return $"USB/IP runtime: {(status.UsbipRuntimeReady ? "ready (driver communication succeeded)" : "not ready")}"; + if (!status.UsbipRuntimeReady && !string.IsNullOrWhiteSpace(status.UsbipProbeMessage)) + yield return "USB/IP diagnostic: " + status.UsbipProbeMessage; + yield return $"USB/IP package verification: {(status.UsbipExecutableSafe && status.UsbipDriverFilesSafe ? "passed" : "not passed")}"; + yield return $"VIIPER: supported version {ViiperSetupManager.SupportedViiperReleaseTag.TrimStart('v')}; selected executable {(status.ViiperPackageCurrent ? "verified" : "not verified")}; server {(status.ServerRunning ? "running and reachable" : "not ready")}"; + yield return status.Ready ? "VIIPER virtual-controller backend ready" + : "VIIPER virtual-controller backend not ready: " + status.DisplayText; + } + } +} diff --git a/DS4Windows/DS4Forms/AutoProfiles.xaml b/DS4Windows/DS4Forms/AutoProfiles.xaml index 7e543f57..76221ff8 100644 --- a/DS4Windows/DS4Forms/AutoProfiles.xaml +++ b/DS4Windows/DS4Forms/AutoProfiles.xaml @@ -277,10 +277,11 @@ - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - -