#nullable enable using System.Diagnostics; using Microsoft.CodeAnalysis; namespace Terminal.Gui; /// /// Provides a collection of objects bound to a . /// public class KeyBindings { /// /// Initializes a new instance. This constructor is used when the are not bound to a /// . This is used for Application.KeyBindings and unit tests. /// public KeyBindings () { } /// Initializes a new instance bound to . public KeyBindings (View boundView) { BoundView = boundView; } /// /// The view that the are bound to. /// /// /// If , the are not bound to a . This is used for Application.KeyBindings. /// public View? BoundView { get; } // TODO: Add a dictionary comparer that ignores Scope // TODO: This should not be public! /// The collection of objects. public Dictionary Bindings { get; } = new (); /// Adds a to the collection. /// /// /// Optional View for bindings. public void Add (Key key, KeyBinding binding, View? boundViewForAppScope = null) { if (BoundView is { } && binding.Scope.FastHasFlags (KeyBindingScope.Application)) { throw new ArgumentException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add"); } if (TryGet (key, out KeyBinding _)) { throw new InvalidOperationException(@$"A key binding for {key} exists ({binding})."); //Bindings [key] = binding; } else { if (BoundView is { }) { binding.BoundView = BoundView; } else { binding.BoundView = boundViewForAppScope; } Bindings.Add (key, binding); } } /// /// Adds a new key combination that will trigger the commands in . /// /// If the key is already bound to a different array of s it will be rebound /// . /// /// /// /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch /// focus to another view and perform multiple commands there). /// /// The key to check. /// The scope for the command. /// Optional View for bindings. /// /// The command to invoked on the when is pressed. When /// multiple commands are provided,they will be applied in sequence. The bound strike will be /// consumed if any took effect. /// public void Add (Key key, KeyBindingScope scope, View? boundViewForAppScope = null, params Command [] commands) { if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application)) { throw new ArgumentException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add"); } if (key is null || !key.IsValid) { //throw new ArgumentException ("Invalid Key", nameof (commands)); return; } if (commands.Length == 0) { throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } if (TryGet (key, out KeyBinding binding)) { throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); //Bindings [key] = new (commands, scope, BoundView); } else { Add (key, new KeyBinding (commands, scope, BoundView), boundViewForAppScope); } } /// /// Adds a new key combination that will trigger the commands in . /// /// If the key is already bound to a different array of s it will be rebound /// . /// /// /// /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch /// focus to another view and perform multiple commands there). /// /// The key to check. /// The scope for the command. /// /// The command to invoked on the when is pressed. When /// multiple commands are provided,they will be applied in sequence. The bound strike will be /// consumed if any took effect. /// public void Add (Key key, KeyBindingScope scope, params Command [] commands) { if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application)) { throw new ArgumentException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add"); } if (key is null || !key.IsValid) { //throw new ArgumentException ("Invalid Key", nameof (commands)); return; } if (commands.Length == 0) { throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } if (TryGet (key, out KeyBinding binding)) { throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); //Bindings [key] = new (commands, scope, BoundView); } else { Add (key, new KeyBinding (commands, scope, BoundView), null); } } /// /// /// Adds a new key combination that will trigger the commands in (if supported by the /// View - see ). /// /// /// This is a helper function for . If used for a View ( is set), the scope will be set to . /// Otherwise, it will be set to . /// /// /// If the key is already bound to a different array of s it will be rebound /// . /// /// /// /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch /// focus to another view and perform multiple commands there). /// /// The key to check. /// Optional View for bindings. /// /// The command to invoked on the when is pressed. When /// multiple commands are provided,they will be applied in sequence. The bound strike will be /// consumed if any took effect. /// public void Add (Key key, View? boundViewForAppScope = null, params Command [] commands) { if (BoundView is null && boundViewForAppScope is null) { throw new ArgumentException (@"Application scoped KeyBindings must provide a bound view to Add.", nameof(boundViewForAppScope)); } Add (key, BoundView is { } ? KeyBindingScope.Focused : KeyBindingScope.Application, boundViewForAppScope, commands); } /// /// /// Adds a new key combination that will trigger the commands in (if supported by the /// View - see ). /// /// /// This is a helper function for . If used for a View ( is set), the scope will be set to . /// Otherwise, it will be set to . /// /// /// If the key is already bound to a different array of s it will be rebound /// . /// /// /// /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch /// focus to another view and perform multiple commands there). /// /// The key to check. /// /// The command to invoked on the when is pressed. When /// multiple commands are provided,they will be applied in sequence. The bound strike will be /// consumed if any took effect. /// public void Add (Key key, params Command [] commands) { if (BoundView is null) { throw new ArgumentException (@"Application scoped KeyBindings must provide a boundViewForAppScope to Add."); } Add (key, BoundView is { } ? KeyBindingScope.Focused : KeyBindingScope.Application, null, commands); } /// Removes all objects from the collection. public void Clear () { Bindings.Clear (); } /// /// Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to /// the same command sets and this method will clear all of them. /// /// public void Clear (params Command [] command) { KeyValuePair [] kvps = Bindings .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) .ToArray (); foreach (KeyValuePair kvp in kvps) { Remove (kvp.Key); } } /// Gets the for the specified . /// /// public KeyBinding Get (Key key) { if (TryGet (key, out KeyBinding binding)) { return binding; } throw new InvalidOperationException ($"Key {key} is not bound."); } /// Gets the for the specified . /// /// /// public KeyBinding Get (Key key, KeyBindingScope scope) { if (TryGet (key, scope, out KeyBinding binding)) { return binding; } throw new InvalidOperationException ($"Key {key}/{scope} is not bound."); } /// Gets the array of s bound to if it exists. /// The key to check. /// /// The array of s if is bound. An empty array /// if not. /// public Command [] GetCommands (Key key) { if (TryGet (key, out KeyBinding bindings)) { return bindings.Commands; } return Array.Empty (); } /// Gets the Key used by a set of commands. /// /// The set of commands to search. /// The used by a /// If no matching set of commands was found. public Key GetKeyFromCommands (params Command [] commands) { return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key; } /// Removes a from the collection. /// /// Optional View for bindings. public void Remove (Key key, View? boundViewForAppScope = null) { if (!TryGet (key, out KeyBinding binding)) { return; } Bindings.Remove (key); } /// Replaces a key combination already bound to a set of s. /// /// The key to be replaced. /// The new key to be used. public void ReplaceKey (Key oldKey, Key newKey) { if (!TryGet (oldKey, out KeyBinding _)) { return; } KeyBinding value = Bindings [oldKey]; Remove (oldKey); Add (newKey, value); } /// Replaces the commands already bound to a key. /// /// /// If the key is not already bound, it will be added. /// /// /// The key bound to the command to be replaced. /// The set of commands to replace the old ones with. public void ReplaceCommands (Key key, params Command [] commands) { if (TryGet (key, out KeyBinding binding)) { binding.Commands = commands; } else { Add (key, commands); } } /// Gets the commands bound with the specified Key. /// /// The key to check. /// /// When this method returns, contains the commands bound with the specified Key, if the Key is /// found; otherwise, null. This parameter is passed uninitialized. /// /// if the Key is bound; otherwise . public bool TryGet (Key key, out KeyBinding binding) { binding = new (Array.Empty (), KeyBindingScope.Disabled, null); if (key.IsValid) { return Bindings.TryGetValue (key, out binding); } return false; } /// Gets the commands bound with the specified Key that are scoped to a particular scope. /// /// The key to check. /// the scope to filter on /// /// When this method returns, contains the commands bound with the specified Key, if the Key is /// found; otherwise, null. This parameter is passed uninitialized. /// /// if the Key is bound; otherwise . public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding) { binding = new (Array.Empty (), KeyBindingScope.Disabled, null); if (key.IsValid && Bindings.TryGetValue (key, out binding)) { if (scope.HasFlag (binding.Scope)) { return true; } } return false; } }