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.
///