From d09c47432f0f5f46603c96915dd4111dda7ebb73 Mon Sep 17 00:00:00 2001 From: Ednunp <29843396+Ednunp@users.noreply.github.com> Date: Mon, 27 Jul 2026 12:40:34 +0100 Subject: [PATCH] Dialog-focus sweep: route every background-reachable dialog through ForegroundDialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ed flagged this as recurring. Swept all ShowDialog call sites and fixed the ones that can appear while RemSound is minimised in the tray (a plain ShowDialog opens BEHIND everything there — no foreground focus, NVDA can't find it): - Quick profile switch (QuickProfileSwitchDialog): opened by a GLOBAL hotkey, so the app is usually minimised when it fires. Was using an ad-hoc SetForegroundWindow that Windows blocks from a background process (only flashes the taskbar). Now via ForegroundDialog; deleted the weak BringToForeground + its P/Invoke. - Close-time unsaved-changes prompt (OnFormClosing): fires from tray -> Exit and OS shutdown while minimised. Now via ForegroundDialog. (Also covers the tray profile- switch, which routes through the same form-close path.) - (Service-profile dialog was fixed in the previous commit.) Verified the rest are safe: startup notices, update notices, the service/mic/Realtek/ About warnings and the password dialogs already use ForegroundDialog; every other ShowDialog(this) is menu- or button-driven with the app already focused (Preferences, EQ band, rename, recording settings, profile picker, hotkey capture nested in the menu-driven shortcuts dialog). Recorded the rule in memory so it stops recurring. Gate 71/71 + 7 relay tests; 0 warnings. Co-Authored-By: Claude Fable 5 --- src/RemSound.App/MainForm.cs | 7 +++-- src/RemSound.App/QuickProfileSwitchDialog.cs | 29 +++++--------------- 2 files changed, 12 insertions(+), 24 deletions(-) diff --git a/src/RemSound.App/MainForm.cs b/src/RemSound.App/MainForm.cs index a90523b..9159af4 100644 --- a/src/RemSound.App/MainForm.cs +++ b/src/RemSound.App/MainForm.cs @@ -9911,13 +9911,16 @@ public sealed partial class MainForm : Form // hook (false-NEGATIVE — user changes something via an unhooked path, no prompt // on close) is acceptable; the previous false-POSITIVE behaviour was nagging. { - var result = MessageBox.Show(this, + // Via ForegroundDialog: OnFormClosing fires from tray → Exit (RemSound minimised) and + // OS shutdown, so a plain MessageBox(this) would open behind everything and a blind + // user couldn't reach it to answer (Ed, 2026-07-27 dialog-focus sweep). + var result = ForegroundDialog.Show(owner => MessageBox.Show(owner, "You have unsaved changes to your profile. Save them before exiting?\n\n" + "Yes — save and exit.\nNo — exit without saving.\nCancel — keep RemSound open.", "RemSound — unsaved changes", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question, - MessageBoxDefaultButton.Button3); + MessageBoxDefaultButton.Button3)); if (result == DialogResult.Cancel) { diff --git a/src/RemSound.App/QuickProfileSwitchDialog.cs b/src/RemSound.App/QuickProfileSwitchDialog.cs index 60f698f..b81f787 100644 --- a/src/RemSound.App/QuickProfileSwitchDialog.cs +++ b/src/RemSound.App/QuickProfileSwitchDialog.cs @@ -22,12 +22,13 @@ internal sealed class QuickProfileSwitchDialog var (dialog, list, chosenPath) = Build(profiles); using (dialog) { - dialog.Shown += (_, _) => - { - BringToForeground(dialog); - list.Focus(); - }; - return dialog.ShowDialog() == DialogResult.OK ? chosenPath() : null; + dialog.Shown += (_, _) => list.Focus(); + // Via ForegroundDialog: this is opened by a GLOBAL hotkey, so RemSound is usually + // minimised in the tray when it fires — the old ad-hoc SetForegroundWindow only flashed + // the taskbar there (Windows blocks foreground from a background process), leaving a blind + // user Alt-Tabbing for it. ForegroundDialog's 1×1-owner + foreground-lock dance actually + // surfaces it with focus wherever RemSound is sitting (Ed, 2026-07-27 dialog-focus sweep). + return ForegroundDialog.Show(owner => dialog.ShowDialog(owner)) == DialogResult.OK ? chosenPath() : null; } } @@ -142,20 +143,4 @@ internal sealed class QuickProfileSwitchDialog return (dialog, list, () => chosenPath); } - - private static void BringToForeground(Form form) - { - try - { - form.Activate(); - // The hotkey press is recent user input, so the foreground lock lets us call this - // (same handshake the tray "restore" uses). - SetForegroundWindow(form.Handle); - } - catch { /* foreground-lock race — best effort, the window is still TopMost */ } - } - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool SetForegroundWindow(IntPtr hWnd); }