Files
RemSound/src/RemSound.App/ForegroundDialog.cs
T
EdnunpandClaude Opus 4.8 55bdcde0af Release v3.6: rebuild the self-updater (in-app, rollback-safe); warnings to front
Replace the generated cmd.exe + robocopy update helper — which silently
failed on some machines — with an in-app C# installer:
  * Stage the new version to a per-user temp folder off the install and run
    the new RemSound.exe from there, so nothing in the install is locked by
    the updater itself.
  * Wait for the old process to fully exit (real WaitForExit), then
    back-up-and-swap files in C# with retry + rename-aside; roll the install
    back to the previous version on any failure, so a failed update can never
    leave a half-installed RemSound.
  * Log every step to updater.log; clean up old stages and legacy batch
    artefacts on launch. Removed the old BuildInstallScript batch generator.

Route the "RemSound is already running" dialog and its follow-up message
through ForegroundDialog so they surface in front from a background-relaunched
copy, matching the earlier post-update fix.

Docs: rewrite the readme update sections for the new mechanism, regenerate
MANUAL.md, refresh the About-box changelog and RELEASE_NOTES.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 13:37:19 +01:00

101 lines
5.1 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace RemSound.App;
/// <summary>
/// Shows a dialog FRONT-AND-CENTRE with keyboard focus, WITHOUT disturbing the main window — it can
/// stay minimised in the tray the whole time. We give the dialog a momentary, top-most, off-taskbar
/// 1×1 owner window at screen centre and force THAT owner to the foreground (the AttachThreadInput
/// dance bypasses Windows' focus-stealing lock), so the modal dialog opens on top with focus and a
/// screen reader lands on it wherever RemSound happens to be sitting. Used for every warning/notice
/// the app raises (mic-privacy, Realtek, About-after-update, config-moved), so a minimised RemSound
/// never leaves a blind user with a dialog dinging away behind everything.
/// </summary>
internal static class ForegroundDialog
{
/// <summary>Run <paramref name="show"/> with a foreground 1×1 owner; returns its result.</summary>
public static T Show<T>(Func<IWin32Window, T> show)
{
var area = Screen.PrimaryScreen?.WorkingArea ?? new Rectangle(0, 0, 800, 600);
using var owner = new Form
{
ShowInTaskbar = false,
FormBorderStyle = FormBorderStyle.None,
StartPosition = FormStartPosition.Manual,
Size = new Size(1, 1),
Location = new Point(area.X + area.Width / 2, area.Y + area.Height / 2),
TopMost = true,
};
owner.Show();
ForceForeground(owner.Handle);
try { return show(owner); }
finally { try { owner.Close(); } catch { /* ignore */ } }
}
/// <summary>Void convenience overload.</summary>
public static void Show(Action<IWin32Window> show) =>
Show<object?>(owner => { show(owner); return null; });
/// <summary>Force <paramref name="hWnd"/> to the foreground even when RemSound isn't the active
/// app — including the hardest case: a process the auto-updater RELAUNCHED in the background while
/// RemSound was minimised. Windows refuses SetForegroundWindow to a background-spawned process (it
/// only flashes the taskbar button — invisible to a screen-reader user), so we (1) drop the system
/// foreground-lock timeout to 0 for the duration of the call, and (2) attach our input queue to the
/// current foreground thread. Both the timeout and the attachment are restored immediately after.</summary>
private static void ForceForeground(IntPtr hWnd)
{
uint savedTimeout = 0;
var loweredTimeout = false;
try
{
// Lift Windows' foreground-stealing lock for this one call — the key fix for a dialog that
// must surface from a background-relaunched (post-update) process, where AttachThreadInput
// alone isn't enough and the dialog would otherwise open behind everything.
if (SystemParametersInfoGet(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, out savedTimeout, 0))
loweredTimeout = SystemParametersInfoSet(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, IntPtr.Zero, SPIF_SENDCHANGE);
var foreThread = GetWindowThreadProcessId(GetForegroundWindow(), out _);
var thisThread = GetCurrentThreadId();
if (foreThread != 0 && foreThread != thisThread)
{
AttachThreadInput(foreThread, thisThread, true);
try
{
BringWindowToTop(hWnd);
SetForegroundWindow(hWnd);
}
finally { AttachThreadInput(foreThread, thisThread, false); }
}
else
{
BringWindowToTop(hWnd);
SetForegroundWindow(hWnd);
}
}
catch { /* best-effort; the owner is top-most anyway */ }
finally
{
if (loweredTimeout)
{
try { SystemParametersInfoSet(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, new IntPtr((long)savedTimeout), SPIF_SENDCHANGE); }
catch { /* restoring the user's setting is best-effort */ }
}
}
}
private const uint SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000;
private const uint SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001;
private const uint SPIF_SENDCHANGE = 0x0002;
[DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
[DllImport("user32.dll")] private static extern uint GetCurrentThreadId();
[DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] private static extern bool BringWindowToTop(IntPtr hWnd);
[DllImport("user32.dll", EntryPoint = "SystemParametersInfoW")] private static extern bool SystemParametersInfoGet(uint uiAction, uint uiParam, out uint pvParam, uint fWinIni);
[DllImport("user32.dll", EntryPoint = "SystemParametersInfoW")] private static extern bool SystemParametersInfoSet(uint uiAction, uint uiParam, IntPtr pvParam, uint fWinIni);
}