From 79273b88f68c138b4bd8d9ab1cb47dca1cffcac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 20:42:42 +0200 Subject: [PATCH 1/8] Add stub of method to deduplicate bindings (and failing test) --- .../Input/RealmKeyBindingStoreTest.cs | 90 +++++++++++++++++++ osu.Game/Input/RealmKeyBindingStore.cs | 11 +++ 2 files changed, 101 insertions(+) create mode 100644 osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs diff --git a/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs b/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs new file mode 100644 index 0000000000..0e8e73b8b4 --- /dev/null +++ b/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs @@ -0,0 +1,90 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Input.Bindings; +using osu.Game.Input; +using osu.Game.Input.Bindings; +using osuTK.Input; + +namespace osu.Game.Tests.Input +{ + [TestFixture] + public class RealmKeyBindingStoreTest + { + [Test] + public void TestBindingsWithoutDuplicatesAreNotModified() + { + var bindings = new List + { + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)), + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromMouseButton(MouseButton.Button1)), + new RealmKeyBinding(GlobalAction.MusicPrev, KeyCombination.FromKey(Key.F1)), + new RealmKeyBinding(GlobalAction.MusicNext, KeyCombination.FromKey(Key.F5)) + }; + + bool anyCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings); + + Assert.Multiple(() => + { + Assert.That(anyCleared, Is.False); + + Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.Escape))); + + Assert.That(bindings[1].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[1].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.ExtraMouseButton1))); + + Assert.That(bindings[2].Action, Is.EqualTo((int)GlobalAction.MusicPrev)); + Assert.That(bindings[2].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F1))); + + Assert.That(bindings[3].Action, Is.EqualTo((int)GlobalAction.MusicNext)); + Assert.That(bindings[3].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F5))); + }); + } + + [Test] + public void TestDuplicateBindingsAreCleared() + { + var bindings = new List + { + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)), + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromMouseButton(MouseButton.Button1)), + new RealmKeyBinding(GlobalAction.MusicPrev, KeyCombination.FromKey(Key.F1)), + new RealmKeyBinding(GlobalAction.IncreaseVolume, KeyCombination.FromKey(Key.Escape)), + new RealmKeyBinding(GlobalAction.MusicNext, KeyCombination.FromKey(Key.F5)), + new RealmKeyBinding(GlobalAction.ExportReplay, KeyCombination.FromKey(Key.F1)), + new RealmKeyBinding(GlobalAction.TakeScreenshot, KeyCombination.FromKey(Key.PrintScreen)), + }; + + bool anyCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings); + + Assert.Multiple(() => + { + Assert.That(anyCleared, Is.True); + + Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None))); + + Assert.That(bindings[1].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[1].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.ExtraMouseButton1))); + + Assert.That(bindings[2].Action, Is.EqualTo((int)GlobalAction.MusicPrev)); + Assert.That(bindings[2].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None))); + + Assert.That(bindings[3].Action, Is.EqualTo((int)GlobalAction.IncreaseVolume)); + Assert.That(bindings[3].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None))); + + Assert.That(bindings[4].Action, Is.EqualTo((int)GlobalAction.MusicNext)); + Assert.That(bindings[4].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F5))); + + Assert.That(bindings[5].Action, Is.EqualTo((int)GlobalAction.ExportReplay)); + Assert.That(bindings[5].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None))); + + Assert.That(bindings[6].Action, Is.EqualTo((int)GlobalAction.TakeScreenshot)); + Assert.That(bindings[6].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.PrintScreen))); + }); + } + } +} diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 10ad731037..6affdab277 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -130,5 +130,16 @@ public static bool CheckValidForGameplay(KeyCombination combination) return true; } + + /// + /// Clears all s from the provided + /// which are assigned to more than one binding. + /// + /// The s to de-duplicate. + /// Whether any bindings have been cleared. + public static bool ClearDuplicateBindings(IQueryable keyBindings) + { + return false; + } } } From 90c44cee5475ebfc4702e9c1cf64040aa1d4fe22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 20:46:20 +0200 Subject: [PATCH 2/8] Implement method to deduplicate keybindings --- .../Input/RealmKeyBindingStoreTest.cs | 5 +++-- osu.Game/Input/RealmKeyBindingStore.cs | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs b/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs index 0e8e73b8b4..fb1e27d9a9 100644 --- a/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs +++ b/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Input.Bindings; using osu.Game.Input; @@ -24,7 +25,7 @@ public void TestBindingsWithoutDuplicatesAreNotModified() new RealmKeyBinding(GlobalAction.MusicNext, KeyCombination.FromKey(Key.F5)) }; - bool anyCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings); + bool anyCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings.AsQueryable()); Assert.Multiple(() => { @@ -58,7 +59,7 @@ public void TestDuplicateBindingsAreCleared() new RealmKeyBinding(GlobalAction.TakeScreenshot, KeyCombination.FromKey(Key.PrintScreen)), }; - bool anyCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings); + bool anyCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings.AsQueryable()); Assert.Multiple(() => { diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 6affdab277..ceef751605 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -139,7 +139,22 @@ public static bool CheckValidForGameplay(KeyCombination combination) /// Whether any bindings have been cleared. public static bool ClearDuplicateBindings(IQueryable keyBindings) { - return false; + bool anyRemoved = false; + + var lookup = keyBindings.ToLookup(kb => kb.KeyCombination); + + foreach (var group in lookup) + { + if (group.Count() <= 1) + continue; + + foreach (var binding in group) + binding.KeyCombination = new KeyCombination(InputKey.None); + + anyRemoved = true; + } + + return anyRemoved; } } } From 639c96e60c6bc4590e65f0775d392788bf176d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 21:02:24 +0200 Subject: [PATCH 3/8] Prevent ruleset input managers from reading duplicate bindings --- osu.Game/Input/RealmKeyBindingStore.cs | 2 +- osu.Game/Rulesets/UI/RulesetInputManager.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index ceef751605..b09e29f48e 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -137,7 +137,7 @@ public static bool CheckValidForGameplay(KeyCombination combination) /// /// The s to de-duplicate. /// Whether any bindings have been cleared. - public static bool ClearDuplicateBindings(IQueryable keyBindings) + public static bool ClearDuplicateBindings(IQueryable keyBindings) { bool anyRemoved = false; diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 26b9d06f73..7f081b6e8a 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -218,6 +218,7 @@ protected override void ReloadMappings(IQueryable realmKeyBindi base.ReloadMappings(realmKeyBindings); KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList(); + RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings.AsQueryable()); } } } From 9c6166ec3dcf9eb805ac0d38b63592f562d0e801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 21:26:46 +0200 Subject: [PATCH 4/8] Add migration to remove duplicate bindings --- .../Input/RealmKeyBindingStoreTest.cs | 9 +++--- osu.Game/Database/RealmAccess.cs | 31 ++++++++++++++++++- osu.Game/Input/RealmKeyBindingStore.cs | 10 +++--- osu.Game/Rulesets/UI/RulesetInputManager.cs | 2 +- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs b/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs index fb1e27d9a9..a0c1b20cd7 100644 --- a/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs +++ b/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using NUnit.Framework; using osu.Framework.Input.Bindings; using osu.Game.Input; @@ -25,11 +24,11 @@ public void TestBindingsWithoutDuplicatesAreNotModified() new RealmKeyBinding(GlobalAction.MusicNext, KeyCombination.FromKey(Key.F5)) }; - bool anyCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings.AsQueryable()); + int countCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings); Assert.Multiple(() => { - Assert.That(anyCleared, Is.False); + Assert.That(countCleared, Is.Zero); Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back)); Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.Escape))); @@ -59,11 +58,11 @@ public void TestDuplicateBindingsAreCleared() new RealmKeyBinding(GlobalAction.TakeScreenshot, KeyCombination.FromKey(Key.PrintScreen)), }; - bool anyCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings.AsQueryable()); + int countCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings); Assert.Multiple(() => { - Assert.That(anyCleared, Is.True); + Assert.That(countCleared, Is.EqualTo(4)); Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back)); Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None))); diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index cd97bb6430..7d4d63a20e 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -25,6 +25,7 @@ using osu.Game.Beatmaps.Legacy; using osu.Game.Configuration; using osu.Game.Extensions; +using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.Models; using osu.Game.Online.API; @@ -84,8 +85,9 @@ public class RealmAccess : IDisposable /// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files. /// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding. /// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures. + /// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section. /// - private const int schema_version = 34; + private const int schema_version = 35; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1031,6 +1033,33 @@ void convertOnlineIDs() where T : RealmObject break; } + + case 35: + { + int countCleared = 0; + + var globalBindings = migration.NewRealm.All().Where(kb => kb.RulesetName == null).ToList(); + + foreach (var category in Enum.GetValues()) + { + var categoryActions = GlobalActionContainer.GetGlobalActionsFor(category).Cast().ToHashSet(); + var categoryBindings = globalBindings.Where(kb => categoryActions.Contains(kb.ActionInt)); + countCleared += RealmKeyBindingStore.ClearDuplicateBindings(categoryBindings); + } + + var rulesetBindings = migration.NewRealm.All().Where(kb => kb.RulesetName != null).ToList(); + + foreach (var variantGroup in rulesetBindings.GroupBy(kb => (kb.RulesetName, kb.Variant))) + countCleared += RealmKeyBindingStore.ClearDuplicateBindings(variantGroup); + + if (countCleared > 0) + { + Logger.Log($"{countCleared} of your keybinding(s) have been cleared due to being bound to multiple actions. " + + "Please choose new unique ones in the settings panel.", level: LogLevel.Important); + } + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index b09e29f48e..0c48a4c16c 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -136,10 +136,10 @@ public static bool CheckValidForGameplay(KeyCombination combination) /// which are assigned to more than one binding. /// /// The s to de-duplicate. - /// Whether any bindings have been cleared. - public static bool ClearDuplicateBindings(IQueryable keyBindings) + /// Number of bindings cleared. + public static int ClearDuplicateBindings(IEnumerable keyBindings) { - bool anyRemoved = false; + int countRemoved = 0; var lookup = keyBindings.ToLookup(kb => kb.KeyCombination); @@ -151,10 +151,10 @@ public static bool ClearDuplicateBindings(IQueryable keyBindings) foreach (var binding in group) binding.KeyCombination = new KeyCombination(InputKey.None); - anyRemoved = true; + countRemoved += group.Count(); } - return anyRemoved; + return countRemoved; } } } diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 7f081b6e8a..39b83ecca1 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -218,7 +218,7 @@ protected override void ReloadMappings(IQueryable realmKeyBindi base.ReloadMappings(realmKeyBindings); KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList(); - RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings.AsQueryable()); + RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings); } } } From 3aae07d1caa8242efd4a0318c6e705ebfc5c99ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 21:28:11 +0200 Subject: [PATCH 5/8] Add failing case for two bindings of single action bound to same key --- .../Input/RealmKeyBindingStoreTest.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs b/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs index a0c1b20cd7..ce31a9ea9d 100644 --- a/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs +++ b/osu.Game.Tests/Input/RealmKeyBindingStoreTest.cs @@ -86,5 +86,32 @@ public void TestDuplicateBindingsAreCleared() Assert.That(bindings[6].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.PrintScreen))); }); } + + [Test] + public void TestDuplicateBindingsAllowedIfBoundToSameAction() + { + var bindings = new List + { + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)), + new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)), + new RealmKeyBinding(GlobalAction.MusicPrev, KeyCombination.FromKey(Key.F1)), + }; + + int countCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings); + + Assert.Multiple(() => + { + Assert.That(countCleared, Is.EqualTo(0)); + + Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.Escape))); + + Assert.That(bindings[1].Action, Is.EqualTo((int)GlobalAction.Back)); + Assert.That(bindings[1].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.Escape))); + + Assert.That(bindings[2].Action, Is.EqualTo((int)GlobalAction.MusicPrev)); + Assert.That(bindings[2].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F1))); + }); + } } } From 828cedea33d85879afd07c7906439284a78cc654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Oct 2023 21:29:42 +0200 Subject: [PATCH 6/8] Fix bindings being cleared if multiple bindings for same action have the same combination This actually seems to be the case in catch (dash is bound to shift twice). This is annoying but harmless, so let's work around it for now to avoid ruining users' configs. --- osu.Game/Input/RealmKeyBindingStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/RealmKeyBindingStore.cs b/osu.Game/Input/RealmKeyBindingStore.cs index 0c48a4c16c..48ace58235 100644 --- a/osu.Game/Input/RealmKeyBindingStore.cs +++ b/osu.Game/Input/RealmKeyBindingStore.cs @@ -145,7 +145,7 @@ public static int ClearDuplicateBindings(IEnumerable keyBindings) foreach (var group in lookup) { - if (group.Count() <= 1) + if (group.Select(kb => kb.Action).Distinct().Count() <= 1) continue; foreach (var binding in group) From 79a4b985fed630c092f60ba7af2146d71687e94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Oct 2023 09:30:44 +0200 Subject: [PATCH 7/8] Use left mouse as alternative default binding for catch dash This mirrors stable. Although the way stable does this is pretty dodgy, see: https://github.com/peppy/osu-stable-reference/blob/master/osu!/GameModes/Play/Rulesets/Fruits/RulesetFruits.cs#L274-L275 --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index efd4b46782..9ceb78893e 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -54,7 +54,7 @@ public override IEnumerable GetDefaultKeyBindings(int variant = 0) = new KeyBinding(InputKey.X, CatchAction.MoveRight), new KeyBinding(InputKey.Right, CatchAction.MoveRight), new KeyBinding(InputKey.Shift, CatchAction.Dash), - new KeyBinding(InputKey.Shift, CatchAction.Dash), + new KeyBinding(InputKey.MouseLeft, CatchAction.Dash), }; public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) From 4cfc95c673f2fe407f04c3884b083a7ab311795e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Oct 2023 09:43:17 +0200 Subject: [PATCH 8/8] Add backwards migration of catch dash binding --- osu.Game/Database/RealmAccess.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 7d4d63a20e..89bc631cfd 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -35,6 +35,7 @@ using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Skinning; +using osuTK.Input; using Realms; using Realms.Exceptions; @@ -1036,6 +1037,20 @@ void convertOnlineIDs() where T : RealmObject case 35: { + // catch used `Shift` twice as a default key combination for dash, which generally was bothersome and causes issues elsewhere. + // the duplicate binding logic below had to account for it, it could also break keybinding conflict resolution on revert-to-default. + // as such, detect this situation and fix it before proceeding further. + var catchDashBindings = migration.NewRealm.All() + .Where(kb => kb.RulesetName == @"fruits" && kb.ActionInt == 2) + .ToList(); + + if (catchDashBindings.All(kb => kb.KeyCombination.Equals(new KeyCombination(InputKey.Shift)))) + { + Debug.Assert(catchDashBindings.Count == 2); + catchDashBindings.Last().KeyCombination = KeyCombination.FromMouseButton(MouseButton.Left); + } + + // with the catch case dealt with, de-duplicate the remaining bindings. int countCleared = 0; var globalBindings = migration.NewRealm.All().Where(kb => kb.RulesetName == null).ToList();