diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 48bb76837..2ef614f68 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -45,7 +45,7 @@ public static partial class Application // Keyboard handling // Invoke any Application-scoped KeyBindings. // The first view that handles the key will stop the loop. - foreach (KeyValuePair binding in KeyBindings.GetBindings (key)) + foreach (KeyValuePair binding in KeyBindings.GetBindings (key)) { if (binding.Value.Target is { }) { @@ -63,7 +63,7 @@ public static partial class Application // Keyboard handling } else { - if (!KeyBindings.TryGet (key, out ApplicationKeyBinding appBinding)) + if (!KeyBindings.TryGet (key, out KeyBinding appBinding)) { continue; } @@ -81,7 +81,7 @@ public static partial class Application // Keyboard handling return false; - static bool? InvokeCommand (Command command, Key key, ApplicationKeyBinding appBinding) + static bool? InvokeCommand (Command command, Key key, KeyBinding appBinding) { if (!_commandImplementations!.ContainsKey (command)) { @@ -92,7 +92,7 @@ public static partial class Application // Keyboard handling if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation)) { - CommandContext context = new (command, appBinding); // Create the context here + CommandContext context = new (command, appBinding); // Create the context here return implementation (context); } @@ -159,7 +159,7 @@ public static partial class Application // Keyboard handling static Application () { AddKeyBindings (); } /// Gets the Application-scoped key bindings. - public static ApplicationKeyBindings KeyBindings { get; internal set; } = new (); + public static KeyBindings KeyBindings { get; internal set; } = new (null); internal static void AddKeyBindings () { diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index a944c4c37..11b1391fb 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -184,7 +184,7 @@ public static partial class Application // Mouse handling } // Create a view-relative mouse event to send to the view that is under the mouse. - MouseEventArgs? viewMouseEvent; + MouseEventArgs viewMouseEvent; if (deepestViewUnderMouse is Adornment adornment) { diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs index a96814208..02d8a03ff 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -509,7 +509,7 @@ internal class WindowsDriver : ConsoleDriver case WindowsConsole.EventType.Mouse: MouseEventArgs me = ToDriverMouse (inputEvent.MouseEvent); - if (me is null || me.Flags == MouseFlags.None) + if (/*me is null ||*/ me.Flags == MouseFlags.None) { break; } diff --git a/Terminal.Gui/Input/Keyboard/ApplicationKeyBinding.cs b/Terminal.Gui/Input/Keyboard/ApplicationKeyBinding.cs deleted file mode 100644 index f29e57cd5..000000000 --- a/Terminal.Gui/Input/Keyboard/ApplicationKeyBinding.cs +++ /dev/null @@ -1,41 +0,0 @@ -#nullable enable - -// These classes use a key binding system based on the design implemented in Scintilla.Net which is an -// MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs - -namespace Terminal.Gui; - -/// -/// Provides a collection of objects that are scoped to the . -/// -/// -public record struct ApplicationKeyBinding -{ - /// Initializes a new instance. - /// The commands this key binding will invoke. - public ApplicationKeyBinding (Command [] commands) - { - Commands = commands; - } - - /// Initializes a new instance. - /// The commands this key binding will invoke. - /// The view the Application-scoped key binding is bound to. If the commands will be invoked on - /// the . - public ApplicationKeyBinding (Command [] commands, View? target) - { - Commands = commands; - Target = target; - } - - /// The commands this binding will invoke. - public Command [] Commands { get; set; } - - /// - /// The Key that is bound to the . - /// - public Key? Key { get; set; } - - /// The view the Application-scoped key binding is bound to. - public View? Target { get; set; } -} diff --git a/Terminal.Gui/Input/Keyboard/ApplicationKeyBindings.cs b/Terminal.Gui/Input/Keyboard/ApplicationKeyBindings.cs deleted file mode 100644 index b0cd604bc..000000000 --- a/Terminal.Gui/Input/Keyboard/ApplicationKeyBindings.cs +++ /dev/null @@ -1,252 +0,0 @@ -#nullable enable -namespace Terminal.Gui; - -/// -/// Provides a collection of objects bound to a . -/// -/// -/// This is used for . -/// -/// -/// -public class ApplicationKeyBindings -{ - /// - /// Initializes a new instance. This constructor is used when the are not bound to a - /// . This is used for . - /// - public ApplicationKeyBindings () { } - - /// Adds a to the collection. - /// - /// - public void Add (Key key, ApplicationKeyBinding binding) - { - if (TryGet (key, out ApplicationKeyBinding _)) - { - throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); - } - - // IMPORTANT: Add a COPY of the key. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy - // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus - // IMPORTANT: Apply will update the Dictionary with the new key, but the old key will still be in the dictionary. - // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. - Bindings.Add (new (key), binding); - } - - /// - /// - /// Adds a new key combination that will trigger the commands in on the View - /// specified by . - /// - /// - /// If the key is already bound to a different array of s it will be rebound - /// . - /// - /// - /// - /// - /// The key to check. - /// The View the commands will be invoked on. If , the key will be bound to . - /// - /// 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? boundView, params Command [] commands) - { - ApplicationKeyBinding binding = new (commands, boundView); - Add (key, binding); - } - - /// - /// - /// Adds a new key combination that will trigger the commands in on . - /// - /// - /// If the key is already bound to a different array of s it will be rebound - /// . - /// - /// - /// - /// - /// The key to check. - /// - /// The commands to invoke on 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) - { - ApplicationKeyBinding binding = new (commands, null); - Add (key, binding); - } - - private Dictionary Bindings { get; } = new (new KeyEqualityComparer ()); - - /// - /// Gets the keys that are bound. - /// - /// - public IEnumerable GetBoundKeys () - { - return Bindings.Keys; - } - - /// - /// Gets the bindings bound to . - /// - /// - /// - public IEnumerable> GetBindings (Key key) - { - return Bindings.Where (b => b.Key == key.KeyCode); - } - - /// 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 ApplicationKeyBinding Get (Key key) - { - if (TryGet (key, out ApplicationKeyBinding binding)) - { - return binding; - } - - throw new InvalidOperationException ($"Key {key} 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 ApplicationKeyBinding bindings)) - { - return bindings.Commands; - } - - return []; - } - - /// Gets the first Key bound to the set of commands specified by . - /// The set of commands to search. - /// The first bound to the set of commands specified by . if the set of caommands was not found. - public Key? GetKeyFromCommands (params Command [] commands) - { - return Bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; - } - - /// Gets Keys bound to the set of commands specified by . - /// The set of commands to search. - /// The s bound to the set of commands specified by . An empty list if the set of caommands was not found. - public IEnumerable GetKeysFromCommands (params Command [] commands) - { - return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); - } - - /// Removes a from the collection. - /// - public void Remove (Key key) - { - if (!TryGet (key, out ApplicationKeyBinding _)) - { - return; - } - - Bindings.Remove (key); - } - - /// 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 [] newCommands) - { - if (TryGet (key, out ApplicationKeyBinding binding)) - { - Remove (key); - Add (key, binding.Target, newCommands); - - return; - } - - throw new InvalidOperationException ($"Key {key} is not bound."); - } - - /// Replaces a key combination bound to a set of s. - /// If is not bound, this method has the same effect as . - /// The key to be replaced. - /// The new key to be used. If this method has the same effect as . - public void ReplaceKey (Key oldKey, Key newKey) - { - if (!newKey.IsValid) - { - throw new InvalidOperationException ($"Key {newKey} is is not valid."); - } - - if (newKey == Key.Empty) - { - Remove (oldKey); - return; - } - - - if (TryGet (oldKey, out ApplicationKeyBinding binding)) - { - Remove (oldKey); - Add (newKey, binding); - } - else - { - Add (newKey, binding); - } - } - - /// 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 ApplicationKeyBinding binding) - { - binding = new ([], null); - - if (key.IsValid) - { - return Bindings.TryGetValue (key, out binding); - } - - return false; - } -} diff --git a/Terminal.Gui/Input/Keyboard/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs index 8ae7103ea..dce67950f 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -24,12 +24,12 @@ public record struct KeyBinding /// Initializes a new instance. /// The commands this key binding will invoke. - /// The view the key binding is bound to. + /// The view the key binding is bound to. /// Arbitrary data that can be associated with this key binding. - public KeyBinding (Command [] commands, View? boundView, object? data = null) + public KeyBinding (Command [] commands, View? target, object? data = null) { Commands = commands; - BoundView = boundView; + Target = target; Data = data; } @@ -42,7 +42,7 @@ public record struct KeyBinding public Key? Key { get; set; } /// The view the key binding is bound to. - public View? BoundView { get; set; } + public View? Target { get; set; } /// /// Arbitrary context that can be associated with this key binding. diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index a95073c9e..5efe34f24 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -9,8 +9,8 @@ namespace Terminal.Gui; /// public class KeyBindings { - /// Initializes a new instance bound to . - public KeyBindings (View? boundView) { BoundView = boundView; } + /// Initializes a new instance bound to . + public KeyBindings (View? target) { Target = target; } /// Adds a to the collection. /// @@ -24,7 +24,7 @@ public class KeyBindings throw new ArgumentException (nameof (key)); } - if (binding.Commands.Length == 0) + if (binding.Commands is { Length: 0 }) { throw new ArgumentException (nameof (binding)); } @@ -36,9 +36,9 @@ public class KeyBindings //Bindings [key] = binding; } - if (BoundView is { }) + if (Target is { }) { - binding.BoundView = BoundView; + binding.Target = Target; } // IMPORTANT: Add a COPY of the key. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy @@ -76,11 +76,47 @@ public class KeyBindings Add (key, new KeyBinding (commands)); } + + /// + /// + /// Adds a new key combination that will trigger the commands in on the View + /// specified by . + /// + /// + /// If the key is already bound to a different array of s it will be rebound + /// . + /// + /// + /// + /// + /// The key to check. + /// The View the commands will be invoked on. If , the key will be bound to . + /// + /// 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? target, params Command [] commands) + { + KeyBinding binding = new (commands, target); + Add (key, binding); + } + // TODO: Add a dictionary comparer that ignores Scope // TODO: This should not be public! /// The collection of objects. public Dictionary Bindings { get; } = new (new KeyEqualityComparer ()); + /// + /// Gets the bindings bound to . + /// + /// + /// + public IEnumerable> GetBindings (Key key) + { + return Bindings.Where (b => b.Key == key.KeyCode); + } + /// /// Gets the keys that are bound. /// @@ -93,7 +129,7 @@ public class KeyBindings /// /// If the KeyBindings object is being used for Application.KeyBindings. /// - public View? BoundView { get; init; } + public View? Target { get; init; } /// Removes all objects from the collection. public void Clear () { Bindings.Clear (); } @@ -202,19 +238,27 @@ public class KeyBindings /// The new key to be used. If no action will be taken. public void ReplaceKey (Key oldKey, Key newKey) { - if (!TryGet (oldKey, out KeyBinding _)) - { - throw new InvalidOperationException ($"Key {oldKey} is not bound."); - } - if (!newKey.IsValid) { throw new InvalidOperationException ($"Key {newKey} is is not valid."); } - KeyBinding value = Bindings [oldKey]; - Remove (oldKey); - Add (newKey, value); + if (newKey == Key.Empty) + { + Remove (oldKey); + return; + } + + + if (TryGet (oldKey, out KeyBinding binding)) + { + Remove (oldKey); + Add (newKey, binding); + } + else + { + Add (newKey, binding); + } } /// Gets the commands bound with the specified Key. diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index eff0b62c3..cd14dfc4f 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -370,7 +370,7 @@ public static class MessageBox { Clicked = (int)button.Data!; } - else if (keyCommandContext.Binding.BoundView is Button btn) + else if (keyCommandContext.Binding.Target is Button btn) { Clicked = (int)btn.Data!; } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index aa3e7b9fb..32b51cc5f 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -69,7 +69,7 @@ public class RadioGroup : View, IDesignable, IOrientation if (HasFocus) { - if (keyCommandContext is { Binding : { } } && (keyCommandContext.Binding.BoundView != this || HotKey == keyCommandContext.Binding.Key?.NoAlt.NoCtrl.NoShift)) + if (keyCommandContext is { Binding : { } } && (keyCommandContext.Binding.Target != this || HotKey == keyCommandContext.Binding.Key?.NoAlt.NoCtrl.NoShift)) { // It's this.HotKey OR Another View (Label?) forwarded the hotkey command to us - Act just like `Space` (Select) return InvokeCommand (Command.Select); @@ -248,7 +248,7 @@ public class RadioGroup : View, IDesignable, IOrientation if (c > -1) { // Just like the user pressing the items' hotkey - e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], boundView: this, data: c)) == true; + e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], target: this, data: c)) == true; } } diff --git a/UnitTests/Application/KeyboardTests.cs b/UnitTests/Application/KeyboardTests.cs index a164b8842..970d6a5b8 100644 --- a/UnitTests/Application/KeyboardTests.cs +++ b/UnitTests/Application/KeyboardTests.cs @@ -140,12 +140,12 @@ public class KeyboardTests Application.KeyBindings.Add (Key.A, Command.Accept); Application.KeyBindings.Add (Key.B, Command.Accept); - Assert.True (Application.KeyBindings.TryGet (Key.A, out ApplicationKeyBinding binding)); + Assert.True (Application.KeyBindings.TryGet (Key.A, out KeyBinding binding)); Assert.Null (binding.Target); Assert.True (Application.KeyBindings.TryGet (Key.B, out binding)); Assert.Null (binding.Target); } - + [Fact] [AutoInitShutdown] public void KeyBindings_Remove_Removes () diff --git a/UnitTests/Input/Keyboard/KeyBindingsTests.cs b/UnitTests/Input/Keyboard/KeyBindingsTests.cs index 5df3a6a14..cce65c6d5 100644 --- a/UnitTests/Input/Keyboard/KeyBindingsTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -58,7 +58,7 @@ public class KeyBindingsTests () public void Add_With_Throws_If_Exists () { var keyBindings = new KeyBindings (new View ()); - keyBindings.Add (Key.A, Command.HotKey); + keyBindings.Add (Key.A, Command.HotKey); Assert.Throws (() => keyBindings.Add (Key.A, Command.Accept)); Command [] resultCommands = keyBindings.GetCommands (Key.A); @@ -105,7 +105,7 @@ public class KeyBindingsTests () var keyBindings = new KeyBindings (new ()); Assert.Empty (keyBindings.Bindings); Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept)); - Assert.NotNull (keyBindings.BoundView); + Assert.NotNull (keyBindings.Target); } [Fact] @@ -243,10 +243,11 @@ public class KeyBindingsTests () } [Fact] - public void ReplaceKey_Throws_If_DoesNotContain_Old () + public void ReplaceKey_Adds_If_DoesNotContain_Old () { var keyBindings = new KeyBindings (new ()); - Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.B)); + keyBindings.ReplaceKey (Key.A, Key.B); + Assert.NotEmpty (keyBindings.GetBindings (Key.B)); } [Fact]