diff --git a/osu.Game.Tests/Input/RealmKeyBindingTest.cs b/osu.Game.Tests/Input/RealmKeyBindingTest.cs new file mode 100644 index 000000000000..366d5ea825cb --- /dev/null +++ b/osu.Game.Tests/Input/RealmKeyBindingTest.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Input.Bindings; +using osu.Framework.Testing; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; +using osu.Game.Tests.Visual; +using osuTK.Input; + +namespace osu.Game.Tests.Input +{ + [HeadlessTest] + public partial class RealmKeyBindingTest : OsuTestScene + { + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + [Test] + public void TestUnmapGlobalAction() + { + var keyBinding = new RealmKeyBinding(GlobalAction.ToggleReplaySettings, KeyCombination.FromKey(Key.Z)); + + AddAssert("action is integer", () => keyBinding.Action, () => Is.EqualTo((int)GlobalAction.ToggleReplaySettings)); + AddAssert("action unmaps correctly", () => keyBinding.GetAction(rulesets), () => Is.EqualTo(GlobalAction.ToggleReplaySettings)); + } + + [TestCase(typeof(OsuRuleset), OsuAction.Smoke, null)] + [TestCase(typeof(TaikoRuleset), TaikoAction.LeftCentre, null)] + [TestCase(typeof(CatchRuleset), CatchAction.MoveRight, null)] + [TestCase(typeof(ManiaRuleset), ManiaAction.Key7, 7)] + public void TestUnmapRulesetActions(Type rulesetType, object action, int? variant) + { + string rulesetName = ((Ruleset)Activator.CreateInstance(rulesetType)!).ShortName; + var keyBinding = new RealmKeyBinding(action, KeyCombination.FromKey(Key.Z), rulesetName, variant); + + AddAssert("action is integer", () => keyBinding.Action, () => Is.EqualTo((int)action)); + AddAssert("action unmaps correctly", () => keyBinding.GetAction(rulesets), () => Is.EqualTo(action)); + } + } +} diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 296232d9ea77..20220d88cd17 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Input; @@ -13,6 +14,8 @@ namespace osu.Game.Input.Bindings { public partial class GlobalActionContainer : DatabasedKeyBindingContainer, IHandleGlobalKeyboardInput, IKeyBindingHandler { + protected override bool Prioritised => true; + private readonly IKeyBindingHandler? handler; public GlobalActionContainer(OsuGameBase? game) @@ -22,22 +25,62 @@ public GlobalActionContainer(OsuGameBase? game) handler = h; } - protected override bool Prioritised => true; - - // IMPORTANT: Take care when changing order of the items in the enumerable. - // It is used to decide the order of precedence, with the earlier items having higher precedence. - public override IEnumerable DefaultKeyBindings => GlobalKeyBindings - .Concat(EditorKeyBindings) - .Concat(InGameKeyBindings) - .Concat(ReplayKeyBindings) - .Concat(SongSelectKeyBindings) - .Concat(AudioControlKeyBindings) + /// + /// All default key bindings across all categories, ordered with highest priority first. + /// + /// + /// IMPORTANT: Take care when changing order of the items in the enumerable. + /// It is used to decide the order of precedence, with the earlier items having higher precedence. + /// + public override IEnumerable DefaultKeyBindings => globalKeyBindings + .Concat(editorKeyBindings) + .Concat(inGameKeyBindings) + .Concat(replayKeyBindings) + .Concat(songSelectKeyBindings) + .Concat(audioControlKeyBindings) // Overlay bindings may conflict with more local cases like the editor so they are checked last. // It has generally been agreed on that local screens like the editor should have priority, // based on such usages potentially requiring a lot more key bindings that may be "shared" with global ones. - .Concat(OverlayKeyBindings); + .Concat(overlayKeyBindings); + + public static IEnumerable GetDefaultBindingsFor(GlobalActionCategory category) + { + switch (category) + { + case GlobalActionCategory.General: + return globalKeyBindings; + + case GlobalActionCategory.Editor: + return editorKeyBindings; + + case GlobalActionCategory.InGame: + return inGameKeyBindings; + + case GlobalActionCategory.Replay: + return replayKeyBindings; + + case GlobalActionCategory.SongSelect: + return songSelectKeyBindings; + + case GlobalActionCategory.AudioControl: + return audioControlKeyBindings; + + case GlobalActionCategory.Overlays: + return overlayKeyBindings; + + default: + throw new ArgumentOutOfRangeException(nameof(category), category, $"Unexpected {nameof(GlobalActionCategory)}"); + } + } + + public static IEnumerable GetGlobalActionsFor(GlobalActionCategory category) + => GetDefaultBindingsFor(category).Select(binding => binding.Action).Cast().Distinct(); + + public bool OnPressed(KeyBindingPressEvent e) => handler?.OnPressed(e) == true; - public IEnumerable GlobalKeyBindings => new[] + public void OnReleased(KeyBindingReleaseEvent e) => handler?.OnReleased(e); + + private static IEnumerable globalKeyBindings => new[] { new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious), new KeyBinding(InputKey.Down, GlobalAction.SelectNext), @@ -67,7 +110,7 @@ public GlobalActionContainer(OsuGameBase? game) new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot), }; - public IEnumerable OverlayKeyBindings => new[] + private static IEnumerable overlayKeyBindings => new[] { new KeyBinding(InputKey.F8, GlobalAction.ToggleChat), new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying), @@ -77,7 +120,7 @@ public GlobalActionContainer(OsuGameBase? game) new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), }; - public IEnumerable EditorKeyBindings => new[] + private static IEnumerable editorKeyBindings => new[] { new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorComposeMode), new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode), @@ -101,7 +144,7 @@ public GlobalActionContainer(OsuGameBase? game) new KeyBinding(new[] { InputKey.Control, InputKey.R }, GlobalAction.EditorToggleRotateControl), }; - public IEnumerable InGameKeyBindings => new[] + private static IEnumerable inGameKeyBindings => new[] { new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene), new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene), @@ -118,7 +161,7 @@ public GlobalActionContainer(OsuGameBase? game) new KeyBinding(InputKey.F2, GlobalAction.ExportReplay), }; - public IEnumerable ReplayKeyBindings => new[] + private static IEnumerable replayKeyBindings => new[] { new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.MouseMiddle, GlobalAction.TogglePauseReplay), @@ -127,7 +170,7 @@ public GlobalActionContainer(OsuGameBase? game) new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.ToggleReplaySettings), }; - public IEnumerable SongSelectKeyBindings => new[] + private static IEnumerable songSelectKeyBindings => new[] { new KeyBinding(InputKey.F1, GlobalAction.ToggleModSelection), new KeyBinding(InputKey.F2, GlobalAction.SelectNextRandom), @@ -136,7 +179,7 @@ public GlobalActionContainer(OsuGameBase? game) new KeyBinding(InputKey.BackSpace, GlobalAction.DeselectAllMods), }; - public IEnumerable AudioControlKeyBindings => new[] + private static IEnumerable audioControlKeyBindings => new[] { new KeyBinding(new[] { InputKey.Alt, InputKey.Up }, GlobalAction.IncreaseVolume), new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume), @@ -153,10 +196,6 @@ public GlobalActionContainer(OsuGameBase? game) new KeyBinding(InputKey.PlayPause, GlobalAction.MusicPlay), new KeyBinding(InputKey.F3, GlobalAction.MusicPlay) }; - - public bool OnPressed(KeyBindingPressEvent e) => handler?.OnPressed(e) == true; - - public void OnReleased(KeyBindingReleaseEvent e) => handler?.OnReleased(e); } public enum GlobalAction @@ -365,4 +404,15 @@ public enum GlobalAction [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorToggleRotateControl))] EditorToggleRotateControl, } + + public enum GlobalActionCategory + { + General, + Editor, + InGame, + Replay, + SongSelect, + AudioControl, + Overlays + } } diff --git a/osu.Game/Input/Bindings/RealmKeyBinding.cs b/osu.Game/Input/Bindings/RealmKeyBinding.cs index 4af035753591..28b142978bac 100644 --- a/osu.Game/Input/Bindings/RealmKeyBinding.cs +++ b/osu.Game/Input/Bindings/RealmKeyBinding.cs @@ -2,9 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Input.Bindings; using osu.Game.Database; +using osu.Game.Rulesets; using Realms; namespace osu.Game.Input.Bindings @@ -26,6 +28,13 @@ public KeyCombination KeyCombination set => KeyCombinationString = value.ToString(); } + /// + /// The resultant action which is triggered by this binding. + /// + /// + /// This implementation always returns an integer. + /// If wanting to get the actual enum-typed value, use . + /// [Ignored] public object Action { @@ -53,5 +62,20 @@ public RealmKeyBinding(object action, KeyCombination keyCombination, string? rul private RealmKeyBinding() { } + + public object GetAction(RulesetStore rulesets) + { + if (string.IsNullOrEmpty(RulesetName)) + return (GlobalAction)ActionInt; + + var ruleset = rulesets.GetRuleset(RulesetName); + var actionType = ruleset!.CreateInstance() + .GetDefaultKeyBindings(Variant ?? 0) + .First() // let's just assume nobody does something stupid like mix multiple types... + .Action + .GetType(); + + return Enum.ToObject(actionType, ActionInt); + } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs index 291e9a93cfbc..5a05d7890581 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; @@ -18,92 +19,19 @@ public partial class GlobalKeyBindingsSection : SettingsSection public override LocalisableString Header => InputSettingsStrings.GlobalKeyBindingHeader; - public GlobalKeyBindingsSection(GlobalActionContainer manager) + [BackgroundDependencyLoader] + private void load() { - Add(new DefaultBindingsSubsection(manager)); - Add(new OverlayBindingsSubsection(manager)); - Add(new AudioControlKeyBindingsSubsection(manager)); - Add(new SongSelectKeyBindingSubsection(manager)); - Add(new InGameKeyBindingsSubsection(manager)); - Add(new ReplayKeyBindingsSubsection(manager)); - Add(new EditorKeyBindingsSubsection(manager)); - } - - private partial class DefaultBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => string.Empty; - - public DefaultBindingsSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.GlobalKeyBindings; - } - } - - private partial class OverlayBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.OverlaysSection; - - public OverlayBindingsSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.OverlayKeyBindings; - } - } - - private partial class SongSelectKeyBindingSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.SongSelectSection; - - public SongSelectKeyBindingSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.SongSelectKeyBindings; - } - } - - private partial class InGameKeyBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.InGameSection; - - public InGameKeyBindingsSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.InGameKeyBindings; - } - } - - private partial class ReplayKeyBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.ReplaySection; - - public ReplayKeyBindingsSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.ReplayKeyBindings; - } - } - - private partial class AudioControlKeyBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.AudioSection; - - public AudioControlKeyBindingsSubsection(GlobalActionContainer manager) - : base(null) - { - Defaults = manager.AudioControlKeyBindings; - } - } - - private partial class EditorKeyBindingsSubsection : KeyBindingsSubsection - { - protected override LocalisableString Header => InputSettingsStrings.EditorSection; - - public EditorKeyBindingsSubsection(GlobalActionContainer manager) - : base(null) + AddRange(new[] { - Defaults = manager.EditorKeyBindings; - } + new GlobalKeyBindingsSubsection(string.Empty, GlobalActionCategory.General), + new GlobalKeyBindingsSubsection(InputSettingsStrings.OverlaysSection, GlobalActionCategory.Overlays), + new GlobalKeyBindingsSubsection(InputSettingsStrings.AudioSection, GlobalActionCategory.AudioControl), + new GlobalKeyBindingsSubsection(InputSettingsStrings.SongSelectSection, GlobalActionCategory.SongSelect), + new GlobalKeyBindingsSubsection(InputSettingsStrings.InGameSection, GlobalActionCategory.InGame), + new GlobalKeyBindingsSubsection(InputSettingsStrings.ReplaySection, GlobalActionCategory.Replay), + new GlobalKeyBindingsSubsection(InputSettingsStrings.EditorSection, GlobalActionCategory.Editor), + }); } } } diff --git a/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSubsection.cs new file mode 100644 index 000000000000..2e42e46330f2 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Input/GlobalKeyBindingsSubsection.cs @@ -0,0 +1,36 @@ +// 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 System.Linq; +using osu.Framework.Localisation; +using osu.Game.Database; +using osu.Game.Input.Bindings; +using Realms; + +namespace osu.Game.Overlays.Settings.Sections.Input +{ + public partial class GlobalKeyBindingsSubsection : KeyBindingsSubsection + { + protected override LocalisableString Header { get; } + + private readonly GlobalActionCategory category; + + public GlobalKeyBindingsSubsection(LocalisableString header, GlobalActionCategory category) + { + Header = header; + this.category = category; + Defaults = GlobalActionContainer.GetDefaultBindingsFor(category); + } + + protected override IEnumerable GetKeyBindings(Realm realm) + { + var bindings = realm.All() + .Where(b => b.RulesetName == null && b.Variant == null) + .Detach(); + + var actionsInSection = GlobalActionContainer.GetGlobalActionsFor(category).Cast().ToHashSet(); + return bindings.Where(kb => actionsInSection.Contains(kb.ActionInt)); + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs index 7296003c7f33..4c5610a15e13 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingPanel.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Input.Bindings; using osu.Game.Localisation; using osu.Game.Rulesets; @@ -14,9 +13,9 @@ public partial class KeyBindingPanel : SettingsSubPanel protected override Drawable CreateHeader() => new SettingsHeader(InputSettingsStrings.KeyBindingPanelHeader, InputSettingsStrings.KeyBindingPanelDescription); [BackgroundDependencyLoader(permitNulls: true)] - private void load(RulesetStore rulesets, GlobalActionContainer global) + private void load(RulesetStore rulesets) { - AddSection(new GlobalKeyBindingsSection(global)); + AddSection(new GlobalKeyBindingsSection()); foreach (var ruleset in rulesets.AvailableRulesets) AddSection(new RulesetBindingsSection(ruleset)); diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs index c0de7a1d163c..2e44d8b02dbe 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingRow.cs @@ -37,7 +37,7 @@ public partial class KeyBindingRow : Container, IFilterable /// /// Invoked when the binding of this row is updated with a change being written. /// - public Action? BindingUpdated { get; init; } + public Action? BindingUpdated { get; set; } /// /// Whether left and right mouse button clicks should be included in the edited bindings. diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs index 8285204bb3fd..1ad944263196 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyBindingsSubsection.cs @@ -11,9 +11,9 @@ using osu.Framework.Localisation; using osu.Game.Database; using osu.Game.Input.Bindings; -using osu.Game.Rulesets; using osu.Game.Localisation; using osuTK; +using Realms; namespace osu.Game.Overlays.Settings.Sections.Input { @@ -27,37 +27,26 @@ public abstract partial class KeyBindingsSubsection : SettingsSubsection protected IEnumerable Defaults { get; init; } = Array.Empty(); - public RulesetInfo? Ruleset { get; protected set; } - - private readonly int? variant; - - protected KeyBindingsSubsection(int? variant) + protected KeyBindingsSubsection() { - this.variant = variant; - FlowContent.Spacing = new Vector2(0, 3); } [BackgroundDependencyLoader] private void load(RealmAccess realm) { - string? rulesetName = Ruleset?.ShortName; - - var bindings = realm.Run(r => r.All() - .Where(b => b.RulesetName == rulesetName && b.Variant == variant) - .Detach()); + var bindings = realm.Run(r => GetKeyBindings(r).Detach()); foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) { int intKey = (int)defaultGroup.Key; // one row per valid action. - Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.ActionInt.Equals(intKey)).ToList()) - { - AllowMainMouseButtons = Ruleset != null, - Defaults = defaultGroup.Select(d => d.KeyCombination), - BindingUpdated = onBindingUpdated - }); + Add(CreateKeyBindingRow( + defaultGroup.Key, + bindings.Where(b => b.ActionInt.Equals(intKey)).ToList(), + defaultGroup) + .With(row => row.BindingUpdated = onBindingUpdated)); } Add(new ResetButton @@ -66,6 +55,15 @@ private void load(RealmAccess realm) }); } + protected abstract IEnumerable GetKeyBindings(Realm realm); + + protected virtual KeyBindingRow CreateKeyBindingRow(object action, IEnumerable keyBindings, IEnumerable defaults) + => new KeyBindingRow(action, keyBindings.ToList()) + { + AllowMainMouseButtons = false, + Defaults = defaults.Select(d => d.KeyCombination), + }; + private void onBindingUpdated(KeyBindingRow sender) { if (AutoAdvanceTarget) diff --git a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs index 3b5002b423cf..f3265920164d 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/RulesetBindingsSection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Localisation; using osu.Game.Rulesets; @@ -18,7 +19,11 @@ public partial class RulesetBindingsSection : SettingsSection public RulesetBindingsSection(RulesetInfo ruleset) { this.ruleset = ruleset; + } + [BackgroundDependencyLoader] + private void load() + { var r = ruleset.CreateInstance(); foreach (int variant in r.AvailableVariants) diff --git a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs index d00de7f549e0..46da8a1453fd 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/VariantBindingsSubsection.cs @@ -1,8 +1,13 @@ // 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 System.Linq; +using osu.Framework.Input.Bindings; using osu.Framework.Localisation; +using osu.Game.Input.Bindings; using osu.Game.Rulesets; +using Realms; namespace osu.Game.Overlays.Settings.Sections.Input { @@ -12,15 +17,33 @@ public partial class VariantBindingsSubsection : KeyBindingsSubsection protected override LocalisableString Header { get; } + public RulesetInfo Ruleset { get; } + private readonly int variant; + public VariantBindingsSubsection(RulesetInfo ruleset, int variant) - : base(variant) { Ruleset = ruleset; + this.variant = variant; var rulesetInstance = ruleset.CreateInstance(); Header = rulesetInstance.GetVariantName(variant); Defaults = rulesetInstance.GetDefaultKeyBindings(variant); } + + protected override IEnumerable GetKeyBindings(Realm realm) + { + string rulesetName = Ruleset.ShortName; + + return realm.All() + .Where(b => b.RulesetName == rulesetName && b.Variant == variant); + } + + protected override KeyBindingRow CreateKeyBindingRow(object action, IEnumerable keyBindings, IEnumerable defaults) + => new KeyBindingRow(action, keyBindings.ToList()) + { + AllowMainMouseButtons = true, + Defaults = defaults.Select(d => d.KeyCombination), + }; } }