From e3e57affb49cc0ceb203abd44b92b7ff653af5d0 Mon Sep 17 00:00:00 2001 From: Ednunp <29843396+Ednunp@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:17:39 +0100 Subject: [PATCH] Fix Service menu shortcut: Alt+J, not Alt+S (Send owns Alt+S) Alt+S didn't open the Service menu because the always-visible "Send my audio" checkbox already owns Alt+S, and a visible control beats a top-level menu for the same Alt key. Every letter in "Service" (S/e/r/v/i/c) collides with a control (Send / Receive / Volume / Connected peers / EQ / ...), so the menu now uses Alt+J -- unused anywhere in the main window, so it opens reliably from every tab. Same approach the Record menu already uses ("Record (Alt+&K)"). Kept Send on Alt+S (frequent control) rather than moving it. New self-test "Menu shortcuts don't clash with controls": builds the main window and asserts no top-level menu mnemonic collides with any control mnemonic. The existing coverage audit couldn't catch this -- menu items are ToolStripItems, not Controls, so its control walk never saw them. Confirms the other four menus (File/Record/Options/Help) were already clean. Gate 31/31. Co-Authored-By: Claude Opus 4.8 --- src/RemSound.App/MainForm.cs | 7 +++++- src/RemSound.App/SelfTest.cs | 41 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/RemSound.App/MainForm.cs b/src/RemSound.App/MainForm.cs index be68c30..243465c 100644 --- a/src/RemSound.App/MainForm.cs +++ b/src/RemSound.App/MainForm.cs @@ -2202,7 +2202,12 @@ public sealed class MainForm : Form /// querying status needs no elevation. private ToolStripMenuItem BuildServiceMenu() { - var serviceMenu = new ToolStripMenuItem("&Service") { AccessibleName = "Service menu" }; + // Mnemonic is Alt+J, NOT Alt+S: the always-visible "Send my audio" checkbox already owns Alt+S, and + // a visible control beats a top-level menu for the same Alt key (that's why Alt+S didn't open this + // menu). J is unused anywhere in the main window, so it opens the menu reliably from every tab — + // same reason the Record menu uses "(Alt+&K)". Every letter in "Service" (S/e/r/v/i/c) collides with + // a control (Send/Receive/Volume/…). The "Menu shortcuts don't clash with controls" self-test guards this. + var serviceMenu = new ToolStripMenuItem("Service (Alt+&J)") { AccessibleName = "Service menu" }; var status = new ToolStripMenuItem("Service: …") { Enabled = false, AccessibleName = "Service status" }; var configure = new ToolStripMenuItem("&Configure service profile...") { AccessibleName = "Configure service profile" }; configure.Click += (_, _) => ConfigureServiceProfile(); diff --git a/src/RemSound.App/SelfTest.cs b/src/RemSound.App/SelfTest.cs index 2a149a1..0d205f2 100644 --- a/src/RemSound.App/SelfTest.cs +++ b/src/RemSound.App/SelfTest.cs @@ -81,6 +81,7 @@ internal static class SelfTest RunStep(results, "Auto-save non-read-only profiles (options + guard + silent timer)", AutoSaveNonReadOnlyProfiles); RunStep(results, "Service verb gate (normal launch stays load-safe)", ServiceVerbGate); RunStep(results, "Service capability probe (feature-detect, cached, safe)", ServiceCapabilityProbe); + RunStep(results, "Menu shortcuts don't clash with controls", MenuShortcutsDontClashWithControls); var failed = results.Count(r => r.Status == "FAIL"); var skipped = results.Count(r => r.Status == "SKIP"); @@ -1329,6 +1330,46 @@ internal static class SelfTest return "service machinery feature-detected as available, and the result is cached"; } + /// A top-level menu opens on Alt+<its mnemonic> — but a VISIBLE control that owns the same + /// Alt key steals it, so the menu never opens (this is exactly why Alt+S didn't open the Service menu: + /// the "Send my audio" checkbox owns Alt+S). Since the user could be on any tab when they press Alt, a + /// top-level menu's mnemonic must avoid EVERY control mnemonic in the window. This is invisible to the + /// main coverage audit because menu items are ToolStripItems, not Controls. + private static string? MenuShortcutsDontClashWithControls() + { + Form form; + try { form = new MainForm(null, RemSound.Core.Profile.NewBlank(), null, null, headless: true); } + catch (Exception ex) { return Skip($"headless MainForm could not be constructed: {ex.GetType().Name}: {ex.Message}"); } + + using (form) + { + var all = new List(); + void Walk(Control p) { foreach (Control c in p.Controls) { all.Add(c); Walk(c); } } + Walk(form); + + var menu = form.MainMenuStrip ?? all.OfType().FirstOrDefault(); + Check(menu is not null, "the main window must have a menu strip to audit"); + + // Top-level menu mnemonics (the Alt+letter that should open each menu). + var menuMnemonics = new Dictionary(); + foreach (ToolStripItem item in menu!.Items) + if (TryMnemonic(item.Text, out var m)) menuMnemonics[m] = item.Text ?? ""; + Check(menuMnemonics.Count >= 3, $"expected several top-level menu shortcuts (found {menuMnemonics.Count})"); + + // Every control mnemonic anywhere in the window (any tab could be showing when Alt is pressed). + var controlMnemonics = new Dictionary(); + foreach (var c in all) + if (TryMnemonic(c.Text, out var m)) controlMnemonics.TryAdd(m, string.IsNullOrWhiteSpace(c.Text) ? c.GetType().Name : c.Text!); + + var clashes = menuMnemonics.Keys.Where(controlMnemonics.ContainsKey) + .Select(k => $"Alt+{char.ToUpperInvariant(k)} — menu \"{menuMnemonics[k]}\" vs control \"{controlMnemonics[k]}\"") + .ToList(); + Check(clashes.Count == 0, "top-level menu shortcuts must not collide with control shortcuts (the control steals the key): " + string.Join("; ", clashes)); + + return $"{menuMnemonics.Count} top-level menu shortcuts, none stolen by a control"; + } + } + private static int CountControls(Control root, Func predicate) { var n = 0;