diff --git a/docs/PLAN.md b/docs/PLAN.md index 97e4636..8d198f6 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -254,8 +254,15 @@ Replaces the legacy Google-Sheet → `.data` → GIMP → Script-Fu pipeline generates + applies the wallpaper on profile activation when `AppConfig.OverlayTemplatePath` is set (best-effort, off by default, never breaks activation). The live `SystemParametersInfo` apply changes a user setting, so it - is gated behind config and not exercised by tests. ⏳ Optional: restore the prior - wallpaper when going dormant. + is gated behind config and not exercised by tests. **Restore-on-dormant — done ✅:** + `RioCoordinator` captures the user's own wallpaper (`WallpaperApplier.GetCurrent`) + the first time it overrides it, and puts it back (`WallpaperApplier.Restore`) on + every `GoDormant` and on `Dispose` — so idle/native-game/exit return the desktop + to what the user had. Capture is once-per-override (switching between cockpit + profiles never records a cockpit wallpaper as the "previous"); only engages when + `OverlayTemplatePath` is set. Known gap: a hard crash between apply and restore + leaves the cockpit wallpaper (SPIF_UPDATEINIFILE persists it) — a future + crash-recovery could persist the saved path to config. - **Wallpaper maker — done ✅.** `RioJoy.Tray/Editor/WallpaperMakerForm` (tray → "Wallpaper maker") is the interactive replacement for the Sheet → GIMP pipeline: it renders the profile's wallpaper live on the template base image, outlines all diff --git a/src/RioJoy.Tray/RioCoordinator.cs b/src/RioJoy.Tray/RioCoordinator.cs index 9d4eedd..16effc1 100644 --- a/src/RioJoy.Tray/RioCoordinator.cs +++ b/src/RioJoy.Tray/RioCoordinator.cs @@ -44,6 +44,11 @@ public sealed class RioCoordinator : IDisposable private IDisposable? _joystick; private string? _activeProfileName; + // The user's own desktop wallpaper, captured the first time we override it with + // a cockpit wallpaper. null = we are not currently overriding (nothing to + // restore). "" is a valid captured value (the user had no wallpaper). + private string? _savedWallpaper; + public RioCoordinator(Func config, Func? transportFactory = null) { _config = config ?? throw new ArgumentNullException(nameof(config)); @@ -257,6 +262,7 @@ public sealed class RioCoordinator : IDisposable return; try { + CaptureWallpaperOnce(); WallpaperApplier.Apply(profile.WallpaperPath!); } catch (Exception ex) @@ -280,6 +286,7 @@ public sealed class RioCoordinator : IDisposable new ProfileWallpaperGenerator().Generate(template, templateDir, profile.OverlayLabels, outPath); profile.WallpaperPath = outPath; + CaptureWallpaperOnce(); WallpaperApplier.Apply(outPath); } catch (Exception ex) @@ -289,6 +296,32 @@ public sealed class RioCoordinator : IDisposable #endif } + /// + /// Remember the user's current desktop wallpaper the first time we override it, + /// so it can be restored when we go dormant. No-op once captured (so switching + /// between cockpit profiles never records a cockpit wallpaper as the "previous"). + /// + private void CaptureWallpaperOnce() + { + if (_savedWallpaper != null) + return; + try { _savedWallpaper = WallpaperApplier.GetCurrent(); } + catch { _savedWallpaper = string.Empty; } // best-effort; empty just clears on restore + } + + /// + /// Put the user's pre-activation wallpaper back, if we overrode it. Called when + /// going dormant and on shutdown; best-effort and idempotent. + /// + private void RestoreWallpaper() + { + if (_savedWallpaper == null) + return; // never overrode — leave the desktop alone + try { WallpaperApplier.Restore(_savedWallpaper); } + catch { /* best-effort — never block going dormant */ } + _savedWallpaper = null; + } + private static string SafeFileName(string name) { foreach (char c in Path.GetInvalidFileNameChars()) @@ -299,6 +332,7 @@ public sealed class RioCoordinator : IDisposable private void GoDormant(string status) { Teardown(); + RestoreWallpaper(); // put the user's own desktop back SetStatus(status); } @@ -330,5 +364,9 @@ public sealed class RioCoordinator : IDisposable StatusChanged?.Invoke(status); } - public void Dispose() => Teardown(); + public void Dispose() + { + Teardown(); + RestoreWallpaper(); // clean exit shouldn't leave a cockpit wallpaper behind + } } diff --git a/src/RioJoy.Tray/WallpaperApplier.cs b/src/RioJoy.Tray/WallpaperApplier.cs index 29d995e..797689e 100644 --- a/src/RioJoy.Tray/WallpaperApplier.cs +++ b/src/RioJoy.Tray/WallpaperApplier.cs @@ -1,4 +1,5 @@ using System.Runtime.InteropServices; +using System.Text; namespace RioJoy.Tray; @@ -16,8 +17,10 @@ namespace RioJoy.Tray; public static class WallpaperApplier { private const int SPI_SETDESKWALLPAPER = 0x0014; + private const int SPI_GETDESKWALLPAPER = 0x0073; private const int SPIF_UPDATEINIFILE = 0x01; // persist across logon private const int SPIF_SENDCHANGE = 0x02; // broadcast WM_SETTINGCHANGE + private const int MaxPath = 260; /// /// Apply as the desktop wallpaper. Returns true on @@ -50,7 +53,36 @@ public static class WallpaperApplier SPI_SETDESKWALLPAPER, 0, full, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); } + /// + /// Read the desktop wallpaper Windows currently has set (empty string when none + /// is set — a solid-color desktop). Capture this before + /// overrides it, so can put it back when going dormant. + /// + public static string GetCurrent() + { + var buffer = new StringBuilder(MaxPath); + bool ok = SystemParametersInfo(SPI_GETDESKWALLPAPER, buffer.Capacity, buffer, 0); + return ok ? buffer.ToString() : string.Empty; + } + + /// + /// Restore a wallpaper previously read by . Unlike + /// this does not require the file to exist and accepts an + /// empty path (which clears the wallpaper to the solid desktop color) — the + /// value came straight from Windows, so it is passed back verbatim. Returns the + /// API result; callers treat it as best-effort. + /// + public static bool Restore(string? previous) + { + return SystemParametersInfo( + SPI_SETDESKWALLPAPER, 0, previous ?? string.Empty, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE); + } + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SystemParametersInfo(int uAction, int uParam, string lpvParam, int fuWinIni); + + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SystemParametersInfo(int uAction, int uParam, StringBuilder lpvParam, int fuWinIni); }