diff --git a/Keybinding.Test/ModifierAliasNormalizationTests.cs b/Keybinding.Test/ModifierAliasNormalizationTests.cs index bdd46f0..449fd09 100644 --- a/Keybinding.Test/ModifierAliasNormalizationTests.cs +++ b/Keybinding.Test/ModifierAliasNormalizationTests.cs @@ -66,6 +66,44 @@ public void EveryConstructionPath_NormalizesModifierAliases(string alias, string } } + [TestMethod] + [DataRow("Esc", "Escape")] + [DataRow("Return", "Enter")] + [DataRow("Del", "Delete")] + [DataRow("Ins", "Insert")] + [DataRow("PgUp", "PageUp")] + [DataRow("PgDn", "PageDown")] + [DataRow("Up", "ArrowUp")] + [DataRow("UpArrow", "ArrowUp")] + [DataRow("Down", "ArrowDown")] + [DataRow("DownArrow", "ArrowDown")] + [DataRow("Left", "ArrowLeft")] + [DataRow("LeftArrow", "ArrowLeft")] + [DataRow("Right", "ArrowRight")] + [DataRow("RightArrow", "ArrowRight")] + [DataRow("Option", "Alt")] + [DataRow("Super", "Meta")] + [DataRow("D0", "0")] + [DataRow("D1", "1")] + [DataRow("D2", "2")] + [DataRow("D3", "3")] + [DataRow("D4", "4")] + [DataRow("D5", "5")] + [DataRow("D6", "6")] + [DataRow("D7", "7")] + [DataRow("D8", "8")] + [DataRow("D9", "9")] + public void ChordParse_KeyAliasesMatchCanonicalName(string alias, string canonical) + { + ArgumentNullException.ThrowIfNull(alias); + Chord expected = Chord.Parse($"Ctrl+{canonical}"); + Chord actual = Chord.Parse($"Ctrl+{alias}"); + + Assert.AreEqual(expected, actual); + Assert.AreEqual(expected.GetHashCode(), actual.GetHashCode()); + Assert.AreEqual(new Note(canonical), new Note(NoteName.Create(alias.ToUpperInvariant()))); + } + [TestMethod] [DataRow("control")] [DataRow("Control")] @@ -125,4 +163,20 @@ public async Task StoredProfileWithAliasSpelling_MatchesAfterReload() Assert.AreEqual(Chord.Parse("Ctrl+S"), profile.GetChord("save")); Assert.AreEqual(Chord.Parse("Meta+F"), profile.GetChord("find")); } + + [TestMethod] + public async Task StoredProfileWithKeyAlias_ExecutesCanonicalChordAfterLoad() + { + string json = """ + [{"id":"p","name":"Profile","chords":{"save":{"notes":["CTRL","ESC"]}}}] + """; + await File.WriteAllTextAsync(Path.Combine(_testDataDirectory, Constants.Files.ProfilesFileName), json).ConfigureAwait(false); + + using KeybindingManager manager = new(_testDataDirectory); + await manager.InitializeAsync().ConfigureAwait(false); + manager.Commands.RegisterCommand(new Command("save", "Save")); + manager.Profiles.SetActiveProfile("p"); + + Assert.AreEqual("save", manager.Keybindings.ExecuteChord(Chord.Parse("Ctrl+Escape"))); + } } diff --git a/Keybinding/Models/MusicalTypes.cs b/Keybinding/Models/MusicalTypes.cs index 16e514c..0ceac48 100644 --- a/Keybinding/Models/MusicalTypes.cs +++ b/Keybinding/Models/MusicalTypes.cs @@ -59,17 +59,36 @@ private static string ValidateKey(string key) => : key; /// - /// Maps a modifier alias to its canonical key name, so that every way of building a note + /// Maps key aliases to canonical key names, so that every way of building a note /// (parsing, constructing directly, or loading a stored profile) compares and hashes the same. /// /// An uppercase key name /// The canonical key name - private static string CanonicalizeKey(string key) => key switch + private static string CanonicalizeKey(string key) { - "CONTROL" => "CTRL", - "WIN" or "WINDOWS" or "CMD" or "COMMAND" => "META", - _ => key - }; + if (key.Length == 2 && key[0] == 'D' && key[1] is >= '0' and <= '9') + { + return key[1..]; + } + + return key switch + { + "CONTROL" => "CTRL", + "OPTION" => "ALT", + "WIN" or "WINDOWS" or "CMD" or "COMMAND" or "SUPER" => "META", + "ESC" => nameof(SpecialKeys.Escape).ToUpperInvariant(), + "RETURN" => nameof(SpecialKeys.Enter).ToUpperInvariant(), + "DEL" => nameof(SpecialKeys.Delete).ToUpperInvariant(), + "INS" => nameof(SpecialKeys.Insert).ToUpperInvariant(), + "PGUP" => nameof(SpecialKeys.PageUp).ToUpperInvariant(), + "PGDN" => nameof(SpecialKeys.PageDown).ToUpperInvariant(), + "UP" or "UPARROW" => nameof(SpecialKeys.ArrowUp).ToUpperInvariant(), + "DOWN" or "DOWNARROW" => nameof(SpecialKeys.ArrowDown).ToUpperInvariant(), + "LEFT" or "LEFTARROW" => nameof(SpecialKeys.ArrowLeft).ToUpperInvariant(), + "RIGHT" or "RIGHTARROW" => nameof(SpecialKeys.ArrowRight).ToUpperInvariant(), + _ => key + }; + } /// /// Gets the key that this note represents @@ -309,7 +328,7 @@ public static Chord Parse(string value) foreach (string part in parts) { - // The Note constructor normalizes modifier aliases such as "Control" and "Cmd" + // The Note constructor normalizes key aliases such as "Control" and "Esc" notes.Add(new Note(part)); }