Wallpaper: restore the user's desktop when going dormant
The wallpaper maker/runtime overrides the desktop with the cockpit wallpaper on profile activation but never put the user's own back. Now RioCoordinator captures the current wallpaper (SPI_GETDESKWALLPAPER, via new WallpaperApplier.GetCurrent) the first time it overrides it, and restores it (WallpaperApplier.Restore) on every GoDormant and on Dispose — so going idle, a native game taking the port, or app exit returns the desktop to what the user had. Capture is once-per-override so switching between cockpit profiles keeps the real previous wallpaper; only engages when OverlayTemplatePath is set. Builds clean on net48 + net40. Known gap (documented in PLAN.md): a hard crash between apply and restore leaves the cockpit wallpaper, since SPIF_UPDATEINIFILE persists it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+9
-2
@@ -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
|
||||
|
||||
@@ -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<AppConfig> config, Func<string, IRioTransport>? 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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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").
|
||||
/// </summary>
|
||||
private void CaptureWallpaperOnce()
|
||||
{
|
||||
if (_savedWallpaper != null)
|
||||
return;
|
||||
try { _savedWallpaper = WallpaperApplier.GetCurrent(); }
|
||||
catch { _savedWallpaper = string.Empty; } // best-effort; empty just clears on restore
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Put the user's pre-activation wallpaper back, if we overrode it. Called when
|
||||
/// going dormant and on shutdown; best-effort and idempotent.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Apply <paramref name="imagePath"/> as the desktop wallpaper. Returns true on
|
||||
@@ -50,7 +53,36 @@ public static class WallpaperApplier
|
||||
SPI_SETDESKWALLPAPER, 0, full, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read the desktop wallpaper Windows currently has set (empty string when none
|
||||
/// is set — a solid-color desktop). Capture this before <see cref="Apply"/>
|
||||
/// overrides it, so <see cref="Restore"/> can put it back when going dormant.
|
||||
/// </summary>
|
||||
public static string GetCurrent()
|
||||
{
|
||||
var buffer = new StringBuilder(MaxPath);
|
||||
bool ok = SystemParametersInfo(SPI_GETDESKWALLPAPER, buffer.Capacity, buffer, 0);
|
||||
return ok ? buffer.ToString() : string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore a wallpaper previously read by <see cref="GetCurrent"/>. Unlike
|
||||
/// <see cref="Apply"/> 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user