diff --git a/src/RemSound.App/AboutDialog.cs b/src/RemSound.App/AboutDialog.cs index bbd94ea..fd8b1c4 100644 --- a/src/RemSound.App/AboutDialog.cs +++ b/src/RemSound.App/AboutDialog.cs @@ -1351,7 +1351,7 @@ internal sealed class AboutDialog : Form var closeButton = new Button { - Text = "Close", + Text = "&Close", AutoSize = true, DialogResult = DialogResult.OK, TabIndex = 1, diff --git a/src/RemSound.App/AddBandDialog.cs b/src/RemSound.App/AddBandDialog.cs index dd47bf8..12501d1 100644 --- a/src/RemSound.App/AddBandDialog.cs +++ b/src/RemSound.App/AddBandDialog.cs @@ -89,8 +89,8 @@ internal sealed class AddBandDialog : Form AddRow(grid, 2, "&End frequency in Hz (Alt+E)", endFreq); AddRow(grid, 3, "&Gain in dB (Alt+G)", gainDb); - var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.None, TabIndex = 0 }; - var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 1 }; + var okButton = new Button { Text = "&OK", AutoSize = true, DialogResult = DialogResult.None, TabIndex = 0 }; + var cancelButton = new Button { Text = "&Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 1 }; okButton.Click += (_, _) => TryAccept(); // OK before Cancel, in both tab order and left-to-right layout. diff --git a/src/RemSound.App/MainForm.cs b/src/RemSound.App/MainForm.cs index 87adcd6..5823207 100644 --- a/src/RemSound.App/MainForm.cs +++ b/src/RemSound.App/MainForm.cs @@ -1434,7 +1434,10 @@ public sealed class MainForm : Form try { processSelfMeter.Dispose(); } catch { } try { deviceChangeNotifier?.Dispose(); } catch { } try { powerResumeHandler?.Dispose(); } catch { } - try { routerPortMapper?.Dispose(); } catch { } + // NOTE: routerPortMapper.Dispose() is NOT called here — it's moved into the bounded + // background teardown below. Its Stop() does a SYNCHRONOUS DeletePortMap call to the router, + // which blocks (or hangs) when the router is slow/unresponsive; on the UI thread that froze the + // close, so enabling UPnP made RemSound impossible to shut (Andre, 2026-07-18). // Reverse every Win32 lever PerformanceMode applied. The kernel would clean // these up on process exit anyway, but doing it explicitly releases the power // request handle and matches our timeBeginPeriod with a timeEndPeriod. @@ -1449,14 +1452,22 @@ public sealed class MainForm : Form // the form-close path run; the OS reclaims any audio resources on process exit. // Worst case the user sees a brief tray-icon stutter; before this they saw a // ~16 s frozen window before the form went away. - var audioDispose = Task.Run(() => + // The UPnP router teardown (DeletePortMap — a synchronous call to a possibly-unresponsive + // router) belongs here too: like the ASIO dispose it can block for many seconds, and on the UI + // thread it froze the close. Run both OFF the UI thread, in parallel, under one hard timeout. + var slowTeardown = Task.Run(() => { - try { sender.Dispose(); } catch { /* ignore */ } - try { receiver.Dispose(); } catch { /* ignore */ } + var upnp = Task.Run(() => { try { routerPortMapper?.Dispose(); } catch { /* ignore */ } }); + var audio = Task.Run(() => + { + try { sender.Dispose(); } catch { /* ignore */ } + try { receiver.Dispose(); } catch { /* ignore */ } + }); + try { Task.WaitAll(upnp, audio); } catch { /* ignore */ } }); - if (!audioDispose.Wait(TimeSpan.FromSeconds(2))) + if (!slowTeardown.Wait(TimeSpan.FromSeconds(3))) { - try { logFile.Event("close: audio dispose taking >2s; letting process exit reclaim"); } catch { } + try { logFile.Event("close: UPnP/audio teardown taking >3s; letting process exit reclaim"); } catch { } } hotkeyController.Dispose(); @@ -2812,8 +2823,8 @@ public sealed class MainForm : Form private bool ShowSaveOnReadOnlyWarningDialog() { var verification = new TaskDialogVerificationCheckBox("Do not show me this message again"); - var saveButton = new TaskDialogButton("Save anyway"); - var cancelButton = new TaskDialogButton("Cancel") { AllowCloseDialog = true }; + var saveButton = new TaskDialogButton("&Save anyway"); + var cancelButton = new TaskDialogButton("&Cancel") { AllowCloseDialog = true }; var page = new TaskDialogPage { Caption = AppName, diff --git a/src/RemSound.App/ProfilePasswordDialog.cs b/src/RemSound.App/ProfilePasswordDialog.cs index 08e22ea..b5e7bef 100644 --- a/src/RemSound.App/ProfilePasswordDialog.cs +++ b/src/RemSound.App/ProfilePasswordDialog.cs @@ -47,8 +47,8 @@ internal static class ProfilePasswordDialog AutoSize = true, }; - var okButton = new Button { Text = "OK", AutoSize = true }; - var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel }; + var okButton = new Button { Text = "&OK", AutoSize = true }; + var cancelButton = new Button { Text = "&Cancel", AutoSize = true, DialogResult = DialogResult.Cancel }; // OK is validated by hand (no auto-close DialogResult) so we can block an empty entry when // a password is being REQUIRED. The trigger is the OK/Enter action, not typing — and diff --git a/src/RemSound.App/ProfilePasswordManagerDialog.cs b/src/RemSound.App/ProfilePasswordManagerDialog.cs index 497465b..27135bd 100644 --- a/src/RemSound.App/ProfilePasswordManagerDialog.cs +++ b/src/RemSound.App/ProfilePasswordManagerDialog.cs @@ -66,8 +66,8 @@ internal static class ProfilePasswordManagerDialog rows.Add((title, current, box)); } - var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.OK }; - var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel }; + var okButton = new Button { Text = "&OK", AutoSize = true, DialogResult = DialogResult.OK }; + var cancelButton = new Button { Text = "&Cancel", AutoSize = true, DialogResult = DialogResult.Cancel }; var buttons = new FlowLayoutPanel { Dock = DockStyle.Bottom, FlowDirection = FlowDirection.RightToLeft, AutoSize = true, Padding = new Padding(8) }; buttons.Controls.Add(okButton); buttons.Controls.Add(cancelButton); diff --git a/src/RemSound.App/QuickProfileSwitchDialog.cs b/src/RemSound.App/QuickProfileSwitchDialog.cs index 9a20225..d2fb8b0 100644 --- a/src/RemSound.App/QuickProfileSwitchDialog.cs +++ b/src/RemSound.App/QuickProfileSwitchDialog.cs @@ -80,7 +80,7 @@ internal sealed class QuickProfileSwitchDialog AutoSize = true, Padding = new Padding(0, 8, 0, 0), }; - var closeButton = new Button { Text = "Close", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 1 }; + var closeButton = new Button { Text = "&Close", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 1 }; buttons.Controls.Add(closeButton); root.Controls.Add(buttons, 0, 2); diff --git a/src/RemSound.App/RenamePeerDialog.cs b/src/RemSound.App/RenamePeerDialog.cs index b0bcb58..5c3a6be 100644 --- a/src/RemSound.App/RenamePeerDialog.cs +++ b/src/RemSound.App/RenamePeerDialog.cs @@ -58,7 +58,8 @@ internal sealed class RenamePeerDialog : Form grid.Controls.Add(nameBox, 1, 2); var clearButton = new Button { Text = "&Clear custom name (Alt+C)", AutoSize = true, AccessibleName = "Clear custom name", TabIndex = 1 }; - var okButton = new Button { Text = "OK", AutoSize = true, DialogResult = DialogResult.OK, TabIndex = 2 }; + var okButton = new Button { Text = "&OK", AutoSize = true, DialogResult = DialogResult.OK, TabIndex = 2 }; + // Cancel keeps Esc (it's the CancelButton) rather than a mnemonic — Alt+C is the Clear button here. var cancelButton = new Button { Text = "Cancel", AutoSize = true, DialogResult = DialogResult.Cancel, TabIndex = 3 }; clearButton.Click += (_, _) => diff --git a/src/RemSound.App/SingleInstanceDialog.cs b/src/RemSound.App/SingleInstanceDialog.cs index 7590269..49a94f2 100644 --- a/src/RemSound.App/SingleInstanceDialog.cs +++ b/src/RemSound.App/SingleInstanceDialog.cs @@ -28,9 +28,9 @@ internal static class SingleInstanceDialog { public static SingleInstanceDecision Ask() { - var switchButton = new TaskDialogButton("Switch to the running copy"); - var forceButton = new TaskDialogButton("Force the running copy to close and start fresh"); - var cancelButton = new TaskDialogButton("Cancel") { AllowCloseDialog = true }; + var switchButton = new TaskDialogButton("&Switch to the running copy"); + var forceButton = new TaskDialogButton("&Force the running copy to close and start fresh"); + var cancelButton = new TaskDialogButton("&Cancel") { AllowCloseDialog = true }; var page = new TaskDialogPage {