diff --git a/src/DiffEngineViewer.Windows.Tests/AmpersandTests.cs b/src/DiffEngineViewer.Windows.Tests/AmpersandTests.cs new file mode 100644 index 00000000..4af421c9 --- /dev/null +++ b/src/DiffEngineViewer.Windows.Tests/AmpersandTests.cs @@ -0,0 +1,34 @@ +/// +/// An ampersand in a solution name, a path or an applier message. WinForms reads one as a mnemonic +/// wherever it renders text, so "R&D" draws as "R_D" - with D live as an accelerator, which is +/// worse than the missing character. +/// +[NotInParallel] +[TUnit.Core.Executors.STAThreadExecutor] +public class AmpersandTests +{ + [Test] + public async Task A_menu_label_keeps_its_ampersand() + { + using var strip = ViewerMenu.Build(new(0, ["Accept all in R&D"])); + + var item = strip.Items + .Cast() + .Single(); + // Doubled, which is how a literal one is written. What is drawn is one + await Assert.That(item.Text).IsEqualTo("Accept all in R&&D"); + } + + [Test] + public async Task The_status_line_does_not_read_one_as_a_mnemonic() + { + using var form = new ViewerForm("title", 800, 600); + + var status = form.Controls + .Find("status", true) + .OfType() + .Single(); + + await Assert.That(status.UseMnemonic).IsFalse(); + } +} diff --git a/src/DiffEngineViewer.Windows/ViewerForm.cs b/src/DiffEngineViewer.Windows/ViewerForm.cs index fe3a3456..cfa7c4ed 100644 --- a/src/DiffEngineViewer.Windows/ViewerForm.cs +++ b/src/DiffEngineViewer.Windows/ViewerForm.cs @@ -20,10 +20,15 @@ sealed class ViewerForm : Form readonly Label status = new() { + Name = "status", Dock = DockStyle.Fill, TextAlign = ContentAlignment.MiddleRight, ForeColor = Palette.Dim, - AutoSize = false + AutoSize = false, + // The status line is built from paths, solution names and whatever the applier said, and a + // Label reads an ampersand in any of those as a mnemonic: "R&D" drew as "R_D" with D live + // as an accelerator. + UseMnemonic = false }; readonly List pool = []; diff --git a/src/DiffEngineViewer.Windows/ViewerMenu.cs b/src/DiffEngineViewer.Windows/ViewerMenu.cs index 50014992..94b145f0 100644 --- a/src/DiffEngineViewer.Windows/ViewerMenu.cs +++ b/src/DiffEngineViewer.Windows/ViewerMenu.cs @@ -41,12 +41,21 @@ public static void Fill(ContextMenuStrip strip, MenuOverlay menu, Action cl for (var index = 0; index < menu.Labels.Count; index++) { var captured = index; - var item = new ToolStripMenuItem(menu.Labels[index]); + var item = new ToolStripMenuItem(Escape(menu.Labels[index])); item.Click += (_, _) => clicked(captured); strip.Items.Add(item); } } + /// + /// Menu labels carry solution names and file names - "Accept all in R&D" - and a menu item + /// reads an ampersand as a mnemonic, so that one drew as "Accept all in R_D" with D live as an + /// accelerator. Doubling it is how a literal one is written; ToolStripItem has no + /// UseMnemonic to turn the reading off. + /// + static string Escape(string label) => + label.Replace("&", "&&"); + /// /// A strip on its own, for tests and for anything that wants one without a window. ///