From a7d53928c1e4c7ea97cf9773a36057275e6abe10 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 5 Dec 2024 09:26:52 -0700 Subject: [PATCH 01/40] WIP: Refactored CommandContext --- Terminal.Gui/Input/CommandContext.cs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs index 095a3976aa..786d283e7e 100644 --- a/Terminal.Gui/Input/CommandContext.cs +++ b/Terminal.Gui/Input/CommandContext.cs @@ -11,24 +11,20 @@ namespace Terminal.Gui; /// use . /// /// -/// -/// -/// #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved -public record struct CommandContext +public record struct CommandContext { /// /// Initializes a new instance of with the specified , /// /// /// - /// + /// /// - public CommandContext (Command command, Key? key, KeyBinding? keyBinding = null, object? data = null) + public CommandContext (Command command, TBindingType? binding, object? data = null) { Command = command; - Key = key; - KeyBinding = keyBinding; + Binding = binding; Data = data; } @@ -38,14 +34,9 @@ public CommandContext (Command command, Key? key, KeyBinding? keyBinding = null, public Command Command { get; set; } /// - /// The that is being invoked. This is the key that was pressed to invoke the . + /// The keyboard or mouse minding that was used to invoke the , if any. /// - public Key? Key { get; set; } - - /// - /// The KeyBinding that was used to invoke the , if any. - /// - public KeyBinding? KeyBinding { get; set; } + public TBindingType? Binding { get; set; } /// /// Arbitrary data. From 76c0ab81d87d1f040b86b6b3f8624130969bdc39 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 5 Dec 2024 11:00:44 -0700 Subject: [PATCH 02/40] WIP: Refactored Mouse stuff. --- .../Application/Application.Keyboard.cs | 4 +- Terminal.Gui/Input/Command.cs | 2 +- Terminal.Gui/Input/CommandContext.cs | 20 +- Terminal.Gui/Input/CommandEventArgs.cs | 2 +- Terminal.Gui/Input/{ => Keyboard}/Key.cs | 0 .../Input/{ => Keyboard}/KeyBinding.cs | 0 .../Input/{ => Keyboard}/KeyBindingScope.cs | 0 .../Input/{ => Keyboard}/KeyBindings.cs | 3 - .../{ => Keyboard}/KeyChangedEventArgs.cs | 0 .../{ => Keyboard}/KeyEqualityComparer.cs | 0 .../KeystrokeNavigatorEventArgs.cs | 0 .../Input/{ => Mouse}/GrabMouseEventArgs.cs | 0 Terminal.Gui/Input/Mouse/MouseBinding.cs | 20 ++ Terminal.Gui/Input/Mouse/MouseBindings.cs | 216 ++++++++++++++++++ .../Input/{ => Mouse}/MouseEventArgs.cs | 0 Terminal.Gui/Input/{ => Mouse}/MouseFlags.cs | 0 .../{ => Mouse}/MouseFlagsChangedEventArgs.cs | 0 Terminal.Gui/Input/PointEventArgs.cs | 12 - Terminal.Gui/View/View.Command.cs | 44 +--- Terminal.Gui/View/View.Keyboard.cs | 2 +- Terminal.Gui/Views/TextView.cs | 30 +-- 21 files changed, 282 insertions(+), 73 deletions(-) rename Terminal.Gui/Input/{ => Keyboard}/Key.cs (100%) rename Terminal.Gui/Input/{ => Keyboard}/KeyBinding.cs (100%) rename Terminal.Gui/Input/{ => Keyboard}/KeyBindingScope.cs (100%) rename Terminal.Gui/Input/{ => Keyboard}/KeyBindings.cs (99%) rename Terminal.Gui/Input/{ => Keyboard}/KeyChangedEventArgs.cs (100%) rename Terminal.Gui/Input/{ => Keyboard}/KeyEqualityComparer.cs (100%) rename Terminal.Gui/Input/{ => Keyboard}/KeystrokeNavigatorEventArgs.cs (100%) rename Terminal.Gui/Input/{ => Mouse}/GrabMouseEventArgs.cs (100%) create mode 100644 Terminal.Gui/Input/Mouse/MouseBinding.cs create mode 100644 Terminal.Gui/Input/Mouse/MouseBindings.cs rename Terminal.Gui/Input/{ => Mouse}/MouseEventArgs.cs (100%) rename Terminal.Gui/Input/{ => Mouse}/MouseFlags.cs (100%) rename Terminal.Gui/Input/{ => Mouse}/MouseFlagsChangedEventArgs.cs (100%) delete mode 100644 Terminal.Gui/Input/PointEventArgs.cs diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 7205841300..e0ebe26717 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -54,7 +54,7 @@ public static bool RaiseKeyDownEvent (Key key) return false; } - bool? handled = binding.Value.BoundView?.InvokeCommands (binding.Value.Commands, binding.Key, binding.Value); + bool? handled = binding.Value.BoundView?.InvokeCommands (binding.Value.Commands, binding.Value); if (handled != null && (bool)handled) { @@ -92,7 +92,7 @@ public static bool RaiseKeyDownEvent (Key key) if (CommandImplementations.TryGetValue (command, out View.CommandImplementation? implementation)) { - var context = new CommandContext (command, key, appBinding); // Create the context here + var context = new CommandContext (command, appBinding); // Create the context here return implementation (context); } diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs index e0af3c2af1..39500a3d2c 100644 --- a/Terminal.Gui/Input/Command.cs +++ b/Terminal.Gui/Input/Command.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui; /// /// Actions which can be performed by a . Commands are typically invoked via -/// and mouse events. +/// and . /// public enum Command { diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs index 786d283e7e..b35e9f8adc 100644 --- a/Terminal.Gui/Input/CommandContext.cs +++ b/Terminal.Gui/Input/CommandContext.cs @@ -12,13 +12,12 @@ namespace Terminal.Gui; /// /// #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved -public record struct CommandContext +public record struct CommandContext : ICommandContext { /// - /// Initializes a new instance of with the specified , + /// Initializes a new instance with the specified , /// /// - /// /// /// public CommandContext (Command command, TBindingType? binding, object? data = null) @@ -29,14 +28,23 @@ public CommandContext (Command command, TBindingType? binding, object? data = nu } /// - /// The that is being invoked. + /// The keyboard or mouse minding that was used to invoke the , if any. /// + public TBindingType? Binding { get; set; } + + /// public Command Command { get; set; } + /// + public object? Data { get; set; } +} + +public interface ICommandContext +{ /// - /// The keyboard or mouse minding that was used to invoke the , if any. + /// The that is being invoked. /// - public TBindingType? Binding { get; set; } + public Command Command { get; set; } /// /// Arbitrary data. diff --git a/Terminal.Gui/Input/CommandEventArgs.cs b/Terminal.Gui/Input/CommandEventArgs.cs index f12d21be89..9782c7dfd6 100644 --- a/Terminal.Gui/Input/CommandEventArgs.cs +++ b/Terminal.Gui/Input/CommandEventArgs.cs @@ -11,5 +11,5 @@ public class CommandEventArgs : CancelEventArgs /// /// The context for the command. /// - public CommandContext Context { get; init; } + public required ICommandContext Context { get; init; } } diff --git a/Terminal.Gui/Input/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs similarity index 100% rename from Terminal.Gui/Input/Key.cs rename to Terminal.Gui/Input/Keyboard/Key.cs diff --git a/Terminal.Gui/Input/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs similarity index 100% rename from Terminal.Gui/Input/KeyBinding.cs rename to Terminal.Gui/Input/Keyboard/KeyBinding.cs diff --git a/Terminal.Gui/Input/KeyBindingScope.cs b/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs similarity index 100% rename from Terminal.Gui/Input/KeyBindingScope.cs rename to Terminal.Gui/Input/Keyboard/KeyBindingScope.cs diff --git a/Terminal.Gui/Input/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs similarity index 99% rename from Terminal.Gui/Input/KeyBindings.cs rename to Terminal.Gui/Input/Keyboard/KeyBindings.cs index 71777d804d..1c989862a1 100644 --- a/Terminal.Gui/Input/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -1,7 +1,4 @@ #nullable enable - -using static System.Formats.Asn1.AsnWriter; - namespace Terminal.Gui; /// diff --git a/Terminal.Gui/Input/KeyChangedEventArgs.cs b/Terminal.Gui/Input/Keyboard/KeyChangedEventArgs.cs similarity index 100% rename from Terminal.Gui/Input/KeyChangedEventArgs.cs rename to Terminal.Gui/Input/Keyboard/KeyChangedEventArgs.cs diff --git a/Terminal.Gui/Input/KeyEqualityComparer.cs b/Terminal.Gui/Input/Keyboard/KeyEqualityComparer.cs similarity index 100% rename from Terminal.Gui/Input/KeyEqualityComparer.cs rename to Terminal.Gui/Input/Keyboard/KeyEqualityComparer.cs diff --git a/Terminal.Gui/Input/KeystrokeNavigatorEventArgs.cs b/Terminal.Gui/Input/Keyboard/KeystrokeNavigatorEventArgs.cs similarity index 100% rename from Terminal.Gui/Input/KeystrokeNavigatorEventArgs.cs rename to Terminal.Gui/Input/Keyboard/KeystrokeNavigatorEventArgs.cs diff --git a/Terminal.Gui/Input/GrabMouseEventArgs.cs b/Terminal.Gui/Input/Mouse/GrabMouseEventArgs.cs similarity index 100% rename from Terminal.Gui/Input/GrabMouseEventArgs.cs rename to Terminal.Gui/Input/Mouse/GrabMouseEventArgs.cs diff --git a/Terminal.Gui/Input/Mouse/MouseBinding.cs b/Terminal.Gui/Input/Mouse/MouseBinding.cs new file mode 100644 index 0000000000..01ceb09e18 --- /dev/null +++ b/Terminal.Gui/Input/Mouse/MouseBinding.cs @@ -0,0 +1,20 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// Provides a collection of objects for mouse events. +/// +/// +public record struct MouseBinding +{ + /// Initializes a new instance. + /// The commands this mouse binding will invoke. + public MouseBinding (Command [] commands) + { + Commands = commands; + } + + /// The commands this key binding will invoke. + public Command [] Commands { get; set; } +} diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs new file mode 100644 index 0000000000..3ceee4aaf0 --- /dev/null +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -0,0 +1,216 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Provides a collection of objects bound to a combination of . +/// +/// +/// +public class MouseBindings +{ + /// + /// Initializes a new instance. This constructor is used when the are not bound to a + /// . This is used for Application.MouseBindings and unit tests. + /// + public MouseBindings () { } + + /// Adds a to the collection. + /// + /// + public void Add (MouseFlags mouseFlag, MouseBinding binding) + { + if (TryGet (mouseFlag, out MouseBinding _)) + { + throw new InvalidOperationException (@$"A binding for {mouseFlag} exists ({binding})."); + + //Bindings [key] = 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 (mouseFlag, binding); + } + + /// + /// Adds a new mouse flag 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 mouse flags to check. + /// + /// The command to invoked on the when is received. When + /// multiple commands are provided,they will be applied in sequence. The bound event will be + /// consumed if any took effect. + /// + public void Add (MouseFlags mouseFlags, params Command [] commands) + { + if (mouseFlags == MouseFlags.None) + { + throw new ArgumentException (@"Invalid MouseFlag", nameof (commands)); + } + + if (commands.Length == 0) + { + throw new ArgumentException (@"At least one command must be specified", nameof (commands)); + } + + if (TryGet (mouseFlags, out MouseBinding binding)) + { + throw new InvalidOperationException (@$"A binding for {mouseFlags} exists ({binding})."); + } + + Add (mouseFlags, new MouseBinding (commands)); + } + + // TODO: Add a dictionary comparer that ignores Scope + // TODO: This should not be public! + /// The collection of objects. + public Dictionary Bindings { get; } = new (); + + /// + /// Gets the that are bound. + /// + /// + public IEnumerable GetBoundMouseFlags () + { + return Bindings.Keys; + } + + /// Removes all objects from the collection. + public void Clear () { Bindings.Clear (); } + + /// + /// Removes all bindings that trigger the given command set. Views can have multiple different events 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 combination of . + /// + /// + public MouseBinding Get (MouseFlags mouseFlags) + { + if (TryGet (mouseFlags, out MouseBinding binding)) + { + return binding; + } + + throw new InvalidOperationException ($"{mouseFlags} 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 (MouseFlags mouseFlags) + { + if (TryGet (mouseFlags, out MouseBinding bindings)) + { + return bindings.Commands; + } + + return []; + } + + /// Gets the first combination of bound to the set of commands specified by . + /// The set of commands to search. + /// The first combination of bound to the set of commands specified by . if the set of caommands was not found. + public MouseFlags? GetMouseFlagsFromCommands (params Command [] commands) + { + return Bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; + } + + /// Gets combination of bound to the set of commands specified by . + /// The set of commands to search. + /// The combination of bound to the set of commands specified by . An empty list if the set of caommands was not found. + public IEnumerable GetAllMouseFlagsFromCommands (params Command [] commands) + { + return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); + } + + /// Removes a from the collection. + /// + public void Remove (MouseFlags mouseFlags) + { + if (!TryGet (mouseFlags, out MouseBinding _)) + { + return; + } + + Bindings.Remove (mouseFlags); + } + + /// Replaces the commands already bound to a combination of . + /// + /// + /// If the combination of is not already bound, it will be added. + /// + /// + /// The combination of bound to the command to be replaced. + /// The set of commands to replace the old ones with. + public void ReplaceCommands (MouseFlags mouseFlags, params Command [] commands) + { + if (TryGet (mouseFlags, out MouseBinding binding)) + { + binding.Commands = commands; + } + else + { + Add (mouseFlags, commands); + } + } + + /// Replaces a combination already bound to a set of s. + /// + /// The to be replaced. + /// The new to be used. If no action will be taken. + public void ReplaceKey (MouseFlags oldMouseFlags, MouseFlags newMouseFlags) + { + if (!TryGet (oldMouseFlags, out MouseBinding _)) + { + throw new InvalidOperationException ($"Key {oldMouseFlags} is not bound."); + } + + MouseBinding value = Bindings [oldMouseFlags]; + Remove (oldMouseFlags); + Add (newMouseFlags, value); + } + + /// Gets the commands bound with the specified . + /// + /// The key to check. + /// + /// When this method returns, contains the commands bound with the specified mouse flags, if the mouse flags are + /// found; otherwise, null. This parameter is passed uninitialized. + /// + /// if the mouse flags are bound; otherwise . + public bool TryGet (MouseFlags mouseFlags, out MouseBinding binding) + { + + binding = new ([]); + + return Bindings.TryGetValue (mouseFlags, out binding); + } +} diff --git a/Terminal.Gui/Input/MouseEventArgs.cs b/Terminal.Gui/Input/Mouse/MouseEventArgs.cs similarity index 100% rename from Terminal.Gui/Input/MouseEventArgs.cs rename to Terminal.Gui/Input/Mouse/MouseEventArgs.cs diff --git a/Terminal.Gui/Input/MouseFlags.cs b/Terminal.Gui/Input/Mouse/MouseFlags.cs similarity index 100% rename from Terminal.Gui/Input/MouseFlags.cs rename to Terminal.Gui/Input/Mouse/MouseFlags.cs diff --git a/Terminal.Gui/Input/MouseFlagsChangedEventArgs.cs b/Terminal.Gui/Input/Mouse/MouseFlagsChangedEventArgs.cs similarity index 100% rename from Terminal.Gui/Input/MouseFlagsChangedEventArgs.cs rename to Terminal.Gui/Input/Mouse/MouseFlagsChangedEventArgs.cs diff --git a/Terminal.Gui/Input/PointEventArgs.cs b/Terminal.Gui/Input/PointEventArgs.cs deleted file mode 100644 index bd362dc62f..0000000000 --- a/Terminal.Gui/Input/PointEventArgs.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Terminal.Gui; - -/// Event args for events which relate to a single -public class PointEventArgs : EventArgs -{ - /// Creates a new instance of the class - /// - public PointEventArgs (Point p) { Point = p; } - - /// The point the event happened at - public Point Point { get; } -} diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index 1ef0dbb279..acafbfce96 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -69,7 +69,7 @@ private void SetupCommands () /// if the event was raised and was not handled (or cancelled); input proessing should continue. /// if the event was raised and handled (or cancelled); input proessing should stop. /// - protected bool? RaiseAccepting (CommandContext ctx) + protected bool? RaiseAccepting (ICommandContext ctx) { CommandEventArgs args = new () { Context = ctx }; @@ -93,14 +93,14 @@ private void SetupCommands () if (isDefaultView != this && isDefaultView is Button { IsDefault: true } button) { - bool? handled = isDefaultView.InvokeCommand (Command.Accept, ctx: new (Command.Accept, null, null, this)); + bool? handled = isDefaultView.InvokeCommand (Command.Accept, new ([Command.Accept], 0, null, this)); if (handled == true) { return true; } } - return SuperView?.InvokeCommand (Command.Accept, ctx: new (Command.Accept, null, null, this)) == true; + return SuperView?.InvokeCommand (Command.Accept, new ([Command.Accept], 0, null, this)) == true; } return Accepting is null ? null : args.Cancel; @@ -142,7 +142,7 @@ private void SetupCommands () /// if the event was raised and was not handled (or cancelled); input proessing should continue. /// if the event was raised and handled (or cancelled); input proessing should stop. /// - protected bool? RaiseSelecting (CommandContext ctx) + protected bool? RaiseSelecting (ICommandContext ctx) { CommandEventArgs args = new () { Context = ctx }; @@ -185,7 +185,7 @@ private void SetupCommands () /// protected bool? RaiseHandlingHotKey () { - CommandEventArgs args = new (); + CommandEventArgs args = new () { Context = new CommandContext () { Command = Command.HotKey } }; // Best practice is to invoke the virtual method first. // This allows derived classes to handle the event and potentially cancel it. @@ -225,7 +225,7 @@ private void SetupCommands () /// if the command was invoked and was not handled (or cancelled); input proessing should continue. /// if the command was invoked the command was handled (or cancelled); input proessing should stop. /// - public delegate bool? CommandImplementation (CommandContext ctx); + public delegate bool? CommandImplementation (ICommandContext? ctx); /// /// @@ -282,7 +282,7 @@ private void SetupCommands () /// if at least one command was invoked and was not handled (or cancelled); input proessing should continue. /// if at least one command was invoked the command was handled (or cancelled); input proessing should stop. /// - public bool? InvokeCommands (Command [] commands, Key? key = null, KeyBinding? keyBinding = null) + public bool? InvokeCommands (Command [] commands, TBindingType binding) { bool? toReturn = null; @@ -291,12 +291,12 @@ private void SetupCommands () if (!CommandImplementations.ContainsKey (command)) { throw new NotSupportedException ( - @$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by this View ({GetType ().Name})" + @$"A Binding was set up for the command {command} ({binding}) but that command is not supported by this View ({GetType ().Name})" ); } // each command has its own return value - bool? thisReturn = InvokeCommand (command, key, keyBinding); + bool? thisReturn = InvokeCommand (command, binding); // if we haven't got anything yet, the current command result should be used toReturn ??= thisReturn; @@ -311,41 +311,21 @@ private void SetupCommands () return toReturn; } - /// Invokes the specified command. - /// The command to invoke. - /// The key that caused the command to be invoked, if any. This will be passed as context with the command. - /// The key binding that was bound to the key and caused the invocation, if any. This will be passed as context with the command. - /// - /// if no command was found; input proessing should continue. - /// if the command was invoked and was not handled (or cancelled); input proessing should continue. - /// if the command was invoked the command was handled (or cancelled); input proessing should stop. - /// - public bool? InvokeCommand (Command command, Key? key = null, KeyBinding? keyBinding = null) - { - if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation)) - { - var context = new CommandContext (command, key, keyBinding); // Create the context here - return implementation (context); - } - - return null; - } - /// /// Invokes the specified command. /// /// The command to invoke. - /// Context to pass with the invocation. + /// /// /// if no command was found; input proessing should continue. /// if the command was invoked and was not handled (or cancelled); input proessing should continue. /// if the command was invoked the command was handled (or cancelled); input proessing should stop. /// - public bool? InvokeCommand (Command command, CommandContext ctx) + public bool? InvokeCommand (Command command, TBindingType binding) { if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { - return implementation (ctx); + return implementation (binding as ICommandContext); } return null; diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 3028402a9f..ab126f24c1 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -705,7 +705,7 @@ public bool IsHotKeyBound (Key key, out View? boundView) } #endif - return InvokeCommands (binding.Commands, key, binding); + return InvokeCommands (binding.Commands, binding); } #endregion Key Bindings diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 829af98ebf..8ab6317bbf 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -3710,8 +3710,8 @@ public override bool OnKeyUp (Key key) /// Invoke the event with the unwrapped . public virtual void OnUnwrappedCursorPosition (int? cRow = null, int? cCol = null) { - int? row = cRow is null ? CurrentRow : cRow; - int? col = cCol is null ? CurrentColumn : cCol; + int? row = cRow ?? CurrentRow; + int? col = cCol ?? CurrentColumn; if (cRow is null && cCol is null && _wordWrap) { @@ -3719,7 +3719,7 @@ public virtual void OnUnwrappedCursorPosition (int? cRow = null, int? cCol = nul col = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn); } - UnwrappedCursorPosition?.Invoke (this, new (new ((int)col, (int)row))); + UnwrappedCursorPosition?.Invoke (this, new Point (row.Value, col.Value)); } /// Paste the clipboard contents into the current selected position. @@ -3956,7 +3956,7 @@ public void Undo () } /// Invoked with the unwrapped . - public event EventHandler? UnwrappedCursorPosition; + public event EventHandler? UnwrappedCursorPosition; /// /// Sets the to an appropriate color for rendering the given @@ -4465,12 +4465,12 @@ private bool DeleteTextForwards () } else { - _historyText.Add ([[.. currentLine]], CursorPosition); + _historyText.Add ([ [.. currentLine]], CursorPosition); currentLine.RemoveAt (CurrentColumn); _historyText.Add ( - [[.. currentLine]], + [ [.. currentLine]], CursorPosition, HistoryText.LineStatus.Replaced ); @@ -5057,7 +5057,7 @@ private void KillToEndOfLine () } _historyText.Add ( - [[.. GetCurrentLine ()]], + [ [.. GetCurrentLine ()]], CursorPosition, HistoryText.LineStatus.Replaced ); @@ -5097,7 +5097,7 @@ private void KillToLeftStart () return; } - _historyText.Add ([[.. currentLine]], CursorPosition); + _historyText.Add ([ [.. currentLine]], CursorPosition); if (currentLine.Count == 0) { @@ -5164,7 +5164,7 @@ private void KillToLeftStart () } _historyText.Add ( - [[.. GetCurrentLine ()]], + [ [.. GetCurrentLine ()]], CursorPosition, HistoryText.LineStatus.Replaced ); @@ -5188,14 +5188,14 @@ private void KillWordBackward () List currentLine = GetCurrentLine (); - _historyText.Add ([[.. GetCurrentLine ()]], CursorPosition); + _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition); if (CurrentColumn == 0) { DeleteTextBackwards (); _historyText.ReplaceLast ( - [[.. GetCurrentLine ()]], + [ [.. GetCurrentLine ()]], CursorPosition, HistoryText.LineStatus.Replaced ); @@ -5234,7 +5234,7 @@ [[.. GetCurrentLine ()]], } _historyText.Add ( - [[.. GetCurrentLine ()]], + [ [.. GetCurrentLine ()]], CursorPosition, HistoryText.LineStatus.Replaced ); @@ -5256,14 +5256,14 @@ private void KillWordForward () List currentLine = GetCurrentLine (); - _historyText.Add ([[.. GetCurrentLine ()]], CursorPosition); + _historyText.Add ([ [.. GetCurrentLine ()]], CursorPosition); if (currentLine.Count == 0 || CurrentColumn == currentLine.Count) { DeleteTextForwards (); _historyText.ReplaceLast ( - [[.. GetCurrentLine ()]], + [ [.. GetCurrentLine ()]], CursorPosition, HistoryText.LineStatus.Replaced ); @@ -5293,7 +5293,7 @@ [[.. GetCurrentLine ()]], } _historyText.Add ( - [[.. GetCurrentLine ()]], + [ [.. GetCurrentLine ()]], CursorPosition, HistoryText.LineStatus.Replaced ); From e6054f7275259624311cc858c78c426e6c5a1798 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 5 Dec 2024 11:40:33 -0700 Subject: [PATCH 03/40] WIP: Builds. No worky. --- Terminal.Gui/Input/Keyboard/KeyBinding.cs | 5 ++++ Terminal.Gui/View/View.Command.cs | 19 ++++++++++++++++ Terminal.Gui/View/View.Mouse.cs | 2 +- Terminal.Gui/Views/Button.cs | 10 +++++--- Terminal.Gui/Views/CheckBox.cs | 7 +++++- Terminal.Gui/Views/ColorPicker.16.cs | 24 ++++++++++++++++---- Terminal.Gui/Views/ComboBox.cs | 6 ++++- Terminal.Gui/Views/FrameView.cs | 2 +- Terminal.Gui/Views/Label.cs | 10 +++++--- Terminal.Gui/Views/ListView.cs | 6 ++++- Terminal.Gui/Views/Menu/Menu.cs | 6 ++--- Terminal.Gui/Views/Menu/MenuBar.cs | 4 ++-- Terminal.Gui/Views/MessageBox.cs | 2 +- Terminal.Gui/Views/RadioGroup.cs | 14 ++++++++---- Terminal.Gui/Views/ScrollBar/ScrollSlider.cs | 2 +- Terminal.Gui/Views/Shortcut.cs | 10 +++++--- Terminal.Gui/Views/Slider.cs | 6 ++++- Terminal.Gui/Views/TextView.cs | 6 ++++- Terminal.Gui/Views/TreeView/TreeView.cs | 6 ++++- UICatalog/Scenarios/Editor.cs | 2 +- UICatalog/Scenarios/KeyBindings.cs | 24 ++++++++++++++++---- 21 files changed, 136 insertions(+), 37 deletions(-) diff --git a/Terminal.Gui/Input/Keyboard/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs index 0073a483ae..e420698e3d 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -43,6 +43,11 @@ public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, /// The scope of the . public KeyBindingScope Scope { get; set; } + /// + /// The Key that is bound to the . + /// + public Key Key { get; set; } + /// The view the key binding is bound to. public View? BoundView { get; set; } diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index acafbfce96..06431eb731 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -330,4 +330,23 @@ private void SetupCommands () return null; } + + /// + /// Invokes the specified command. + /// + /// The command to invoke. + /// + /// if no command was found; input proessing should continue. + /// if the command was invoked and was not handled (or cancelled); input proessing should continue. + /// if the command was invoked the command was handled (or cancelled); input proessing should stop. + /// + public bool? InvokeCommand (Command command) + { + if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation)) + { + return implementation (null); + } + + return null; + } } diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index 4dbb65e38c..29edd7188f 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -360,7 +360,7 @@ protected bool RaiseMouseClickEvent (MouseEventArgs args) // Always invoke Select command on MouseClick // By default, this will raise Selecting/OnSelecting - Subclasses can override this via AddCommand (Command.Select ...). - args.Handled = InvokeCommand (Command.Select, ctx: new (Command.Select, key: null, data: args)) == true; + args.Handled = InvokeCommand (Command.Select, new ([Command.Select], KeyBindingScope.Focused, null, args)) == true; return args.Handled; } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index eaf2d3956e..5036c07bf0 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -70,8 +70,12 @@ public Button () HighlightStyle = DefaultHighlightStyle; } - private bool? HandleHotKeyCommand (CommandContext ctx) + private bool? HandleHotKeyCommand (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } bool cachedIsDefault = IsDefault; // Supports "Swap Default" in Buttons scenario where IsDefault changes if (RaiseSelecting (ctx) is true) @@ -93,7 +97,7 @@ public Button () // If Accept was not handled... if (cachedIsDefault && SuperView is { }) { - return SuperView.InvokeCommand (Command.Accept); + return SuperView.InvokeCommand (Command.Accept, ctx.Binding); } return false; @@ -133,7 +137,7 @@ private void Button_MouseClick (object sender, MouseEventArgs e) } // TODO: With https://github.com/gui-cs/Terminal.Gui/issues/3778 we won't have to pass data: - e.Handled = InvokeCommand (Command.HotKey, new (Command.HotKey, null, data: this)) == true; + e.Handled = InvokeCommand (Command.HotKey, new KeyBinding([Command.HotKey], KeyBindingScope.HotKey, this, null)) == true; } private void Button_TitleChanged (object sender, EventArgs e) diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index 04156637e1..d2a4d60c99 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -34,8 +34,13 @@ public CheckBox () HighlightStyle = DefaultHighlightStyle; } - private bool? AdvanceAndSelect (CommandContext ctx) + private bool? AdvanceAndSelect (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } + bool? cancelled = AdvanceCheckState (); if (cancelled is true) diff --git a/Terminal.Gui/Views/ColorPicker.16.cs b/Terminal.Gui/Views/ColorPicker.16.cs index 39a02f351e..ea1a95f68f 100644 --- a/Terminal.Gui/Views/ColorPicker.16.cs +++ b/Terminal.Gui/Views/ColorPicker.16.cs @@ -67,8 +67,12 @@ public Point Cursor /// Moves the selected item index to the next row. /// - private bool MoveDown (CommandContext ctx) + private bool MoveDown (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } if (RaiseSelecting (ctx) == true) { return true; @@ -83,8 +87,12 @@ private bool MoveDown (CommandContext ctx) /// Moves the selected item index to the previous column. /// - private bool MoveLeft (CommandContext ctx) + private bool MoveLeft (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } if (RaiseSelecting (ctx) == true) { return true; @@ -100,8 +108,12 @@ private bool MoveLeft (CommandContext ctx) /// Moves the selected item index to the next column. /// - private bool MoveRight (CommandContext ctx) + private bool MoveRight (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } if (RaiseSelecting (ctx) == true) { return true; @@ -116,8 +128,12 @@ private bool MoveRight (CommandContext ctx) /// Moves the selected item index to the previous row. /// - private bool MoveUp (CommandContext ctx) + private bool MoveUp (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } if (RaiseSelecting (ctx) == true) { return true; diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index abfee0fde5..4fcdc315a2 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -395,8 +395,12 @@ public void SetSource (ObservableCollection source) } } - private bool ActivateSelected (CommandContext ctx) + private bool ActivateSelected (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } if (HasItems ()) { if (SelectText ()) diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index de2f862150..94c894faac 100644 --- a/Terminal.Gui/Views/FrameView.cs +++ b/Terminal.Gui/Views/FrameView.cs @@ -25,7 +25,7 @@ public FrameView () private void FrameView_MouseClick (object sender, MouseEventArgs e) { // base sets focus on HotKey - e.Handled = InvokeCommand (Command.HotKey, ctx: new (Command.HotKey, key: null, data: this)) == true; + e.Handled = InvokeCommand (Command.HotKey, new ([Command.HotKey], KeyBindingScope.Focused, null, this)) == true; } diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 07eca7983c..3ada1f11e9 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -36,7 +36,7 @@ private void Label_MouseClick (object sender, MouseEventArgs e) { if (!CanFocus) { - e.Handled = InvokeCommand (Command.HotKey, ctx: new (Command.HotKey, key: null, data: this)) == true; + e.Handled = InvokeCommand (Command.HotKey, new ([Command.HotKey], KeyBindingScope.HotKey, this, this)) == true; } } @@ -60,8 +60,12 @@ public override Rune HotKeySpecifier set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value; } - private bool? InvokeHotKeyOnNext (CommandContext context) + private bool? InvokeHotKeyOnNext (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } if (RaiseHandlingHotKey () == true) { return true; @@ -78,7 +82,7 @@ public override Rune HotKeySpecifier if (me != -1 && me < SuperView?.Subviews.Count - 1) { - return SuperView?.Subviews [me + 1].InvokeCommand (Command.HotKey, context.Key, context.KeyBinding) == true; + return SuperView?.Subviews [me + 1].InvokeCommand (Command.HotKey, ctx.Binding) == true; } return false; diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 8b19c91475..89ad3c6608 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -141,7 +141,11 @@ public ListView () return !SetFocus (); }); - AddCommand (Command.SelectAll, (ctx) => MarkAll ((bool)ctx.KeyBinding?.Context!)); + AddCommand (Command.SelectAll, (ctx) => + { + // BUGBUG: This probably isn't right + return MarkAll ((bool)ctx!.Data); + }); // Default keybindings for all ListViews KeyBindings.Add (Key.CursorUp, Command.Up); diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index ac3d617977..6e620c91d3 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -198,9 +198,9 @@ public Menu () return true; } ); - AddCommand (Command.Select, ctx => _host?.SelectItem ((ctx.KeyBinding?.Context as MenuItem)!)); - AddCommand (Command.Toggle, ctx => ExpandCollapse ((ctx.KeyBinding?.Context as MenuItem)!)); - AddCommand (Command.HotKey, ctx => _host?.SelectItem ((ctx.KeyBinding?.Context as MenuItem)!)); + AddCommand (Command.Select, ctx => _host?.SelectItem ((ctx.Data as MenuItem)!)); + AddCommand (Command.Toggle, ctx => ExpandCollapse ((ctx.Data as MenuItem)!)); + AddCommand (Command.HotKey, ctx => _host?.SelectItem ((ctx.Data as MenuItem)!)); // Default key bindings for this view KeyBindings.Add (Key.CursorUp, Command.Up); diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index 21c1a5b9f4..cb24070018 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -133,7 +133,7 @@ public MenuBar () { CloseOtherOpenedMenuBar (); - return Select (Menus.IndexOf (ctx.KeyBinding?.Context)); + return Select (Menus.IndexOf (ctx.Data)); }); AddCommand (Command.Select, ctx => { @@ -142,7 +142,7 @@ public MenuBar () // HACK: Work around the fact that View.MouseClick always invokes Select return false; } - var res = Run ((ctx.KeyBinding?.Context as MenuItem)?.Action!); + var res = Run ((ctx.Data as MenuItem)?.Action!); CloseAllMenus (); return res; diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index f36e41ace1..60384b9e54 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -365,7 +365,7 @@ params string [] buttons { Clicked = (int)button.Data!; } - else if (e.Context.KeyBinding?.BoundView is Button btn) + else if (e.Context is CommandContext { Binding.BoundView: Button btn }) { Clicked = (int)btn.Data!; } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index ac2867a3af..01e7590290 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -59,14 +59,20 @@ public RadioGroup () AddCommand (Command.HotKey, ctx => { - var item = ctx.KeyBinding?.Context as int?; + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + var item = keyCommandContext.Data as int?; + if (HasFocus) { - if (ctx is { KeyBinding: { } } && (ctx.KeyBinding.Value.BoundView != this || HotKey == ctx.Key?.NoAlt.NoCtrl.NoShift)) + if (keyCommandContext is { Binding : { } } && (keyCommandContext.Binding.BoundView != 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, ctx.Key, ctx.KeyBinding); + return InvokeCommand (Command.Select); } } @@ -242,7 +248,7 @@ private void RadioGroup_MouseClick (object? sender, MouseEventArgs e) if (c > -1) { // Just like the user pressing the items' hotkey - e.Handled = InvokeCommand (Command.HotKey, null, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, boundView: this, context: c)) == true; + e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, boundView: this, context: c)) == true; } } diff --git a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs index 899ee56e08..52bfa0bea7 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs @@ -241,7 +241,7 @@ private void RaisePositionChangeEvents (int newPosition) OnScrolled (distance); Scrolled?.Invoke (this, new (in distance)); - RaiseSelecting (new (Command.Select, null, null, distance)); + RaiseSelecting (new CommandContext (Command.Select, new KeyBinding ([Command.Select], KeyBindingScope.Focused, null, distance))); } /// diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index f10bb423b7..d35c46850e 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -300,8 +300,12 @@ private void AddCommands () AddCommand (Command.Select, DispatchCommand); } - private bool? DispatchCommand (CommandContext ctx) + private bool? DispatchCommand (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } if (ctx.Data != this) { // Invoke Select on the command view to cause it to change state if it wants to @@ -342,7 +346,7 @@ private void AddCommands () if (_targetView is { }) { - _targetView.InvokeCommand (Command); + _targetView.InvokeCommand (Command, ctx); } return cancel; @@ -496,7 +500,7 @@ void CommandViewOnSelecting (object? sender, CommandEventArgs e) if (e.Context.Data != this) { // Forward command to ourselves - InvokeCommand (Command.Select, new (Command.Select, null, null, this)); + InvokeCommand (Command.Select, new ([Command.Select], KeyBindingScope.Focused, null, this)); } // BUGBUG: This prevents NumericUpDown on statusbar in HexEditor from working diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index ff37d513c0..c0cef3915b 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -1785,8 +1785,12 @@ internal bool Select () return SetFocusedOption (); } - internal bool Accept (CommandContext ctx) + internal bool Accept (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } SetFocusedOption (); return RaiseAccepting (ctx) == true; diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 8ab6317bbf..35b5f67aa4 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -6143,8 +6143,12 @@ private void ProcessPaste () Paste (); } - private bool ProcessEnterKey (CommandContext ctx) + private bool ProcessEnterKey (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } ResetColumnTrack (); if (_isReadOnly) diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index 728a411bdb..c7f1c1f6ff 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -462,8 +462,12 @@ public void ClearObjects () /// This method also ensures that the selected object is visible. /// /// if was fired. - public bool? ActivateSelectedObjectIfAny (CommandContext ctx) + public bool? ActivateSelectedObjectIfAny (ICommandContext commandContext) { + if (commandContext is not CommandContext ctx) + { + return false; + } // By default, Command.Accept calls OnAccept, so we need to call it here to ensure that the event is fired. if (RaiseAccepting (ctx) == true) { diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index e0abc19b82..91f669447d 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -265,7 +265,7 @@ public override void Main () _textView.VerticalScrollBar.AutoShow = false; _textView.UnwrappedCursorPosition += (s, e) => { - siCursorPosition.Title = $"Ln {e.Point.Y + 1}, Col {e.Point.X + 1}"; + siCursorPosition.Title = $"Ln {e.Y + 1}, Col {e.X + 1}"; }; _appWindow.Add (statusBar); diff --git a/UICatalog/Scenarios/KeyBindings.cs b/UICatalog/Scenarios/KeyBindings.cs index 2b5de89ac5..fb4aab947b 100644 --- a/UICatalog/Scenarios/KeyBindings.cs +++ b/UICatalog/Scenarios/KeyBindings.cs @@ -165,17 +165,29 @@ public KeyBindingsDemo () AddCommand (Command.Save, ctx => { - MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + MessageBox.Query ($"{keyCommandContext.Binding.Scope}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); return true; }); AddCommand (Command.New, ctx => { - MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + MessageBox.Query ($"{keyCommandContext.Binding.Scope}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); return true; }); AddCommand (Command.HotKey, ctx => { - MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + MessageBox.Query ($"{keyCommandContext.Binding.Scope}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); SetFocus (); return true; }); @@ -186,7 +198,11 @@ public KeyBindingsDemo () AddCommand (Command.Quit, ctx => { - MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + MessageBox.Query ($"{keyCommandContext.Binding.Scope}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); Application.RequestStop (); return true; }); From dd36aa567d6a0a11a5b1e8a7ffe61c54c313980f Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 5 Dec 2024 12:29:15 -0700 Subject: [PATCH 04/40] ICommandContext -> separate file --- Terminal.Gui/Input/ICommandContext.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 Terminal.Gui/Input/ICommandContext.cs diff --git a/Terminal.Gui/Input/ICommandContext.cs b/Terminal.Gui/Input/ICommandContext.cs new file mode 100644 index 0000000000..77124922aa --- /dev/null +++ b/Terminal.Gui/Input/ICommandContext.cs @@ -0,0 +1,19 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Describes the context in which a is being invoked. +/// +public interface ICommandContext +{ + /// + /// The that is being invoked. + /// + public Command Command { get; set; } + + // TODO: Remove this property. With CommandContext being a generic type, there should be no need for arbitrary data. + /// + /// Arbitrary data. + /// + public object? Data { get; set; } +} From 80db8e6036e449f22211ee5d8f8112c055820232 Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 5 Dec 2024 12:29:21 -0700 Subject: [PATCH 05/40] ICommandContext -> separate file --- Terminal.Gui/Input/CommandContext.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs index b35e9f8adc..613b353203 100644 --- a/Terminal.Gui/Input/CommandContext.cs +++ b/Terminal.Gui/Input/CommandContext.cs @@ -37,17 +37,4 @@ public CommandContext (Command command, TBindingType? binding, object? data = nu /// public object? Data { get; set; } -} - -public interface ICommandContext -{ - /// - /// The that is being invoked. - /// - public Command Command { get; set; } - - /// - /// Arbitrary data. - /// - public object? Data { get; set; } -} +} \ No newline at end of file From 53d7449c853b7fdd0bccdc71086a7f9e727d2f4d Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 5 Dec 2024 13:15:41 -0700 Subject: [PATCH 06/40] Unit tests pass. Most things actually work, but not all --- .../Application/Application.Keyboard.cs | 2 +- Terminal.Gui/Input/CommandContext.cs | 22 +- Terminal.Gui/Input/CommandEventArgs.cs | 7 +- Terminal.Gui/Input/ICommandContext.cs | 10 +- Terminal.Gui/Input/Keyboard/KeyBinding.cs | 12 +- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 1 + Terminal.Gui/Input/Mouse/MouseBindings.cs | 118 +- Terminal.Gui/View/View.Command.cs | 71 +- Terminal.Gui/Views/ColorPicker.16.cs | 5 +- Terminal.Gui/Views/ComboBox.cs | 6 +- Terminal.Gui/Views/ListView.cs | 8 +- Terminal.Gui/Views/Menu/Menu.cs | 1048 +++++++++-------- Terminal.Gui/Views/Menu/MenuBar.cs | 14 +- Terminal.Gui/Views/MessageBox.cs | 13 +- Terminal.Gui/Views/RadioGroup.cs | 4 +- Terminal.Gui/Views/Shortcut.cs | 9 +- UICatalog/Scenarios/Editors/EventLog.cs | 15 +- UnitTests/Views/TextViewTests.cs | 2 +- 18 files changed, 714 insertions(+), 653 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index e0ebe26717..34b63ceb75 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -321,7 +321,7 @@ private static void ReplaceKey (Key oldKey, Key newKey) /// /// /// - /// This version of AddCommand is for commands that do not require a . + /// This version of AddCommand is for commands that do not require a . /// /// /// The command. diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs index 613b353203..61c21f4e68 100644 --- a/Terminal.Gui/Input/CommandContext.cs +++ b/Terminal.Gui/Input/CommandContext.cs @@ -3,14 +3,9 @@ namespace Terminal.Gui; #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// -/// Provides context for a that is being invoked. +/// Provides context for a invocation. /// -/// -/// -/// To define a that is invoked with context, -/// use . -/// -/// +/// . #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved public record struct CommandContext : ICommandContext { @@ -19,22 +14,17 @@ public record struct CommandContext : ICommandContext /// /// /// - /// - public CommandContext (Command command, TBindingType? binding, object? data = null) + public CommandContext (Command command, TBindingType? binding) { Command = command; Binding = binding; - Data = data; } + /// + public Command Command { get; set; } + /// /// The keyboard or mouse minding that was used to invoke the , if any. /// public TBindingType? Binding { get; set; } - - /// - public Command Command { get; set; } - - /// - public object? Data { get; set; } } \ No newline at end of file diff --git a/Terminal.Gui/Input/CommandEventArgs.cs b/Terminal.Gui/Input/CommandEventArgs.cs index 9782c7dfd6..70e2751d71 100644 --- a/Terminal.Gui/Input/CommandEventArgs.cs +++ b/Terminal.Gui/Input/CommandEventArgs.cs @@ -9,7 +9,10 @@ namespace Terminal.Gui; public class CommandEventArgs : CancelEventArgs { /// - /// The context for the command. + /// The context for the command, if any. /// - public required ICommandContext Context { get; init; } + /// + /// If the command was invoked without context. + /// + public required ICommandContext? Context { get; init; } } diff --git a/Terminal.Gui/Input/ICommandContext.cs b/Terminal.Gui/Input/ICommandContext.cs index 77124922aa..1aa5c8660f 100644 --- a/Terminal.Gui/Input/ICommandContext.cs +++ b/Terminal.Gui/Input/ICommandContext.cs @@ -11,9 +11,9 @@ public interface ICommandContext /// public Command Command { get; set; } - // TODO: Remove this property. With CommandContext being a generic type, there should be no need for arbitrary data. - /// - /// Arbitrary data. - /// - public object? Data { get; set; } + //// TODO: Remove this property. With CommandContext being a generic type, there should be no need for arbitrary data. + ///// + ///// Arbitrary data. + ///// + //public object? Data { get; set; } } diff --git a/Terminal.Gui/Input/Keyboard/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs index e420698e3d..a67aabb504 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -21,20 +21,20 @@ public KeyBinding (Command [] commands, KeyBindingScope scope, object? context = { Commands = commands; Scope = scope; - Context = context; + Data = context; } /// Initializes a new instance. /// The commands this key binding will invoke. /// The scope of the . /// The view the key binding is bound to. - /// Arbitrary context that can be associated with this key binding. - public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, object? context = null) + /// Arbitrary data that can be associated with this key binding. + public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, object? data = null) { Commands = commands; Scope = scope; BoundView = boundView; - Context = context; + Data = data; } /// The commands this key binding will invoke. @@ -46,7 +46,7 @@ public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, /// /// The Key that is bound to the . /// - public Key Key { get; set; } + public Key? Key { get; set; } /// The view the key binding is bound to. public View? BoundView { get; set; } @@ -54,5 +54,5 @@ public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, /// /// Arbitrary context that can be associated with this key binding. /// - public object? Context { get; set; } + public object? Data { get; set; } } diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index 1c989862a1..e0cebe7cf7 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -431,6 +431,7 @@ public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding) { if (scope.HasFlag (binding.Scope)) { + binding.Key = key; return true; } } diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index 3ceee4aaf0..93af18c251 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; /// -/// Provides a collection of objects bound to a combination of . +/// Provides a collection of objects bound to a combination of . /// /// /// @@ -15,15 +15,13 @@ public class MouseBindings public MouseBindings () { } /// Adds a to the collection. - /// + /// /// - public void Add (MouseFlags mouseFlag, MouseBinding binding) + public void Add (MouseEventArgs mouseEvent, MouseBinding binding) { - if (TryGet (mouseFlag, out MouseBinding _)) + if (TryGet (mouseEvent, out MouseBinding _)) { - throw new InvalidOperationException (@$"A binding for {mouseFlag} exists ({binding})."); - - //Bindings [key] = binding; + throw new InvalidOperationException (@$"A binding for {mouseEvent} exists ({binding})."); } @@ -31,7 +29,7 @@ public void Add (MouseFlags mouseFlag, MouseBinding binding) // 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 (mouseFlag, binding); + Bindings.Add (mouseEvent, binding); } /// @@ -45,15 +43,15 @@ public void Add (MouseFlags mouseFlag, MouseBinding binding) /// 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 mouse flags to check. + /// The mouse flags to check. /// - /// The command to invoked on the when is received. When - /// multiple commands are provided,they will be applied in sequence. The bound event will be + /// The command to invoked on the when is received. When + /// multiple commands are provided,they will be applied in sequence. The bound event will be /// consumed if any took effect. /// - public void Add (MouseFlags mouseFlags, params Command [] commands) + public void Add (MouseEventArgs mouseEvents, params Command [] commands) { - if (mouseFlags == MouseFlags.None) + if (mouseEvents.Flags == MouseFlags.None) { throw new ArgumentException (@"Invalid MouseFlag", nameof (commands)); } @@ -63,24 +61,24 @@ public void Add (MouseFlags mouseFlags, params Command [] commands) throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } - if (TryGet (mouseFlags, out MouseBinding binding)) + if (TryGet (mouseEvents, out MouseBinding binding)) { - throw new InvalidOperationException (@$"A binding for {mouseFlags} exists ({binding})."); + throw new InvalidOperationException (@$"A binding for {mouseEvents} exists ({binding})."); } - Add (mouseFlags, new MouseBinding (commands)); + Add (mouseEvents, new MouseBinding (commands)); } // TODO: Add a dictionary comparer that ignores Scope // TODO: This should not be public! /// The collection of objects. - public Dictionary Bindings { get; } = new (); + public Dictionary Bindings { get; } = new (); /// - /// Gets the that are bound. + /// Gets the that are bound. /// /// - public IEnumerable GetBoundMouseFlags () + public IEnumerable GetBoundMouseEventArgs () { return Bindings.Keys; } @@ -95,38 +93,38 @@ public IEnumerable GetBoundMouseFlags () /// public void Clear (params Command [] command) { - KeyValuePair [] kvps = Bindings + KeyValuePair [] kvps = Bindings .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) .ToArray (); - foreach (KeyValuePair kvp in kvps) + foreach (KeyValuePair kvp in kvps) { Remove (kvp.Key); } } - /// Gets the for the specified combination of . - /// + /// Gets the for the specified combination of . + /// /// - public MouseBinding Get (MouseFlags mouseFlags) + public MouseBinding Get (MouseEventArgs mouseEvents) { - if (TryGet (mouseFlags, out MouseBinding binding)) + if (TryGet (mouseEvents, out MouseBinding binding)) { return binding; } - throw new InvalidOperationException ($"{mouseFlags} is not bound."); + throw new InvalidOperationException ($"{mouseEvents} is not bound."); } - /// Gets the array of s bound to if it exists. - /// The key to check. + /// Gets the array of s bound to if it exists. + /// The key to check. /// - /// The array of s if is bound. An empty array + /// The array of s if is bound. An empty array /// if not. /// - public Command [] GetCommands (MouseFlags mouseFlags) + public Command [] GetCommands (MouseEventArgs mouseEvents) { - if (TryGet (mouseFlags, out MouseBinding bindings)) + if (TryGet (mouseEvents, out MouseBinding bindings)) { return bindings.Commands; } @@ -134,83 +132,83 @@ public Command [] GetCommands (MouseFlags mouseFlags) return []; } - /// Gets the first combination of bound to the set of commands specified by . + /// Gets the first combination of bound to the set of commands specified by . /// The set of commands to search. - /// The first combination of bound to the set of commands specified by . if the set of caommands was not found. - public MouseFlags? GetMouseFlagsFromCommands (params Command [] commands) + /// The first combination of bound to the set of commands specified by . if the set of caommands was not found. + public MouseEventArgs? GetMouseEventArgsFromCommands (params Command [] commands) { return Bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } - /// Gets combination of bound to the set of commands specified by . + /// Gets combination of bound to the set of commands specified by . /// The set of commands to search. - /// The combination of bound to the set of commands specified by . An empty list if the set of caommands was not found. - public IEnumerable GetAllMouseFlagsFromCommands (params Command [] commands) + /// The combination of bound to the set of commands specified by . An empty list if the set of caommands was not found. + public IEnumerable GetAllMouseEventArgsFromCommands (params Command [] commands) { return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); } /// Removes a from the collection. - /// - public void Remove (MouseFlags mouseFlags) + /// + public void Remove (MouseEventArgs mouseEvents) { - if (!TryGet (mouseFlags, out MouseBinding _)) + if (!TryGet (mouseEvents, out MouseBinding _)) { return; } - Bindings.Remove (mouseFlags); + Bindings.Remove (mouseEvents); } - /// Replaces the commands already bound to a combination of . + /// Replaces the commands already bound to a combination of . /// /// - /// If the combination of is not already bound, it will be added. + /// If the combination of is not already bound, it will be added. /// /// - /// The combination of bound to the command to be replaced. + /// The combination of bound to the command to be replaced. /// The set of commands to replace the old ones with. - public void ReplaceCommands (MouseFlags mouseFlags, params Command [] commands) + public void ReplaceCommands (MouseEventArgs mouseEvents, params Command [] commands) { - if (TryGet (mouseFlags, out MouseBinding binding)) + if (TryGet (mouseEvents, out MouseBinding binding)) { binding.Commands = commands; } else { - Add (mouseFlags, commands); + Add (mouseEvents, commands); } } - /// Replaces a combination already bound to a set of s. + /// Replaces a combination already bound to a set of s. /// - /// The to be replaced. - /// The new to be used. If no action will be taken. - public void ReplaceKey (MouseFlags oldMouseFlags, MouseFlags newMouseFlags) + /// The to be replaced. + /// The new to be used. If no action will be taken. + public void ReplaceKey (MouseEventArgs oldMouseEventArgs, MouseEventArgs newMouseEventArgs) { - if (!TryGet (oldMouseFlags, out MouseBinding _)) + if (!TryGet (oldMouseEventArgs, out MouseBinding _)) { - throw new InvalidOperationException ($"Key {oldMouseFlags} is not bound."); + throw new InvalidOperationException ($"Key {oldMouseEventArgs} is not bound."); } - MouseBinding value = Bindings [oldMouseFlags]; - Remove (oldMouseFlags); - Add (newMouseFlags, value); + MouseBinding value = Bindings [oldMouseEventArgs]; + Remove (oldMouseEventArgs); + Add (newMouseEventArgs, value); } - /// Gets the commands bound with the specified . + /// Gets the commands bound with the specified . /// - /// The key to check. + /// The key to check. /// /// When this method returns, contains the commands bound with the specified mouse flags, if the mouse flags are /// found; otherwise, null. This parameter is passed uninitialized. /// /// if the mouse flags are bound; otherwise . - public bool TryGet (MouseFlags mouseFlags, out MouseBinding binding) + public bool TryGet (MouseEventArgs mouseEvents, out MouseBinding binding) { binding = new ([]); - return Bindings.TryGetValue (mouseFlags, out binding); + return Bindings.TryGetValue (mouseEvents, out binding); } } diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index 06431eb731..c3a921bd65 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -30,7 +30,7 @@ private void SetupCommands () }); // Space or single-click - Raise Selecting - AddCommand (Command.Select, (ctx) => + AddCommand (Command.Select, ctx => { if (RaiseSelecting (ctx) is true) { @@ -54,7 +54,7 @@ private void SetupCommands () /// /// /// - /// The event should raised after the state of the View has changed (after is raised). + /// The event should be raised after the state of the View has changed (after is raised). /// /// /// If the Accepting event is not handled, will be invoked on the SuperView, enabling default Accept behavior. @@ -65,11 +65,11 @@ private void SetupCommands () /// /// /// - /// if no event was raised; input proessing should continue. - /// if the event was raised and was not handled (or cancelled); input proessing should continue. - /// if the event was raised and handled (or cancelled); input proessing should stop. + /// if no event was raised; input processing should continue. + /// if the event was raised and was not handled (or cancelled); input processing should continue. + /// if the event was raised and handled (or cancelled); input processing should stop. /// - protected bool? RaiseAccepting (ICommandContext ctx) + protected bool? RaiseAccepting (ICommandContext? ctx) { CommandEventArgs args = new () { Context = ctx }; @@ -135,14 +135,14 @@ private void SetupCommands () /// event. The default handler calls this method. /// /// - /// The event should raised after the state of the View has been changed and before see . + /// The event should be raised after the state of the View has been changed and before see . /// /// - /// if no event was raised; input proessing should continue. - /// if the event was raised and was not handled (or cancelled); input proessing should continue. - /// if the event was raised and handled (or cancelled); input proessing should stop. + /// if no event was raised; input processing should continue. + /// if the event was raised and was not handled (or cancelled); input processing should continue. + /// if the event was raised and handled (or cancelled); input processing should stop. /// - protected bool? RaiseSelecting (ICommandContext ctx) + protected bool? RaiseSelecting (ICommandContext? ctx) { CommandEventArgs args = new () { Context = ctx }; @@ -179,9 +179,9 @@ private void SetupCommands () /// event. The default handler calls this method. /// /// - /// if no event was raised; input proessing should continue. - /// if the event was raised and was not handled (or cancelled); input proessing should continue. - /// if the event was raised and handled (or cancelled); input proessing should stop. + /// if no event was raised; input processing should continue. + /// if the event was raised and was not handled (or cancelled); input processing should continue. + /// if the event was raised and handled (or cancelled); input processing should stop. /// protected bool? RaiseHandlingHotKey () { @@ -219,11 +219,11 @@ private void SetupCommands () /// /// Function signature commands. /// - /// Provides information about the circumstances of invoking the command (e.g. ) + /// Provides context about the circumstances of invoking the command. /// - /// if no command was found; input proessing should continue. - /// if the command was invoked and was not handled (or cancelled); input proessing should continue. - /// if the command was invoked the command was handled (or cancelled); input proessing should stop. + /// if no event was raised; input processing should continue. + /// if the event was raised and was not handled (or cancelled); input processing should continue. + /// if the event was raised and handled (or cancelled); input processing should stop. /// public delegate bool? CommandImplementation (ICommandContext? ctx); @@ -239,7 +239,7 @@ private void SetupCommands () /// /// /// - /// This version of AddCommand is for commands that require . + /// This version of AddCommand is for commands that require . /// /// /// The command. @@ -258,7 +258,7 @@ private void SetupCommands () /// /// /// - /// This version of AddCommand is for commands that do not require a . + /// This version of AddCommand is for commands that do not require context. /// If the command requires context, use /// /// @@ -275,12 +275,11 @@ private void SetupCommands () /// Invokes the specified commands. /// /// The set of commands to invoke. - /// The key that caused the command to be invoked, if any. This will be passed as context with the command. - /// The key binding that was bound to the key and caused the invocation, if any. This will be passed as context with the command. + /// The binding that caused the invocation, if any. This will be passed as context with the command. /// - /// if no command was found; input proessing should continue. - /// if at least one command was invoked and was not handled (or cancelled); input proessing should continue. - /// if at least one command was invoked the command was handled (or cancelled); input proessing should stop. + /// if no command was found; input processing should continue. + /// if the command was invoked and was not handled (or cancelled); input processing should continue. + /// if the command was invoked the command was handled (or cancelled); input processing should stop. /// public bool? InvokeCommands (Command [] commands, TBindingType binding) { @@ -315,30 +314,34 @@ private void SetupCommands () /// Invokes the specified command. /// /// The command to invoke. - /// + /// The binding that caused the invocation, if any. This will be passed as context with the command. /// - /// if no command was found; input proessing should continue. - /// if the command was invoked and was not handled (or cancelled); input proessing should continue. - /// if the command was invoked the command was handled (or cancelled); input proessing should stop. + /// if no command was found; input processing should continue. + /// if the command was invoked and was not handled (or cancelled); input processing should continue. + /// if the command was invoked the command was handled (or cancelled); input processing should stop. /// public bool? InvokeCommand (Command command, TBindingType binding) { if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { - return implementation (binding as ICommandContext); + return implementation (new CommandContext () + { + Command = command, + Binding = binding, + }); } return null; } /// - /// Invokes the specified command. + /// Invokes the specified command without context. /// /// The command to invoke. /// - /// if no command was found; input proessing should continue. - /// if the command was invoked and was not handled (or cancelled); input proessing should continue. - /// if the command was invoked the command was handled (or cancelled); input proessing should stop. + /// if no command was found; input processing should continue. + /// if the command was invoked and was not handled (or cancelled); input processing should continue. + /// if the command was invoked the command was handled (or cancelled); input processing should stop. /// public bool? InvokeCommand (Command command) { diff --git a/Terminal.Gui/Views/ColorPicker.16.cs b/Terminal.Gui/Views/ColorPicker.16.cs index ea1a95f68f..f699f5686a 100644 --- a/Terminal.Gui/Views/ColorPicker.16.cs +++ b/Terminal.Gui/Views/ColorPicker.16.cs @@ -205,9 +205,10 @@ private void AddCommands () AddCommand (Command.Select, (ctx) => { bool set = false; - if (ctx.Data is MouseEventArgs me) + + if (ctx is CommandContext { Binding: { } } mouseCommandContext) { - Cursor = new (me.Position.X / _boxWidth, me.Position.Y / _boxHeight); + Cursor = new (mouseCommandContext.Binding.Position.X / _boxWidth, mouseCommandContext.Binding.Position.Y / _boxHeight); set = true; } return RaiseAccepting (ctx) == true || set; diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 4fcdc315a2..2e3763a9b6 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -80,7 +80,11 @@ public ComboBox () // Things this view knows how to do AddCommand (Command.Accept, (ctx) => { - if (ctx.Data == _search) + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + if (keyCommandContext.Binding.Data == _search) { return null; } diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 89ad3c6608..6cc15b3385 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -143,8 +143,12 @@ public ListView () AddCommand (Command.SelectAll, (ctx) => { - // BUGBUG: This probably isn't right - return MarkAll ((bool)ctx!.Data); + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + return keyCommandContext.Binding.Data is { } && MarkAll ((bool)keyCommandContext.Binding.Data); }); // Default keybindings for all ListViews diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 6e620c91d3..9c7072538a 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -1,7 +1,5 @@ #nullable enable -using static System.Formats.Asn1.AsnWriter; - namespace Terminal.Gui; /// @@ -10,76 +8,115 @@ namespace Terminal.Gui; /// internal sealed class Menu : View { - private readonly MenuBarItem? _barItems; - private readonly MenuBar _host; - internal int _currentChild; - internal View? _previousSubFocused; - - internal static Rectangle MakeFrame (int x, int y, MenuItem? []? items, Menu? parent = null) + public Menu () { - if (items is null || items.Length == 0) + if (Application.Top is { }) { - return Rectangle.Empty; + Application.Top.DrawComplete += Top_DrawComplete; + Application.Top.SizeChanging += Current_TerminalResized; } - int minX = x; - int minY = y; - const int borderOffset = 2; // This 2 is for the space around - int maxW = (items.Max (z => z?.Width) ?? 0) + borderOffset; - int maxH = items.Length + borderOffset; + Application.MouseEvent += Application_RootMouseEvent; - if (parent is { } && x + maxW > Driver.Cols) - { - minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0); - } + // Things this view knows how to do + AddCommand (Command.Up, () => MoveUp ()); + AddCommand (Command.Down, () => MoveDown ()); - if (y + maxH > Driver.Rows) - { - minY = Math.Max (Driver.Rows - maxH, 0); - } + AddCommand ( + Command.Left, + () => + { + _host!.PreviousMenu (true); - return new (minX, minY, maxW, maxH); - } + return true; + } + ); - internal required MenuBar Host - { - get => _host; - init - { - ArgumentNullException.ThrowIfNull (value); - _host = value; - } - } + AddCommand ( + Command.Cancel, + () => + { + CloseAllMenus (); - internal required MenuBarItem? BarItems - { - get => _barItems!; - init - { - ArgumentNullException.ThrowIfNull (value); - _barItems = value; + return true; + } + ); - // Debugging aid so ToString() is helpful - Text = _barItems.Title; - } + AddCommand ( + Command.Accept, + () => + { + RunSelected (); + + return true; + } + ); + + AddCommand ( + Command.Select, + ctx => + { + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + return _host?.SelectItem ((keyCommandContext.Binding.Data as MenuItem)!); + }); + + AddCommand ( + Command.Toggle, + ctx => + { + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + return ExpandCollapse ((keyCommandContext.Binding.Data as MenuItem)!); + }); + + AddCommand ( + Command.HotKey, + ctx => + { + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + return _host?.SelectItem ((keyCommandContext.Binding.Data as MenuItem)!); + }); + + // Default key bindings for this view + KeyBindings.Add (Key.CursorUp, Command.Up); + KeyBindings.Add (Key.CursorDown, Command.Down); + KeyBindings.Add (Key.CursorLeft, Command.Left); + KeyBindings.Add (Key.CursorRight, Command.Right); + KeyBindings.Add (Key.Esc, Command.Cancel); } - internal Menu? Parent { get; init; } + internal int _currentChild; + internal View? _previousSubFocused; + private readonly MenuBarItem? _barItems; + private readonly MenuBar _host; public override void BeginInit () { base.BeginInit (); - var frame = MakeFrame (Frame.X, Frame.Y, _barItems!.Children!, Parent); + Rectangle frame = MakeFrame (Frame.X, Frame.Y, _barItems!.Children!, Parent); if (Frame.X != frame.X) { X = frame.X; } + if (Frame.Y != frame.Y) { Y = frame.Y; } + Width = frame.Width; Height = frame.Height; @@ -94,6 +131,7 @@ public override void BeginInit () if (menuItem.ShortcutKey != Key.Empty) { KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem); + // Remove an existent ShortcutKey menuItem._menuBar.KeyBindings.Remove (menuItem.ShortcutKey!); menuItem._menuBar.KeyBindings.Add (menuItem.ShortcutKey!, keyBinding); @@ -155,196 +193,301 @@ _barItems.Children [_currentChild]! AddKeyBindingsHotKey (_barItems); } - public Menu () + public override Point? PositionCursor () { - if (Application.Top is { }) + if (_host.IsMenuOpen) { - Application.Top.DrawComplete += Top_DrawComplete; - Application.Top.SizeChanging += Current_TerminalResized; - } - - Application.MouseEvent += Application_RootMouseEvent; - - // Things this view knows how to do - AddCommand (Command.Up, () => MoveUp ()); - AddCommand (Command.Down, () => MoveDown ()); - - AddCommand ( - Command.Left, - () => - { - _host!.PreviousMenu (true); - - return true; - } - ); - - AddCommand ( - Command.Cancel, - () => - { - CloseAllMenus (); - - return true; - } - ); + if (_barItems!.IsTopLevel) + { + return _host.PositionCursor (); + } - AddCommand ( - Command.Accept, - () => - { - RunSelected (); + Move (2, 1 + _currentChild); - return true; - } - ); - AddCommand (Command.Select, ctx => _host?.SelectItem ((ctx.Data as MenuItem)!)); - AddCommand (Command.Toggle, ctx => ExpandCollapse ((ctx.Data as MenuItem)!)); - AddCommand (Command.HotKey, ctx => _host?.SelectItem ((ctx.Data as MenuItem)!)); + return null; // Don't show the cursor + } - // Default key bindings for this view - KeyBindings.Add (Key.CursorUp, Command.Up); - KeyBindings.Add (Key.CursorDown, Command.Down); - KeyBindings.Add (Key.CursorLeft, Command.Left); - KeyBindings.Add (Key.CursorRight, Command.Right); - KeyBindings.Add (Key.Esc, Command.Cancel); + return _host.PositionCursor (); } - private void AddKeyBindingsHotKey (MenuBarItem? menuBarItem) + public void Run (Action? action) { - if (menuBarItem is null || menuBarItem.Children is null) + if (action is null) { return; } - IEnumerable menuItems = menuBarItem.Children.Where (m => m is { })!; - - foreach (MenuItem menuItem in menuItems) - { - KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, menuItem); + Application.UngrabMouse (); + _host.CloseAllMenus (); + Application.LayoutAndDraw (true); - if (menuItem.HotKey != Key.Empty) - { - KeyBindings.Remove (menuItem.HotKey!); - KeyBindings.Add (menuItem.HotKey!, keyBinding); - KeyBindings.Remove (menuItem.HotKey!.WithAlt); - KeyBindings.Add (menuItem.HotKey.WithAlt, keyBinding); - } - } + _host.Run (action); } - private void RemoveKeyBindingsHotKey (MenuBarItem? menuBarItem) + protected override void Dispose (bool disposing) { - if (menuBarItem is null || menuBarItem.Children is null) + RemoveKeyBindingsHotKey (_barItems); + + if (Application.Top is { }) { - return; + Application.Top.DrawComplete -= Top_DrawComplete; + Application.Top.SizeChanging -= Current_TerminalResized; } - IEnumerable menuItems = menuBarItem.Children.Where (m => m is { })!; + Application.MouseEvent -= Application_RootMouseEvent; + base.Dispose (disposing); + } - foreach (MenuItem menuItem in menuItems) + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) + { + if (!newHasFocus) { - if (menuItem.HotKey != Key.Empty) - { - KeyBindings.Remove (menuItem.HotKey!); - KeyBindings.Remove (menuItem.HotKey!.WithAlt); - } + _host.LostFocus (previousFocusedView!); } } - /// Called when a key bound to Command.ToggleExpandCollapse is pressed. This means a hot key was pressed. - /// - private bool ExpandCollapse (MenuItem? menuItem) + /// + protected override bool OnKeyDownNotHandled (Key keyEvent) { - if (!IsInitialized || !Visible) + // We didn't handle the key, pass it on to host + return _host.InvokeCommandsBoundToKey (keyEvent) == true; + } + + protected override bool OnMouseEvent (MouseEventArgs me) + { + if (!_host._handled && !_host.HandleGrabView (me, this)) { - return true; + return false; } + _host._handled = false; + bool disabled; - for (var c = 0; c < _barItems!.Children!.Length; c++) + if (me.Flags == MouseFlags.Button1Clicked) { - if (_barItems.Children [c] == menuItem) + disabled = false; + + if (me.Position.Y < 0) { - _currentChild = c; + return me.Handled = true; + } - break; + if (me.Position.Y >= _barItems!.Children!.Length) + { + return me.Handled = true; } - } - if (menuItem is { }) - { - var m = menuItem as MenuBarItem; + MenuItem item = _barItems.Children [me.Position.Y]!; - if (m?.Children?.Length > 0) + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (item is null || !item.IsEnabled ()) { - MenuItem? item = _barItems.Children [_currentChild]; + disabled = true; + } - if (item is null) - { - return true; - } + if (disabled) + { + return me.Handled = true; + } - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - bool disabled = item is null || !item.IsEnabled (); + _currentChild = me.Position.Y; + RunSelected (); - if (!disabled && (_host.UseSubMenusSingleFrame || !CheckSubMenu ())) - { - SetNeedsDraw (); - SetParentSetNeedsDisplay (); + return me.Handled = true; + } - return true; - } + if (me.Flags != MouseFlags.Button1Pressed + && me.Flags != MouseFlags.Button1DoubleClicked + && me.Flags != MouseFlags.Button1TripleClicked + && me.Flags != MouseFlags.ReportMousePosition + && !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) + { + return false; + } - if (!disabled) - { - _host.OnMenuOpened (); - } + { + disabled = false; + + if (me.Position.Y < 0 || me.Position.Y >= _barItems!.Children!.Length) + { + return me.Handled = true; } - else + + MenuItem item = _barItems.Children [me.Position.Y]!; + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (item is null) { - _host.SelectItem (menuItem); + return me.Handled = true; + } + + if (item.IsEnabled () != true) + { + disabled = true; + } + + if (!disabled) + { + _currentChild = me.Position.Y; + } + + if (_host.UseSubMenusSingleFrame || !CheckSubMenu ()) + { + SetNeedsDraw (); + SetParentSetNeedsDisplay (); + + return me.Handled = true; } + + _host.OnMenuOpened (); + + return me.Handled = true; } - else if (_host.IsMenuOpen) + } + + /// + protected override void OnVisibleChanged () + { + base.OnVisibleChanged (); + + if (Visible) { - _host.CloseAllMenus (); + Application.MouseEvent += Application_RootMouseEvent; } else { - _host.OpenMenu (); + Application.MouseEvent -= Application_RootMouseEvent; + } + } + + internal required MenuBarItem? BarItems + { + get => _barItems!; + init + { + ArgumentNullException.ThrowIfNull (value); + _barItems = value; + + // Debugging aid so ToString() is helpful + Text = _barItems.Title; + } + } + + internal bool CheckSubMenu () + { + if (_currentChild == -1 || _barItems?.Children? [_currentChild] is null) + { + return true; + } + + MenuBarItem? subMenu = _barItems.SubMenu (_barItems.Children [_currentChild]!); + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (subMenu is { }) + { + int pos = -1; + + if (_host._openSubMenu is { }) + { + pos = _host._openSubMenu.FindIndex (o => o._barItems == subMenu); + } + + if (pos == -1 + && this != _host.OpenCurrentMenu + && subMenu.Children != _host.OpenCurrentMenu!._barItems!.Children + && !_host.CloseMenu (false, true)) + { + return false; + } + + _host.Activate (_host._selected, pos, subMenu); + } + else if (_host._openSubMenu?.Count == 0 || _host._openSubMenu?.Last ()._barItems!.IsSubMenuOf (_barItems.Children [_currentChild]!) == false) + { + return _host.CloseMenu (false, true); + } + else + { + SetNeedsDraw (); + SetParentSetNeedsDisplay (); } return true; } - /// - protected override bool OnKeyDownNotHandled (Key keyEvent) + internal Attribute DetermineColorSchemeFor (MenuItem? item, int index) { - // We didn't handle the key, pass it on to host - return _host.InvokeCommandsBoundToKey (keyEvent) == true; + if (item is null) + { + return GetNormalColor (); + } + + if (index == _currentChild) + { + return GetFocusColor (); + } + + return !item.IsEnabled () ? ColorScheme!.Disabled : GetNormalColor (); } - private void Current_TerminalResized (object? sender, SizeChangedEventArgs e) + internal required MenuBar Host { - if (_host.IsMenuOpen) + get => _host; + init { - _host.CloseAllMenus (); + ArgumentNullException.ThrowIfNull (value); + _host = value; } } - /// - protected override void OnVisibleChanged () + internal static Rectangle MakeFrame (int x, int y, MenuItem? []? items, Menu? parent = null) { - base.OnVisibleChanged (); + if (items is null || items.Length == 0) + { + return Rectangle.Empty; + } - if (Visible) + int minX = x; + int minY = y; + const int borderOffset = 2; // This 2 is for the space around + int maxW = (items.Max (z => z?.Width) ?? 0) + borderOffset; + int maxH = items.Length + borderOffset; + + if (parent is { } && x + maxW > Driver.Cols) { - Application.MouseEvent += Application_RootMouseEvent; + minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0); } - else + + if (y + maxH > Driver.Rows) { - Application.MouseEvent -= Application_RootMouseEvent; + minY = Math.Max (Driver.Rows - maxH, 0); + } + + return new (minX, minY, maxW, maxH); + } + + internal Menu? Parent { get; init; } + + private void AddKeyBindingsHotKey (MenuBarItem? menuBarItem) + { + if (menuBarItem is null || menuBarItem.Children is null) + { + return; + } + + IEnumerable menuItems = menuBarItem.Children.Where (m => m is { })!; + + foreach (MenuItem menuItem in menuItems) + { + KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, menuItem); + + if (menuItem.HotKey != Key.Empty) + { + KeyBindings.Remove (menuItem.HotKey!); + KeyBindings.Add (menuItem.HotKey!, keyBinding); + KeyBindings.Remove (menuItem.HotKey!.WithAlt); + KeyBindings.Add (menuItem.HotKey.WithAlt, keyBinding); + } } } @@ -378,279 +521,83 @@ private void Application_RootMouseEvent (object? sender, MouseEventArgs a) } } - internal Attribute DetermineColorSchemeFor (MenuItem? item, int index) + private void CloseAllMenus () { - if (item is null) - { - return GetNormalColor (); - } + Application.UngrabMouse (); + _host.CloseAllMenus (); + } - if (index == _currentChild) + private void Current_TerminalResized (object? sender, SizeChangedEventArgs e) + { + if (_host.IsMenuOpen) { - return GetFocusColor (); + _host.CloseAllMenus (); } - - return !item.IsEnabled () ? ColorScheme!.Disabled : GetNormalColor (); } - // By doing this we draw last, over everything else. - private void Top_DrawComplete (object? sender, DrawEventArgs e) + /// Called when a key bound to Command.ToggleExpandCollapse is pressed. This means a hot key was pressed. + /// + private bool ExpandCollapse (MenuItem? menuItem) { - if (!Visible) + if (!IsInitialized || !Visible) { - return; + return true; } - if (_barItems!.Children is null) + for (var c = 0; c < _barItems!.Children!.Length; c++) { - return; - } - - DrawBorderAndPadding (); - RenderLineCanvas (); - - // BUGBUG: Views should not change the clip. Doing so is an indcation of poor design or a bug in the framework. - Region? savedClip = View.SetClipToScreen (); + if (_barItems.Children [c] == menuItem) + { + _currentChild = c; - SetAttribute (GetNormalColor ()); + break; + } + } - for (int i = Viewport.Y; i < _barItems!.Children.Length; i++) + if (menuItem is { }) { - if (i < 0) - { - continue; - } + var m = menuItem as MenuBarItem; - if (ViewportToScreen (Viewport).Y + i >= Driver.Rows) + if (m?.Children?.Length > 0) { - break; - } + MenuItem? item = _barItems.Children [_currentChild]; - MenuItem? item = _barItems.Children [i]; + if (item is null) + { + return true; + } - SetAttribute ( - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - item is null ? GetNormalColor () : - i == _currentChild ? GetFocusColor () : GetNormalColor () - ); + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + bool disabled = item is null || !item.IsEnabled (); - if (item is null && BorderStyle != LineStyle.None) - { - Point s = ViewportToScreen (new Point (-1, i)); - Driver.Move (s.X, s.Y); - Driver.AddRune (Glyphs.LeftTee); - } - else if (Frame.X < Driver.Cols) - { - Move (0, i); - } - - SetAttribute (DetermineColorSchemeFor (item, i)); - - for (int p = Viewport.X; p < Frame.Width - 2; p++) - { - // This - 2 is for the border - if (p < 0) - { - continue; - } - - if (ViewportToScreen (Viewport).X + p >= Driver.Cols) - { - break; - } - - if (item is null) - { - Driver.AddRune (Glyphs.HLine); - } - else if (i == 0 && p == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { }) + if (!disabled && (_host.UseSubMenusSingleFrame || !CheckSubMenu ())) { - Driver.AddRune (Glyphs.LeftArrow); - } + SetNeedsDraw (); + SetParentSetNeedsDisplay (); - // This `- 3` is left border + right border + one row in from right - else if (p == Frame.Width - 3 && _barItems?.SubMenu (_barItems.Children [i]!) is { }) - { - Driver.AddRune (Glyphs.RightArrow); - } - else - { - Driver.AddRune ((Rune)' '); + return true; } - } - if (item is null) - { - if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width) + if (!disabled) { - Point s = ViewportToScreen (new Point (Frame.Width - 2, i)); - Driver.Move (s.X, s.Y); - Driver.AddRune (Glyphs.RightTee); + _host.OnMenuOpened (); } - - continue; - } - - string? textToDraw; - Rune nullCheckedChar = Glyphs.CheckStateNone; - Rune checkChar = Glyphs.Selected; - Rune uncheckedChar = Glyphs.UnSelected; - - if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) - { - checkChar = Glyphs.CheckStateChecked; - uncheckedChar = Glyphs.CheckStateUnChecked; - } - - // Support Checked even though CheckType wasn't set - if (item is { CheckType: MenuItemCheckStyle.Checked, Checked: null }) - { - textToDraw = $"{nullCheckedChar} {item.Title}"; - } - else if (item.Checked == true) - { - textToDraw = $"{checkChar} {item.Title}"; - } - else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) - { - textToDraw = $"{uncheckedChar} {item.Title}"; } else { - textToDraw = item.Title; - } - - Point screen = ViewportToScreen (new Point (0, i)); - - if (screen.X < Driver.Cols) - { - Driver.Move (screen.X + 1, screen.Y); - - if (!item.IsEnabled ()) - { - DrawHotString (textToDraw, ColorScheme!.Disabled, ColorScheme.Disabled); - } - else if (i == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { }) - { - var tf = new TextFormatter - { - ConstrainToWidth = Frame.Width - 3, - ConstrainToHeight = 1, - Alignment = Alignment.Center, HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw - }; - - // The -3 is left/right border + one space (not sure what for) - tf.Draw ( - ViewportToScreen (new Rectangle (1, i, Frame.Width - 3, 1)), - i == _currentChild ? GetFocusColor () : GetNormalColor (), - i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal, - SuperView?.ViewportToScreen (SuperView.Viewport) ?? Rectangle.Empty - ); - } - else - { - DrawHotString ( - textToDraw, - i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal, - i == _currentChild ? GetFocusColor () : GetNormalColor () - ); - } - - // The help string - int l = item.ShortcutTag.GetColumns () == 0 - ? item.Help.GetColumns () - : item.Help.GetColumns () + item.ShortcutTag.GetColumns () + 2; - int col = Frame.Width - l - 3; - screen = ViewportToScreen (new Point (col, i)); - - if (screen.X < Driver.Cols) - { - Driver.Move (screen.X, screen.Y); - Driver.AddStr (item.Help); - - // The shortcut tag string - if (!string.IsNullOrEmpty (item.ShortcutTag)) - { - Driver.Move (screen.X + l - item.ShortcutTag.GetColumns (), screen.Y); - Driver.AddStr (item.ShortcutTag); - } - } - } - } - - View.SetClip (savedClip); - } - - public override Point? PositionCursor () - { - if (_host.IsMenuOpen) - { - if (_barItems!.IsTopLevel) - { - return _host.PositionCursor (); + _host.SelectItem (menuItem); } - - Move (2, 1 + _currentChild); - - return null; // Don't show the cursor - } - - return _host.PositionCursor (); - } - - public void Run (Action? action) - { - if (action is null) - { - return; } - - Application.UngrabMouse (); - _host.CloseAllMenus (); - Application.LayoutAndDraw (true); - - _host.Run (action); - } - - protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) - { - if (!newHasFocus) - { - _host.LostFocus (previousFocusedView!); - } - } - - private void RunSelected () - { - if (_barItems!.IsTopLevel) + else if (_host.IsMenuOpen) { - Run (_barItems.Action); + _host.CloseAllMenus (); } else { - switch (_currentChild) - { - case > -1 when _barItems.Children! [_currentChild]!.Action != null!: - Run (_barItems.Children [_currentChild]?.Action); - - break; - case 0 when _host.UseSubMenusSingleFrame && _barItems.Children [_currentChild]?.Parent!.Parent != null: - _host.PreviousMenu (_barItems.Children [_currentChild]!.Parent!.IsFromSubMenu, true); - - break; - case > -1 when _barItems.SubMenu (_barItems.Children [_currentChild]) != null!: - CheckSubMenu (); - - break; - } + _host.OpenMenu (); } - } - private void CloseAllMenus () - { - Application.UngrabMouse (); - _host.CloseAllMenus (); + return true; } private bool MoveDown () @@ -801,6 +748,51 @@ private bool MoveUp () return true; } + private void RemoveKeyBindingsHotKey (MenuBarItem? menuBarItem) + { + if (menuBarItem is null || menuBarItem.Children is null) + { + return; + } + + IEnumerable menuItems = menuBarItem.Children.Where (m => m is { })!; + + foreach (MenuItem menuItem in menuItems) + { + if (menuItem.HotKey != Key.Empty) + { + KeyBindings.Remove (menuItem.HotKey!); + KeyBindings.Remove (menuItem.HotKey!.WithAlt); + } + } + } + + private void RunSelected () + { + if (_barItems!.IsTopLevel) + { + Run (_barItems.Action); + } + else + { + switch (_currentChild) + { + case > -1 when _barItems.Children! [_currentChild]!.Action != null!: + Run (_barItems.Children [_currentChild]?.Action); + + break; + case 0 when _host.UseSubMenusSingleFrame && _barItems.Children [_currentChild]?.Parent!.Parent != null: + _host.PreviousMenu (_barItems.Children [_currentChild]!.Parent!.IsFromSubMenu, true); + + break; + case > -1 when _barItems.SubMenu (_barItems.Children [_currentChild]) != null!: + CheckSubMenu (); + + break; + } + } + } + private void SetParentSetNeedsDisplay () { if (_host._openSubMenu is { }) @@ -815,151 +807,193 @@ private void SetParentSetNeedsDisplay () _host.SetNeedsDraw (); } - protected override bool OnMouseEvent (MouseEventArgs me) + // By doing this we draw last, over everything else. + private void Top_DrawComplete (object? sender, DrawEventArgs e) { - if (!_host._handled && !_host.HandleGrabView (me, this)) + if (!Visible) { - return false; + return; } - _host._handled = false; - bool disabled; - - if (me.Flags == MouseFlags.Button1Clicked) + if (_barItems!.Children is null) { - disabled = false; + return; + } - if (me.Position.Y < 0) + DrawBorderAndPadding (); + RenderLineCanvas (); + + // BUGBUG: Views should not change the clip. Doing so is an indcation of poor design or a bug in the framework. + Region? savedClip = SetClipToScreen (); + + SetAttribute (GetNormalColor ()); + + for (int i = Viewport.Y; i < _barItems!.Children.Length; i++) + { + if (i < 0) { - return me.Handled = true; + continue; } - if (me.Position.Y >= _barItems!.Children!.Length) + if (ViewportToScreen (Viewport).Y + i >= Driver.Rows) { - return me.Handled = true; + break; } - MenuItem item = _barItems.Children [me.Position.Y]!; + MenuItem? item = _barItems.Children [i]; - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (item is null || !item.IsEnabled ()) + SetAttribute ( + + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + item is null ? GetNormalColor () : + i == _currentChild ? GetFocusColor () : GetNormalColor () + ); + + if (item is null && BorderStyle != LineStyle.None) { - disabled = true; + Point s = ViewportToScreen (new Point (-1, i)); + Driver.Move (s.X, s.Y); + Driver.AddRune (Glyphs.LeftTee); } - - if (disabled) + else if (Frame.X < Driver.Cols) { - return me.Handled = true; + Move (0, i); } - _currentChild = me.Position.Y; - RunSelected (); + SetAttribute (DetermineColorSchemeFor (item, i)); - return me.Handled = true; - } + for (int p = Viewport.X; p < Frame.Width - 2; p++) + { + // This - 2 is for the border + if (p < 0) + { + continue; + } - if (me.Flags != MouseFlags.Button1Pressed - && me.Flags != MouseFlags.Button1DoubleClicked - && me.Flags != MouseFlags.Button1TripleClicked - && me.Flags != MouseFlags.ReportMousePosition - && !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) - { - return false; - } + if (ViewportToScreen (Viewport).X + p >= Driver.Cols) + { + break; + } - { - disabled = false; + if (item is null) + { + Driver.AddRune (Glyphs.HLine); + } + else if (i == 0 && p == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { }) + { + Driver.AddRune (Glyphs.LeftArrow); + } - if (me.Position.Y < 0 || me.Position.Y >= _barItems!.Children!.Length) - { - return me.Handled = true; + // This `- 3` is left border + right border + one row in from right + else if (p == Frame.Width - 3 && _barItems?.SubMenu (_barItems.Children [i]!) is { }) + { + Driver.AddRune (Glyphs.RightArrow); + } + else + { + Driver.AddRune ((Rune)' '); + } } - MenuItem item = _barItems.Children [me.Position.Y]!; - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (item is null) { - return me.Handled = true; + if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width) + { + Point s = ViewportToScreen (new Point (Frame.Width - 2, i)); + Driver.Move (s.X, s.Y); + Driver.AddRune (Glyphs.RightTee); + } + + continue; } - if (item.IsEnabled () != true) + string? textToDraw; + Rune nullCheckedChar = Glyphs.CheckStateNone; + Rune checkChar = Glyphs.Selected; + Rune uncheckedChar = Glyphs.UnSelected; + + if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) { - disabled = true; + checkChar = Glyphs.CheckStateChecked; + uncheckedChar = Glyphs.CheckStateUnChecked; } - if (!disabled) + // Support Checked even though CheckType wasn't set + if (item is { CheckType: MenuItemCheckStyle.Checked, Checked: null }) { - _currentChild = me.Position.Y; + textToDraw = $"{nullCheckedChar} {item.Title}"; } - - if (_host.UseSubMenusSingleFrame || !CheckSubMenu ()) + else if (item.Checked == true) { - SetNeedsDraw (); - SetParentSetNeedsDisplay (); - - return me.Handled = true; + textToDraw = $"{checkChar} {item.Title}"; } - - _host.OnMenuOpened (); - - return me.Handled = true; - } - } - - internal bool CheckSubMenu () - { - if (_currentChild == -1 || _barItems?.Children? [_currentChild] is null) - { - return true; - } - - MenuBarItem? subMenu = _barItems.SubMenu (_barItems.Children [_currentChild]!); - - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (subMenu is { }) - { - int pos = -1; - - if (_host._openSubMenu is { }) + else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) { - pos = _host._openSubMenu.FindIndex (o => o._barItems == subMenu); + textToDraw = $"{uncheckedChar} {item.Title}"; } - - if (pos == -1 - && this != _host.OpenCurrentMenu - && subMenu.Children != _host.OpenCurrentMenu!._barItems!.Children - && !_host.CloseMenu (false, true)) + else { - return false; + textToDraw = item.Title; } - _host.Activate (_host._selected, pos, subMenu); - } - else if (_host._openSubMenu?.Count == 0 || _host._openSubMenu?.Last ()._barItems!.IsSubMenuOf (_barItems.Children [_currentChild]!) == false) - { - return _host.CloseMenu (false, true); - } - else - { - SetNeedsDraw (); - SetParentSetNeedsDisplay (); - } + Point screen = ViewportToScreen (new Point (0, i)); - return true; - } + if (screen.X < Driver.Cols) + { + Driver.Move (screen.X + 1, screen.Y); - protected override void Dispose (bool disposing) - { - RemoveKeyBindingsHotKey (_barItems); + if (!item.IsEnabled ()) + { + DrawHotString (textToDraw, ColorScheme!.Disabled, ColorScheme.Disabled); + } + else if (i == 0 && _host.UseSubMenusSingleFrame && item.Parent!.Parent is { }) + { + var tf = new TextFormatter + { + ConstrainToWidth = Frame.Width - 3, + ConstrainToHeight = 1, + Alignment = Alignment.Center, HotKeySpecifier = MenuBar.HotKeySpecifier, Text = textToDraw + }; - if (Application.Top is { }) - { - Application.Top.DrawComplete -= Top_DrawComplete; - Application.Top.SizeChanging -= Current_TerminalResized; + // The -3 is left/right border + one space (not sure what for) + tf.Draw ( + ViewportToScreen (new Rectangle (1, i, Frame.Width - 3, 1)), + i == _currentChild ? GetFocusColor () : GetNormalColor (), + i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal, + SuperView?.ViewportToScreen (SuperView.Viewport) ?? Rectangle.Empty + ); + } + else + { + DrawHotString ( + textToDraw, + i == _currentChild ? ColorScheme!.HotFocus : ColorScheme!.HotNormal, + i == _currentChild ? GetFocusColor () : GetNormalColor () + ); + } + + // The help string + int l = item.ShortcutTag.GetColumns () == 0 + ? item.Help.GetColumns () + : item.Help.GetColumns () + item.ShortcutTag.GetColumns () + 2; + int col = Frame.Width - l - 3; + screen = ViewportToScreen (new Point (col, i)); + + if (screen.X < Driver.Cols) + { + Driver.Move (screen.X, screen.Y); + Driver.AddStr (item.Help); + + // The shortcut tag string + if (!string.IsNullOrEmpty (item.ShortcutTag)) + { + Driver.Move (screen.X + l - item.ShortcutTag.GetColumns (), screen.Y); + Driver.AddStr (item.ShortcutTag); + } + } + } } - Application.MouseEvent -= Application_RootMouseEvent; - base.Dispose (disposing); + SetClip (savedClip); } } diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index cb24070018..fb3aedd7b7 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -133,16 +133,24 @@ public MenuBar () { CloseOtherOpenedMenuBar (); - return Select (Menus.IndexOf (ctx.Data)); + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + return Select (Menus.IndexOf (keyCommandContext.Binding.Data)); }); AddCommand (Command.Select, ctx => { - if (ctx.Data is MouseEventArgs) + if (ctx is not CommandContext keyCommandContext) + { + return false ; + } + if (keyCommandContext.Binding.Data is MouseEventArgs) { // HACK: Work around the fact that View.MouseClick always invokes Select return false; } - var res = Run ((ctx.Data as MenuItem)?.Action!); + var res = Run ((keyCommandContext.Binding.Data as MenuItem)?.Action!); CloseAllMenus (); return res; diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index 60384b9e54..eff0b62c3a 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -358,14 +358,19 @@ params string [] buttons if (count == defaultButton) { b.IsDefault = true; - b.Accepting += (s, e) => + b.Accepting += (_, e) => { + if (e.Context is not CommandContext keyCommandContext) + { + return; + } + // TODO: With https://github.com/gui-cs/Terminal.Gui/issues/3778 we can simplify this - if (e.Context.Data is Button button) + if (keyCommandContext.Binding.Data is Button button) { Clicked = (int)button.Data!; - } - else if (e.Context is CommandContext { Binding.BoundView: Button btn }) + } + else if (keyCommandContext.Binding.BoundView is Button btn) { Clicked = (int)btn.Data!; } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 01e7590290..5a8dad689c 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -64,7 +64,7 @@ public RadioGroup () return false; } - var item = keyCommandContext.Data as int?; + var item = keyCommandContext.Binding.Data as int?; if (HasFocus) @@ -248,7 +248,7 @@ private void RadioGroup_MouseClick (object? sender, MouseEventArgs e) if (c > -1) { // Just like the user pressing the items' hotkey - e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, boundView: this, context: c)) == true; + e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, boundView: this, data: c)) == true; } } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index d35c46850e..0695194949 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -300,17 +300,18 @@ private void AddCommands () AddCommand (Command.Select, DispatchCommand); } - private bool? DispatchCommand (ICommandContext commandContext) + private bool? DispatchCommand (ICommandContext? commandContext) { if (commandContext is not CommandContext ctx) { return false; } - if (ctx.Data != this) + + if (ctx.Binding.Data != this) { // Invoke Select on the command view to cause it to change state if it wants to // If this causes CommandView to raise Accept, we eat it - ctx.Data = this; + ctx.Binding = ctx.Binding with { Data = this }; CommandView.InvokeCommand (Command.Select, ctx); } @@ -497,7 +498,7 @@ void CommandViewOnAccepted (object? sender, CommandEventArgs e) void CommandViewOnSelecting (object? sender, CommandEventArgs e) { - if (e.Context.Data != this) + if (e.Context is CommandContext keyCommandContext && keyCommandContext.Binding.Data != this) { // Forward command to ourselves InvokeCommand (Command.Select, new ([Command.Select], KeyBindingScope.Focused, null, this)); diff --git a/UICatalog/Scenarios/Editors/EventLog.cs b/UICatalog/Scenarios/Editors/EventLog.cs index e9ce2efbaa..cf0042482d 100644 --- a/UICatalog/Scenarios/Editors/EventLog.cs +++ b/UICatalog/Scenarios/Editors/EventLog.cs @@ -86,15 +86,24 @@ public View? ViewToLog _viewToLog.HandlingHotKey += (s, args) => { - Log ($"HandlingHotKey: {args.Context.Command} {args.Context.Data}"); + if (args.Context is CommandContext keyCommandContext) + { + Log ($"HandlingHotKey: {args.Context.Command} {keyCommandContext.Binding.Data}"); + } }; _viewToLog.Selecting += (s, args) => { - Log ($"Selecting: {args.Context.Command} {args.Context.Data}"); + if (args.Context is CommandContext keyCommandContext) + { + Log ($"Selecting: {args.Context.Command} {keyCommandContext.Binding.Data}"); + } }; _viewToLog.Accepting += (s, args) => { - Log ($"Accepting: {args.Context.Command} {args.Context.Data}"); + if (args.Context is CommandContext keyCommandContext) + { + Log ($"Accepting: {args.Context.Command} {keyCommandContext.Binding.Data}"); + } }; } } diff --git a/UnitTests/Views/TextViewTests.cs b/UnitTests/Views/TextViewTests.cs index 3d0dd21856..ab04015ec4 100644 --- a/UnitTests/Views/TextViewTests.cs +++ b/UnitTests/Views/TextViewTests.cs @@ -6999,7 +6999,7 @@ public void UnwrappedCursorPosition_Event () { Width = Dim.Fill (), Height = Dim.Fill (), Text = "This is the first line.\nThis is the second line.\n" }; - tv.UnwrappedCursorPosition += (s, e) => { cp = e.Point; }; + tv.UnwrappedCursorPosition += (s, e) => { cp = e; }; var top = new Toplevel (); top.Add (tv); Application.Begin (top); From 65cf641685ed92736384043751014d24ef1fe70b Mon Sep 17 00:00:00 2001 From: Tig Date: Thu, 5 Dec 2024 15:04:23 -0700 Subject: [PATCH 07/40] Fixed CheckBox issue --- Terminal.Gui/Input/Mouse/MouseBinding.cs | 9 +- Terminal.Gui/Input/Mouse/MouseBindings.cs | 132 ++++++++++++---------- Terminal.Gui/View/View.Mouse.cs | 8 +- Terminal.Gui/Views/Button.cs | 12 +- Terminal.Gui/Views/CheckBox.cs | 20 ++-- Terminal.Gui/Views/ColorPicker.16.cs | 28 +---- Terminal.Gui/Views/ComboBox.cs | 6 +- Terminal.Gui/Views/Label.cs | 7 +- Terminal.Gui/Views/Shortcut.cs | 22 ++-- Terminal.Gui/Views/Slider.cs | 8 +- Terminal.Gui/Views/TextView.cs | 10 +- Terminal.Gui/Views/TreeView/TreeView.cs | 6 +- UnitTests/Views/AllViewsTests.cs | 10 +- 13 files changed, 127 insertions(+), 151 deletions(-) diff --git a/Terminal.Gui/Input/Mouse/MouseBinding.cs b/Terminal.Gui/Input/Mouse/MouseBinding.cs index 01ceb09e18..f9f7911c13 100644 --- a/Terminal.Gui/Input/Mouse/MouseBinding.cs +++ b/Terminal.Gui/Input/Mouse/MouseBinding.cs @@ -10,11 +10,18 @@ public record struct MouseBinding { /// Initializes a new instance. /// The commands this mouse binding will invoke. - public MouseBinding (Command [] commands) + /// The mouse event arguments, to be passed as context. + public MouseBinding (Command [] commands, MouseEventArgs? mouseEventArgs) { Commands = commands; + MouseEventArgs = mouseEventArgs; } /// The commands this key binding will invoke. public Command [] Commands { get; set; } + + /// + /// The mouse event arguments. + /// + public MouseEventArgs? MouseEventArgs { get; set; } } diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index 93af18c251..8f2d5d0669 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -15,21 +15,20 @@ public class MouseBindings public MouseBindings () { } /// Adds a to the collection. - /// + /// /// - public void Add (MouseEventArgs mouseEvent, MouseBinding binding) + public void Add (MouseEventArgs mouseEventArgs, MouseBinding binding) { - if (TryGet (mouseEvent, out MouseBinding _)) + if (TryGet (mouseEventArgs, out MouseBinding _)) { - throw new InvalidOperationException (@$"A binding for {mouseEvent} exists ({binding})."); + throw new InvalidOperationException (@$"A binding for {mouseEventArgs} exists ({binding})."); } - - // IMPORTANT: Add a COPY of the key. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy + // IMPORTANT: Add a COPY of the mouseEventArgs. 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: Apply will update the Dictionary with the new mouseEventArgs, but the old mouseEventArgs will still be in the dictionary. // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. - Bindings.Add (mouseEvent, binding); + Bindings.Add (mouseEventArgs, binding); } /// @@ -43,15 +42,16 @@ public void Add (MouseEventArgs mouseEvent, MouseBinding binding) /// 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 mouse flags to check. + /// The mouse flags to check. /// - /// The command to invoked on the when is received. When - /// multiple commands are provided,they will be applied in sequence. The bound event will be + /// The command to invoked on the when is received. When + /// multiple commands are provided,they will be applied in sequence. The bound event + /// will be /// consumed if any took effect. /// - public void Add (MouseEventArgs mouseEvents, params Command [] commands) + public void Add (MouseEventArgs mouseEventArgs, params Command [] commands) { - if (mouseEvents.Flags == MouseFlags.None) + if (mouseEventArgs.Flags == MouseFlags.None) { throw new ArgumentException (@"Invalid MouseFlag", nameof (commands)); } @@ -61,12 +61,12 @@ public void Add (MouseEventArgs mouseEvents, params Command [] commands) throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } - if (TryGet (mouseEvents, out MouseBinding binding)) + if (TryGet (mouseEventArgs, out MouseBinding binding)) { - throw new InvalidOperationException (@$"A binding for {mouseEvents} exists ({binding})."); + throw new InvalidOperationException (@$"A binding for {mouseEventArgs} exists ({binding})."); } - Add (mouseEvents, new MouseBinding (commands)); + Add (mouseEventArgs, new MouseBinding (commands, mouseEventArgs)); } // TODO: Add a dictionary comparer that ignores Scope @@ -74,15 +74,6 @@ public void Add (MouseEventArgs mouseEvents, params Command [] commands) /// The collection of objects. public Dictionary Bindings { get; } = new (); - /// - /// Gets the that are bound. - /// - /// - public IEnumerable GetBoundMouseEventArgs () - { - return Bindings.Keys; - } - /// Removes all objects from the collection. public void Clear () { Bindings.Clear (); } @@ -94,8 +85,8 @@ public IEnumerable GetBoundMouseEventArgs () public void Clear (params Command [] command) { KeyValuePair [] kvps = Bindings - .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) - .ToArray (); + .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) + .ToArray (); foreach (KeyValuePair kvp in kvps) { @@ -104,27 +95,48 @@ public void Clear (params Command [] command) } /// Gets the for the specified combination of . - /// + /// /// - public MouseBinding Get (MouseEventArgs mouseEvents) + public MouseBinding Get (MouseEventArgs mouseEventArgs) { - if (TryGet (mouseEvents, out MouseBinding binding)) + if (TryGet (mouseEventArgs, out MouseBinding binding)) { return binding; } - throw new InvalidOperationException ($"{mouseEvents} is not bound."); + throw new InvalidOperationException ($"{mouseEventArgs} is not bound."); } - /// Gets the array of s bound to if it exists. - /// The key to check. + /// + /// Gets combination of bound to the set of commands specified by + /// . + /// + /// The set of commands to search. /// - /// The array of s if is bound. An empty array + /// The combination of bound to the set of commands specified by + /// . An empty list if the set of caommands was not found. + /// + public IEnumerable GetAllMouseEventArgsFromCommands (params Command [] commands) + { + return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); + } + + /// + /// Gets the that are bound. + /// + /// + public IEnumerable GetBoundMouseEventArgs () { return Bindings.Keys; } + + /// 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 (MouseEventArgs mouseEvents) + public Command [] GetCommands (MouseEventArgs mouseEventArgs) { - if (TryGet (mouseEvents, out MouseBinding bindings)) + if (TryGet (mouseEventArgs, out MouseBinding bindings)) { return bindings.Commands; } @@ -132,32 +144,30 @@ public Command [] GetCommands (MouseEventArgs mouseEvents) return []; } - /// Gets the first combination of bound to the set of commands specified by . + /// + /// Gets the first combination of bound to the set of commands specified by + /// . + /// /// The set of commands to search. - /// The first combination of bound to the set of commands specified by . if the set of caommands was not found. + /// + /// The first combination of bound to the set of commands specified by + /// . if the set of caommands was not found. + /// public MouseEventArgs? GetMouseEventArgsFromCommands (params Command [] commands) { return Bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } - /// Gets combination of bound to the set of commands specified by . - /// The set of commands to search. - /// The combination of bound to the set of commands specified by . An empty list if the set of caommands was not found. - public IEnumerable GetAllMouseEventArgsFromCommands (params Command [] commands) - { - return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); - } - /// Removes a from the collection. - /// - public void Remove (MouseEventArgs mouseEvents) + /// + public void Remove (MouseEventArgs mouseEventArgs) { - if (!TryGet (mouseEvents, out MouseBinding _)) + if (!TryGet (mouseEventArgs, out MouseBinding _)) { return; } - Bindings.Remove (mouseEvents); + Bindings.Remove (mouseEventArgs); } /// Replaces the commands already bound to a combination of . @@ -166,24 +176,27 @@ public void Remove (MouseEventArgs mouseEvents) /// If the combination of is not already bound, it will be added. /// /// - /// The combination of bound to the command to be replaced. + /// The combination of bound to the command to be replaced. /// The set of commands to replace the old ones with. - public void ReplaceCommands (MouseEventArgs mouseEvents, params Command [] commands) + public void ReplaceCommands (MouseEventArgs mouseEventArgs, params Command [] commands) { - if (TryGet (mouseEvents, out MouseBinding binding)) + if (TryGet (mouseEventArgs, out MouseBinding binding)) { binding.Commands = commands; } else { - Add (mouseEvents, commands); + Add (mouseEventArgs, commands); } } /// Replaces a combination already bound to a set of s. /// /// The to be replaced. - /// The new to be used. If no action will be taken. + /// + /// The new to be used. If no action + /// will be taken. + /// public void ReplaceKey (MouseEventArgs oldMouseEventArgs, MouseEventArgs newMouseEventArgs) { if (!TryGet (oldMouseEventArgs, out MouseBinding _)) @@ -198,17 +211,16 @@ public void ReplaceKey (MouseEventArgs oldMouseEventArgs, MouseEventArgs newMous /// Gets the commands bound with the specified . /// - /// The key to check. + /// The key to check. /// /// When this method returns, contains the commands bound with the specified mouse flags, if the mouse flags are /// found; otherwise, null. This parameter is passed uninitialized. /// /// if the mouse flags are bound; otherwise . - public bool TryGet (MouseEventArgs mouseEvents, out MouseBinding binding) + public bool TryGet (MouseEventArgs mouseEventArgs, out MouseBinding binding) { + binding = new ([], mouseEventArgs); - binding = new ([]); - - return Bindings.TryGetValue (mouseEvents, out binding); + return Bindings.TryGetValue (mouseEventArgs, out binding); } } diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index 29edd7188f..a897f2ef3a 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -360,7 +360,7 @@ protected bool RaiseMouseClickEvent (MouseEventArgs args) // Always invoke Select command on MouseClick // By default, this will raise Selecting/OnSelecting - Subclasses can override this via AddCommand (Command.Select ...). - args.Handled = InvokeCommand (Command.Select, new ([Command.Select], KeyBindingScope.Focused, null, args)) == true; + args.Handled = InvokeCommand (Command.Select, new ([Command.Select], args)) == true; return args.Handled; } @@ -666,15 +666,15 @@ internal bool SetPressedHighlight (HighlightStyle newHighlightStyle) if (start is not Adornment) { - if (start.Margin is {} && start.Margin.Contains (currentLocation)) + if (start.Margin is { } && start.Margin.Contains (currentLocation)) { found = start.Margin; } - else if (start.Border is {} && start.Border.Contains (currentLocation)) + else if (start.Border is { } && start.Border.Contains (currentLocation)) { found = start.Border; } - else if (start.Padding is { } && start.Padding.Contains(currentLocation)) + else if (start.Padding is { } && start.Padding.Contains (currentLocation)) { found = start.Padding; } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 5036c07bf0..79118a5c91 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -72,18 +72,14 @@ public Button () private bool? HandleHotKeyCommand (ICommandContext commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } bool cachedIsDefault = IsDefault; // Supports "Swap Default" in Buttons scenario where IsDefault changes - if (RaiseSelecting (ctx) is true) + if (RaiseSelecting (commandContext) is true) { return true; } - bool? handled = RaiseAccepting (ctx); + bool? handled = RaiseAccepting (commandContext); if (handled == true) { @@ -97,7 +93,7 @@ public Button () // If Accept was not handled... if (cachedIsDefault && SuperView is { }) { - return SuperView.InvokeCommand (Command.Accept, ctx.Binding); + return SuperView.InvokeCommand (Command.Accept); } return false; @@ -137,7 +133,7 @@ private void Button_MouseClick (object sender, MouseEventArgs e) } // TODO: With https://github.com/gui-cs/Terminal.Gui/issues/3778 we won't have to pass data: - e.Handled = InvokeCommand (Command.HotKey, new KeyBinding([Command.HotKey], KeyBindingScope.HotKey, this, null)) == true; + e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, this, null)) == true; } private void Button_TitleChanged (object sender, EventArgs e) diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index d2a4d60c99..fcdc9beb82 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -24,7 +24,14 @@ public CheckBox () AddCommand (Command.Select, AdvanceAndSelect); // Hotkey - Advance state and raise Select event - DO NOT raise Accept - AddCommand (Command.HotKey, AdvanceAndSelect); + AddCommand (Command.HotKey, ctx => + { + if (RaiseHandlingHotKey () is true) + { + return true; + } + return AdvanceAndSelect (ctx); + }); // Accept (Enter key) - Raise Accept event - DO NOT advance state AddCommand (Command.Accept, RaiseAccepting); @@ -34,13 +41,8 @@ public CheckBox () HighlightStyle = DefaultHighlightStyle; } - private bool? AdvanceAndSelect (ICommandContext commandContext) + private bool? AdvanceAndSelect (ICommandContext? commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } - bool? cancelled = AdvanceCheckState (); if (cancelled is true) @@ -48,12 +50,12 @@ public CheckBox () return true; } - if (RaiseSelecting (ctx) is true) + if (RaiseSelecting (commandContext) is true) { return true; } - return ctx.Command == Command.HotKey ? cancelled : cancelled is false; + return commandContext?.Command == Command.HotKey ? cancelled : cancelled is false; } private void Checkbox_TitleChanged (object? sender, EventArgs e) diff --git a/Terminal.Gui/Views/ColorPicker.16.cs b/Terminal.Gui/Views/ColorPicker.16.cs index f699f5686a..bce9dcf28b 100644 --- a/Terminal.Gui/Views/ColorPicker.16.cs +++ b/Terminal.Gui/Views/ColorPicker.16.cs @@ -69,11 +69,7 @@ public Point Cursor /// private bool MoveDown (ICommandContext commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } - if (RaiseSelecting (ctx) == true) + if (RaiseSelecting (commandContext) == true) { return true; } @@ -89,11 +85,7 @@ private bool MoveDown (ICommandContext commandContext) /// private bool MoveLeft (ICommandContext commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } - if (RaiseSelecting (ctx) == true) + if (RaiseSelecting (commandContext) == true) { return true; } @@ -110,11 +102,7 @@ private bool MoveLeft (ICommandContext commandContext) /// private bool MoveRight (ICommandContext commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } - if (RaiseSelecting (ctx) == true) + if (RaiseSelecting (commandContext) == true) { return true; } @@ -130,11 +118,7 @@ private bool MoveRight (ICommandContext commandContext) /// private bool MoveUp (ICommandContext commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } - if (RaiseSelecting (ctx) == true) + if (RaiseSelecting (commandContext) == true) { return true; } @@ -206,9 +190,9 @@ private void AddCommands () { bool set = false; - if (ctx is CommandContext { Binding: { } } mouseCommandContext) + if (ctx is CommandContext { Binding.MouseEventArgs: { } } mouseCommandContext) { - Cursor = new (mouseCommandContext.Binding.Position.X / _boxWidth, mouseCommandContext.Binding.Position.Y / _boxHeight); + Cursor = new (mouseCommandContext.Binding.MouseEventArgs.Position.X / _boxWidth, mouseCommandContext.Binding.MouseEventArgs.Position.Y / _boxHeight); set = true; } return RaiseAccepting (ctx) == true || set; diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 2e3763a9b6..7b30e09cfb 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -401,10 +401,6 @@ public void SetSource (ObservableCollection source) private bool ActivateSelected (ICommandContext commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } if (HasItems ()) { if (SelectText ()) @@ -412,7 +408,7 @@ private bool ActivateSelected (ICommandContext commandContext) return false; } - return RaiseAccepting (ctx) == true; + return RaiseAccepting (commandContext) == true; } return false; diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 3ada1f11e9..e277357f45 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -62,10 +62,6 @@ public override Rune HotKeySpecifier private bool? InvokeHotKeyOnNext (ICommandContext commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } if (RaiseHandlingHotKey () == true) { return true; @@ -82,7 +78,8 @@ public override Rune HotKeySpecifier if (me != -1 && me < SuperView?.Subviews.Count - 1) { - return SuperView?.Subviews [me + 1].InvokeCommand (Command.HotKey, ctx.Binding) == true; + + return SuperView?.Subviews [me + 1].InvokeCommand (Command.HotKey) == true; } return false; diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 0695194949..5305ea3175 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -302,20 +302,17 @@ private void AddCommands () private bool? DispatchCommand (ICommandContext? commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } + CommandContext? keyCommandContext = commandContext is CommandContext ? (CommandContext)commandContext : default; - if (ctx.Binding.Data != this) + if (keyCommandContext?.Binding.Data != this) { // Invoke Select on the command view to cause it to change state if it wants to // If this causes CommandView to raise Accept, we eat it - ctx.Binding = ctx.Binding with { Data = this }; - CommandView.InvokeCommand (Command.Select, ctx); + keyCommandContext = keyCommandContext!.Value with { Binding = keyCommandContext.Value.Binding with { Data = this } }; + CommandView.InvokeCommand (Command.Select, keyCommandContext); } - if (RaiseSelecting (ctx) is true) + if (RaiseSelecting (keyCommandContext) is true) { return true; } @@ -325,14 +322,14 @@ private void AddCommands () var cancel = false; - cancel = RaiseAccepting (ctx) is true; + cancel = RaiseAccepting (commandContext) is true; if (cancel) { return true; } - if (ctx.Command != Command.Accept) + if (commandContext?.Command != Command.Accept) { // return false; } @@ -347,7 +344,7 @@ private void AddCommands () if (_targetView is { }) { - _targetView.InvokeCommand (Command, ctx); + _targetView.InvokeCommand (Command, commandContext); } return cancel; @@ -498,7 +495,8 @@ void CommandViewOnAccepted (object? sender, CommandEventArgs e) void CommandViewOnSelecting (object? sender, CommandEventArgs e) { - if (e.Context is CommandContext keyCommandContext && keyCommandContext.Binding.Data != this) + if ((e.Context is CommandContext keyCommandContext && keyCommandContext.Binding.Data != this) || + e.Context is CommandContext) { // Forward command to ourselves InvokeCommand (Command.Select, new ([Command.Select], KeyBindingScope.Focused, null, this)); diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index c0cef3915b..2008f22e49 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -850,7 +850,7 @@ private void DrawSlider () if (IsInitialized) { - normalAttr = GetNormalColor(); + normalAttr = GetNormalColor (); setAttr = Style.SetChar.Attribute ?? GetHotNormalColor (); } @@ -1787,13 +1787,9 @@ internal bool Select () internal bool Accept (ICommandContext commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } SetFocusedOption (); - return RaiseAccepting (ctx) == true; + return RaiseAccepting (commandContext) == true; } internal bool MovePlus () diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 35b5f67aa4..065a346096 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -3719,7 +3719,7 @@ public virtual void OnUnwrappedCursorPosition (int? cRow = null, int? cCol = nul col = _wrapManager.GetModelColFromWrappedLines (CurrentRow, CurrentColumn); } - UnwrappedCursorPosition?.Invoke (this, new Point (row.Value, col.Value)); + UnwrappedCursorPosition?.Invoke (this, new Point (col.Value, row.Value)); } /// Paste the clipboard contents into the current selected position. @@ -6143,12 +6143,8 @@ private void ProcessPaste () Paste (); } - private bool ProcessEnterKey (ICommandContext commandContext) + private bool ProcessEnterKey (ICommandContext? commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } ResetColumnTrack (); if (_isReadOnly) @@ -6160,7 +6156,7 @@ private bool ProcessEnterKey (ICommandContext commandContext) { // By Default pressing ENTER should be ignored (OnAccept will return false or null). Only cancel if the // event was fired and set Cancel = true. - return RaiseAccepting (ctx) is null or false; + return RaiseAccepting (commandContext) is null or false; } SetWrapModel (); diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index c7f1c1f6ff..74de1047b1 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -464,12 +464,8 @@ public void ClearObjects () /// if was fired. public bool? ActivateSelectedObjectIfAny (ICommandContext commandContext) { - if (commandContext is not CommandContext ctx) - { - return false; - } // By default, Command.Accept calls OnAccept, so we need to call it here to ensure that the event is fired. - if (RaiseAccepting (ctx) == true) + if (RaiseAccepting (commandContext) == true) { return true; } diff --git a/UnitTests/Views/AllViewsTests.cs b/UnitTests/Views/AllViewsTests.cs index ccad6302fb..bc20443618 100644 --- a/UnitTests/Views/AllViewsTests.cs +++ b/UnitTests/Views/AllViewsTests.cs @@ -188,25 +188,21 @@ public void AllViews_Command_HotKey_Raises_HandlingHotKey (Type viewType) view.HotKey = Key.T; } - var selectingCount = 0; - view.Selecting += (s, e) => selectingCount++; - var acceptedCount = 0; view.Accepting += (s, e) => { acceptedCount++; }; - var hotkeyHandledCount = 0; + var handlingHotKeyCount = 0; view.HandlingHotKey += (s, e) => { - hotkeyHandledCount++; + handlingHotKeyCount++; }; if (view.InvokeCommand (Command.HotKey) == true) { - Assert.Equal (1, hotkeyHandledCount); - Assert.Equal (0, selectingCount); + Assert.Equal (1, handlingHotKeyCount); Assert.Equal (0, acceptedCount); } } From dc47125c16ef018b0bc58b4405d962dc1700ff45 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 6 Dec 2024 04:40:00 -0700 Subject: [PATCH 08/40] API docsz' --- Terminal.Gui/Input/ICommandContext.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/Input/ICommandContext.cs b/Terminal.Gui/Input/ICommandContext.cs index 1aa5c8660f..e94df202ff 100644 --- a/Terminal.Gui/Input/ICommandContext.cs +++ b/Terminal.Gui/Input/ICommandContext.cs @@ -2,7 +2,8 @@ namespace Terminal.Gui; /// -/// Describes the context in which a is being invoked. +/// Describes the context in which a is being invoked. When a is invoked, +/// a context object is passed to Command handlers. See . /// public interface ICommandContext { @@ -10,10 +11,4 @@ public interface ICommandContext /// The that is being invoked. /// public Command Command { get; set; } - - //// TODO: Remove this property. With CommandContext being a generic type, there should be no need for arbitrary data. - ///// - ///// Arbitrary data. - ///// - //public object? Data { get; set; } } From 3e8d91670ede7bde10255baaaf3fc33327325a42 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 6 Dec 2024 04:42:24 -0700 Subject: [PATCH 09/40] API docsz' --- Terminal.Gui/Input/ICommandContext.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Input/ICommandContext.cs b/Terminal.Gui/Input/ICommandContext.cs index e94df202ff..740f059c9d 100644 --- a/Terminal.Gui/Input/ICommandContext.cs +++ b/Terminal.Gui/Input/ICommandContext.cs @@ -2,9 +2,11 @@ namespace Terminal.Gui; /// -/// Describes the context in which a is being invoked. When a is invoked, -/// a context object is passed to Command handlers. See . +/// Describes the context in which a is being invoked. inherits from this interface. +/// When a is invoked, +/// a context object is passed to Command handlers as an reference. /// +/// . public interface ICommandContext { /// From f6b65c6f087c04a35ed2c89d78462ebb2379bee1 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 6 Dec 2024 09:35:59 -0700 Subject: [PATCH 10/40] Added mouse support. update charmap --- Terminal.Gui/Input/Mouse/MouseBinding.cs | 10 +- Terminal.Gui/Input/Mouse/MouseBindings.cs | 70 ++-- Terminal.Gui/Resources/Strings.Designer.cs | 54 +++ Terminal.Gui/Resources/Strings.resx | 18 + Terminal.Gui/View/View.Command.cs | 2 + Terminal.Gui/View/View.Keyboard.cs | 6 +- Terminal.Gui/View/View.Mouse.cs | 250 +++++++++---- Terminal.Gui/View/View.cs | 3 +- .../Views/CharMap}/CharMap.cs | 336 ++++++++---------- .../Views/CharMap}/UcdApiClient.cs | 24 +- .../Views/CharMap}/UnicodeRange.cs | 4 +- Terminal.Gui/Views/ColorPicker.16.cs | 2 +- Terminal.Gui/Views/ColorPicker.Prompt.cs | 6 +- Terminal.Gui/Views/TextField.cs | 2 +- Terminal.Gui/Views/TextView.cs | 2 +- UICatalog/Scenario.cs | 2 +- UICatalog/Scenarios/Editors/EventLog.cs | 64 ++-- UICatalog/Scenarios/TableEditor.cs | 4 +- 18 files changed, 487 insertions(+), 372 deletions(-) rename {UICatalog/Scenarios/CharacterMap => Terminal.Gui/Views/CharMap}/CharMap.cs (73%) rename {UICatalog/Scenarios/CharacterMap => Terminal.Gui/Views/CharMap}/UcdApiClient.cs (61%) rename {UICatalog/Scenarios/CharacterMap => Terminal.Gui/Views/CharMap}/UnicodeRange.cs (97%) diff --git a/Terminal.Gui/Input/Mouse/MouseBinding.cs b/Terminal.Gui/Input/Mouse/MouseBinding.cs index f9f7911c13..03238a53df 100644 --- a/Terminal.Gui/Input/Mouse/MouseBinding.cs +++ b/Terminal.Gui/Input/Mouse/MouseBinding.cs @@ -10,11 +10,15 @@ public record struct MouseBinding { /// Initializes a new instance. /// The commands this mouse binding will invoke. - /// The mouse event arguments, to be passed as context. - public MouseBinding (Command [] commands, MouseEventArgs? mouseEventArgs) + /// The mouse flags that trigger this binding. + public MouseBinding (Command [] commands, MouseFlags mouseFlags) { Commands = commands; - MouseEventArgs = mouseEventArgs; + + MouseEventArgs = new MouseEventArgs() + { + Flags = mouseFlags + }; } /// The commands this key binding will invoke. diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index 8f2d5d0669..cdddf5b9eb 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; /// -/// Provides a collection of objects bound to a combination of . +/// Provides a collection of objects bound to a combination of . /// /// /// @@ -17,7 +17,7 @@ public MouseBindings () { } /// Adds a to the collection. /// /// - public void Add (MouseEventArgs mouseEventArgs, MouseBinding binding) + public void Add (MouseFlags mouseEventArgs, MouseBinding binding) { if (TryGet (mouseEventArgs, out MouseBinding _)) { @@ -49,9 +49,9 @@ public void Add (MouseEventArgs mouseEventArgs, MouseBinding binding) /// will be /// consumed if any took effect. /// - public void Add (MouseEventArgs mouseEventArgs, params Command [] commands) + public void Add (MouseFlags mouseEventArgs, params Command [] commands) { - if (mouseEventArgs.Flags == MouseFlags.None) + if (mouseEventArgs == MouseFlags.None) { throw new ArgumentException (@"Invalid MouseFlag", nameof (commands)); } @@ -72,7 +72,7 @@ public void Add (MouseEventArgs mouseEventArgs, params Command [] commands) // TODO: Add a dictionary comparer that ignores Scope // TODO: This should not be public! /// The collection of objects. - public Dictionary Bindings { get; } = new (); + public Dictionary Bindings { get; } = new (); /// Removes all objects from the collection. public void Clear () { Bindings.Clear (); } @@ -84,20 +84,20 @@ public void Add (MouseEventArgs mouseEventArgs, params Command [] commands) /// public void Clear (params Command [] command) { - KeyValuePair [] kvps = Bindings + KeyValuePair [] kvps = Bindings .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) .ToArray (); - foreach (KeyValuePair kvp in kvps) + foreach (KeyValuePair kvp in kvps) { Remove (kvp.Key); } } - /// Gets the for the specified combination of . + /// Gets the for the specified combination of . /// /// - public MouseBinding Get (MouseEventArgs mouseEventArgs) + public MouseBinding Get (MouseFlags mouseEventArgs) { if (TryGet (mouseEventArgs, out MouseBinding binding)) { @@ -108,24 +108,24 @@ public MouseBinding Get (MouseEventArgs mouseEventArgs) } /// - /// Gets combination of bound to the set of commands specified by + /// Gets combination of bound to the set of commands specified by /// . /// /// The set of commands to search. /// - /// The combination of bound to the set of commands specified by + /// The combination of bound to the set of commands specified by /// . An empty list if the set of caommands was not found. /// - public IEnumerable GetAllMouseEventArgsFromCommands (params Command [] commands) + public IEnumerable GetAllMouseFlagsFromCommands (params Command [] commands) { return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); } /// - /// Gets the that are bound. + /// Gets the that are bound. /// /// - public IEnumerable GetBoundMouseEventArgs () { return Bindings.Keys; } + public IEnumerable GetBoundMouseFlags () { return Bindings.Keys; } /// Gets the array of s bound to if it exists. /// The key to check. @@ -134,7 +134,7 @@ public IEnumerable GetAllMouseEventArgsFromCommands (params Comm /// array /// if not. /// - public Command [] GetCommands (MouseEventArgs mouseEventArgs) + public Command [] GetCommands (MouseFlags mouseEventArgs) { if (TryGet (mouseEventArgs, out MouseBinding bindings)) { @@ -145,22 +145,22 @@ public Command [] GetCommands (MouseEventArgs mouseEventArgs) } /// - /// Gets the first combination of bound to the set of commands specified by + /// Gets the first combination of bound to the set of commands specified by /// . /// /// The set of commands to search. /// - /// The first combination of bound to the set of commands specified by + /// The first combination of bound to the set of commands specified by /// . if the set of caommands was not found. /// - public MouseEventArgs? GetMouseEventArgsFromCommands (params Command [] commands) + public MouseFlags? GetMouseFlagsFromCommands (params Command [] commands) { return Bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } /// Removes a from the collection. /// - public void Remove (MouseEventArgs mouseEventArgs) + public void Remove (MouseFlags mouseEventArgs) { if (!TryGet (mouseEventArgs, out MouseBinding _)) { @@ -170,15 +170,15 @@ public void Remove (MouseEventArgs mouseEventArgs) Bindings.Remove (mouseEventArgs); } - /// Replaces the commands already bound to a combination of . + /// Replaces the commands already bound to a combination of . /// /// - /// If the combination of is not already bound, it will be added. + /// If the combination of is not already bound, it will be added. /// /// - /// The combination of bound to the command to be replaced. + /// The combination of bound to the command to be replaced. /// The set of commands to replace the old ones with. - public void ReplaceCommands (MouseEventArgs mouseEventArgs, params Command [] commands) + public void ReplaceCommands (MouseFlags mouseEventArgs, params Command [] commands) { if (TryGet (mouseEventArgs, out MouseBinding binding)) { @@ -190,26 +190,26 @@ public void ReplaceCommands (MouseEventArgs mouseEventArgs, params Command [] co } } - /// Replaces a combination already bound to a set of s. + /// Replaces a combination already bound to a set of s. /// - /// The to be replaced. - /// - /// The new to be used. If no action + /// The to be replaced. + /// + /// The new to be used. If no action /// will be taken. /// - public void ReplaceKey (MouseEventArgs oldMouseEventArgs, MouseEventArgs newMouseEventArgs) + public void ReplaceKey (MouseFlags oldMouseFlags, MouseFlags newMouseFlags) { - if (!TryGet (oldMouseEventArgs, out MouseBinding _)) + if (!TryGet (oldMouseFlags, out MouseBinding _)) { - throw new InvalidOperationException ($"Key {oldMouseEventArgs} is not bound."); + throw new InvalidOperationException ($"Key {oldMouseFlags} is not bound."); } - MouseBinding value = Bindings [oldMouseEventArgs]; - Remove (oldMouseEventArgs); - Add (newMouseEventArgs, value); + MouseBinding value = Bindings [oldMouseFlags]; + Remove (oldMouseFlags); + Add (newMouseFlags, value); } - /// Gets the commands bound with the specified . + /// Gets the commands bound with the specified . /// /// The key to check. /// @@ -217,7 +217,7 @@ public void ReplaceKey (MouseEventArgs oldMouseEventArgs, MouseEventArgs newMous /// found; otherwise, null. This parameter is passed uninitialized. /// /// if the mouse flags are bound; otherwise . - public bool TryGet (MouseEventArgs mouseEventArgs, out MouseBinding binding) + public bool TryGet (MouseFlags mouseEventArgs, out MouseBinding binding) { binding = new ([], mouseEventArgs); diff --git a/Terminal.Gui/Resources/Strings.Designer.cs b/Terminal.Gui/Resources/Strings.Designer.cs index 2befd2737b..e329b718e7 100644 --- a/Terminal.Gui/Resources/Strings.Designer.cs +++ b/Terminal.Gui/Resources/Strings.Designer.cs @@ -1437,6 +1437,42 @@ internal static string btnYes { } } + /// + /// Looks up a localized string similar to Copy Code_point. + /// + internal static string charMapCopyCP { + get { + return ResourceManager.GetString("charMapCopyCP", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy _Glyph. + /// + internal static string charMapCopyGlyph { + get { + return ResourceManager.GetString("charMapCopyGlyph", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Getting Codepoint Information. + /// + internal static string charMapCPInfoDlgTitle { + get { + return ResourceManager.GetString("charMapCPInfoDlgTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Codepoint Information from. + /// + internal static string charMapInfoDlgInfoLabel { + get { + return ResourceManager.GetString("charMapInfoDlgInfoLabel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Co_lors. /// @@ -1518,6 +1554,24 @@ internal static string dpTitle { } } + /// + /// Looks up a localized string similar to Error. + /// + internal static string error { + get { + return ResourceManager.GetString("error", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to failed getting. + /// + internal static string failedGetting { + get { + return ResourceManager.GetString("failedGetting", resourceCulture); + } + } + /// /// Looks up a localized string similar to Any Files. /// diff --git a/Terminal.Gui/Resources/Strings.resx b/Terminal.Gui/Resources/Strings.resx index 9333d71578..773ee55937 100644 --- a/Terminal.Gui/Resources/Strings.resx +++ b/Terminal.Gui/Resources/Strings.resx @@ -721,4 +721,22 @@ Co_lors + + Getting Codepoint Information + + + Copy _Glyph + + + Copy Code_point + + + Codepoint Information from + + + Error + + + failed getting + \ No newline at end of file diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index c3a921bd65..d0c4937838 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -5,6 +5,8 @@ namespace Terminal.Gui; public partial class View // Command APIs { + private Dictionary CommandImplementations { get; } = new (); + #region Default Implementation /// diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index ab126f24c1..b3dbf94f91 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -502,8 +502,6 @@ bool RaiseKeyUp (Key k) /// Gets the key bindings for this view. public KeyBindings KeyBindings { get; internal set; } = null!; - private Dictionary CommandImplementations { get; } = new (); - /// /// INTERNAL API: Invokes any commands bound to on this view, adornments, and subviews. /// @@ -526,7 +524,7 @@ bool RaiseKeyUp (Key k) // `InvokeKeyBindings` returns `false`. Continue passing the event (return `false` from `OnInvokeKeyBindings`).. // * If key bindings were found, and any handled the key (at least one `Command` returned `true`), // `InvokeKeyBindings` returns `true`. Continue passing the event (return `false` from `OnInvokeKeyBindings`). - bool? handled = InvokeCommands (key, scope); + bool? handled = InvokeCommandsBoundToKey (key, scope); if (handled is true) { @@ -681,7 +679,7 @@ public bool IsHotKeyBound (Key key, out View? boundView) /// if at least one command was invoked and handled (or cancelled); input processing should /// stop. /// - protected bool? InvokeCommands (Key key, KeyBindingScope scope) + protected bool? InvokeCommandsBoundToKey (Key key, KeyBindingScope scope) { if (!KeyBindings.TryGet (key, scope, out KeyBinding binding)) { diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index a897f2ef3a..cb9b1522e2 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -1,10 +1,46 @@ #nullable enable using System.ComponentModel; +using System.Diagnostics; namespace Terminal.Gui; public partial class View // Mouse APIs { + /// Gets the mouse bindings for this view. + public MouseBindings MouseBindings { get; internal set; } = null!; + + private void SetupMouse () + { + MouseBindings = new (); + + MouseBindings.Add (MouseFlags.Button1Clicked, Command.Select); + } + + + /// + /// Invokes the Commands bound to the MouseFlags specified by . + /// See for an overview of Terminal.Gui mouse APIs. + /// + /// The mouse event passed. + /// + /// if no command was invoked; input processing should continue. + /// if at least one command was invoked and was not handled (or cancelled); input processing + /// should continue. + /// if at least one command was invoked and handled (or cancelled); input processing should + /// stop. + /// + protected bool? InvokeCommandsBoundToMouse (MouseEventArgs mouseEventArgs) + { + if (!MouseBindings.TryGet (mouseEventArgs.Flags, out MouseBinding binding)) + { + return null; + } + + binding.MouseEventArgs = mouseEventArgs; + + return InvokeCommands (binding.Commands, binding); + } + #region MouseEnterLeave private bool _hovering; @@ -269,14 +305,18 @@ protected virtual void OnMouseLeave () { } } } + // We get here if the view did not handle the mouse event via OnMouseEvent/MouseEvent and + // it did not handle the press/release/clicked events via HandlePress/HandleRelease/HandleClicked if (mouseEvent.IsSingleDoubleOrTripleClicked) { - // If it's a click, and we didn't handle it, then we need to generate a click event - // We get here if the view did not handle the mouse event via OnMouseEvent/MouseEvent and - // it did not handle the press/release/clicked events via HandlePress/HandleRelease/HandleClicked return RaiseMouseClickEvent (mouseEvent); } + if (mouseEvent.IsWheel) + { + return RaiseMouseWheelEvent (mouseEvent); + } + return false; } @@ -320,6 +360,96 @@ protected virtual bool OnMouseEvent (MouseEventArgs mouseEvent) #endregion Low Level Mouse Events + #region Mouse Pressed Events + + /// + /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically + /// when or are set). + /// + /// + /// Marked internal just to support unit tests + /// + /// + /// , if the event was handled, otherwise. + internal bool WhenGrabbedHandleReleased (MouseEventArgs mouseEvent) + { + mouseEvent.Handled = false; + + if (mouseEvent.IsReleased) + { + if (Application.MouseGrabView == this) + { + SetPressedHighlight (HighlightStyle.None); + } + + return mouseEvent.Handled = true; + } + + return false; + } + + /// + /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically + /// when or are set). + /// + /// + /// + /// Marked internal just to support unit tests + /// + /// + /// + /// , if the event was handled, otherwise. + private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent) + { + mouseEvent.Handled = false; + + if (mouseEvent.IsPressed) + { + // The first time we get pressed event, grab the mouse and set focus + if (Application.MouseGrabView != this) + { + Application.GrabMouse (this); + + if (!HasFocus && CanFocus) + { + // Set the focus, but don't invoke Accept + SetFocus (); + } + + mouseEvent.Handled = true; + } + + if (Viewport.Contains (mouseEvent.Position)) + { + if (this is not Adornment + && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.Pressed) ? HighlightStyle.Pressed : HighlightStyle.None)) + { + return true; + } + } + else + { + if (this is not Adornment + && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.PressedOutside) ? HighlightStyle.PressedOutside : HighlightStyle.None)) + + { + return true; + } + } + + if (WantContinuousButtonPressed && Application.MouseGrabView == this) + { + return RaiseMouseClickEvent (mouseEvent); + } + + return mouseEvent.Handled = true; + } + + return false; + } + + #endregion Mouse Pressed Events + #region Mouse Click Events /// Raises the / event. @@ -358,9 +488,8 @@ protected bool RaiseMouseClickEvent (MouseEventArgs args) // Post-conditions - // Always invoke Select command on MouseClick // By default, this will raise Selecting/OnSelecting - Subclasses can override this via AddCommand (Command.Select ...). - args.Handled = InvokeCommand (Command.Select, new ([Command.Select], args)) == true; + args.Handled = InvokeCommandsBoundToMouse (args) == true; return args.Handled; } @@ -428,93 +557,61 @@ internal bool WhenGrabbedHandleClicked (MouseEventArgs mouseEvent) return false; } - /// - /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically - /// when or are set). - /// - /// - /// Marked internal just to support unit tests - /// - /// - /// , if the event was handled, otherwise. - internal bool WhenGrabbedHandleReleased (MouseEventArgs mouseEvent) - { - mouseEvent.Handled = false; - if (mouseEvent.IsReleased) - { - if (Application.MouseGrabView == this) - { - SetPressedHighlight (HighlightStyle.None); - } + #endregion Mouse Clicked Events - return mouseEvent.Handled = true; - } - return false; - } + #region Mouse Wheel Events - /// - /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically - /// when or are set). - /// + /// Raises the / event. /// - /// - /// Marked internal just to support unit tests - /// /// - /// /// , if the event was handled, otherwise. - private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent) + protected bool RaiseMouseWheelEvent (MouseEventArgs args) { - mouseEvent.Handled = false; - - if (mouseEvent.IsPressed) + // Pre-conditions + if (!Enabled) { - // The first time we get pressed event, grab the mouse and set focus - if (Application.MouseGrabView != this) - { - Application.GrabMouse (this); + // QUESTION: Is this right? Should a disabled view eat mouse? + return args.Handled = false; + } - if (!HasFocus && CanFocus) - { - // Set the focus, but don't invoke Accept - SetFocus (); - } + // Cancellable event - mouseEvent.Handled = true; - } + if (OnMouseWheel (args) || args.Handled) + { + return args.Handled; + } - if (Viewport.Contains (mouseEvent.Position)) - { - if (this is not Adornment - && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.Pressed) ? HighlightStyle.Pressed : HighlightStyle.None)) - { - return true; - } - } - else - { - if (this is not Adornment - && SetPressedHighlight (HighlightStyle.HasFlag (HighlightStyle.PressedOutside) ? HighlightStyle.PressedOutside : HighlightStyle.None)) + MouseWheel?.Invoke (this, args); - { - return true; - } - } + if (args.Handled) + { + return true; + } - if (WantContinuousButtonPressed && Application.MouseGrabView == this) - { - return RaiseMouseClickEvent (mouseEvent); - } + // Post-conditions - return mouseEvent.Handled = true; - } + args.Handled = InvokeCommandsBoundToMouse (args) == true; - return false; + return args.Handled; } - #endregion Mouse Click Events + /// + /// Called when a mouse wheel event occurs. Check to see which wheel was moved was clicked. + /// + /// + /// + /// + /// , if the event was handled, otherwise. + protected virtual bool OnMouseWheel (MouseEventArgs args) { return false; } + + /// Raised when a mouse wheel event occurs. + /// + /// + public event EventHandler? MouseWheel; + + #endregion Mouse Wheel Events #region Highlight Handling @@ -720,4 +817,9 @@ internal bool SetPressedHighlight (HighlightStyle newHighlightStyle) return viewsUnderMouse; } + + private void DisposeMouse () + { + + } } diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 46689dba01..35de80f2b3 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -147,7 +147,7 @@ public View () SetupKeyboard (); - //SetupMouse (); + SetupMouse (); SetupText (); @@ -550,6 +550,7 @@ protected virtual void Dispose (bool disposing) { LineCanvas.Dispose (); + DisposeMouse (); DisposeKeyboard (); DisposeAdornments (); DisposeScrollBars (); diff --git a/UICatalog/Scenarios/CharacterMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs similarity index 73% rename from UICatalog/Scenarios/CharacterMap/CharMap.cs rename to Terminal.Gui/Views/CharMap/CharMap.cs index 8b7d283ad9..2da64ec22d 100644 --- a/UICatalog/Scenarios/CharacterMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -1,13 +1,10 @@ #nullable enable -using System; +using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; -using System.Net.Http; -using System.Text; using System.Text.Json; -using Terminal.Gui; +using Terminal.Gui.Resources; -namespace UICatalog.Scenarios; +namespace Terminal.Gui; /// /// A scrollable map of the Unicode codepoints. @@ -32,97 +29,24 @@ public CharMap () CanFocus = true; CursorVisibility = CursorVisibility.Default; - AddCommand ( - Command.Up, - () => - { - SelectedCodePoint -= 16; - - return true; - } - ); - - AddCommand ( - Command.Down, - () => - { - SelectedCodePoint += 16; - - return true; - } - ); - - AddCommand ( - Command.Left, - () => - { - SelectedCodePoint--; + AddCommand (Command.Up, commandContext => Move (commandContext, -16)); + AddCommand (Command.Down, commandContext => Move (commandContext, 16)); + AddCommand (Command.Left, commandContext => Move (commandContext, -1)); + AddCommand (Command.Right, commandContext => Move (commandContext, 1)); - return true; - } - ); + AddCommand (Command.PageUp, commandContext => Move (commandContext, -(Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16)); + AddCommand (Command.PageDown, commandContext => Move (commandContext, (Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16)); + AddCommand (Command.Start, commandContext => Move (commandContext, -SelectedCodePoint)); + AddCommand (Command.End, commandContext => Move (commandContext, MAX_CODE_POINT - SelectedCodePoint)); - AddCommand ( - Command.Right, - () => - { - SelectedCodePoint++; + AddCommand (Command.ScrollDown, () => ScrollVertical (1)); + AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); + AddCommand (Command.ScrollRight, () => ScrollHorizontal (1)); + AddCommand (Command.ScrollLeft, () => ScrollHorizontal (-1)); - return true; - } - ); - - AddCommand ( - Command.PageUp, - () => - { - int page = (Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16; - SelectedCodePoint -= page; - - return true; - } - ); - - AddCommand ( - Command.PageDown, - () => - { - int page = (Viewport.Height - HEADER_HEIGHT / _rowHeight) * 16; - SelectedCodePoint += page; - - return true; - } - ); - - AddCommand ( - Command.Start, - () => - { - SelectedCodePoint = 0; - - return true; - } - ); - - AddCommand ( - Command.End, - () => - { - SelectedCodePoint = MAX_CODE_POINT; - - return true; - } - ); - - AddCommand ( - Command.Accept, - () => - { - ShowDetails (); - - return true; - } - ); + AddCommand (Command.Accept, HandleAcceptCommand); + AddCommand (Command.Select, HandleSelectCommand); + AddCommand (Command.Context, HandleContextCommand); KeyBindings.Add (Key.CursorUp, Command.Up); KeyBindings.Add (Key.CursorDown, Command.Down); @@ -132,8 +56,14 @@ public CharMap () KeyBindings.Add (Key.PageDown, Command.PageDown); KeyBindings.Add (Key.Home, Command.Start); KeyBindings.Add (Key.End, Command.End); + KeyBindings.Add (ContextMenu.DefaultKey, Command.Context); - MouseClick += Handle_MouseClick; + MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept); + MouseBindings.Add (MouseFlags.Button3Clicked, Command.Context); + MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown); + MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp); + MouseBindings.Add (MouseFlags.WheeledLeft, Command.ScrollLeft); + MouseBindings.Add (MouseFlags.WheeledRight, Command.ScrollRight); SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, MAX_CODE_POINT / 16 * _rowHeight + HEADER_HEIGHT)); @@ -169,6 +99,18 @@ public CharMap () VerticalScrollBar.Y = HEADER_HEIGHT; // Header } + private bool? Move (ICommandContext? commandContext, int cpOffset) + { + if (RaiseSelecting (commandContext) is true) + { + return true; + } + + SelectedCodePoint += cpOffset; + + return true; + } + private void ScrollToMakeCursorVisible (Point offsetToNewCursor) { // Adjust vertical scrolling @@ -203,6 +145,7 @@ private Point GetCursor (int codePoint) return new (x, y); } + /// public override Point? PositionCursor () { Point cursor = GetCursor (SelectedCodePoint); @@ -297,6 +240,7 @@ public bool ShowGlyphWidths private static int RowLabelWidth => $"U+{MAX_CODE_POINT:x5}".Length + 1; + /// protected override bool OnDrawingContent () { if (Viewport.Height == 0 || Viewport.Width == 0) @@ -465,79 +409,91 @@ public static string ToCamelCase (string str) #region Mouse Handling // TODO: Use this to demonstrate using a popover to show glyph info on hover - public event EventHandler? Hover; + // public event EventHandler? Hover; - /// - protected override bool OnMouseEvent (MouseEventArgs mouseEvent) + private bool? HandleSelectCommand (ICommandContext? commandContext) { - if (mouseEvent.Flags == MouseFlags.WheeledDown) + Point position = GetCursor (SelectedCodePoint); + + if (commandContext is CommandContext { Binding.MouseEventArgs: { } } mouseCommandContext) { - if (Viewport.Y + Viewport.Height - HEADER_HEIGHT < GetContentSize ().Height) + // If the mouse is clicked on the headers, map it to the first glyph of the row/col + position = mouseCommandContext.Binding.MouseEventArgs.Position; + + if (position.Y == 0) { - ScrollVertical (1); + position = position with { Y = GetCursor (SelectedCodePoint).Y }; + } + + if (position.X < RowLabelWidth || position.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1) + { + position = position with { X = GetCursor (SelectedCodePoint).X }; } - return mouseEvent.Handled = true; } - if (mouseEvent.Flags == MouseFlags.WheeledUp) + if (RaiseSelecting (commandContext) is true) { - ScrollVertical (-1); - return mouseEvent.Handled = true; + return true; } - if (mouseEvent.Flags == MouseFlags.WheeledRight) + if (!TryGetCodePointFromPosition (position, out int cp)) { - if (Viewport.X + Viewport.Width < GetContentSize ().Width) - { - ScrollHorizontal (1); - } - return mouseEvent.Handled = true; + return false; } - if (mouseEvent.Flags == MouseFlags.WheeledLeft) + if (cp != SelectedCodePoint) { - ScrollHorizontal (-1); - return mouseEvent.Handled = true; + if (!HasFocus && CanFocus) + { + SetFocus (); + } + + SelectedCodePoint = cp; } - return false; + return true; } - private void Handle_MouseClick (object? sender, MouseEventArgs me) - { - if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && me.Flags != MouseFlags.Button1DoubleClicked) - { - return; - } + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] - if (me.Position.Y == 0) + private bool? HandleAcceptCommand (ICommandContext? commandContext) + { + if (RaiseAccepting (commandContext) is true) { - me.Position = me.Position with { Y = GetCursor (SelectedCodePoint).Y }; + return true; } - if (me.Position.X < RowLabelWidth || me.Position.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1) + if (commandContext is CommandContext { Binding.MouseEventArgs: { } } mouseCommandContext) { - me.Position = me.Position with { X = GetCursor (SelectedCodePoint).X }; - } + if (!HasFocus && CanFocus) + { + SetFocus (); + } - int row = (me.Position.Y - 1 - -Viewport.Y) / _rowHeight; // -1 for header - int col = (me.Position.X - RowLabelWidth - -Viewport.X) / COLUMN_WIDTH; + if (!TryGetCodePointFromPosition (mouseCommandContext.Binding.MouseEventArgs.Position, out int cp)) + { + return false; + } - if (col > 15) - { - col = 15; + SelectedCodePoint = cp; } - int val = row * 16 + col; + ShowDetails (); - if (val > MAX_CODE_POINT) - { - return; - } + return true; + } - if (me.Flags == MouseFlags.ReportMousePosition) + private bool? HandleContextCommand (ICommandContext? commandContext) + { + int newCodePoint = SelectedCodePoint; + + if (commandContext is CommandContext { Binding.MouseEventArgs: { } } mouseCommandContext) { - Hover?.Invoke (this, new (val, null)); + if (!TryGetCodePointFromPosition (mouseCommandContext.Binding.MouseEventArgs.Position, out newCodePoint)) + { + return false; + } } if (!HasFocus && CanFocus) @@ -545,62 +501,64 @@ private void Handle_MouseClick (object? sender, MouseEventArgs me) SetFocus (); } - me.Handled = true; + SelectedCodePoint = newCodePoint; - if (me.Flags == MouseFlags.Button1Clicked) + _contextMenu = new () { - SelectedCodePoint = val; + Position = ViewportToScreen (GetCursor (SelectedCodePoint)) + }; - return; - } + MenuBarItem menuItems = new ( + [ + new ( + Strings.charMapCopyGlyph, + "", + CopyGlyph, + null, + null, + (KeyCode)Key.G.WithCtrl + ), + new ( + Strings.charMapCopyCP, + "", + CopyCodePoint, + null, + null, + (KeyCode)Key.P.WithCtrl + ) + ] + ); + _contextMenu.Show (menuItems); - if (me.Flags == MouseFlags.Button1DoubleClicked) - { - SelectedCodePoint = val; - ShowDetails (); + return true; + } - return; - } + private bool TryGetCodePointFromPosition (Point position, out int codePoint) + { + int row = (position.Y - 1 - -Viewport.Y) / _rowHeight; // -1 for header + int col = (position.X - RowLabelWidth - -Viewport.X) / COLUMN_WIDTH; - if (me.Flags == _contextMenu.MouseFlags) + if (col > 15) { - SelectedCodePoint = val; + col = 15; + } - _contextMenu = new () - { - Position = new (me.Position.X + 1, me.Position.Y + 1) - }; + codePoint = row * 16 + col; - MenuBarItem menuItems = new ( - new MenuItem [] - { - new ( - "_Copy Glyph", - "", - CopyGlyph, - null, - null, - (KeyCode)Key.C.WithCtrl - ), - new ( - "Copy Code _Point", - "", - CopyCodePoint, - null, - null, - (KeyCode)Key.C.WithCtrl - .WithShift - ) - } - ); - _contextMenu.Show (menuItems); + if (codePoint > MAX_CODE_POINT) + { + return false; } + + return true; } #endregion Mouse Handling #region Details Dialog + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] private void ShowDetails () { UcdApiClient? client = new (); @@ -609,12 +567,12 @@ private void ShowDetails () Dialog? waitIndicator = new () { - Title = "Getting Code Point Information", + Title = Strings.charMapCPInfoDlgTitle, X = Pos.Center (), Y = Pos.Center (), Width = 40, Height = 10, - Buttons = [new () { Text = "_Cancel" }] + Buttons = [new () { Text = Strings.btnCancel }] }; var errorLabel = new Label @@ -641,7 +599,7 @@ private void ShowDetails () { try { - decResponse = await client.GetCodepointDec (SelectedCodePoint); + decResponse = await client.GetCodepointDec (SelectedCodePoint).ConfigureAwait (false); Application.Invoke (() => waitIndicator.RequestStop ()); } catch (HttpRequestException e) @@ -676,15 +634,15 @@ private void ShowDetails () document.RootElement, new JsonSerializerOptions - { WriteIndented = true } + { WriteIndented = true } ); } var title = $"{ToCamelCase (name!)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}"; - Button? copyGlyph = new () { Text = "Copy _Glyph" }; - Button? copyCodepoint = new () { Text = "Copy Code _Point" }; - Button? cancel = new () { Text = "Cancel" }; + Button? copyGlyph = new () { Text = Strings.charMapCopyGlyph }; + Button? copyCodepoint = new () { Text = Strings.charMapCopyCP }; + Button? cancel = new () { Text = Strings.btnCancel }; var dlg = new Dialog { Title = title, Buttons = [copyGlyph, copyCodepoint, cancel] }; @@ -747,7 +705,7 @@ private void ShowDetails () label = new () { Text = - $"Code Point Information from {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:", + $"{Strings.charMapInfoDlgInfoLabel} {UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint}:", X = 0, Y = Pos.Bottom (label) }; @@ -771,9 +729,9 @@ private void ShowDetails () else { MessageBox.ErrorQuery ( - "Code Point API", - $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} did not return a result for\r\n {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.", - "_Ok" + Strings.error, + $"{UcdApiClient.BaseUrl}codepoint/dec/{SelectedCodePoint} {Strings.failedGetting}{Environment.NewLine}{new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}.", + Strings.btnOk ); } diff --git a/UICatalog/Scenarios/CharacterMap/UcdApiClient.cs b/Terminal.Gui/Views/CharMap/UcdApiClient.cs similarity index 61% rename from UICatalog/Scenarios/CharacterMap/UcdApiClient.cs rename to Terminal.Gui/Views/CharMap/UcdApiClient.cs index 658a59c3c1..b6b2efce00 100644 --- a/UICatalog/Scenarios/CharacterMap/UcdApiClient.cs +++ b/Terminal.Gui/Views/CharMap/UcdApiClient.cs @@ -1,48 +1,44 @@ #nullable enable -using System; -using System.Net.Http; -using System.Threading.Tasks; - -namespace UICatalog.Scenarios; +namespace Terminal.Gui; /// /// A helper class for accessing the ucdapi.org API. /// -public class UcdApiClient +internal class UcdApiClient { public const string BaseUrl = "https://ucdapi.org/unicode/latest/"; private static readonly HttpClient _httpClient = new (); public async Task GetChars (string chars) { - HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}"); + HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}").ConfigureAwait (false); response.EnsureSuccessStatusCode (); - return await response.Content.ReadAsStringAsync (); + return await response.Content.ReadAsStringAsync ().ConfigureAwait (false); } public async Task GetCharsName (string chars) { HttpResponseMessage response = - await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name"); + await _httpClient.GetAsync ($"{BaseUrl}chars/{Uri.EscapeDataString (chars)}/name").ConfigureAwait (false); response.EnsureSuccessStatusCode (); - return await response.Content.ReadAsStringAsync (); + return await response.Content.ReadAsStringAsync ().ConfigureAwait (false); } public async Task GetCodepointDec (int dec) { - HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}"); + HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/dec/{dec}").ConfigureAwait (false); response.EnsureSuccessStatusCode (); - return await response.Content.ReadAsStringAsync (); + return await response.Content.ReadAsStringAsync ().ConfigureAwait (false); } public async Task GetCodepointHex (string hex) { - HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}"); + HttpResponseMessage response = await _httpClient.GetAsync ($"{BaseUrl}codepoint/hex/{hex}").ConfigureAwait (false); response.EnsureSuccessStatusCode (); - return await response.Content.ReadAsStringAsync (); + return await response.Content.ReadAsStringAsync ().ConfigureAwait (false); } } diff --git a/UICatalog/Scenarios/CharacterMap/UnicodeRange.cs b/Terminal.Gui/Views/CharMap/UnicodeRange.cs similarity index 97% rename from UICatalog/Scenarios/CharacterMap/UnicodeRange.cs rename to Terminal.Gui/Views/CharMap/UnicodeRange.cs index 686823486c..1880b2671e 100644 --- a/UICatalog/Scenarios/CharacterMap/UnicodeRange.cs +++ b/Terminal.Gui/Views/CharMap/UnicodeRange.cs @@ -1,10 +1,8 @@ #nullable enable -using System.Collections.Generic; -using System.Linq; using System.Reflection; using System.Text.Unicode; -namespace UICatalog.Scenarios; +namespace Terminal.Gui; /// /// Represents all of the Uniicode ranges.from System.Text.Unicode.UnicodeRange plus diff --git a/Terminal.Gui/Views/ColorPicker.16.cs b/Terminal.Gui/Views/ColorPicker.16.cs index bce9dcf28b..c70565a445 100644 --- a/Terminal.Gui/Views/ColorPicker.16.cs +++ b/Terminal.Gui/Views/ColorPicker.16.cs @@ -133,7 +133,7 @@ private bool MoveUp (ICommandContext commandContext) /// protected override bool OnDrawingContent () { - SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ()); + SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); var colorIndex = 0; for (var y = 0; y < Math.Max (2, Viewport.Height / BoxHeight); y++) diff --git a/Terminal.Gui/Views/ColorPicker.Prompt.cs b/Terminal.Gui/Views/ColorPicker.Prompt.cs index 0b79d3e9ff..8afa35a877 100644 --- a/Terminal.Gui/Views/ColorPicker.Prompt.cs +++ b/Terminal.Gui/Views/ColorPicker.Prompt.cs @@ -1,4 +1,6 @@ -namespace Terminal.Gui; +using Terminal.Gui.Resources; + +namespace Terminal.Gui; public partial class ColorPicker { @@ -42,7 +44,7 @@ public static bool Prompt (string title, Attribute? currentAttribute, out Attrib { X = Pos.Center () + 5, Y = 4, - Text = "Cancel", + Text = Strings.btnCancel, Width = Dim.Auto () }; diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index df2d9ddf41..6b9b1a3aee 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -402,7 +402,7 @@ public TextField () ContextMenu = new () { Host = this }; ContextMenu.KeyChanged += ContextMenu_KeyChanged; - KeyBindings.Add (ContextMenu.Key, KeyBindingScope.HotKey, Command.Context); + KeyBindings.Add (ContextMenu.Key, Command.Context); KeyBindings.Remove (Key.Space); } diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 065a346096..7773f882d3 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -2413,7 +2413,7 @@ public TextView () ContextMenu = new (); ContextMenu.KeyChanged += ContextMenu_KeyChanged!; - KeyBindings.Add ((KeyCode)ContextMenu.Key, KeyBindingScope.HotKey, Command.Context); + KeyBindings.Add (ContextMenu.Key, Command.Context); } private void TextView_Added1 (object? sender, SuperViewChangedEventArgs e) { throw new NotImplementedException (); } diff --git a/UICatalog/Scenario.cs b/UICatalog/Scenario.cs index b98d44b363..02d9c98df1 100644 --- a/UICatalog/Scenario.cs +++ b/UICatalog/Scenario.cs @@ -219,7 +219,7 @@ private void OnApplicationOnInitializedChanged (object? s, EventArgs a) private void OnApplicationOnIteration (object? s, IterationEventArgs a) { BenchmarkResults.IterationCount++; - if (BenchmarkResults.IterationCount > BENCHMARK_MAX_NATURAL_ITERATIONS + (_demoKeys.Count * BENCHMARK_KEY_PACING)) + if (BenchmarkResults.IterationCount > BENCHMARK_MAX_NATURAL_ITERATIONS + (_demoKeys!.Count * BENCHMARK_KEY_PACING)) { Application.RequestStop (); } diff --git a/UICatalog/Scenarios/Editors/EventLog.cs b/UICatalog/Scenarios/Editors/EventLog.cs index cf0042482d..6e71e27e76 100644 --- a/UICatalog/Scenarios/Editors/EventLog.cs +++ b/UICatalog/Scenarios/Editors/EventLog.cs @@ -1,8 +1,6 @@ #nullable enable using System; using System.Collections.ObjectModel; -using System.Diagnostics.Tracing; -using System.Text; using Terminal.Gui; namespace UICatalog.Scenarios; @@ -22,12 +20,15 @@ public EventLog () X = Pos.AnchorEnd (); Y = 0; - Width = Dim.Func (() => + + Width = Dim.Func ( + () => { if (!IsInitialized) { return 0; } + return Math.Min (SuperView!.Viewport.Width / 3, MaxLength + GetAdornmentsThickness ().Horizontal); }); Height = Dim.Fill (); @@ -42,17 +43,18 @@ public EventLog () HorizontalScrollBar.AutoShow = true; VerticalScrollBar.AutoShow = true; - AddCommand (Command.DeleteAll, - () => - { - _eventSource.Clear (); + AddCommand ( + Command.DeleteAll, + () => + { + _eventSource.Clear (); - return true; - }); + return true; + }); KeyBindings.Add (Key.Delete, Command.DeleteAll); - } + public ExpanderButton? ExpandButton { get; } private readonly ObservableCollection _eventSource = []; @@ -74,37 +76,16 @@ public View? ViewToLog if (_viewToLog is { }) { _viewToLog.Initialized += (s, args) => - { - View? sender = s as View; - Log ($"Initialized: {GetIdentifyingString (sender)}"); - }; - - _viewToLog.MouseClick += (s, args) => - { - Log ($"MouseClick: {args}"); - }; - - _viewToLog.HandlingHotKey += (s, args) => - { - if (args.Context is CommandContext keyCommandContext) - { - Log ($"HandlingHotKey: {args.Context.Command} {keyCommandContext.Binding.Data}"); - } - }; - _viewToLog.Selecting += (s, args) => - { - if (args.Context is CommandContext keyCommandContext) - { - Log ($"Selecting: {args.Context.Command} {keyCommandContext.Binding.Data}"); - } - }; - _viewToLog.Accepting += (s, args) => - { - if (args.Context is CommandContext keyCommandContext) - { - Log ($"Accepting: {args.Context.Command} {keyCommandContext.Binding.Data}"); - } - }; + { + var sender = s as View; + Log ($"Initialized: {GetIdentifyingString (sender)}"); + }; + + _viewToLog.MouseClick += (s, args) => { Log ($"MouseClick: {args}"); }; + _viewToLog.MouseWheel += (s, args) => { Log ($"MouseWheel: {args}"); }; + _viewToLog.HandlingHotKey += (s, args) => { Log ($"HandlingHotKey: {args.Context}"); }; + _viewToLog.Selecting += (s, args) => { Log ($"Selecting: {args.Context}"); }; + _viewToLog.Accepting += (s, args) => { Log ($"Accepting: {args.Context}"); }; } } } @@ -120,6 +101,7 @@ private void EventLog_Initialized (object? _, EventArgs e) Border?.Add (ExpandButton!); Source = new ListWrapper (_eventSource); } + private string GetIdentifyingString (View? view) { if (view is null) diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index e1407506ae..22bafcfe1a 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -65,8 +65,8 @@ public class TableEditor : Scenario "Cuneiform Numbers and Punctuation" ), new ( - (uint)(UICatalog.Scenarios.UnicodeRange.Ranges.Max (r => r.End) - 16), - (uint)UICatalog.Scenarios.UnicodeRange.Ranges.Max (r => r.End), + (uint)(Terminal.Gui.UnicodeRange.Ranges.Max (r => r.End) - 16), + (uint)Terminal.Gui.UnicodeRange.Ranges.Max (r => r.End), "End" ), new (0x0020, 0x007F, "Basic Latin"), From 7d4f4e4735cc16c842c3b980201c9e9ffdb396b1 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 6 Dec 2024 10:31:49 -0700 Subject: [PATCH 11/40] Tweaks --- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 17 ++++++------ Terminal.Gui/View/View.Command.cs | 5 +++- Terminal.Gui/View/View.Mouse.cs | 5 ++++ Terminal.Gui/Views/CharMap/CharMap.cs | 8 +++++- Terminal.Gui/Views/CheckBox.cs | 2 ++ UnitTests/Input/Keyboard/KeyBindingTests.cs | 10 +++++++ .../KeyBindingsTests.cs} | 26 ++++++++++++++----- UnitTests/Input/{ => Keyboard}/KeyTests.cs | 0 UnitTests/Views/CheckBoxTests.cs | 17 +++++++++--- UnitTests/Views/TimeFieldTests.cs | 4 +-- 10 files changed, 71 insertions(+), 23 deletions(-) create mode 100644 UnitTests/Input/Keyboard/KeyBindingTests.cs rename UnitTests/Input/{KeyBindingTests.cs => Keyboard/KeyBindingsTests.cs} (95%) rename UnitTests/Input/{ => Keyboard}/KeyTests.cs (100%) diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index e0cebe7cf7..a3ad842357 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -346,16 +346,17 @@ public void Remove (Key key, View? boundViewForAppScope = null) /// /// /// 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) + /// The set of commands to replace the old ones with. + public void ReplaceCommands (Key key, params Command [] newCommands) { if (TryGet (key, out KeyBinding binding)) { - binding.Commands = commands; + Remove (key); + Add (key, binding.Scope, newCommands); } else { - Add (key, commands); + Add (key, newCommands); } } @@ -418,10 +419,10 @@ public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding) { if (!key.IsValid) { - //if (BoundView is null) - //{ - // throw new InvalidOperationException ("KeyBindings must be bound to a View to use this method."); - //} + //if (BoundView is null) + //{ + // throw new InvalidOperationException ("KeyBindings must be bound to a View to use this method."); + //} binding = new (Array.Empty (), KeyBindingScope.Disabled, null); return false; diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index d0c4937838..5a8498df06 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -102,7 +102,10 @@ private void SetupCommands () } } - return SuperView?.InvokeCommand (Command.Accept, new ([Command.Accept], 0, null, this)) == true; + if (SuperView is { }) + { + return SuperView?.InvokeCommand (Command.Accept, new ([Command.Accept], 0, null, this)) is true; + } } return Accepting is null ? null : args.Cancel; diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index cb9b1522e2..1f52b41f6f 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -13,7 +13,12 @@ private void SetupMouse () { MouseBindings = new (); + // TODO: Should the default really work with any button or just button1? MouseBindings.Add (MouseFlags.Button1Clicked, Command.Select); + MouseBindings.Add (MouseFlags.Button2Clicked, Command.Select); + MouseBindings.Add (MouseFlags.Button3Clicked, Command.Select); + MouseBindings.Add (MouseFlags.Button4Clicked, Command.Select); + MouseBindings.Add (MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl, Command.Select); } diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 2da64ec22d..9f706bd514 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -59,7 +59,8 @@ public CharMap () KeyBindings.Add (ContextMenu.DefaultKey, Command.Context); MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept); - MouseBindings.Add (MouseFlags.Button3Clicked, Command.Context); + MouseBindings.ReplaceCommands(MouseFlags.Button3Clicked, Command.Context); + MouseBindings.ReplaceCommands (MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl, Command.Context); MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown); MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp); MouseBindings.Add (MouseFlags.WheeledLeft, Command.ScrollLeft); @@ -561,6 +562,11 @@ private bool TryGetCodePointFromPosition (Point position, out int codePoint) [RequiresDynamicCode ("AOT")] private void ShowDetails () { + if (!Application.Initialized) + { + // Some unit tests invoke Accept without Init + return; + } UcdApiClient? client = new (); var decResponse = string.Empty; var getCodePointError = string.Empty; diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index fcdc9beb82..6b7c7ad190 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -36,6 +36,8 @@ public CheckBox () // Accept (Enter key) - Raise Accept event - DO NOT advance state AddCommand (Command.Accept, RaiseAccepting); + MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept); + TitleChanged += Checkbox_TitleChanged; HighlightStyle = DefaultHighlightStyle; diff --git a/UnitTests/Input/Keyboard/KeyBindingTests.cs b/UnitTests/Input/Keyboard/KeyBindingTests.cs new file mode 100644 index 0000000000..9d011e407e --- /dev/null +++ b/UnitTests/Input/Keyboard/KeyBindingTests.cs @@ -0,0 +1,10 @@ +using Terminal.Gui.EnumExtensions; +using Xunit.Abstractions; + +namespace Terminal.Gui.InputTests; + +public class KeyBindingTests () +{ + // TODO: Add tests for KeyBinding + +} diff --git a/UnitTests/Input/KeyBindingTests.cs b/UnitTests/Input/Keyboard/KeyBindingsTests.cs similarity index 95% rename from UnitTests/Input/KeyBindingTests.cs rename to UnitTests/Input/Keyboard/KeyBindingsTests.cs index edade42391..f9a7d68ecd 100644 --- a/UnitTests/Input/KeyBindingTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -1,13 +1,11 @@ using Terminal.Gui.EnumExtensions; using Xunit.Abstractions; +using static Unix.Terminal.Delegates; namespace Terminal.Gui.InputTests; -public class KeyBindingTests +public class KeyBindingsTests () { - public KeyBindingTests (ITestOutputHelper output) { _output = output; } - private readonly ITestOutputHelper _output; - [Fact] public void Add_Invalid_Key_Throws () { @@ -72,7 +70,7 @@ public void Add_With_Bound_View_Throws_If_App_Scope () } // Add should not allow duplicates - [Fact] + [Fact] public void Add_With_Throws_If_Exists () { var keyBindings = new KeyBindings (new View ()); @@ -282,7 +280,7 @@ public void ReplaceKey_Throws_If_New_Is_Empty () [InlineData (KeyBindingScope.Application)] public void Scope_Add_Adds (KeyBindingScope scope) { - var keyBindings = new KeyBindings (scope.FastHasFlags(KeyBindingScope.Application) ? null : new ()); + var keyBindings = new KeyBindings (scope.FastHasFlags (KeyBindingScope.Application) ? null : new ()); Command [] commands = { Command.Right, Command.Left }; var key = new Key (Key.A); @@ -356,7 +354,7 @@ public void TryGet_Succeeds () keyBindings.Add (Key.Q.WithCtrl, KeyBindingScope.Application, Command.HotKey); var key = new Key (Key.Q.WithCtrl); bool result = keyBindings.TryGet (key, out KeyBinding _); - Assert.True (result);; + Assert.True (result); ; result = keyBindings.Bindings.TryGetValue (key, out KeyBinding _); Assert.True (result); @@ -379,4 +377,18 @@ public void TryGet_WithCommands_ReturnsTrue () Assert.True (result); Assert.Contains (Command.HotKey, bindings.Commands); } + + [Fact] + public void ReplaceCommands_Replaces () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); + + keyBindings.ReplaceCommands (Key.A, Command.Refresh); + + bool result = keyBindings.TryGet (Key.A, out KeyBinding bindings); + Assert.True (result); + Assert.Contains (Command.Refresh, bindings.Commands); + + } } diff --git a/UnitTests/Input/KeyTests.cs b/UnitTests/Input/Keyboard/KeyTests.cs similarity index 100% rename from UnitTests/Input/KeyTests.cs rename to UnitTests/Input/Keyboard/KeyTests.cs diff --git a/UnitTests/Views/CheckBoxTests.cs b/UnitTests/Views/CheckBoxTests.cs index 1579a1c2b1..a64c2e7912 100644 --- a/UnitTests/Views/CheckBoxTests.cs +++ b/UnitTests/Views/CheckBoxTests.cs @@ -252,7 +252,7 @@ void ViewOnAccept (object sender, CommandEventArgs e) [Fact] [SetupFakeDriver] - public void Mouse_Click () + public void Mouse_Click_Selects () { var checkBox = new CheckBox { Text = "_Checkbox" }; Assert.True (checkBox.CanFocus); @@ -296,7 +296,7 @@ public void Mouse_Click () [Fact] [SetupFakeDriver] - public void Mouse_DoubleClick () + public void Mouse_DoubleClick_Accepts () { var checkBox = new CheckBox { Text = "_Checkbox" }; Assert.True (checkBox.CanFocus); @@ -308,7 +308,11 @@ public void Mouse_DoubleClick () checkBox.Selecting += (s, e) => selectCount++; int acceptCount = 0; - checkBox.Accepting += (s, e) => acceptCount++; + checkBox.Accepting += (s, e) => + { + acceptCount++; + e.Cancel = true; + }; checkBox.HasFocus = true; Assert.True (checkBox.HasFocus); @@ -319,9 +323,14 @@ public void Mouse_DoubleClick () Assert.True (checkBox.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked })); + Assert.Equal (CheckState.UnChecked, checkBox.CheckedState); + Assert.Equal (0, checkedStateChangingCount); + Assert.Equal (0, selectCount); + Assert.Equal (1, acceptCount); + } -#endregion Mouse Tests + #endregion Mouse Tests [Fact] [AutoInitShutdown] diff --git a/UnitTests/Views/TimeFieldTests.cs b/UnitTests/Views/TimeFieldTests.cs index 1964472d20..1d901fcdc1 100644 --- a/UnitTests/Views/TimeFieldTests.cs +++ b/UnitTests/Views/TimeFieldTests.cs @@ -147,8 +147,8 @@ public void KeyBindings_Command () Assert.True (tf.NewKeyDownEvent (Key.End)); Assert.Equal (8, tf.CursorPosition); Assert.True (tf.NewKeyDownEvent (Key.A.WithCtrl)); - Assert.Equal (9, tf.CursorPosition); - Assert.Equal (tf.SelectedLength, tf.Text.Length); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (9, tf.Text.Length); Assert.True (tf.NewKeyDownEvent (Key.E.WithCtrl)); Assert.Equal (8, tf.CursorPosition); Assert.True (tf.NewKeyDownEvent (Key.CursorLeft)); From b443d529179980898d09a470e09073f91a4dd997 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 6 Dec 2024 12:22:29 -0700 Subject: [PATCH 12/40] More refactoring. Application scope is gone. --- .../Application/Application.Keyboard.cs | 48 ++-- .../Input/Keyboard/ApplicationKeyBinding.cs | 41 +++ .../Input/Keyboard/ApplicationKeyBindings.cs | 236 ++++++++++++++++++ .../Input/Keyboard/KeyBindingScope.cs | 17 +- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 134 +--------- Terminal.Gui/Views/Shortcut.cs | 39 ++- UICatalog/Scenarios/Arrangement.cs | 2 +- UICatalog/Scenarios/Bars.cs | 2 +- UICatalog/Scenarios/KeyBindings.cs | 20 +- UICatalog/Scenarios/Navigation.cs | 2 +- UICatalog/Scenarios/Shortcuts.cs | 10 +- UICatalog/UICatalog.cs | 2 +- UnitTests/Application/ApplicationTests.cs | 2 +- UnitTests/Application/KeyboardTests.cs | 50 +--- UnitTests/Input/Keyboard/KeyBindingsTests.cs | 105 ++++---- UnitTests/Views/ShortcutTests.cs | 14 +- 16 files changed, 402 insertions(+), 322 deletions(-) create mode 100644 Terminal.Gui/Input/Keyboard/ApplicationKeyBinding.cs create mode 100644 Terminal.Gui/Input/Keyboard/ApplicationKeyBindings.cs diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 34b63ceb75..0080ee0108 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -45,16 +45,16 @@ public static bool RaiseKeyDownEvent (Key key) // Invoke any Application-scoped KeyBindings. // The first view that handles the key will stop the loop. - foreach (KeyValuePair binding in KeyBindings.Bindings.Where (b => b.Key == key.KeyCode)) + foreach (KeyValuePair binding in KeyBindings.Bindings.Where (b => b.Key == key.KeyCode)) { - if (binding.Value.BoundView is { }) + if (binding.Value.Target is { }) { - if (!binding.Value.BoundView.Enabled) + if (!binding.Value.Target.Enabled) { return false; } - bool? handled = binding.Value.BoundView?.InvokeCommands (binding.Value.Commands, binding.Value); + bool? handled = binding.Value.Target?.InvokeCommands (binding.Value.Commands, binding.Value); if (handled != null && (bool)handled) { @@ -63,7 +63,7 @@ public static bool RaiseKeyDownEvent (Key key) } else { - if (!KeyBindings.TryGet (key, KeyBindingScope.Application, out KeyBinding appBinding)) + if (!KeyBindings.TryGet (key, out ApplicationKeyBinding appBinding)) { continue; } @@ -81,7 +81,7 @@ public static bool RaiseKeyDownEvent (Key key) return false; - static bool? InvokeCommand (Command command, Key key, KeyBinding appBinding) + static bool? InvokeCommand (Command command, Key key, ApplicationKeyBinding appBinding) { if (!CommandImplementations!.ContainsKey (command)) { @@ -92,7 +92,7 @@ public static bool RaiseKeyDownEvent (Key key) if (CommandImplementations.TryGetValue (command, out View.CommandImplementation? implementation)) { - var context = new CommandContext (command, appBinding); // Create the context here + var context = new CommandContext (command, appBinding); // Create the context here return implementation (context); } @@ -158,7 +158,7 @@ public static bool RaiseKeyUpEvent (Key key) static Application () { AddApplicationKeyBindings (); } /// Gets the Application-scoped key bindings. - public static KeyBindings KeyBindings { get; internal set; } = new (); + public static ApplicationKeyBindings KeyBindings { get; internal set; } = new (); internal static void AddApplicationKeyBindings () { @@ -241,42 +241,42 @@ internal static void AddApplicationKeyBindings () QuitKey = Key.Esc; ArrangeKey = Key.F5.WithCtrl; - KeyBindings.Add (QuitKey, KeyBindingScope.Application, Command.Quit); + KeyBindings.Add (QuitKey, Command.Quit); - KeyBindings.Add (Key.CursorRight, KeyBindingScope.Application, Command.NextTabStop); - KeyBindings.Add (Key.CursorDown, KeyBindingScope.Application, Command.NextTabStop); - KeyBindings.Add (Key.CursorLeft, KeyBindingScope.Application, Command.PreviousTabStop); - KeyBindings.Add (Key.CursorUp, KeyBindingScope.Application, Command.PreviousTabStop); - KeyBindings.Add (NextTabKey, KeyBindingScope.Application, Command.NextTabStop); - KeyBindings.Add (PrevTabKey, KeyBindingScope.Application, Command.PreviousTabStop); + KeyBindings.Add (Key.CursorRight, Command.NextTabStop); + KeyBindings.Add (Key.CursorDown, Command.NextTabStop); + KeyBindings.Add (Key.CursorLeft, Command.PreviousTabStop); + KeyBindings.Add (Key.CursorUp, Command.PreviousTabStop); + KeyBindings.Add (NextTabKey, Command.NextTabStop); + KeyBindings.Add (PrevTabKey, Command.PreviousTabStop); - KeyBindings.Add (NextTabGroupKey, KeyBindingScope.Application, Command.NextTabGroup); - KeyBindings.Add (PrevTabGroupKey, KeyBindingScope.Application, Command.PreviousTabGroup); + KeyBindings.Add (NextTabGroupKey, Command.NextTabGroup); + KeyBindings.Add (PrevTabGroupKey, Command.PreviousTabGroup); - KeyBindings.Add (ArrangeKey, KeyBindingScope.Application, Command.Edit); + KeyBindings.Add (ArrangeKey, Command.Edit); // TODO: Refresh Key should be configurable - KeyBindings.Add (Key.F5, KeyBindingScope.Application, Command.Refresh); + KeyBindings.Add (Key.F5, Command.Refresh); // TODO: Suspend Key should be configurable if (Environment.OSVersion.Platform == PlatformID.Unix) { - KeyBindings.Add (Key.Z.WithCtrl, KeyBindingScope.Application, Command.Suspend); + KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend); } } /// - /// Gets the list of Views that have key bindings. + /// Gets the list of s. /// /// /// This is an internal method used by the class to add Application key bindings. /// /// The list of Views that have Application-scoped key bindings. - internal static List GetViewKeyBindings () + internal static List GetViewKeyBindings () { // Get the list of views that do not have Application-scoped key bindings return KeyBindings.Bindings - .Where (kv => kv.Value.Scope != KeyBindingScope.Application) + .Where (kv => kv.Value.Target is {}) .Select (kv => kv.Value) .Distinct () .ToList (); @@ -295,7 +295,7 @@ private static void ReplaceKey (Key oldKey, Key newKey) } else { - if (KeyBindings.TryGet(oldKey, out KeyBinding binding)) + if (KeyBindings.TryGet (oldKey, out ApplicationKeyBinding binding)) { KeyBindings.Remove (oldKey); KeyBindings.Add (newKey, binding); diff --git a/Terminal.Gui/Input/Keyboard/ApplicationKeyBinding.cs b/Terminal.Gui/Input/Keyboard/ApplicationKeyBinding.cs new file mode 100644 index 0000000000..f29e57cd59 --- /dev/null +++ b/Terminal.Gui/Input/Keyboard/ApplicationKeyBinding.cs @@ -0,0 +1,41 @@ +#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 new file mode 100644 index 0000000000..f4002203a1 --- /dev/null +++ b/Terminal.Gui/Input/Keyboard/ApplicationKeyBindings.cs @@ -0,0 +1,236 @@ +#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); + } + + // TODO: This should not be public! + /// The collection of objects. + public Dictionary Bindings { get; } = new (new KeyEqualityComparer ()); + + /// + /// Gets the keys that are bound. + /// + /// + public IEnumerable GetBoundKeys () + { + return Bindings.Keys; + } + + /// 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 already bound to a set of s. + /// + /// The key to be replaced. + /// The new key to be used. If no action will be taken. + public void ReplaceKey (Key oldKey, Key newKey) + { + if (!TryGet (oldKey, out ApplicationKeyBinding _)) + { + throw new InvalidOperationException ($"Key {oldKey} is not bound."); + } + + if (!newKey.IsValid) + { + throw new InvalidOperationException ($"Key {newKey} is is not valid."); + } + + ApplicationKeyBinding binding = Bindings [oldKey]; + Remove (oldKey); + 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/KeyBindingScope.cs b/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs index 4c14fecd57..066cedc747 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs @@ -39,20 +39,5 @@ public enum KeyBindingScope /// /// /// - HotKey = 2, - - /// - /// The and the bound s will be invoked regardless of which View has focus. This is typically used - /// for global - /// commands, which are called Shortcuts. - /// - /// The View does not need to be visible. - /// - /// - /// Application-scoped key bindings are only invoked if the key down event was not handled by the focused view or - /// any of its subviews, and if the key was not bound to a . - /// - /// - /// - Application = 4 + HotKey = 2 } diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index a3ad842357..7264f09777 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -9,11 +9,11 @@ namespace Terminal.Gui; /// 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. 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; } @@ -21,18 +21,8 @@ public KeyBindings () { } /// Adds a to the collection. /// /// - /// Optional View for bindings. - public void Add (Key key, KeyBinding binding, View? boundViewForAppScope = null) + public void Add (Key key, KeyBinding binding) { - if (BoundView is { } && binding.Scope.FastHasFlags (KeyBindingScope.Application)) - { - throw new InvalidOperationException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add"); - } - - if (BoundView is { } && boundViewForAppScope is null) - { - boundViewForAppScope = BoundView; - } if (TryGet (key, out KeyBinding _)) { @@ -45,10 +35,6 @@ public void Add (Key key, KeyBinding binding, View? boundViewForAppScope = null) { binding.BoundView = BoundView; } - else - { - binding.BoundView = boundViewForAppScope; - } // 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 @@ -57,55 +43,6 @@ public void Add (Key key, KeyBinding binding, View? boundViewForAppScope = null) Bindings.Add (new (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"); - } - else - { - // boundViewForAppScope = BoundView; - } - - 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})."); - } - - Add (key, new KeyBinding (commands, scope, boundViewForAppScope), boundViewForAppScope); - } - /// /// Adds a new key combination that will trigger the commands in . /// @@ -126,16 +63,6 @@ public void Add (Key key, KeyBindingScope scope, View? boundViewForAppScope = nu /// public void Add (Key key, KeyBindingScope scope, params Command [] commands) { - if (BoundView is null && !scope.FastHasFlags (KeyBindingScope.Application)) - { - throw new InvalidOperationException ("BoundView cannot be null."); - } - - if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application)) - { - throw new InvalidOperationException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add"); - } - if (key == Key.Empty || !key.IsValid) { throw new ArgumentException (@"Invalid Key", nameof (commands)); @@ -151,43 +78,7 @@ public void Add (Key key, KeyBindingScope scope, params Command [] commands) throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); } - Add (key, new KeyBinding (commands, scope, BoundView), BoundView); - } - - /// - /// - /// 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); + Add (key, new KeyBinding (commands, scope, BoundView)); } /// @@ -197,7 +88,7 @@ public void Add (Key key, View? boundViewForAppScope = null, params Command [] c /// /// /// This is a helper function for . If used for a View ( - /// is set), the scope will be set to . + /// is set), the scope will be set to . /// Otherwise, it will be set to . /// /// @@ -217,12 +108,7 @@ public void Add (Key key, View? boundViewForAppScope = null, params Command [] c /// 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); + Add (key, KeyBindingScope.Focused, commands); } // TODO: Add a dictionary comparer that ignores Scope @@ -245,7 +131,7 @@ public IEnumerable GetBoundKeys () /// /// If the KeyBindings object is being used for Application.KeyBindings. /// - internal View? BoundView { get; } + public View? BoundView { get; init; } /// Removes all objects from the collection. public void Clear () { Bindings.Clear (); } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 5305ea3175..9029352178 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -18,7 +18,7 @@ namespace Terminal.Gui; /// - Pressing the HotKey specified by . /// /// -/// If is , will invoke +/// If is , will invoke /// /// regardless of what View has focus, enabling an application-wide keyboard shortcut. /// @@ -117,7 +117,7 @@ public Shortcut (Key key, string? commandText, Action? action, string? helpText { Id = "CommandView", Width = Dim.Auto (), - Height = Dim.Fill() + Height = Dim.Fill () }; Title = commandText ?? string.Empty; @@ -144,7 +144,7 @@ internal Dim GetWidthDimAuto () DimAutoStyle.Content, minimumContentDim: Dim.Func (() => _minimumNaturalWidth ?? 0), maximumContentDim: Dim.Func (() => _minimumNaturalWidth ?? 0))!; -} + } private AlignmentModes _alignmentModes = AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast; @@ -224,7 +224,7 @@ private void ForceCalculateNaturalWidth () _minimumNaturalWidth = PosAlign.CalculateMinDimension (0, Subviews, Dimension.Width); // Reset our relative layout - SetRelativeLayout (SuperView?.GetContentSize() ?? Application.Screen.Size); + SetRelativeLayout (SuperView?.GetContentSize () ?? Application.Screen.Size); } // TODO: Enable setting of the margin thickness @@ -331,7 +331,7 @@ private void AddCommands () if (commandContext?.Command != Command.Accept) { - // return false; + // return false; } if (Action is { }) @@ -615,32 +615,31 @@ public Key Key } } - private KeyBindingScope _keyBindingScope = KeyBindingScope.HotKey; + private bool _bindKeyToApplication = false; /// - /// Gets or sets the scope for the key binding for how is bound to . + /// Gets or sets whether is bound to via (as a HotKey) or . /// - public KeyBindingScope KeyBindingScope + public bool BindKeyToApplication { - get => _keyBindingScope; + get => _bindKeyToApplication; set { - if (value == _keyBindingScope) + if (value == _bindKeyToApplication) { return; } - if (_keyBindingScope == KeyBindingScope.Application) + if (_bindKeyToApplication) { - Application.KeyBindings.Remove (Key, this); + Application.KeyBindings.Remove (Key); } - - if (_keyBindingScope is KeyBindingScope.HotKey or KeyBindingScope.Focused) + else { KeyBindings.Remove (Key); } - _keyBindingScope = value; + _bindKeyToApplication = value; UpdateKeyBindings (Key.Empty); } @@ -703,14 +702,14 @@ private void UpdateKeyBindings (Key oldKey) { if (Key.IsValid) { - if (KeyBindingScope.FastHasFlags (KeyBindingScope.Application)) + if (BindKeyToApplication) { if (oldKey != Key.Empty) { - Application.KeyBindings.Remove (oldKey, this); + Application.KeyBindings.Remove (oldKey); } - Application.KeyBindings.Remove (Key, this); + Application.KeyBindings.Remove (Key); Application.KeyBindings.Add (Key, this, Command.HotKey); } else @@ -721,7 +720,7 @@ private void UpdateKeyBindings (Key oldKey) } KeyBindings.Remove (Key); - KeyBindings.Add (Key, KeyBindingScope | KeyBindingScope.HotKey, Command.HotKey); + KeyBindings.Add (Key, KeyBindingScope.HotKey, Command.HotKey); } } } @@ -769,7 +768,7 @@ internal void SetColors (bool highlight = false) if (_nonFocusColorScheme is { }) { base.ColorScheme = _nonFocusColorScheme; - //_nonFocusColorScheme = null; + //_nonFocusColorScheme = null; } else { diff --git a/UICatalog/Scenarios/Arrangement.cs b/UICatalog/Scenarios/Arrangement.cs index 48ed9f225e..33ac9ee84c 100644 --- a/UICatalog/Scenarios/Arrangement.cs +++ b/UICatalog/Scenarios/Arrangement.cs @@ -134,7 +134,7 @@ public override void Main () { Title = "Toggle Hide", Text = "App", - KeyBindingScope = KeyBindingScope.Application, + BindKeyToApplication = true, Key = Key.F4.WithCtrl, Action = () => { diff --git a/UICatalog/Scenarios/Bars.cs b/UICatalog/Scenarios/Bars.cs index 766b4f83a9..2c0a78d1ec 100644 --- a/UICatalog/Scenarios/Bars.cs +++ b/UICatalog/Scenarios/Bars.cs @@ -164,7 +164,7 @@ private void App_Loaded (object sender, EventArgs e) { Title = "Toggle Hide", Text = "App", - KeyBindingScope = KeyBindingScope.Application, + BindKeyToApplication = true, Key = Key.F4.WithCtrl, }; popOverMenu.Add (toggleShortcut); diff --git a/UICatalog/Scenarios/KeyBindings.cs b/UICatalog/Scenarios/KeyBindings.cs index fb4aab947b..fc0db78a98 100644 --- a/UICatalog/Scenarios/KeyBindings.cs +++ b/UICatalog/Scenarios/KeyBindings.cs @@ -83,7 +83,7 @@ Pressing Esc or {Application.QuitKey} will cause it to quit the app. foreach (var key in Application.KeyBindings.GetBoundKeys()) { var binding = Application.KeyBindings.Get (key); - appBindings.Add ($"{key} -> {binding.BoundView?.GetType ().Name} - {binding.Commands [0]}"); + appBindings.Add ($"{key} -> {binding.Target?.GetType ().Name} - {binding.Commands [0]}"); } ObservableCollection hotkeyBindings = new (); @@ -165,29 +165,17 @@ public KeyBindingsDemo () AddCommand (Command.Save, ctx => { - if (ctx is not CommandContext keyCommandContext) - { - return false; - } - MessageBox.Query ($"{keyCommandContext.Binding.Scope}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); return true; }); AddCommand (Command.New, ctx => { - if (ctx is not CommandContext keyCommandContext) - { - return false; - } - MessageBox.Query ($"{keyCommandContext.Binding.Scope}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); return true; }); AddCommand (Command.HotKey, ctx => { - if (ctx is not CommandContext keyCommandContext) - { - return false; - } - MessageBox.Query ($"{keyCommandContext.Binding.Scope}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}\nCommand: {ctx.Command}", buttons: "Ok"); SetFocus (); return true; }); diff --git a/UICatalog/Scenarios/Navigation.cs b/UICatalog/Scenarios/Navigation.cs index 7352fc7053..7027e13794 100644 --- a/UICatalog/Scenarios/Navigation.cs +++ b/UICatalog/Scenarios/Navigation.cs @@ -150,7 +150,7 @@ public override void Main () { Title = "Toggle Hide", Text = "App", - KeyBindingScope = KeyBindingScope.Application, + BindKeyToApplication = true, Key = Key.F4.WithCtrl, Action = () => { diff --git a/UICatalog/Scenarios/Shortcuts.cs b/UICatalog/Scenarios/Shortcuts.cs index 5390e015d8..32d82491bc 100644 --- a/UICatalog/Scenarios/Shortcuts.cs +++ b/UICatalog/Scenarios/Shortcuts.cs @@ -63,7 +63,6 @@ private void App_Loaded (object? sender, EventArgs e) HighlightStyle = HighlightStyle.None, }, Key = Key.F5.WithCtrl.WithAlt.WithShift, - KeyBindingScope = KeyBindingScope.HotKey, }; // ((CheckBox)vShortcut3.CommandView).CheckedStateChanging += (_, args) => @@ -111,7 +110,6 @@ private void App_Loaded (object? sender, EventArgs e) HighlightStyle = HighlightStyle.None, }, Key = Key.F.WithCtrl, - KeyBindingScope = KeyBindingScope.HotKey, }; ((CheckBox)commandFirstShortcut.CommandView).CheckedState = commandFirstShortcut.AlignmentModes.HasFlag (AlignmentModes.EndToStart) ? CheckState.UnChecked : CheckState.Checked; @@ -151,7 +149,6 @@ private void App_Loaded (object? sender, EventArgs e) Width = Dim.Fill ()! - Dim.Width (eventLog), Key = Key.F4, HelpText = "Changes all Command.CanFocus", - KeyBindingScope = KeyBindingScope.HotKey, CommandView = new CheckBox { Text = "_CanFocus" }, }; @@ -183,7 +180,7 @@ private void App_Loaded (object? sender, EventArgs e) Title = "A_pp Shortcut", Key = Key.F1, Text = "Width is DimFill", - KeyBindingScope = KeyBindingScope.Application, + BindKeyToApplication = true, }; Application.Top.Add (appShortcut); @@ -203,7 +200,6 @@ private void App_Loaded (object? sender, EventArgs e) HighlightStyle = HighlightStyle.None }, Key = Key.K, - KeyBindingScope = KeyBindingScope.HotKey, }; var button = (Button)buttonShortcut.CommandView; buttonShortcut.Accepting += Button_Clicked; @@ -218,7 +214,6 @@ private void App_Loaded (object? sender, EventArgs e) Y = Pos.Bottom (buttonShortcut), Key = Key.F2, Width = Dim.Fill ()! - Dim.Width (eventLog), - KeyBindingScope = KeyBindingScope.HotKey, CommandView = new RadioGroup { Orientation = Orientation.Vertical, @@ -244,7 +239,6 @@ private void App_Loaded (object? sender, EventArgs e) X = 0, Y = Pos.Bottom (radioGroupShortcut), Width = Dim.Fill ()! - Dim.Width (eventLog), - KeyBindingScope = KeyBindingScope.HotKey, HelpText = "Sliders work!", CommandView = new Slider { @@ -457,7 +451,7 @@ private void App_Loaded (object? sender, EventArgs e) X = Pos.Align (Alignment.Start, AlignmentModes.IgnoreFirstOrLast, 1), Y = Pos.AnchorEnd () - 1, Key = Key.Esc, - KeyBindingScope = KeyBindingScope.Application, + BindKeyToApplication = true, Title = "Quit", HelpText = "App Scope", }; diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 98617276a9..ff453b3743 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -761,7 +761,7 @@ public UICatalogTopLevel () CanFocus = false }, HelpText = "", - KeyBindingScope = KeyBindingScope.Application, + BindKeyToApplication = true, Key = Key.F7 }; diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 333e4f9422..cc7e1cbca8 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -358,7 +358,7 @@ void CheckReset () Application.PrevTabGroupKey = Key.A; Application.NextTabGroupKey = Key.B; Application.QuitKey = Key.C; - Application.KeyBindings.Add (Key.D, KeyBindingScope.Application, Command.Cancel); + Application.KeyBindings.Add (Key.D, Command.Cancel); Application._cachedViewsUnderMouse.Clear (); diff --git a/UnitTests/Application/KeyboardTests.cs b/UnitTests/Application/KeyboardTests.cs index 99ea9df29e..2869e55bcf 100644 --- a/UnitTests/Application/KeyboardTests.cs +++ b/UnitTests/Application/KeyboardTests.cs @@ -135,22 +135,22 @@ public void EnsuresTopOnFront_CanFocus_True_By_Keyboard () [Fact] [AutoInitShutdown] - public void KeyBinding_Application_KeyBindings_Add_Adds () + public void KeyBindings_Add_Adds () { - Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); - Application.KeyBindings.Add (Key.B, KeyBindingScope.Application, Command.Accept); + Application.KeyBindings.Add (Key.A, Command.Accept); + Application.KeyBindings.Add (Key.B, Command.Accept); - Assert.True (Application.KeyBindings.TryGet (Key.A, out KeyBinding binding)); - Assert.Null (binding.BoundView); + Assert.True (Application.KeyBindings.TryGet (Key.A, out ApplicationKeyBinding binding)); + Assert.Null (binding.Target); Assert.True (Application.KeyBindings.TryGet (Key.B, out binding)); - Assert.Null (binding.BoundView); + Assert.Null (binding.Target); } [Fact] [AutoInitShutdown] - public void KeyBinding_Application_RemoveKeyBinding_Removes () + public void KeyBindings_Remove_Removes () { - Application.KeyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); + Application.KeyBindings.Add (Key.A, Command.Accept); Assert.True (Application.KeyBindings.TryGet (Key.A, out _)); @@ -159,7 +159,7 @@ public void KeyBinding_Application_RemoveKeyBinding_Removes () } [Fact] - public void KeyBinding_OnKeyDown () + public void KeyBindings_OnKeyDown () { Application.Top = new Toplevel (); var view = new ScopedKeyBindingView (); @@ -204,7 +204,7 @@ public void KeyBinding_OnKeyDown () [Fact] [AutoInitShutdown] - public void KeyBinding_OnKeyDown_Negative () + public void KeyBindings_OnKeyDown_Negative () { var view = new ScopedKeyBindingView (); var keyWasHandled = false; @@ -230,36 +230,6 @@ public void KeyBinding_OnKeyDown_Negative () top.Dispose (); } - [Fact] - [AutoInitShutdown] - public void KeyBinding_View_KeyBindings_Add_Adds () - { - View view1 = new (); - Application.KeyBindings.Add (Key.A, view1, Command.Accept); - - View view2 = new (); - Application.KeyBindings.Add (Key.B, view2, Command.Accept); - - Assert.True (Application.KeyBindings.TryGet (Key.A, out KeyBinding binding)); - Assert.Equal (view1, binding.BoundView); - Assert.True (Application.KeyBindings.TryGet (Key.B, out binding)); - Assert.Equal (view2, binding.BoundView); - } - - [Fact] - [AutoInitShutdown] - public void KeyBinding_View_KeyBindings_RemoveKeyBinding_Removes () - { - View view1 = new (); - Application.KeyBindings.Add (Key.A, view1, Command.Accept); - - View view2 = new (); - Application.KeyBindings.Add (Key.B, view1, Command.Accept); - - Application.KeyBindings.Remove (Key.A, view1); - Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); - } - [Fact] public void KeyUp_Event () { diff --git a/UnitTests/Input/Keyboard/KeyBindingsTests.cs b/UnitTests/Input/Keyboard/KeyBindingsTests.cs index f9a7d68ecd..7fd67ca8fb 100644 --- a/UnitTests/Input/Keyboard/KeyBindingsTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -14,26 +14,18 @@ public void Add_Invalid_Key_Throws () Assert.Throws (() => keyBindings.Add (Key.Empty, KeyBindingScope.HotKey, Command.Accept)); } - [Fact] - public void Add_BoundView_Null_Non_AppScope_Throws () - { - var keyBindings = new KeyBindings (); - List commands = new (); - Assert.Throws (() => keyBindings.Add (Key.Empty, KeyBindingScope.HotKey, Command.Accept)); - } - [Fact] public void Add_Multiple_Adds () { - var keyBindings = new KeyBindings (); - Command [] commands = { Command.Right, Command.Left }; + var keyBindings = new KeyBindings (new ()); + Command [] commands = [Command.Right, Command.Left]; - keyBindings.Add (Key.A, KeyBindingScope.Application, commands); + keyBindings.Add (Key.A, commands); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.Right, resultCommands); Assert.Contains (Command.Left, resultCommands); - keyBindings.Add (Key.B, KeyBindingScope.Application, commands); + keyBindings.Add (Key.B, commands); resultCommands = keyBindings.GetCommands (Key.B); Assert.Contains (Command.Right, resultCommands); Assert.Contains (Command.Left, resultCommands); @@ -42,7 +34,7 @@ public void Add_Multiple_Adds () [Fact] public void Add_No_Commands_Throws () { - var keyBindings = new KeyBindings (); + var keyBindings = new KeyBindings (new()); List commands = new (); Assert.Throws (() => keyBindings.Add (Key.A, commands.ToArray ())); } @@ -50,25 +42,17 @@ public void Add_No_Commands_Throws () [Fact] public void Add_Single_Adds () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); + var keyBindings = new KeyBindings (new()); + keyBindings.Add (Key.A, Command.HotKey); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.HotKey, resultCommands); - keyBindings.Add (Key.B, KeyBindingScope.Application, Command.HotKey); + keyBindings.Add (Key.B, Command.HotKey); resultCommands = keyBindings.GetCommands (Key.B); Assert.Contains (Command.HotKey, resultCommands); } - // Add should not allow duplicates - [Fact] - public void Add_With_Bound_View_Throws_If_App_Scope () - { - var keyBindings = new KeyBindings (new View ()); - Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept)); - } - // Add should not allow duplicates [Fact] public void Add_With_Throws_If_Exists () @@ -106,8 +90,8 @@ public void Add_With_Throws_If_Exists () [Fact] public void Clear_Clears () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.B, KeyBindingScope.Application, Command.HotKey); + var keyBindings = new KeyBindings (new ()); + keyBindings.Add (Key.B, Command.HotKey); keyBindings.Clear (); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Empty (resultCommands); @@ -118,25 +102,25 @@ public void Clear_Clears () [Fact] public void Defaults () { - var keyBindings = new KeyBindings (); + var keyBindings = new KeyBindings (new()); Assert.Empty (keyBindings.Bindings); Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept)); - Assert.Null (keyBindings.BoundView); + Assert.NotNull (keyBindings.BoundView); } [Fact] public void Get_Binding_Not_Found_Throws () { - var keyBindings = new KeyBindings (); + var keyBindings = new KeyBindings (new()); Assert.Throws (() => keyBindings.Get (Key.A)); - Assert.Throws (() => keyBindings.Get (Key.B, KeyBindingScope.Application)); + Assert.Throws (() => keyBindings.Get (Key.B)); } // GetCommands [Fact] public void GetCommands_Unknown_ReturnsEmpty () { - var keyBindings = new KeyBindings (); + var keyBindings = new KeyBindings (new ()); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Empty (resultCommands); } @@ -144,8 +128,8 @@ public void GetCommands_Unknown_ReturnsEmpty () [Fact] public void GetCommands_WithCommands_ReturnsCommands () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); + var keyBindings = new KeyBindings (new()); + keyBindings.Add (Key.A, Command.HotKey); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.HotKey, resultCommands); } @@ -153,10 +137,10 @@ public void GetCommands_WithCommands_ReturnsCommands () [Fact] public void GetCommands_WithMultipleBindings_ReturnsCommands () { - var keyBindings = new KeyBindings (); + var keyBindings = new KeyBindings (new ()); Command [] commands = { Command.Right, Command.Left }; - keyBindings.Add (Key.A, KeyBindingScope.Application, commands); - keyBindings.Add (Key.B, KeyBindingScope.Application, commands); + keyBindings.Add (Key.A,commands); + keyBindings.Add (Key.B,commands); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.Right, resultCommands); Assert.Contains (Command.Left, resultCommands); @@ -168,9 +152,9 @@ public void GetCommands_WithMultipleBindings_ReturnsCommands () [Fact] public void GetCommands_WithMultipleCommands_ReturnsCommands () { - var keyBindings = new KeyBindings (); + var keyBindings = new KeyBindings (new()); Command [] commands = { Command.Right, Command.Left }; - keyBindings.Add (Key.A, KeyBindingScope.Application, commands); + keyBindings.Add (Key.A, commands); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.Right, resultCommands); Assert.Contains (Command.Left, resultCommands); @@ -179,12 +163,12 @@ public void GetCommands_WithMultipleCommands_ReturnsCommands () [Fact] public void GetKeyFromCommands_MultipleCommands () { - var keyBindings = new KeyBindings (); + var keyBindings = new KeyBindings (new()); Command [] commands1 = { Command.Right, Command.Left }; - keyBindings.Add (Key.A, KeyBindingScope.Application, commands1); + keyBindings.Add (Key.A, commands1); Command [] commands2 = { Command.Up, Command.Down }; - keyBindings.Add (Key.B, KeyBindingScope.Application, commands2); + keyBindings.Add (Key.B,commands2); Key key = keyBindings.GetKeyFromCommands (commands1); Assert.Equal (Key.A, key); @@ -196,8 +180,8 @@ public void GetKeyFromCommands_MultipleCommands () [Fact] public void GetKeyFromCommands_OneCommand () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Right); + var keyBindings = new KeyBindings (new()); + keyBindings.Add (Key.A, Command.Right); Key key = keyBindings.GetKeyFromCommands (Command.Right); Assert.Equal (Key.A, key); @@ -207,15 +191,15 @@ public void GetKeyFromCommands_OneCommand () [Fact] public void GetKeyFromCommands_Unknown_Returns_Key_Empty () { - var keyBindings = new KeyBindings (); + var keyBindings = new KeyBindings(new()); Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept)); } [Fact] public void GetKeyFromCommands_WithCommands_ReturnsKey () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); + var keyBindings = new KeyBindings (new()); + keyBindings.Add (Key.A, Command.HotKey); Key resultKey = keyBindings.GetKeyFromCommands (Command.HotKey); Assert.Equal (Key.A, resultKey); } @@ -261,15 +245,15 @@ public void ReplaceKey_Replaces_Leaves_Old_Binding () [Fact] public void ReplaceKey_Throws_If_DoesNotContain_Old () { - var keyBindings = new KeyBindings (); + var keyBindings = new KeyBindings(new()); Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.B)); } [Fact] public void ReplaceKey_Throws_If_New_Is_Empty () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); + var keyBindings = new KeyBindings(new()); + keyBindings.Add (Key.A,Command.HotKey); Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.Empty)); } @@ -277,10 +261,9 @@ public void ReplaceKey_Throws_If_New_Is_Empty () [Theory] [InlineData (KeyBindingScope.Focused)] [InlineData (KeyBindingScope.HotKey)] - [InlineData (KeyBindingScope.Application)] public void Scope_Add_Adds (KeyBindingScope scope) { - var keyBindings = new KeyBindings (scope.FastHasFlags (KeyBindingScope.Application) ? null : new ()); + var keyBindings = new KeyBindings (new ()); Command [] commands = { Command.Right, Command.Left }; var key = new Key (Key.A); @@ -301,10 +284,9 @@ public void Scope_Add_Adds (KeyBindingScope scope) [Theory] [InlineData (KeyBindingScope.Focused)] [InlineData (KeyBindingScope.HotKey)] - [InlineData (KeyBindingScope.Application)] public void Scope_Get_Filters (KeyBindingScope scope) { - var keyBindings = new KeyBindings (scope.FastHasFlags (KeyBindingScope.Application) ? null : new ()); + var keyBindings = new KeyBindings (new ()); Command [] commands = { Command.Right, Command.Left }; var key = new Key (Key.A); @@ -321,10 +303,9 @@ public void Scope_Get_Filters (KeyBindingScope scope) [Theory] [InlineData (KeyBindingScope.Focused)] [InlineData (KeyBindingScope.HotKey)] - [InlineData (KeyBindingScope.Application)] public void Scope_TryGet_Filters (KeyBindingScope scope) { - var keyBindings = new KeyBindings (scope.FastHasFlags (KeyBindingScope.Application) ? null : new ()); + var keyBindings = new KeyBindings (new ()); Command [] commands = { Command.Right, Command.Left }; var key = new Key (Key.A); @@ -350,8 +331,8 @@ public void Scope_TryGet_Filters (KeyBindingScope scope) [Fact] public void TryGet_Succeeds () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.Q.WithCtrl, KeyBindingScope.Application, Command.HotKey); + var keyBindings = new KeyBindings(new()); + keyBindings.Add (Key.Q.WithCtrl,Command.HotKey); var key = new Key (Key.Q.WithCtrl); bool result = keyBindings.TryGet (key, out KeyBinding _); Assert.True (result); ; @@ -363,7 +344,7 @@ public void TryGet_Succeeds () [Fact] public void TryGet_Unknown_ReturnsFalse () { - var keyBindings = new KeyBindings (); + var keyBindings = new KeyBindings(new()); bool result = keyBindings.TryGet (Key.A, out KeyBinding _); Assert.False (result); } @@ -371,8 +352,8 @@ public void TryGet_Unknown_ReturnsFalse () [Fact] public void TryGet_WithCommands_ReturnsTrue () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); + var keyBindings = new KeyBindings(new()); + keyBindings.Add (Key.A,Command.HotKey); bool result = keyBindings.TryGet (Key.A, out KeyBinding bindings); Assert.True (result); Assert.Contains (Command.HotKey, bindings.Commands); @@ -381,8 +362,8 @@ public void TryGet_WithCommands_ReturnsTrue () [Fact] public void ReplaceCommands_Replaces () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.Accept); + var keyBindings = new KeyBindings(new()); + keyBindings.Add (Key.A,Command.Accept); keyBindings.ReplaceCommands (Key.A, Command.Refresh); diff --git a/UnitTests/Views/ShortcutTests.cs b/UnitTests/Views/ShortcutTests.cs index 691d862888..50f0e399ad 100644 --- a/UnitTests/Views/ShortcutTests.cs +++ b/UnitTests/Views/ShortcutTests.cs @@ -294,7 +294,7 @@ public void KeyBindingScope_Defaults_To_HotKey () { var shortcut = new Shortcut (); - Assert.Equal (KeyBindingScope.HotKey, shortcut.KeyBindingScope); + Assert.False (shortcut.BindKeyToApplication); } [Fact] @@ -302,9 +302,9 @@ public void KeyBindingScope_Can_Be_Set () { var shortcut = new Shortcut (); - shortcut.KeyBindingScope = KeyBindingScope.Application; + shortcut.BindKeyToApplication = true; - Assert.Equal (KeyBindingScope.Application, shortcut.KeyBindingScope); + Assert.True (shortcut.BindKeyToApplication); } [Fact] @@ -315,11 +315,11 @@ public void KeyBindingScope_Changing_Adjusts_KeyBindings () shortcut.Key = Key.A; Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys); - shortcut.KeyBindingScope = KeyBindingScope.Application; + shortcut.BindKeyToApplication = true; Assert.DoesNotContain (Key.A, shortcut.KeyBindings.Bindings.Keys); Assert.Contains (Key.A, Application.KeyBindings.Bindings.Keys); - shortcut.KeyBindingScope = KeyBindingScope.HotKey; + shortcut.BindKeyToApplication = false; Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys); Assert.DoesNotContain (Key.A, Application.KeyBindings.Bindings.Keys); } @@ -789,7 +789,7 @@ public void KeyDown_App_Scope_Invokes_Accept (KeyCode key, int expectedAccept) var shortcut = new Shortcut { Key = Key.A, - KeyBindingScope = KeyBindingScope.Application, + BindKeyToApplication = true, Text = "0", Title = "_C" }; @@ -867,7 +867,7 @@ public void KeyDown_App_Scope_Invokes_Action (bool canFocus, KeyCode key, int ex var shortcut = new Shortcut { Key = Key.A, - KeyBindingScope = KeyBindingScope.Application, + BindKeyToApplication = true, Text = "0", Title = "_C", CanFocus = canFocus From 0c7d1aee8fa99e0478d753df4bedae325c3d5788 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 6 Dec 2024 14:16:22 -0700 Subject: [PATCH 13/40] More refactoring. Broke CM --- Terminal.Gui/Input/Command.cs | 9 ++- .../Input/Keyboard/KeyBindingScope.cs | 2 +- Terminal.Gui/View/View.Keyboard.cs | 62 +++++++------------ Terminal.Gui/Views/Menu/ContextMenu.cs | 5 ++ UnitTests/Application/KeyboardTests.cs | 1 + 5 files changed, 37 insertions(+), 42 deletions(-) diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs index 39500a3d2c..0d7be7ea80 100644 --- a/Terminal.Gui/Input/Command.cs +++ b/Terminal.Gui/Input/Command.cs @@ -4,9 +4,14 @@ namespace Terminal.Gui; /// -/// Actions which can be performed by a . Commands are typically invoked via -/// and . +/// Actions which can be performed by a . /// +/// +/// +/// +/// +/// supports a subset of these commands by default, which can be overriden via . +/// public enum Command { #region Base View Commands diff --git a/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs b/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs index 066cedc747..4f553a3761 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs @@ -38,6 +38,6 @@ public enum KeyBindingScope /// /// /// - /// + /// HotKey = 2 } diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index b3dbf94f91..7f3f97d02e 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -1,5 +1,6 @@ #nullable enable using System.Diagnostics; +using System.Reflection.Metadata; namespace Terminal.Gui; @@ -296,6 +297,12 @@ public bool NewKeyDownEvent (Key key) return true; } + bool? handled = false; + if (InvokeCommandsBoundToHotKeyOnSubviews (key, ref handled)) + { + return true; + } + // After if (RaiseKeyDownNotHandled (key) || key.Handled) { @@ -499,9 +506,12 @@ bool RaiseKeyUp (Key k) #region Key Bindings - /// Gets the key bindings for this view. + /// Gets the bindings for this view that will be invoked only if this view has focus. public KeyBindings KeyBindings { get; internal set; } = null!; + /// Gets the bindings for this view that will be invoked regardless of whehter this view has focus or not. + public KeyBindings HotKeyBindings { get; internal set; } = null!; + /// /// INTERNAL API: Invokes any commands bound to on this view, adornments, and subviews. /// @@ -516,15 +526,13 @@ bool RaiseKeyUp (Key k) /// internal bool? InvokeCommandsBoundToKey (Key key) { - KeyBindingScope scope = KeyBindingScope.Focused | KeyBindingScope.HotKey; - // * If no key binding was found, `InvokeKeyBindings` returns `null`. // Continue passing the event (return `false` from `OnInvokeKeyBindings`). // * If key bindings were found, but none handled the key (all `Command`s returned `false`), // `InvokeKeyBindings` returns `false`. Continue passing the event (return `false` from `OnInvokeKeyBindings`).. // * If key bindings were found, and any handled the key (at least one `Command` returned `true`), // `InvokeKeyBindings` returns `true`. Continue passing the event (return `false` from `OnInvokeKeyBindings`). - bool? handled = InvokeCommandsBoundToKey (key, scope); + bool? handled = InvokeCommandsBoundToKey (key, KeyBindingScope.Focused); if (handled is true) { @@ -533,22 +541,17 @@ bool RaiseKeyUp (Key k) return handled; } - if (Margin is { } && InvokeCommandsBoundToKeyOnAdornment (Margin, key, scope, ref handled)) - { - return true; - } - - if (Padding is { } && InvokeCommandsBoundToKeyOnAdornment (Padding, key, scope, ref handled)) + if (Margin is { } && InvokeCommandsBoundToKeyOnAdornment (Margin, key, KeyBindingScope.Focused, ref handled)) { return true; } - if (Border is { } && InvokeCommandsBoundToKeyOnAdornment (Border, key, scope, ref handled)) + if (Padding is { } && InvokeCommandsBoundToKeyOnAdornment (Padding, key, KeyBindingScope.Focused, ref handled)) { return true; } - if (InvokeCommandsBoundToKeyOnSubviews (key, scope, ref handled)) + if (Border is { } && InvokeCommandsBoundToKeyOnAdornment (Border, key, KeyBindingScope.Focused, ref handled)) { return true; } @@ -588,8 +591,14 @@ private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Ke return false; } - private bool InvokeCommandsBoundToKeyOnSubviews (Key key, KeyBindingScope scope, ref bool? handled, bool invoke = true) + private bool InvokeCommandsBoundToHotKeyOnSubviews (Key key, ref bool? handled, bool invoke = true) { + bool? weHandled = InvokeCommandsBoundToKey (key, KeyBindingScope.HotKey); + if (weHandled is true) + { + return true; + } + // Now, process any key bindings in the subviews that are tagged to KeyBindingScope.HotKey. foreach (View subview in Subviews) { @@ -598,32 +607,7 @@ private bool InvokeCommandsBoundToKeyOnSubviews (Key key, KeyBindingScope scope, continue; } - if (subview.KeyBindings.TryGet (key, scope, out KeyBinding binding)) - { - if (binding.Scope == KeyBindingScope.Focused && !subview.HasFocus) - { - continue; - } - - if (!invoke) - { - return true; - } - - bool? subViewHandled = subview.InvokeCommandsBoundToKey (key); - - if (subViewHandled is { }) - { - handled = subViewHandled; - - if ((bool)subViewHandled) - { - return true; - } - } - } - - bool recurse = subview.InvokeCommandsBoundToKeyOnSubviews (key, scope, ref handled, invoke); + bool recurse = subview.InvokeCommandsBoundToHotKeyOnSubviews (key, ref handled, invoke); if (recurse || (handled is { } && (bool)handled)) { diff --git a/Terminal.Gui/Views/Menu/ContextMenu.cs b/Terminal.Gui/Views/Menu/ContextMenu.cs index a963036dcc..614df8ea73 100644 --- a/Terminal.Gui/Views/Menu/ContextMenu.cs +++ b/Terminal.Gui/Views/Menu/ContextMenu.cs @@ -110,6 +110,8 @@ public void Dispose () _menuBar = null; IsShow = false; + Application.KeyBindings.Remove (Key); + if (_container is { }) { _container.Closing -= Container_Closing; @@ -250,6 +252,9 @@ public void Show (MenuBarItem? menuItems) _menuBar._isContextMenuLoading = true; _menuBar.MenuAllClosed += MenuBar_MenuAllClosed; + + Application.KeyBindings.Add (Key, _menuBar, Command.Cancel); + _menuBar.BeginInit (); _menuBar.EndInit (); IsShow = true; diff --git a/UnitTests/Application/KeyboardTests.cs b/UnitTests/Application/KeyboardTests.cs index 2869e55bcf..a164b8842d 100644 --- a/UnitTests/Application/KeyboardTests.cs +++ b/UnitTests/Application/KeyboardTests.cs @@ -189,6 +189,7 @@ public void KeyBindings_OnKeyDown () keyWasHandled = false; Application.RaiseKeyDownEvent (Key.H); Assert.False (keyWasHandled); + Assert.True (view.HotKeyCommand); keyWasHandled = false; Assert.False (view.HasFocus); From 0586c303a7550619eac5434956fbf18f12cad8bb Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 6 Dec 2024 14:32:09 -0700 Subject: [PATCH 14/40] Fixed ContextMenu --- Terminal.Gui/View/View.Keyboard.cs | 2 +- Terminal.Gui/Views/Menu/ContextMenu.cs | 6 +----- Terminal.Gui/Views/Menu/Menu.cs | 3 ++- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 7f3f97d02e..6e70ad7a55 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -591,7 +591,7 @@ private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Ke return false; } - private bool InvokeCommandsBoundToHotKeyOnSubviews (Key key, ref bool? handled, bool invoke = true) + internal bool InvokeCommandsBoundToHotKeyOnSubviews (Key key, ref bool? handled, bool invoke = true) { bool? weHandled = InvokeCommandsBoundToKey (key, KeyBindingScope.HotKey); if (weHandled is true) diff --git a/Terminal.Gui/Views/Menu/ContextMenu.cs b/Terminal.Gui/Views/Menu/ContextMenu.cs index 614df8ea73..76908ce3a1 100644 --- a/Terminal.Gui/Views/Menu/ContextMenu.cs +++ b/Terminal.Gui/Views/Menu/ContextMenu.cs @@ -109,9 +109,7 @@ public void Dispose () _menuBar?.Dispose (); _menuBar = null; IsShow = false; - - Application.KeyBindings.Remove (Key); - + ` if (_container is { }) { _container.Closing -= Container_Closing; @@ -253,8 +251,6 @@ public void Show (MenuBarItem? menuItems) _menuBar._isContextMenuLoading = true; _menuBar.MenuAllClosed += MenuBar_MenuAllClosed; - Application.KeyBindings.Add (Key, _menuBar, Command.Cancel); - _menuBar.BeginInit (); _menuBar.EndInit (); IsShow = true; diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 9c7072538a..ff7c83bf41 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -250,7 +250,8 @@ protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocus protected override bool OnKeyDownNotHandled (Key keyEvent) { // We didn't handle the key, pass it on to host - return _host.InvokeCommandsBoundToKey (keyEvent) == true; + bool? handled = null; + return _host.InvokeCommandsBoundToHotKeyOnSubviews (keyEvent, ref handled, true ) == true; } protected override bool OnMouseEvent (MouseEventArgs me) From 7360683152a240e8d672dc04645f856b9745d319 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 6 Dec 2024 15:25:23 -0700 Subject: [PATCH 15/40] Prepping to combine keybinding classes --- .../Application/Application.Initialization.cs | 2 +- .../Application/Application.Keyboard.cs | 87 ++++++++----------- Terminal.Gui/Application/Application.cs | 3 +- .../Input/Keyboard/ApplicationKeyBindings.cs | 42 ++++++--- Terminal.Gui/Views/Menu/ContextMenu.cs | 2 +- UnitTests/Application/ApplicationTests.cs | 5 +- UnitTests/Views/ShortcutTests.cs | 4 +- 7 files changed, 73 insertions(+), 72 deletions(-) diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index d7ce7874bc..56e774a14d 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -92,7 +92,7 @@ internal static void InternalInit ( } } - AddApplicationKeyBindings (); + AddKeyBindings (); // Start the process of configuration management. // Note that we end up calling LoadConfigurationFromAllSources diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 0080ee0108..143b63f913 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -45,7 +45,7 @@ public static bool RaiseKeyDownEvent (Key key) // Invoke any Application-scoped KeyBindings. // The first view that handles the key will stop the loop. - foreach (KeyValuePair binding in KeyBindings.Bindings.Where (b => b.Key == key.KeyCode)) + foreach (KeyValuePair binding in KeyBindings.GetBindings (key)) { if (binding.Value.Target is { }) { @@ -155,12 +155,12 @@ public static bool RaiseKeyUpEvent (Key key) #region Application-scoped KeyBindings - static Application () { AddApplicationKeyBindings (); } + static Application () { AddKeyBindings (); } /// Gets the Application-scoped key bindings. public static ApplicationKeyBindings KeyBindings { get; internal set; } = new (); - internal static void AddApplicationKeyBindings () + internal static void AddKeyBindings () { CommandImplementations = new (); @@ -231,30 +231,31 @@ internal static void AddApplicationKeyBindings () return false; }); - KeyBindings.Clear (); // Resources/config.json overrides + QuitKey = Key.Esc; NextTabKey = Key.Tab; PrevTabKey = Key.Tab.WithShift; NextTabGroupKey = Key.F6; PrevTabGroupKey = Key.F6.WithShift; - QuitKey = Key.Esc; ArrangeKey = Key.F5.WithCtrl; - KeyBindings.Add (QuitKey, Command.Quit); + // Need to clear after setting the above to ensure actually clear + // because set_QuitKey etc.. may call Add + KeyBindings.Clear (); - KeyBindings.Add (Key.CursorRight, Command.NextTabStop); - KeyBindings.Add (Key.CursorDown, Command.NextTabStop); - KeyBindings.Add (Key.CursorLeft, Command.PreviousTabStop); - KeyBindings.Add (Key.CursorUp, Command.PreviousTabStop); + KeyBindings.Add (QuitKey, Command.Quit); KeyBindings.Add (NextTabKey, Command.NextTabStop); KeyBindings.Add (PrevTabKey, Command.PreviousTabStop); - KeyBindings.Add (NextTabGroupKey, Command.NextTabGroup); KeyBindings.Add (PrevTabGroupKey, Command.PreviousTabGroup); - KeyBindings.Add (ArrangeKey, Command.Edit); + KeyBindings.Add (Key.CursorRight, Command.NextTabStop); + KeyBindings.Add (Key.CursorDown, Command.NextTabStop); + KeyBindings.Add (Key.CursorLeft, Command.PreviousTabStop); + KeyBindings.Add (Key.CursorUp, Command.PreviousTabStop); + // TODO: Refresh Key should be configurable KeyBindings.Add (Key.F5, Command.Refresh); @@ -265,46 +266,32 @@ internal static void AddApplicationKeyBindings () } } - /// - /// Gets the list of s. - /// - /// - /// This is an internal method used by the class to add Application key bindings. - /// - /// The list of Views that have Application-scoped key bindings. - internal static List GetViewKeyBindings () - { - // Get the list of views that do not have Application-scoped key bindings - return KeyBindings.Bindings - .Where (kv => kv.Value.Target is {}) - .Select (kv => kv.Value) - .Distinct () - .ToList (); - } - private static void ReplaceKey (Key oldKey, Key newKey) { - if (KeyBindings.Bindings.Count == 0) - { - return; - } - - if (newKey == Key.Empty) - { - KeyBindings.Remove (oldKey); - } - else - { - if (KeyBindings.TryGet (oldKey, out ApplicationKeyBinding binding)) - { - KeyBindings.Remove (oldKey); - KeyBindings.Add (newKey, binding); - } - else - { - KeyBindings.Add (newKey, binding); - } - } + KeyBindings.ReplaceKey (oldKey, newKey); + + //return; + //if (KeyBindings.Bindings.Count == 0) + //{ + // return; + //} + + //if (newKey == Key.Empty) + //{ + // KeyBindings.Remove (oldKey); + //} + //else + //{ + // if (KeyBindings.TryGet (oldKey, out ApplicationKeyBinding binding)) + // { + // KeyBindings.Remove (oldKey); + // KeyBindings.Add (newKey, binding); + // } + // else + // { + // KeyBindings.Add (newKey, binding); + // } + //} } diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 7c0ff55296..79198349f1 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -214,7 +214,8 @@ internal static void ResetState (bool ignoreDisposed = false) ClearScreenNextIteration = false; - AddApplicationKeyBindings (); + KeyBindings.Clear (); + AddKeyBindings (); // Reset synchronization context to allow the user to run async/await, // as the main loop has been ended, the synchronization context from diff --git a/Terminal.Gui/Input/Keyboard/ApplicationKeyBindings.cs b/Terminal.Gui/Input/Keyboard/ApplicationKeyBindings.cs index f4002203a1..b0cd604bc6 100644 --- a/Terminal.Gui/Input/Keyboard/ApplicationKeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/ApplicationKeyBindings.cs @@ -82,9 +82,7 @@ public void Add (Key key, params Command [] commands) Add (key, binding); } - // TODO: This should not be public! - /// The collection of objects. - public Dictionary Bindings { get; } = new (new KeyEqualityComparer ()); + private Dictionary Bindings { get; } = new (new KeyEqualityComparer ()); /// /// Gets the keys that are bound. @@ -95,6 +93,16 @@ 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 (); } @@ -193,25 +201,33 @@ public void ReplaceCommands (Key key, params Command [] newCommands) throw new InvalidOperationException ($"Key {key} is not bound."); } - /// Replaces a key combination already bound to a set of s. - /// + /// 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 no action will be taken. + /// The new key to be used. If this method has the same effect as . public void ReplaceKey (Key oldKey, Key newKey) { - if (!TryGet (oldKey, out ApplicationKeyBinding _)) + if (!newKey.IsValid) { - throw new InvalidOperationException ($"Key {oldKey} is not bound."); + throw new InvalidOperationException ($"Key {newKey} is is not valid."); } - if (!newKey.IsValid) + if (newKey == Key.Empty) { - throw new InvalidOperationException ($"Key {newKey} is is not valid."); + Remove (oldKey); + return; } - ApplicationKeyBinding binding = Bindings [oldKey]; - Remove (oldKey); - Add (newKey, binding); + + if (TryGet (oldKey, out ApplicationKeyBinding binding)) + { + Remove (oldKey); + Add (newKey, binding); + } + else + { + Add (newKey, binding); + } } /// Gets the commands bound with the specified Key. diff --git a/Terminal.Gui/Views/Menu/ContextMenu.cs b/Terminal.Gui/Views/Menu/ContextMenu.cs index 76908ce3a1..851aae01e3 100644 --- a/Terminal.Gui/Views/Menu/ContextMenu.cs +++ b/Terminal.Gui/Views/Menu/ContextMenu.cs @@ -109,7 +109,7 @@ public void Dispose () _menuBar?.Dispose (); _menuBar = null; IsShow = false; - ` + if (_container is { }) { _container.Closing -= Container_Closing; diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index cc7e1cbca8..82e6846e10 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -317,9 +317,6 @@ void CheckReset () Assert.Empty (Application.TopLevels); Assert.Empty (Application._cachedViewsUnderMouse); - // Keyboard - Assert.Empty (Application.GetViewKeyBindings ()); - // Mouse Assert.Null (Application._lastMousePosition); @@ -555,7 +552,7 @@ public void Init_KeyBindings_Set_To_Custom () Assert.Equal (Key.Q.WithCtrl, Application.QuitKey); - Assert.Contains (Key.Q.WithCtrl, Application.KeyBindings.Bindings); + Assert.NotEmpty (Application.KeyBindings.GetBindings (Key.Q.WithCtrl)); Application.Shutdown (); Locations = ConfigLocations.Default; diff --git a/UnitTests/Views/ShortcutTests.cs b/UnitTests/Views/ShortcutTests.cs index 50f0e399ad..760cb70f6f 100644 --- a/UnitTests/Views/ShortcutTests.cs +++ b/UnitTests/Views/ShortcutTests.cs @@ -317,11 +317,11 @@ public void KeyBindingScope_Changing_Adjusts_KeyBindings () shortcut.BindKeyToApplication = true; Assert.DoesNotContain (Key.A, shortcut.KeyBindings.Bindings.Keys); - Assert.Contains (Key.A, Application.KeyBindings.Bindings.Keys); + Assert.NotEmpty (Application.KeyBindings.GetBindings(Key.A)); shortcut.BindKeyToApplication = false; Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys); - Assert.DoesNotContain (Key.A, Application.KeyBindings.Bindings.Keys); + Assert.Empty (Application.KeyBindings.GetBindings (Key.A)); } [Theory] From 725df05f7b266b79e42f09ea638d55347d55ee6d Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 6 Dec 2024 16:14:51 -0700 Subject: [PATCH 16/40] Prepping to combine keybinding classes 2 --- .../Application/Application.Keyboard.cs | 48 ++------ .../Application/Application.Navigation.cs | 10 +- Terminal.Gui/Application/Application.Run.cs | 4 +- Terminal.Gui/Input/Keyboard/KeyBinding.cs | 10 +- .../Input/Keyboard/KeyBindingScope.cs | 19 +-- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 108 +----------------- Terminal.Gui/View/Adornment/Border.cs | 20 ++-- Terminal.Gui/View/View.Command.cs | 18 +-- Terminal.Gui/View/View.Keyboard.cs | 98 +++++++++++----- Terminal.Gui/Views/Button.cs | 2 +- Terminal.Gui/Views/FrameView.cs | 2 +- Terminal.Gui/Views/Label.cs | 2 +- Terminal.Gui/Views/ListView.cs | 4 +- Terminal.Gui/Views/Menu/Menu.cs | 14 +-- Terminal.Gui/Views/Menu/MenuBar.cs | 28 ++--- Terminal.Gui/Views/Menu/MenuBarItem.cs | 2 +- Terminal.Gui/Views/Menu/MenuItem.cs | 24 ++-- Terminal.Gui/Views/RadioGroup.cs | 2 +- Terminal.Gui/Views/ScrollBar/ScrollSlider.cs | 2 +- Terminal.Gui/Views/Shortcut.cs | 14 +-- UICatalog/Scenarios/Editor.cs | 2 +- UICatalog/Scenarios/KeyBindings.cs | 8 +- UnitTests/Input/Keyboard/KeyBindingsTests.cs | 93 ++++++++------- UnitTests/View/Keyboard/HotKeyTests.cs | 2 +- .../View/Keyboard/ViewKeyBindingTests.cs | 2 +- 25 files changed, 214 insertions(+), 324 deletions(-) diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 143b63f913..48bb76837d 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -54,7 +54,7 @@ public static bool RaiseKeyDownEvent (Key key) return false; } - bool? handled = binding.Value.Target?.InvokeCommands (binding.Value.Commands, binding.Value); + bool? handled = binding.Value.Target?.InvokeCommands (binding.Value.Commands, binding.Value); if (handled != null && (bool)handled) { @@ -83,16 +83,16 @@ public static bool RaiseKeyDownEvent (Key key) static bool? InvokeCommand (Command command, Key key, ApplicationKeyBinding appBinding) { - if (!CommandImplementations!.ContainsKey (command)) + if (!_commandImplementations!.ContainsKey (command)) { throw new NotSupportedException ( @$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by Application." ); } - if (CommandImplementations.TryGetValue (command, out View.CommandImplementation? implementation)) + if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation)) { - var context = new CommandContext (command, appBinding); // Create the context here + CommandContext context = new (command, appBinding); // Create the context here return implementation (context); } @@ -116,7 +116,8 @@ public static bool RaiseKeyDownEvent (Key key) public static event EventHandler? KeyDown; /// - /// Called when the user releases a key (by the ). Raises the cancelable + /// Called when the user releases a key (by the ). Raises the cancelable + /// /// event /// then calls on all top level views. Called after . /// @@ -162,7 +163,7 @@ public static bool RaiseKeyUpEvent (Key key) internal static void AddKeyBindings () { - CommandImplementations = new (); + _commandImplementations.Clear (); // Things this view knows how to do AddCommand ( @@ -231,7 +232,6 @@ internal static void AddKeyBindings () return false; }); - // Resources/config.json overrides QuitKey = Key.Esc; NextTabKey = Key.Tab; @@ -266,35 +266,6 @@ internal static void AddKeyBindings () } } - private static void ReplaceKey (Key oldKey, Key newKey) - { - KeyBindings.ReplaceKey (oldKey, newKey); - - //return; - //if (KeyBindings.Bindings.Count == 0) - //{ - // return; - //} - - //if (newKey == Key.Empty) - //{ - // KeyBindings.Remove (oldKey); - //} - //else - //{ - // if (KeyBindings.TryGet (oldKey, out ApplicationKeyBinding binding)) - // { - // KeyBindings.Remove (oldKey); - // KeyBindings.Add (newKey, binding); - // } - // else - // { - // KeyBindings.Add (newKey, binding); - // } - //} - } - - #endregion Application-scoped KeyBindings /// @@ -313,11 +284,10 @@ private static void ReplaceKey (Key oldKey, Key newKey) /// /// The command. /// The function. - private static void AddCommand (Command command, Func f) { CommandImplementations! [command] = ctx => f (); } + private static void AddCommand (Command command, Func f) { _commandImplementations! [command] = ctx => f (); } /// /// Commands for Application. /// - private static Dictionary? CommandImplementations { get; set; } - + private static readonly Dictionary _commandImplementations = new (); } diff --git a/Terminal.Gui/Application/Application.Navigation.cs b/Terminal.Gui/Application/Application.Navigation.cs index a4f9dcc545..893b06afb6 100644 --- a/Terminal.Gui/Application/Application.Navigation.cs +++ b/Terminal.Gui/Application/Application.Navigation.cs @@ -22,7 +22,7 @@ public static Key NextTabGroupKey { if (_nextTabGroupKey != value) { - ReplaceKey (_nextTabGroupKey, value); + KeyBindings.ReplaceKey (_nextTabGroupKey, value); _nextTabGroupKey = value; } } @@ -37,7 +37,7 @@ public static Key NextTabKey { if (_nextTabKey != value) { - ReplaceKey (_nextTabKey, value); + KeyBindings.ReplaceKey (_nextTabKey, value); _nextTabKey = value; } } @@ -66,7 +66,7 @@ public static Key PrevTabGroupKey { if (_prevTabGroupKey != value) { - ReplaceKey (_prevTabGroupKey, value); + KeyBindings.ReplaceKey (_prevTabGroupKey, value); _prevTabGroupKey = value; } } @@ -78,10 +78,10 @@ public static Key PrevTabKey { get => _prevTabKey; set - { + { if (_prevTabKey != value) { - ReplaceKey (_prevTabKey, value); + KeyBindings.ReplaceKey (_prevTabKey, value); _prevTabKey = value; } } diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 64f7960079..051c81223c 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -19,7 +19,7 @@ public static Key QuitKey { if (_quitKey != value) { - ReplaceKey (_quitKey, value); + KeyBindings.ReplaceKey (_quitKey, value); _quitKey = value; } } @@ -37,7 +37,7 @@ public static Key ArrangeKey { if (_arrangeKey != value) { - ReplaceKey (_arrangeKey, value); + KeyBindings.ReplaceKey (_arrangeKey, value); _arrangeKey = value; } } diff --git a/Terminal.Gui/Input/Keyboard/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs index a67aabb504..872429b9d0 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -15,12 +15,10 @@ public record struct KeyBinding { /// Initializes a new instance. /// The commands this key binding will invoke. - /// The scope of the . /// Arbitrary context that can be associated with this key binding. - public KeyBinding (Command [] commands, KeyBindingScope scope, object? context = null) + public KeyBinding (Command [] commands, object? context = null) { Commands = commands; - Scope = scope; Data = context; } @@ -29,10 +27,9 @@ public KeyBinding (Command [] commands, KeyBindingScope scope, object? context = /// The scope of the . /// The view the key binding is bound to. /// Arbitrary data that can be associated with this key binding. - public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, object? data = null) + public KeyBinding (Command [] commands, View? boundView, object? data = null) { Commands = commands; - Scope = scope; BoundView = boundView; Data = data; } @@ -40,9 +37,6 @@ public KeyBinding (Command [] commands, KeyBindingScope scope, View? boundView, /// The commands this key binding will invoke. public Command [] Commands { get; set; } - /// The scope of the . - public KeyBindingScope Scope { get; set; } - /// /// The Key that is bound to the . /// diff --git a/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs b/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs index 4f553a3761..cfd31f6bca 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs @@ -22,22 +22,5 @@ public enum KeyBindingScope /// /// /// - Focused = 1, - - /// - /// The key binding is scoped to the View's Superview hierarchy and the bound s will be invoked - /// even when the View does not have - /// focus, as - /// long as some View up the SuperView hierachy does have focus. This is typically used for s. - /// - /// The View must be visible. - /// - /// - /// HotKey-scoped key bindings are only invoked if the key down event was not handled by the focused view or - /// any of its subviews. - /// - /// - /// - /// - HotKey = 2 + Focused = 1 } diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index 7264f09777..d2360430c1 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -9,12 +9,6 @@ namespace Terminal.Gui; /// 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; } @@ -43,43 +37,6 @@ public void Add (Key key, KeyBinding binding) Bindings.Add (new (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. - /// - /// 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 (key == Key.Empty || !key.IsValid) - { - throw new ArgumentException (@"Invalid Key", nameof (commands)); - } - - 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})."); - } - - Add (key, new KeyBinding (commands, scope, BoundView)); - } /// /// @@ -88,7 +45,7 @@ public void Add (Key key, KeyBindingScope scope, params Command [] commands) /// /// /// This is a helper function for . If used for a View ( - /// is set), the scope will be set to . + /// is set), the scope will be set to . /// Otherwise, it will be set to . /// /// @@ -108,7 +65,7 @@ public void Add (Key key, KeyBindingScope scope, params Command [] commands) /// public void Add (Key key, params Command [] commands) { - Add (key, KeyBindingScope.Focused, commands); + Add (key, new KeyBinding(commands)); } // TODO: Add a dictionary comparer that ignores Scope @@ -166,20 +123,6 @@ public KeyBinding Get (Key key) 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. /// @@ -238,7 +181,7 @@ public void ReplaceCommands (Key key, params Command [] newCommands) if (TryGet (key, out KeyBinding binding)) { Remove (key); - Add (key, binding.Scope, newCommands); + Add (key, newCommands); } else { @@ -277,12 +220,7 @@ public void ReplaceKey (Key oldKey, Key newKey) /// if the Key is bound; otherwise . public bool TryGet (Key key, out KeyBinding binding) { - //if (BoundView is null) - //{ - // throw new InvalidOperationException ("KeyBindings must be bound to a View to use this method."); - //} - - binding = new (Array.Empty (), KeyBindingScope.Disabled, null); + binding = new ([], null); if (key.IsValid) { @@ -291,42 +229,4 @@ public bool TryGet (Key key, out KeyBinding 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) - { - if (!key.IsValid) - { - //if (BoundView is null) - //{ - // throw new InvalidOperationException ("KeyBindings must be bound to a View to use this method."); - //} - - binding = new (Array.Empty (), KeyBindingScope.Disabled, null); - return false; - } - - if (Bindings.TryGetValue (key, out binding)) - { - if (scope.HasFlag (binding.Scope)) - { - binding.Key = key; - return true; - } - } - else - { - binding = new (Array.Empty (), KeyBindingScope.Disabled, null); - } - - return false; - } } diff --git a/Terminal.Gui/View/Adornment/Border.cs b/Terminal.Gui/View/Adornment/Border.cs index 3a51eb54bb..26fed69f8c 100644 --- a/Terminal.Gui/View/Adornment/Border.cs +++ b/Terminal.Gui/View/Adornment/Border.cs @@ -1390,15 +1390,15 @@ private void AddArrangeModeKeyBindings () return true; // Always eat }); - KeyBindings.Add (Key.Esc, KeyBindingScope.HotKey, Command.Quit); - KeyBindings.Add (Application.ArrangeKey, KeyBindingScope.HotKey, Command.Quit); - KeyBindings.Add (Key.CursorUp, KeyBindingScope.HotKey, Command.Up); - KeyBindings.Add (Key.CursorDown, KeyBindingScope.HotKey, Command.Down); - KeyBindings.Add (Key.CursorLeft, KeyBindingScope.HotKey, Command.Left); - KeyBindings.Add (Key.CursorRight, KeyBindingScope.HotKey, Command.Right); - - KeyBindings.Add (Key.Tab, KeyBindingScope.HotKey, Command.Tab); - KeyBindings.Add (Key.Tab.WithShift, KeyBindingScope.HotKey, Command.BackTab); + HotKeyBindings.Add (Key.Esc, Command.Quit); + HotKeyBindings.Add (Application.ArrangeKey, Command.Quit); + HotKeyBindings.Add (Key.CursorUp, Command.Up); + HotKeyBindings.Add (Key.CursorDown, Command.Down); + HotKeyBindings.Add (Key.CursorLeft, Command.Left); + HotKeyBindings.Add (Key.CursorRight, Command.Right); + + HotKeyBindings.Add (Key.Tab, Command.Tab); + HotKeyBindings.Add (Key.Tab.WithShift, Command.BackTab); } private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e) @@ -1472,7 +1472,7 @@ private void ApplicationOnMouseEvent (object? sender, MouseEventArgs e) _bottomSizeButton = null; } - KeyBindings.Clear (); + HotKeyBindings.Clear (); if (CanFocus) { diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index 5a8498df06..6b33782265 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui; public partial class View // Command APIs { - private Dictionary CommandImplementations { get; } = new (); + private readonly Dictionary _commandImplementations = new (); #region Default Implementation @@ -95,7 +95,7 @@ private void SetupCommands () if (isDefaultView != this && isDefaultView is Button { IsDefault: true } button) { - bool? handled = isDefaultView.InvokeCommand (Command.Accept, new ([Command.Accept], 0, null, this)); + bool? handled = isDefaultView.InvokeCommand (Command.Accept, new ([Command.Accept], null, this)); if (handled == true) { return true; @@ -104,7 +104,7 @@ private void SetupCommands () if (SuperView is { }) { - return SuperView?.InvokeCommand (Command.Accept, new ([Command.Accept], 0, null, this)) is true; + return SuperView?.InvokeCommand (Command.Accept, new ([Command.Accept], null, this)) is true; } } @@ -249,7 +249,7 @@ private void SetupCommands () /// /// The command. /// The delegate. - protected void AddCommand (Command command, CommandImplementation impl) { CommandImplementations [command] = impl; } + protected void AddCommand (Command command, CommandImplementation impl) { _commandImplementations [command] = impl; } /// /// @@ -270,11 +270,11 @@ private void SetupCommands () /// /// The command. /// The delegate. - protected void AddCommand (Command command, Func impl) { CommandImplementations [command] = ctx => impl (); } + protected void AddCommand (Command command, Func impl) { _commandImplementations [command] = ctx => impl (); } /// Returns all commands that are supported by this . /// - public IEnumerable GetSupportedCommands () { return CommandImplementations.Keys; } + public IEnumerable GetSupportedCommands () { return _commandImplementations.Keys; } /// /// Invokes the specified commands. @@ -292,7 +292,7 @@ private void SetupCommands () foreach (Command command in commands) { - if (!CommandImplementations.ContainsKey (command)) + if (!_commandImplementations.ContainsKey (command)) { throw new NotSupportedException ( @$"A Binding was set up for the command {command} ({binding}) but that command is not supported by this View ({GetType ().Name})" @@ -327,7 +327,7 @@ private void SetupCommands () /// public bool? InvokeCommand (Command command, TBindingType binding) { - if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation)) + if (_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { return implementation (new CommandContext () { @@ -350,7 +350,7 @@ private void SetupCommands () /// public bool? InvokeCommand (Command command) { - if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation)) + if (_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { return implementation (null); } diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 6e70ad7a55..87921c66f7 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -15,6 +15,8 @@ private void SetupKeyboard () KeyBindings.Add (Key.Space, Command.Select); KeyBindings.Add (Key.Enter, Command.Accept); + HotKeyBindings = new (this); + // Note, setting HotKey will bind HotKey to Command.HotKey HotKeySpecifier = (Rune)'_'; TitleTextFormatter.HotKeyChanged += TitleTextFormatter_HotKeyChanged; @@ -153,50 +155,54 @@ public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, object? } // Remove base version - if (KeyBindings.TryGet (prevHotKey, out _)) + if (HotKeyBindings.TryGet (prevHotKey, out _)) { - KeyBindings.Remove (prevHotKey); + HotKeyBindings.Remove (prevHotKey); } // Remove the Alt version - if (KeyBindings.TryGet (prevHotKey.WithAlt, out _)) + if (HotKeyBindings.TryGet (prevHotKey.WithAlt, out _)) { - KeyBindings.Remove (prevHotKey.WithAlt); + HotKeyBindings.Remove (prevHotKey.WithAlt); } if (_hotKey.IsKeyCodeAtoZ) { // Remove the shift version - if (KeyBindings.TryGet (prevHotKey.WithShift, out _)) + if (HotKeyBindings.TryGet (prevHotKey.WithShift, out _)) { - KeyBindings.Remove (prevHotKey.WithShift); + HotKeyBindings.Remove (prevHotKey.WithShift); } // Remove alt | shift version - if (KeyBindings.TryGet (prevHotKey.WithShift.WithAlt, out _)) + if (HotKeyBindings.TryGet (prevHotKey.WithShift.WithAlt, out _)) { - KeyBindings.Remove (prevHotKey.WithShift.WithAlt); + HotKeyBindings.Remove (prevHotKey.WithShift.WithAlt); } } // Add the new if (newKey != Key.Empty) { - KeyBinding keyBinding = new ([Command.HotKey], KeyBindingScope.HotKey, context); + KeyBinding keyBinding = new () + { + Commands = [Command.HotKey], + Data = context + }; // Add the base and Alt key - KeyBindings.Remove (newKey); - KeyBindings.Add (newKey, keyBinding); - KeyBindings.Remove (newKey.WithAlt); - KeyBindings.Add (newKey.WithAlt, keyBinding); + HotKeyBindings.Remove (newKey); + HotKeyBindings.Add (newKey, keyBinding); + HotKeyBindings.Remove (newKey.WithAlt); + HotKeyBindings.Add (newKey.WithAlt, keyBinding); // If the Key is A..Z, add ShiftMask and AltMask | ShiftMask if (newKey.IsKeyCodeAtoZ) { - KeyBindings.Remove (newKey.WithShift); - KeyBindings.Add (newKey.WithShift, keyBinding); - KeyBindings.Remove (newKey.WithShift.WithAlt); - KeyBindings.Add (newKey.WithShift.WithAlt, keyBinding); + HotKeyBindings.Remove (newKey.WithShift); + HotKeyBindings.Add (newKey.WithShift, keyBinding); + HotKeyBindings.Remove (newKey.WithShift.WithAlt); + HotKeyBindings.Add (newKey.WithShift.WithAlt, keyBinding); } } @@ -532,7 +538,7 @@ bool RaiseKeyUp (Key k) // `InvokeKeyBindings` returns `false`. Continue passing the event (return `false` from `OnInvokeKeyBindings`).. // * If key bindings were found, and any handled the key (at least one `Command` returned `true`), // `InvokeKeyBindings` returns `true`. Continue passing the event (return `false` from `OnInvokeKeyBindings`). - bool? handled = InvokeCommandsBoundToKey (key, KeyBindingScope.Focused); + bool? handled = InvokeCommandsBoundToFocusedKey (key); if (handled is true) { @@ -541,17 +547,17 @@ bool RaiseKeyUp (Key k) return handled; } - if (Margin is { } && InvokeCommandsBoundToKeyOnAdornment (Margin, key, KeyBindingScope.Focused, ref handled)) + if (Margin is { } && InvokeCommandsBoundToKeyOnAdornment (Margin, key, ref handled)) { return true; } - if (Padding is { } && InvokeCommandsBoundToKeyOnAdornment (Padding, key, KeyBindingScope.Focused, ref handled)) + if (Padding is { } && InvokeCommandsBoundToKeyOnAdornment (Padding, key, ref handled)) { return true; } - if (Border is { } && InvokeCommandsBoundToKeyOnAdornment (Border, key, KeyBindingScope.Focused, ref handled)) + if (Border is { } && InvokeCommandsBoundToKeyOnAdornment (Border, key, ref handled)) { return true; } @@ -559,7 +565,7 @@ bool RaiseKeyUp (Key k) return handled; } - private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Key key, KeyBindingScope scope, ref bool? handled) + private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Key key, ref bool? handled) { bool? adornmentHandled = adornment.InvokeCommandsBoundToKey (key); @@ -593,7 +599,7 @@ private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Ke internal bool InvokeCommandsBoundToHotKeyOnSubviews (Key key, ref bool? handled, bool invoke = true) { - bool? weHandled = InvokeCommandsBoundToKey (key, KeyBindingScope.HotKey); + bool? weHandled = InvokeCommandsBoundToHotKey (key); if (weHandled is true) { return true; @@ -634,7 +640,7 @@ public bool IsHotKeyBound (Key key, out View? boundView) foreach (View subview in Subviews) { - if (subview.KeyBindings.TryGet (key, KeyBindingScope.HotKey, out _)) + if (subview.HotKeyBindings.TryGet (key, out _)) { boundView = subview; @@ -650,6 +656,46 @@ public bool IsHotKeyBound (Key key, out View? boundView) return false; } + /// + /// Invokes the Commands bound to . + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// The key event passed. + /// + /// if no command was invoked; input processing should continue. + /// if at least one command was invoked and was not handled (or cancelled); input processing + /// should continue. + /// if at least one command was invoked and handled (or cancelled); input processing should + /// stop. + /// + protected bool? InvokeCommandsBoundToFocusedKey (Key key) + { + if (!KeyBindings.TryGet (key, out KeyBinding binding)) + { + return null; + } + +#if DEBUG + + //if (Application.KeyBindings.TryGet (key, out KeyBinding b)) + //{ + // Debug.WriteLine ( + // $"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command."); + //} + + // TODO: This is a "prototype" debug check. It may be too annoying vs. useful. + // Scour the bindings up our View hierarchy + // to ensure that the key is not already bound to a different set of commands. + if (SuperView?.IsHotKeyBound (key, out View? previouslyBoundView) ?? false) + { + Debug.WriteLine ($"WARNING: InvokeKeyBindings ({key}) - A subview or peer has bound this Key and will not see it: {previouslyBoundView}."); + } + +#endif + return InvokeCommands (binding.Commands, binding); + } + + /// /// Invokes the Commands bound to . /// See for an overview of Terminal.Gui keyboard APIs. @@ -663,9 +709,9 @@ public bool IsHotKeyBound (Key key, out View? boundView) /// if at least one command was invoked and handled (or cancelled); input processing should /// stop. /// - protected bool? InvokeCommandsBoundToKey (Key key, KeyBindingScope scope) + protected bool? InvokeCommandsBoundToHotKey (Key key) { - if (!KeyBindings.TryGet (key, scope, out KeyBinding binding)) + if (!HotKeyBindings.TryGet (key, out KeyBinding binding)) { return null; } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 79118a5c91..46a1314e3e 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -133,7 +133,7 @@ private void Button_MouseClick (object sender, MouseEventArgs e) } // TODO: With https://github.com/gui-cs/Terminal.Gui/issues/3778 we won't have to pass data: - e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, this, null)) == true; + e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], this, data: null)) == true; } private void Button_TitleChanged (object sender, EventArgs e) diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index 94c894faac..20bbbec466 100644 --- a/Terminal.Gui/Views/FrameView.cs +++ b/Terminal.Gui/Views/FrameView.cs @@ -25,7 +25,7 @@ public FrameView () private void FrameView_MouseClick (object sender, MouseEventArgs e) { // base sets focus on HotKey - e.Handled = InvokeCommand (Command.HotKey, new ([Command.HotKey], KeyBindingScope.Focused, null, this)) == true; + e.Handled = InvokeCommand (Command.HotKey, new ([Command.HotKey], this, this)) == true; } diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index e277357f45..2a67a9bc0e 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -36,7 +36,7 @@ private void Label_MouseClick (object sender, MouseEventArgs e) { if (!CanFocus) { - e.Handled = InvokeCommand (Command.HotKey, new ([Command.HotKey], KeyBindingScope.HotKey, this, this)) == true; + e.Handled = InvokeCommand (Command.HotKey, new ([Command.HotKey], this, data: this)) == true; } } diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 6cc15b3385..7bb79a530a 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -171,8 +171,8 @@ public ListView () KeyBindings.Add (Key.Space.WithShift, [Command.Select, Command.Down]); // Use the form of Add that lets us pass context to the handler - KeyBindings.Add (Key.A.WithCtrl, new KeyBinding ([Command.SelectAll], KeyBindingScope.Focused, true)); - KeyBindings.Add (Key.U.WithCtrl, new KeyBinding ([Command.SelectAll], KeyBindingScope.Focused, false)); + KeyBindings.Add (Key.A.WithCtrl, new KeyBinding ([Command.SelectAll], true)); + KeyBindings.Add (Key.U.WithCtrl, new KeyBinding ([Command.SelectAll], false)); } /// diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index ff7c83bf41..eae459098b 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -130,7 +130,7 @@ public override void BeginInit () if (menuItem.ShortcutKey != Key.Empty) { - KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuItem); + KeyBinding keyBinding = new ([Command.Select], this, data: menuItem); // Remove an existent ShortcutKey menuItem._menuBar.KeyBindings.Remove (menuItem.ShortcutKey!); @@ -251,7 +251,7 @@ protected override bool OnKeyDownNotHandled (Key keyEvent) { // We didn't handle the key, pass it on to host bool? handled = null; - return _host.InvokeCommandsBoundToHotKeyOnSubviews (keyEvent, ref handled, true ) == true; + return _host.InvokeCommandsBoundToHotKeyOnSubviews (keyEvent, ref handled, true) == true; } protected override bool OnMouseEvent (MouseEventArgs me) @@ -480,14 +480,14 @@ private void AddKeyBindingsHotKey (MenuBarItem? menuBarItem) foreach (MenuItem menuItem in menuItems) { - KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, menuItem); + KeyBinding keyBinding = new ([Command.Toggle], this, data: menuItem); if (menuItem.HotKey != Key.Empty) { - KeyBindings.Remove (menuItem.HotKey!); - KeyBindings.Add (menuItem.HotKey!, keyBinding); - KeyBindings.Remove (menuItem.HotKey!.WithAlt); - KeyBindings.Add (menuItem.HotKey.WithAlt, keyBinding); + HotKeyBindings.Remove (menuItem.HotKey!); + HotKeyBindings.Add (menuItem.HotKey!, keyBinding); + HotKeyBindings.Remove (menuItem.HotKey!.WithAlt); + HotKeyBindings.Add (menuItem.HotKey.WithAlt, keyBinding); } } } diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index fb3aedd7b7..ed6e42a62a 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -162,8 +162,8 @@ public MenuBar () KeyBindings.Add (Key.Esc, Command.Cancel); KeyBindings.Add (Key.CursorDown, Command.Accept); - KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, -1); // -1 indicates Key was used - KeyBindings.Add (Key, keyBinding); + KeyBinding keyBinding = new ([Command.Toggle], this, data: -1); // -1 indicates Key was used + HotKeyBindings.Add (Key, keyBinding); // TODO: Why do we have two keybindings for opening the menu? Ctrl-Space and Key? KeyBindings.Add (Key.Space.WithCtrl, keyBinding); @@ -204,21 +204,21 @@ public MenuBarItem [] Menus if (menuBarItem.HotKey != Key.Empty) { - KeyBindings.Remove (menuBarItem.HotKey!); - KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.Focused, menuBarItem); - KeyBindings.Add (menuBarItem.HotKey!, keyBinding); - KeyBindings.Remove (menuBarItem.HotKey!.WithAlt); - keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, menuBarItem); - KeyBindings.Add (menuBarItem.HotKey.WithAlt, keyBinding); + HotKeyBindings.Remove (menuBarItem.HotKey!); + KeyBinding keyBinding = new ([Command.Toggle], this, menuBarItem); + HotKeyBindings.Add (menuBarItem.HotKey!, keyBinding); + HotKeyBindings.Remove (menuBarItem.HotKey!.WithAlt); + keyBinding = new ([Command.Toggle], this, data: menuBarItem); + HotKeyBindings.Add (menuBarItem.HotKey.WithAlt, keyBinding); } if (menuBarItem.ShortcutKey != Key.Empty) { // Technically this will never run because MenuBarItems don't have shortcuts // unless the IsTopLevel is true - KeyBindings.Remove (menuBarItem.ShortcutKey!); - KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, menuBarItem); - KeyBindings.Add (menuBarItem.ShortcutKey!, keyBinding); + HotKeyBindings.Remove (menuBarItem.ShortcutKey!); + KeyBinding keyBinding = new ([Command.Select], this, data: menuBarItem); + HotKeyBindings.Add (menuBarItem.ShortcutKey!, keyBinding); } menuBarItem.AddShortcutKeyBindings (this); @@ -1310,9 +1310,9 @@ public Key Key return; } - KeyBindings.Remove (_key); - KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, -1); // -1 indicates Key was used - KeyBindings.Add (value, keyBinding); + HotKeyBindings.Remove (_key); + KeyBinding keyBinding = new ([Command.Toggle], this, data: -1); // -1 indicates Key was used + HotKeyBindings.Add (value, keyBinding); _key = value; } } diff --git a/Terminal.Gui/Views/Menu/MenuBarItem.cs b/Terminal.Gui/Views/Menu/MenuBarItem.cs index ff57a35451..5281c2da98 100644 --- a/Terminal.Gui/Views/Menu/MenuBarItem.cs +++ b/Terminal.Gui/Views/Menu/MenuBarItem.cs @@ -239,7 +239,7 @@ public override void RemoveMenuItem () if (_menuBar.Menus [index].HotKey != Key.Empty) { // Remove an existent HotKey - _menuBar.KeyBindings.Remove (HotKey!.WithAlt); + _menuBar.HotKeyBindings.Remove (HotKey!.WithAlt); } _menuBar.Menus [index] = null!; diff --git a/Terminal.Gui/Views/Menu/MenuItem.cs b/Terminal.Gui/Views/Menu/MenuItem.cs index 016cf28750..5fe52f063b 100644 --- a/Terminal.Gui/Views/Menu/MenuItem.cs +++ b/Terminal.Gui/Views/Menu/MenuItem.cs @@ -149,11 +149,11 @@ public void ToggleChecked () if (AllowNullChecked) { Checked = previousChecked switch - { - null => true, - true => false, - false => null - }; + { + null => true, + true => false, + false => null + }; } else { @@ -242,9 +242,9 @@ private void GetHotKey () } else if (nextIsHot) { - HotKey = char.ToLower (x); + HotKey = char.ToLower (x); - return; + return; } } @@ -294,7 +294,7 @@ private void AddOrUpdateShortcutKeyBinding (Key key) if (ShortcutKey != Key.Empty) { - KeyBinding keyBinding = new ([Command.Select], KeyBindingScope.HotKey, this); + KeyBinding keyBinding = new ([Command.Select], null, data: this); // Remove an existent ShortcutKey _menuBar.KeyBindings.Remove (ShortcutKey!); _menuBar.KeyBindings.Add (ShortcutKey!, keyBinding); @@ -314,7 +314,7 @@ private void UpdateHotKeyBinding (Key oldKey) if (index > -1) { - _menuBar.KeyBindings.Remove (oldKey.WithAlt); + _menuBar.HotKeyBindings.Remove (oldKey.WithAlt); } } @@ -324,9 +324,9 @@ private void UpdateHotKeyBinding (Key oldKey) if (index > -1) { - _menuBar.KeyBindings.Remove (HotKey!.WithAlt); - KeyBinding keyBinding = new ([Command.Toggle], KeyBindingScope.HotKey, this); - _menuBar.KeyBindings.Add (HotKey.WithAlt, keyBinding); + _menuBar.HotKeyBindings.Remove (HotKey!.WithAlt); + KeyBinding keyBinding = new ([Command.Toggle], null, data: this); + _menuBar.HotKeyBindings.Add (HotKey.WithAlt, keyBinding); } } } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 5a8dad689c..aa3e7b9fb1 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -248,7 +248,7 @@ private void RadioGroup_MouseClick (object? sender, MouseEventArgs e) if (c > -1) { // Just like the user pressing the items' hotkey - e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, boundView: this, data: c)) == true; + e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], boundView: this, data: c)) == true; } } diff --git a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs index 52bfa0bea7..78682dcfb7 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs @@ -241,7 +241,7 @@ private void RaisePositionChangeEvents (int newPosition) OnScrolled (distance); Scrolled?.Invoke (this, new (in distance)); - RaiseSelecting (new CommandContext (Command.Select, new KeyBinding ([Command.Select], KeyBindingScope.Focused, null, distance))); + RaiseSelecting (new CommandContext (Command.Select, new KeyBinding ([Command.Select], null, distance))); } /// diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 9029352178..56129e0367 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -69,7 +69,7 @@ public Shortcut () : this (Key.Empty, null, null, null) { } /// The help text to display. public Shortcut (View targetView, Command command, string commandText, string? helpText = null) : this ( - targetView?.KeyBindings.GetKeyFromCommands (command)!, + targetView?.HotKeyBindings.GetKeyFromCommands (command)!, commandText, null, helpText) @@ -499,7 +499,7 @@ void CommandViewOnSelecting (object? sender, CommandEventArgs e) e.Context is CommandContext) { // Forward command to ourselves - InvokeCommand (Command.Select, new ([Command.Select], KeyBindingScope.Focused, null, this)); + InvokeCommand (Command.Select, new ([Command.Select], null, this)); } // BUGBUG: This prevents NumericUpDown on statusbar in HexEditor from working @@ -618,7 +618,7 @@ public Key Key private bool _bindKeyToApplication = false; /// - /// Gets or sets whether is bound to via (as a HotKey) or . + /// Gets or sets whether is bound to via or . /// public bool BindKeyToApplication { @@ -636,7 +636,7 @@ public bool BindKeyToApplication } else { - KeyBindings.Remove (Key); + HotKeyBindings.Remove (Key); } _bindKeyToApplication = value; @@ -716,11 +716,11 @@ private void UpdateKeyBindings (Key oldKey) { if (oldKey != Key.Empty) { - KeyBindings.Remove (oldKey); + HotKeyBindings.Remove (oldKey); } - KeyBindings.Remove (Key); - KeyBindings.Add (Key, KeyBindingScope.HotKey, Command.HotKey); + HotKeyBindings.Remove (Key); + HotKeyBindings.Add (Key, Command.HotKey); } } } diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index 91f669447d..438d9ab508 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -699,7 +699,7 @@ public FindReplaceWindow (TextView textView) Height = 11; Arrangement = ViewArrangement.Movable; - KeyBindings.Add (Key.Esc, KeyBindingScope.Focused, Command.Cancel); + KeyBindings.Add (Key.Esc, Command.Cancel); AddCommand (Command.Cancel, () => { Visible = false; diff --git a/UICatalog/Scenarios/KeyBindings.cs b/UICatalog/Scenarios/KeyBindings.cs index fc0db78a98..d09c8850c0 100644 --- a/UICatalog/Scenarios/KeyBindings.cs +++ b/UICatalog/Scenarios/KeyBindings.cs @@ -104,7 +104,7 @@ Pressing Esc or {Application.QuitKey} will cause it to quit the app. foreach (var subview in appWindow.Subviews) { - foreach (var binding in subview.KeyBindings.Bindings.Where (b => b.Value.Scope == KeyBindingScope.HotKey)) + foreach (var binding in subview.HotKeyBindings.Bindings) { hotkeyBindings.Add ($"{binding.Key} -> {subview.GetType ().Name} - {binding.Value.Commands [0]}"); } @@ -149,7 +149,7 @@ private void Application_HasFocusChanged (object sender, EventArgs e) _focusedBindingsListView.Title = $"_Focused ({focused?.GetType ().Name}) Bindings"; _focusedBindings.Clear(); - foreach (var binding in focused?.KeyBindings!.Bindings.Where (b => b.Value.Scope == KeyBindingScope.Focused)!) + foreach (var binding in focused?.KeyBindings!.Bindings) { _focusedBindings.Add ($"{binding.Key} -> {binding.Value.Commands [0]}"); } @@ -180,7 +180,7 @@ public KeyBindingsDemo () return true; }); - KeyBindings.Add (Key.F2, KeyBindingScope.Focused, Command.Save); + KeyBindings.Add (Key.F2, Command.Save); KeyBindings.Add (Key.F3, Command.New); // same as specifying KeyBindingScope.Focused Application.KeyBindings.Add (Key.F4, this, Command.New); @@ -190,7 +190,7 @@ public KeyBindingsDemo () { return false; } - MessageBox.Query ($"{keyCommandContext.Binding.Scope}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query ($"{keyCommandContext.Binding}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); Application.RequestStop (); return true; }); diff --git a/UnitTests/Input/Keyboard/KeyBindingsTests.cs b/UnitTests/Input/Keyboard/KeyBindingsTests.cs index 7fd67ca8fb..cf4a7815b0 100644 --- a/UnitTests/Input/Keyboard/KeyBindingsTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -11,7 +11,7 @@ public void Add_Invalid_Key_Throws () { var keyBindings = new KeyBindings (new View ()); List commands = new (); - Assert.Throws (() => keyBindings.Add (Key.Empty, KeyBindingScope.HotKey, Command.Accept)); + Assert.Throws (() => keyBindings.Add (Key.Empty, Command.Accept)); } [Fact] @@ -34,7 +34,7 @@ public void Add_Multiple_Adds () [Fact] public void Add_No_Commands_Throws () { - var keyBindings = new KeyBindings (new()); + var keyBindings = new KeyBindings (new ()); List commands = new (); Assert.Throws (() => keyBindings.Add (Key.A, commands.ToArray ())); } @@ -42,7 +42,7 @@ public void Add_No_Commands_Throws () [Fact] public void Add_Single_Adds () { - var keyBindings = new KeyBindings (new()); + var keyBindings = new KeyBindings (new ()); keyBindings.Add (Key.A, Command.HotKey); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.HotKey, resultCommands); @@ -58,29 +58,29 @@ public void Add_Single_Adds () public void Add_With_Throws_If_Exists () { var keyBindings = new KeyBindings (new View ()); - keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey); - Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.Accept)); + keyBindings.Add (Key.A, Command.HotKey); + Assert.Throws (() => keyBindings.Add (Key.A, Command.Accept)); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.HotKey, resultCommands); keyBindings = new (new View ()); - keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.HotKey); - Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept)); + keyBindings.Add (Key.A, Command.HotKey); + Assert.Throws (() => keyBindings.Add (Key.A, Command.Accept)); resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.HotKey, resultCommands); keyBindings = new (new View ()); - keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey); - Assert.Throws (() => keyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.Accept)); + keyBindings.Add (Key.A, Command.HotKey); + Assert.Throws (() => keyBindings.Add (Key.A, Command.Accept)); resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.HotKey, resultCommands); keyBindings = new (new View ()); - keyBindings.Add (Key.A, new KeyBinding (new [] { Command.HotKey }, KeyBindingScope.HotKey)); - Assert.Throws (() => keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept }, KeyBindingScope.HotKey))); + keyBindings.Add (Key.A, new KeyBinding (new [] { Command.HotKey })); + Assert.Throws (() => keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept }))); resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.HotKey, resultCommands); @@ -102,7 +102,7 @@ public void Clear_Clears () [Fact] public void Defaults () { - var keyBindings = new KeyBindings (new()); + var keyBindings = new KeyBindings (new ()); Assert.Empty (keyBindings.Bindings); Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept)); Assert.NotNull (keyBindings.BoundView); @@ -111,7 +111,7 @@ public void Defaults () [Fact] public void Get_Binding_Not_Found_Throws () { - var keyBindings = new KeyBindings (new()); + var keyBindings = new KeyBindings (new ()); Assert.Throws (() => keyBindings.Get (Key.A)); Assert.Throws (() => keyBindings.Get (Key.B)); } @@ -128,7 +128,7 @@ public void GetCommands_Unknown_ReturnsEmpty () [Fact] public void GetCommands_WithCommands_ReturnsCommands () { - var keyBindings = new KeyBindings (new()); + var keyBindings = new KeyBindings (new ()); keyBindings.Add (Key.A, Command.HotKey); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.HotKey, resultCommands); @@ -139,8 +139,8 @@ public void GetCommands_WithMultipleBindings_ReturnsCommands () { var keyBindings = new KeyBindings (new ()); Command [] commands = { Command.Right, Command.Left }; - keyBindings.Add (Key.A,commands); - keyBindings.Add (Key.B,commands); + keyBindings.Add (Key.A, commands); + keyBindings.Add (Key.B, commands); Command [] resultCommands = keyBindings.GetCommands (Key.A); Assert.Contains (Command.Right, resultCommands); Assert.Contains (Command.Left, resultCommands); @@ -152,7 +152,7 @@ public void GetCommands_WithMultipleBindings_ReturnsCommands () [Fact] public void GetCommands_WithMultipleCommands_ReturnsCommands () { - var keyBindings = new KeyBindings (new()); + var keyBindings = new KeyBindings (new ()); Command [] commands = { Command.Right, Command.Left }; keyBindings.Add (Key.A, commands); Command [] resultCommands = keyBindings.GetCommands (Key.A); @@ -163,12 +163,12 @@ public void GetCommands_WithMultipleCommands_ReturnsCommands () [Fact] public void GetKeyFromCommands_MultipleCommands () { - var keyBindings = new KeyBindings (new()); + var keyBindings = new KeyBindings (new ()); Command [] commands1 = { Command.Right, Command.Left }; keyBindings.Add (Key.A, commands1); Command [] commands2 = { Command.Up, Command.Down }; - keyBindings.Add (Key.B,commands2); + keyBindings.Add (Key.B, commands2); Key key = keyBindings.GetKeyFromCommands (commands1); Assert.Equal (Key.A, key); @@ -180,7 +180,7 @@ public void GetKeyFromCommands_MultipleCommands () [Fact] public void GetKeyFromCommands_OneCommand () { - var keyBindings = new KeyBindings (new()); + var keyBindings = new KeyBindings (new ()); keyBindings.Add (Key.A, Command.Right); Key key = keyBindings.GetKeyFromCommands (Command.Right); @@ -191,14 +191,14 @@ public void GetKeyFromCommands_OneCommand () [Fact] public void GetKeyFromCommands_Unknown_Returns_Key_Empty () { - var keyBindings = new KeyBindings(new()); + var keyBindings = new KeyBindings (new ()); Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept)); } [Fact] public void GetKeyFromCommands_WithCommands_ReturnsKey () { - var keyBindings = new KeyBindings (new()); + var keyBindings = new KeyBindings (new ()); keyBindings.Add (Key.A, Command.HotKey); Key resultKey = keyBindings.GetKeyFromCommands (Command.HotKey); Assert.Equal (Key.A, resultKey); @@ -208,10 +208,10 @@ public void GetKeyFromCommands_WithCommands_ReturnsKey () public void ReplaceKey_Replaces () { var keyBindings = new KeyBindings (new ()); - keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.HotKey); - keyBindings.Add (Key.B, KeyBindingScope.Focused, Command.HotKey); - keyBindings.Add (Key.C, KeyBindingScope.Focused, Command.HotKey); - keyBindings.Add (Key.D, KeyBindingScope.Focused, Command.HotKey); + keyBindings.Add (Key.A, Command.HotKey); + keyBindings.Add (Key.B, Command.HotKey); + keyBindings.Add (Key.C, Command.HotKey); + keyBindings.Add (Key.D, Command.HotKey); keyBindings.ReplaceKey (Key.A, Key.E); Assert.Empty (keyBindings.GetCommands (Key.A)); @@ -234,8 +234,8 @@ public void ReplaceKey_Replaces () public void ReplaceKey_Replaces_Leaves_Old_Binding () { var keyBindings = new KeyBindings (new ()); - keyBindings.Add (Key.A, KeyBindingScope.Focused, Command.Accept); - keyBindings.Add (Key.B, KeyBindingScope.Focused, Command.HotKey); + keyBindings.Add (Key.A, Command.Accept); + keyBindings.Add (Key.B, Command.HotKey); keyBindings.ReplaceKey (keyBindings.GetKeyFromCommands (Command.Accept), Key.C); Assert.Empty (keyBindings.GetCommands (Key.A)); @@ -245,34 +245,33 @@ public void ReplaceKey_Replaces_Leaves_Old_Binding () [Fact] public void ReplaceKey_Throws_If_DoesNotContain_Old () { - var keyBindings = new KeyBindings(new()); + var keyBindings = new KeyBindings (new ()); Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.B)); } [Fact] public void ReplaceKey_Throws_If_New_Is_Empty () { - var keyBindings = new KeyBindings(new()); - keyBindings.Add (Key.A,Command.HotKey); + var keyBindings = new KeyBindings (new ()); + keyBindings.Add (Key.A, Command.HotKey); Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.Empty)); } // Add with scope does the right things [Theory] [InlineData (KeyBindingScope.Focused)] - [InlineData (KeyBindingScope.HotKey)] public void Scope_Add_Adds (KeyBindingScope scope) { var keyBindings = new KeyBindings (new ()); Command [] commands = { Command.Right, Command.Left }; var key = new Key (Key.A); - keyBindings.Add (Key.A, scope, commands); + keyBindings.Add (Key.A, commands); KeyBinding binding = keyBindings.Get (key); Assert.Contains (Command.Right, binding.Commands); Assert.Contains (Command.Left, binding.Commands); - binding = keyBindings.Get (key, scope); + binding = keyBindings.Get (key); Assert.Contains (Command.Right, binding.Commands); Assert.Contains (Command.Left, binding.Commands); @@ -283,43 +282,41 @@ public void Scope_Add_Adds (KeyBindingScope scope) [Theory] [InlineData (KeyBindingScope.Focused)] - [InlineData (KeyBindingScope.HotKey)] public void Scope_Get_Filters (KeyBindingScope scope) { var keyBindings = new KeyBindings (new ()); Command [] commands = { Command.Right, Command.Left }; var key = new Key (Key.A); - keyBindings.Add (key, scope, commands); + keyBindings.Add (key, commands); KeyBinding binding = keyBindings.Get (key); Assert.Contains (Command.Right, binding.Commands); Assert.Contains (Command.Left, binding.Commands); - binding = keyBindings.Get (key, scope); + binding = keyBindings.Get (key); Assert.Contains (Command.Right, binding.Commands); Assert.Contains (Command.Left, binding.Commands); } [Theory] [InlineData (KeyBindingScope.Focused)] - [InlineData (KeyBindingScope.HotKey)] public void Scope_TryGet_Filters (KeyBindingScope scope) { var keyBindings = new KeyBindings (new ()); Command [] commands = { Command.Right, Command.Left }; var key = new Key (Key.A); - keyBindings.Add (key, scope, commands); + keyBindings.Add (key, commands); bool success = keyBindings.TryGet (key, out KeyBinding binding); Assert.Contains (Command.Right, binding.Commands); Assert.Contains (Command.Left, binding.Commands); - success = keyBindings.TryGet (key, scope, out binding); + success = keyBindings.TryGet (key, out binding); Assert.Contains (Command.Right, binding.Commands); Assert.Contains (Command.Left, binding.Commands); // negative test - success = keyBindings.TryGet (key, 0, out binding); + success = keyBindings.TryGet (key, out binding); Assert.False (success); Command [] resultCommands = keyBindings.GetCommands (key); @@ -331,8 +328,8 @@ public void Scope_TryGet_Filters (KeyBindingScope scope) [Fact] public void TryGet_Succeeds () { - var keyBindings = new KeyBindings(new()); - keyBindings.Add (Key.Q.WithCtrl,Command.HotKey); + var keyBindings = new KeyBindings (new ()); + keyBindings.Add (Key.Q.WithCtrl, Command.HotKey); var key = new Key (Key.Q.WithCtrl); bool result = keyBindings.TryGet (key, out KeyBinding _); Assert.True (result); ; @@ -344,7 +341,7 @@ public void TryGet_Succeeds () [Fact] public void TryGet_Unknown_ReturnsFalse () { - var keyBindings = new KeyBindings(new()); + var keyBindings = new KeyBindings (new ()); bool result = keyBindings.TryGet (Key.A, out KeyBinding _); Assert.False (result); } @@ -352,8 +349,8 @@ public void TryGet_Unknown_ReturnsFalse () [Fact] public void TryGet_WithCommands_ReturnsTrue () { - var keyBindings = new KeyBindings(new()); - keyBindings.Add (Key.A,Command.HotKey); + var keyBindings = new KeyBindings (new ()); + keyBindings.Add (Key.A, Command.HotKey); bool result = keyBindings.TryGet (Key.A, out KeyBinding bindings); Assert.True (result); Assert.Contains (Command.HotKey, bindings.Commands); @@ -362,8 +359,8 @@ public void TryGet_WithCommands_ReturnsTrue () [Fact] public void ReplaceCommands_Replaces () { - var keyBindings = new KeyBindings(new()); - keyBindings.Add (Key.A,Command.Accept); + var keyBindings = new KeyBindings (new ()); + keyBindings.Add (Key.A, Command.Accept); keyBindings.ReplaceCommands (Key.A, Command.Refresh); diff --git a/UnitTests/View/Keyboard/HotKeyTests.cs b/UnitTests/View/Keyboard/HotKeyTests.cs index 4c1e7812cd..614200ec01 100644 --- a/UnitTests/View/Keyboard/HotKeyTests.cs +++ b/UnitTests/View/Keyboard/HotKeyTests.cs @@ -108,7 +108,7 @@ public void NewKeyDownEvent_Ignores_Focus_KeyBindings_SuperView () public void NewKeyDownEvent_Honors_HotKey_KeyBindings_SuperView () { var view = new View (); - view.KeyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey); + view.HotKeyBindings.Add (Key.A, Command.HotKey); bool hotKeyInvoked = false; view.HandlingHotKey += (s, e) => { hotKeyInvoked = true; }; diff --git a/UnitTests/View/Keyboard/ViewKeyBindingTests.cs b/UnitTests/View/Keyboard/ViewKeyBindingTests.cs index b1483c4dad..ef32fb8846 100644 --- a/UnitTests/View/Keyboard/ViewKeyBindingTests.cs +++ b/UnitTests/View/Keyboard/ViewKeyBindingTests.cs @@ -139,7 +139,7 @@ public ScopedKeyBindingView () Application.KeyBindings.Add (Key.A, this, Command.Save); HotKey = KeyCode.H; - KeyBindings.Add (Key.F, KeyBindingScope.Focused, Command.Left); + KeyBindings.Add (Key.F, Command.Left); } public bool ApplicationCommand { get; set; } From 3edcf643dca275eddc9ad834553ba166aef85156 Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 7 Dec 2024 10:58:34 -0800 Subject: [PATCH 17/40] Fixed unit tests --- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 37 ++-- Terminal.Gui/View/View.Keyboard.cs | 3 +- Terminal.Gui/Views/Menu/ContextMenu.cs | 2 +- Terminal.Gui/Views/Menu/Menu.cs | 4 +- Terminal.Gui/Views/Menu/MenuBar.cs | 4 +- Terminal.Gui/Views/Menu/MenuBarItem.cs | 4 +- Terminal.Gui/Views/Menu/MenuItem.cs | 8 +- UnitTests/Input/Keyboard/KeyBindingsTests.cs | 39 +---- UnitTests/View/Keyboard/HotKeyTests.cs | 79 ++++++--- ...KeyBindingTests.cs => KeyBindingsTests.cs} | 11 +- UnitTests/Views/ContextMenuTests.cs | 158 +++++++++--------- UnitTests/Views/MenuBarTests.cs | 16 +- UnitTests/Views/RadioGroupTests.cs | 44 ++--- UnitTests/Views/ShortcutTests.cs | 18 +- 14 files changed, 222 insertions(+), 205 deletions(-) rename UnitTests/View/Keyboard/{ViewKeyBindingTests.cs => KeyBindingsTests.cs} (95%) diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index d2360430c1..5fcaa99bc4 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -15,9 +15,20 @@ public class KeyBindings /// Adds a to the collection. /// /// + /// If has no Commands or is invalid. public void Add (Key key, KeyBinding binding) { + if (!key.IsValid) + { + throw new ArgumentException (nameof (key)); + } + + if (binding.Commands.Length == 0) + { + throw new ArgumentException (nameof (binding)); + } + if (TryGet (key, out KeyBinding _)) { throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); @@ -37,7 +48,6 @@ public void Add (Key key, KeyBinding binding) Bindings.Add (new (key), binding); } - /// /// /// Adds a new key combination that will trigger the commands in (if supported by the @@ -63,9 +73,10 @@ public void Add (Key key, KeyBinding binding) /// multiple commands are provided,they will be applied in sequence. The bound strike will be /// consumed if any took effect. /// + /// If is empty. public void Add (Key key, params Command [] commands) { - Add (key, new KeyBinding(commands)); + Add (key, new KeyBinding (commands)); } // TODO: Add a dictionary comparer that ignores Scope @@ -77,10 +88,7 @@ public void Add (Key key, params Command [] commands) /// Gets the keys that are bound. /// /// - public IEnumerable GetBoundKeys () - { - return Bindings.Keys; - } + public IEnumerable GetBoundKeys () { return Bindings.Keys; } /// /// The view that the are bound to. @@ -136,20 +144,23 @@ public Command [] GetCommands (Key key) return bindings.Commands; } - return Array.Empty (); + 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; - } + /// + /// 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. + /// + /// 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); diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 87921c66f7..e4bca89b32 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -187,7 +187,8 @@ public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, object? KeyBinding keyBinding = new () { Commands = [Command.HotKey], - Data = context + Key = newKey, + Data = context, }; // Add the base and Alt key diff --git a/Terminal.Gui/Views/Menu/ContextMenu.cs b/Terminal.Gui/Views/Menu/ContextMenu.cs index 851aae01e3..7c6f2d623b 100644 --- a/Terminal.Gui/Views/Menu/ContextMenu.cs +++ b/Terminal.Gui/Views/Menu/ContextMenu.cs @@ -150,7 +150,7 @@ private void RemoveKeyBindings (MenuBarItem? menuBarItem) if (menuItem.ShortcutKey != Key.Empty) { // Remove an existent ShortcutKey - _menuBar?.KeyBindings.Remove (menuItem.ShortcutKey!); + _menuBar?.HotKeyBindings.Remove (menuItem.ShortcutKey!); } } } diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index eae459098b..9c2ac63435 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -133,8 +133,8 @@ public override void BeginInit () KeyBinding keyBinding = new ([Command.Select], this, data: menuItem); // Remove an existent ShortcutKey - menuItem._menuBar.KeyBindings.Remove (menuItem.ShortcutKey!); - menuItem._menuBar.KeyBindings.Add (menuItem.ShortcutKey!, keyBinding); + menuItem._menuBar.HotKeyBindings.Remove (menuItem.ShortcutKey!); + menuItem._menuBar.HotKeyBindings.Add (menuItem.ShortcutKey!, keyBinding); } } } diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs index ed6e42a62a..749ec1a5a2 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -166,9 +166,9 @@ public MenuBar () HotKeyBindings.Add (Key, keyBinding); // TODO: Why do we have two keybindings for opening the menu? Ctrl-Space and Key? - KeyBindings.Add (Key.Space.WithCtrl, keyBinding); + HotKeyBindings.Add (Key.Space.WithCtrl, keyBinding); // This is needed for macOS because Key.Space.WithCtrl doesn't work - KeyBindings.Add (Key.Space.WithAlt, keyBinding); + HotKeyBindings.Add (Key.Space.WithAlt, keyBinding); // TODO: Figure out how to make Alt work (on Windows) //KeyBindings.Add (Key.WithAlt, keyBinding); diff --git a/Terminal.Gui/Views/Menu/MenuBarItem.cs b/Terminal.Gui/Views/Menu/MenuBarItem.cs index 5281c2da98..e68b1f87b6 100644 --- a/Terminal.Gui/Views/Menu/MenuBarItem.cs +++ b/Terminal.Gui/Views/Menu/MenuBarItem.cs @@ -222,7 +222,7 @@ public override void RemoveMenuItem () if (menuItem?.ShortcutKey != Key.Empty) { // Remove an existent ShortcutKey - _menuBar?.KeyBindings.Remove (menuItem?.ShortcutKey!); + _menuBar?.HotKeyBindings.Remove (menuItem?.ShortcutKey!); } } } @@ -230,7 +230,7 @@ public override void RemoveMenuItem () if (ShortcutKey != Key.Empty) { // Remove an existent ShortcutKey - _menuBar?.KeyBindings.Remove (ShortcutKey!); + _menuBar?.HotKeyBindings.Remove (ShortcutKey!); } var index = _menuBar!.Menus.IndexOf (this); diff --git a/Terminal.Gui/Views/Menu/MenuItem.cs b/Terminal.Gui/Views/Menu/MenuItem.cs index 5fe52f063b..7a222ebca1 100644 --- a/Terminal.Gui/Views/Menu/MenuItem.cs +++ b/Terminal.Gui/Views/Menu/MenuItem.cs @@ -289,15 +289,15 @@ private void AddOrUpdateShortcutKeyBinding (Key key) { if (key != Key.Empty) { - _menuBar.KeyBindings.Remove (key); + _menuBar.HotKeyBindings.Remove (key); } if (ShortcutKey != Key.Empty) { KeyBinding keyBinding = new ([Command.Select], null, data: this); // Remove an existent ShortcutKey - _menuBar.KeyBindings.Remove (ShortcutKey!); - _menuBar.KeyBindings.Add (ShortcutKey!, keyBinding); + _menuBar.HotKeyBindings.Remove (ShortcutKey!); + _menuBar.HotKeyBindings.Add (ShortcutKey!, keyBinding); } } @@ -378,7 +378,7 @@ public virtual void RemoveMenuItem () if (ShortcutKey != Key.Empty) { // Remove an existent ShortcutKey - _menuBar.KeyBindings.Remove (ShortcutKey!); + _menuBar.HotKeyBindings.Remove (ShortcutKey!); } } } diff --git a/UnitTests/Input/Keyboard/KeyBindingsTests.cs b/UnitTests/Input/Keyboard/KeyBindingsTests.cs index cf4a7815b0..5df3a6a143 100644 --- a/UnitTests/Input/Keyboard/KeyBindingsTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -257,10 +257,8 @@ public void ReplaceKey_Throws_If_New_Is_Empty () Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.Empty)); } - // Add with scope does the right things - [Theory] - [InlineData (KeyBindingScope.Focused)] - public void Scope_Add_Adds (KeyBindingScope scope) + [Fact] + public void Add_Adds () { var keyBindings = new KeyBindings (new ()); Command [] commands = { Command.Right, Command.Left }; @@ -280,12 +278,11 @@ public void Scope_Add_Adds (KeyBindingScope scope) Assert.Contains (Command.Left, resultCommands); } - [Theory] - [InlineData (KeyBindingScope.Focused)] - public void Scope_Get_Filters (KeyBindingScope scope) + [Fact] + public void Get_Gets () { var keyBindings = new KeyBindings (new ()); - Command [] commands = { Command.Right, Command.Left }; + Command [] commands = [Command.Right, Command.Left]; var key = new Key (Key.A); keyBindings.Add (key, commands); @@ -298,32 +295,6 @@ public void Scope_Get_Filters (KeyBindingScope scope) Assert.Contains (Command.Left, binding.Commands); } - [Theory] - [InlineData (KeyBindingScope.Focused)] - public void Scope_TryGet_Filters (KeyBindingScope scope) - { - var keyBindings = new KeyBindings (new ()); - Command [] commands = { Command.Right, Command.Left }; - - var key = new Key (Key.A); - keyBindings.Add (key, commands); - bool success = keyBindings.TryGet (key, out KeyBinding binding); - Assert.Contains (Command.Right, binding.Commands); - Assert.Contains (Command.Left, binding.Commands); - - success = keyBindings.TryGet (key, out binding); - Assert.Contains (Command.Right, binding.Commands); - Assert.Contains (Command.Left, binding.Commands); - - // negative test - success = keyBindings.TryGet (key, out binding); - Assert.False (success); - - Command [] resultCommands = keyBindings.GetCommands (key); - Assert.Contains (Command.Right, resultCommands); - Assert.Contains (Command.Left, resultCommands); - } - // TryGet [Fact] public void TryGet_Succeeds () diff --git a/UnitTests/View/Keyboard/HotKeyTests.cs b/UnitTests/View/Keyboard/HotKeyTests.cs index 614200ec01..4a70ff3487 100644 --- a/UnitTests/View/Keyboard/HotKeyTests.cs +++ b/UnitTests/View/Keyboard/HotKeyTests.cs @@ -1,4 +1,5 @@ using System.Text; +using UICatalog.Scenarios; using Xunit.Abstractions; namespace Terminal.Gui.ViewTests; @@ -27,9 +28,9 @@ public void AddKeyBindingsForHotKey_Sets (KeyCode key) // Verify key bindings were set // As passed - Command [] commands = view.KeyBindings.GetCommands (key); + Command [] commands = view.HotKeyBindings.GetCommands (key); Assert.Contains (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (key | KeyCode.AltMask); + commands = view.HotKeyBindings.GetCommands (key | KeyCode.AltMask); Assert.Contains (Command.HotKey, commands); KeyCode baseKey = key & ~KeyCode.ShiftMask; @@ -37,13 +38,13 @@ public void AddKeyBindingsForHotKey_Sets (KeyCode key) // If A...Z, with and without shift if (baseKey is >= KeyCode.A and <= KeyCode.Z) { - commands = view.KeyBindings.GetCommands (key | KeyCode.ShiftMask); + commands = view.HotKeyBindings.GetCommands (key | KeyCode.ShiftMask); Assert.Contains (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (key & ~KeyCode.ShiftMask); + commands = view.HotKeyBindings.GetCommands (key & ~KeyCode.ShiftMask); Assert.Contains (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (key | KeyCode.AltMask); + commands = view.HotKeyBindings.GetCommands (key | KeyCode.AltMask); Assert.Contains (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands ((key & ~KeyCode.ShiftMask) | KeyCode.AltMask); + commands = view.HotKeyBindings.GetCommands ((key & ~KeyCode.ShiftMask) | KeyCode.AltMask); Assert.Contains (Command.HotKey, commands); } else @@ -51,17 +52,40 @@ public void AddKeyBindingsForHotKey_Sets (KeyCode key) // Non A..Z keys should not have shift bindings if (key.HasFlag (KeyCode.ShiftMask)) { - commands = view.KeyBindings.GetCommands (key & ~KeyCode.ShiftMask); + commands = view.HotKeyBindings.GetCommands (key & ~KeyCode.ShiftMask); Assert.Empty (commands); } else { - commands = view.KeyBindings.GetCommands (key | KeyCode.ShiftMask); + commands = view.HotKeyBindings.GetCommands (key | KeyCode.ShiftMask); Assert.Empty (commands); } } } + [Fact] + public void AddKeyBindingsForHotKey_SetsBinding_Key () + { + var view = new View (); + view.HotKey = KeyCode.Z; + Assert.Equal (string.Empty, view.Title); + Assert.Equal (KeyCode.Z, view.HotKey); + + view.AddKeyBindingsForHotKey (view.HotKey, Key.A); + Assert.Equal (Key.A, view.HotKeyBindings.Bindings [Key.A].Key); + } + + [Fact] + public void AddKeyBindingsForHotKey_SetsBinding_Data () + { + var view = new View (); + view.HotKey = KeyCode.Z; + Assert.Equal (KeyCode.Z, view.HotKey); + + view.AddKeyBindingsForHotKey (view.HotKey, Key.A, "data"); + Assert.Equal ("data", view.HotKeyBindings.Bindings [Key.A].Data); + } + [Fact] public void Defaults () { @@ -72,6 +96,11 @@ public void Defaults () // Verify key bindings were set Command [] commands = view.KeyBindings.GetCommands (KeyCode.Null); Assert.Empty (commands); + + commands = view.HotKeyBindings.GetCommands (KeyCode.Null); + Assert.Empty (commands); + + Assert.Empty (view.HotKeyBindings.GetBoundKeys ()); } [Theory] @@ -94,7 +123,7 @@ public void NewKeyDownEvent_Runs_Default_HotKey_Command (KeyCode mask, bool expe public void NewKeyDownEvent_Ignores_Focus_KeyBindings_SuperView () { var view = new View (); - view.KeyBindings.Add (Key.A, Command.HotKey); // implies KeyBindingScope.Focused - so this should not be invoked + view.HotKeyBindings.Add (Key.A, Command.HotKey); view.KeyDownNotHandled += (s, e) => { Assert.Fail (); }; var superView = new View (); @@ -167,16 +196,16 @@ public void Set_RemovesOldKeyBindings () Assert.Equal (KeyCode.A, view.HotKey); // Verify key bindings were set - Command [] commands = view.KeyBindings.GetCommands (KeyCode.A); + Command [] commands = view.HotKeyBindings.GetCommands (KeyCode.A); Assert.Contains (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask); + commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask); Assert.Contains (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.AltMask); + commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.AltMask); Assert.Contains (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask); + commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask); Assert.Contains (Command.HotKey, commands); // Now set again @@ -184,16 +213,16 @@ public void Set_RemovesOldKeyBindings () Assert.Equal (string.Empty, view.Title); Assert.Equal (KeyCode.B, view.HotKey); - commands = view.KeyBindings.GetCommands (KeyCode.A); + commands = view.HotKeyBindings.GetCommands (KeyCode.A); Assert.DoesNotContain (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask); + commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask); Assert.DoesNotContain (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.AltMask); + commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.AltMask); Assert.DoesNotContain (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask); + commands = view.HotKeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask); Assert.DoesNotContain (Command.HotKey, commands); } @@ -232,7 +261,7 @@ public void Set_SetsKeyBindings (KeyCode key) // Verify key bindings were set // As passed - Command [] commands = view.KeyBindings.GetCommands (view.HotKey); + Command [] commands = view.HotKeyBindings.GetCommands (view.HotKey); Assert.Contains (Command.HotKey, commands); Key baseKey = view.HotKey.NoShift; @@ -240,13 +269,13 @@ public void Set_SetsKeyBindings (KeyCode key) // If A...Z, with and without shift if (baseKey.IsKeyCodeAtoZ) { - commands = view.KeyBindings.GetCommands (view.HotKey.WithShift); + commands = view.HotKeyBindings.GetCommands (view.HotKey.WithShift); Assert.Contains (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (view.HotKey.NoShift); + commands = view.HotKeyBindings.GetCommands (view.HotKey.NoShift); Assert.Contains (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (view.HotKey.WithAlt); + commands = view.HotKeyBindings.GetCommands (view.HotKey.WithAlt); Assert.Contains (Command.HotKey, commands); - commands = view.KeyBindings.GetCommands (view.HotKey.NoShift.WithAlt); + commands = view.HotKeyBindings.GetCommands (view.HotKey.NoShift.WithAlt); Assert.Contains (Command.HotKey, commands); } else @@ -254,12 +283,12 @@ public void Set_SetsKeyBindings (KeyCode key) // Non A..Z keys should not have shift bindings if (view.HotKey.IsShift) { - commands = view.KeyBindings.GetCommands (view.HotKey.NoShift); + commands = view.HotKeyBindings.GetCommands (view.HotKey.NoShift); Assert.Empty (commands); } else { - commands = view.KeyBindings.GetCommands (view.HotKey.WithShift); + commands = view.HotKeyBindings.GetCommands (view.HotKey.WithShift); Assert.Empty (commands); } } @@ -366,7 +395,7 @@ public void HotKey_Raises_HotKeyCommand () view.Selecting += (s, e) => selectRaised = true; Assert.Equal (KeyCode.T, view.HotKey); - Assert.True (Application.RaiseKeyDownEvent (Key.T)); + Assert.True (Application.RaiseKeyDownEvent (Key.T)); Assert.True (hotKeyRaised); Assert.False (acceptRaised); Assert.False (selectRaised); diff --git a/UnitTests/View/Keyboard/ViewKeyBindingTests.cs b/UnitTests/View/Keyboard/KeyBindingsTests.cs similarity index 95% rename from UnitTests/View/Keyboard/ViewKeyBindingTests.cs rename to UnitTests/View/Keyboard/KeyBindingsTests.cs index ef32fb8846..a07730b7ac 100644 --- a/UnitTests/View/Keyboard/ViewKeyBindingTests.cs +++ b/UnitTests/View/Keyboard/KeyBindingsTests.cs @@ -2,13 +2,14 @@ namespace Terminal.Gui.ViewTests; -public class ViewKeyBindingTests (ITestOutputHelper output) +/// +/// Tests for View.KeyBindings +/// +public class KeyBindingsTests () { - private readonly ITestOutputHelper _output = output; - [Fact] [AutoInitShutdown] - public void Focus_KeyBinding () + public void Focused_HotKey_Application_All_Work () { var view = new ScopedKeyBindingView (); var keyWasHandled = false; @@ -48,7 +49,7 @@ public void Focus_KeyBinding () [Fact] [AutoInitShutdown] - public void Focus_KeyBinding_Negative () + public void KeyBinding_Negative () { var view = new ScopedKeyBindingView (); var keyWasHandled = false; diff --git a/UnitTests/Views/ContextMenuTests.cs b/UnitTests/Views/ContextMenuTests.cs index 2d4da3ffbd..066848a501 100644 --- a/UnitTests/Views/ContextMenuTests.cs +++ b/UnitTests/Views/ContextMenuTests.cs @@ -1483,9 +1483,9 @@ public void KeyBindings_Removed_On_Close_ContextMenu () Assert.False (deleteFile); cm.Show (menuItems); - Assert.True (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.True (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); - Assert.True (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.WithCtrl)); + Assert.True (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.True (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.WithCtrl)); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); Application.MainLoop!.RunIteration (); @@ -1502,9 +1502,9 @@ public void KeyBindings_Removed_On_Close_ContextMenu () Assert.True (deleteFile); Assert.False (cm.MenuBar.IsMenuOpen); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.WithCtrl)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.WithCtrl)); newFile = false; renameFile = false; @@ -1555,8 +1555,8 @@ public void KeyBindings_With_ContextMenu_And_MenuBar () top.Add (menuBar); Application.Begin (top); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); Assert.Null (cm.MenuBar); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); @@ -1568,10 +1568,10 @@ public void KeyBindings_With_ContextMenu_And_MenuBar () newFile = false; cm.Show (menuItems); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); - Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.True (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); Assert.True (cm.MenuBar.IsMenuOpen); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); @@ -1584,10 +1584,10 @@ public void KeyBindings_With_ContextMenu_And_MenuBar () Assert.True (renameFile); Assert.False (cm.MenuBar.IsMenuOpen); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); newFile = false; renameFile = false; @@ -1634,7 +1634,7 @@ public void KeyBindings_With_Same_Shortcut_ContextMenu_And_MenuBar () top.Add (menuBar); Application.Begin (top); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); Assert.Null (cm.MenuBar); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); @@ -1645,8 +1645,8 @@ public void KeyBindings_With_Same_Shortcut_ContextMenu_And_MenuBar () newMenuBar = false; cm.Show (menuItems); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.True (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); Assert.True (cm.MenuBar.IsMenuOpen); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); @@ -1657,8 +1657,8 @@ public void KeyBindings_With_Same_Shortcut_ContextMenu_And_MenuBar () Assert.True (newContextMenu); Assert.False (cm.MenuBar!.IsMenuOpen); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); newMenuBar = false; newContextMenu = false; @@ -1704,20 +1704,20 @@ public void HotKeys_Removed_On_Close_ContextMenu () cm.Show (menuItems); Assert.True (cm.MenuBar!.IsMenuOpen); - Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.NoShift)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.WithAlt)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.NoShift)); + Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); + Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.WithAlt)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.NoShift)); Assert.Single (Application.Top!.Subviews); View [] menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray (); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.R.NoShift)); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.D.WithAlt)); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.D.NoShift)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.D.WithAlt)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.D.NoShift)); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithAlt)); Assert.False (cm.MenuBar!.IsMenuOpen); @@ -1734,12 +1734,12 @@ public void HotKeys_Removed_On_Close_ContextMenu () Application.MainLoop!.RunIteration (); Assert.True (deleteFile); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.NoShift)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.WithAlt)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.D.NoShift)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.WithAlt)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.NoShift)); newFile = false; renameFile = false; @@ -1796,9 +1796,9 @@ public void HotKeys_With_ContextMenu_And_MenuBar () top.Add (menuBar); Application.Begin (top); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); View [] menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == menuBar).ToArray (); Assert.Empty (menus); Assert.Null (cm.MenuBar); @@ -1807,7 +1807,7 @@ public void HotKeys_With_ContextMenu_And_MenuBar () Assert.True (menuBar.IsMenuOpen); Assert.Equal (2, Application.Top!.Subviews.Count); menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == menuBar).ToArray (); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithAlt)); Assert.False (menuBar.IsMenuOpen); Assert.False (Application.RaiseKeyDownEvent (Key.R.WithAlt)); @@ -1818,29 +1818,29 @@ public void HotKeys_With_ContextMenu_And_MenuBar () newFile = false; cm.Show (menuItems); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.F.NoShift)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.F.NoShift)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); Assert.True (cm.MenuBar!.IsMenuOpen); - Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); - Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.F.NoShift)); - Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.False (cm.MenuBar!.KeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); + Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.F.NoShift)); + Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); + Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); + Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); + Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); Assert.Equal (3, Application.Top!.Subviews.Count); menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray (); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.True (menus [1].KeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.True (menus [1].KeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); + Assert.True (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.True (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); Assert.True (cm.MenuBar.IsMenuOpen); Assert.True (Application.RaiseKeyDownEvent (Key.F.WithAlt)); Assert.False (cm.MenuBar.IsMenuOpen); @@ -1852,14 +1852,14 @@ public void HotKeys_With_ContextMenu_And_MenuBar () Assert.True (cm.MenuBar.IsMenuOpen); Assert.Equal (3, Application.Top!.Subviews.Count); menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray (); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.True (menus [0].KeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.False (menus [0].KeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (menus [0].KeyBindings.Bindings.ContainsKey (Key.R.NoShift)); - Assert.False (menus [1].KeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.False (menus [1].KeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.True (menus [1].KeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.True (menus [1].KeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); + Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); + Assert.False (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.False (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.False (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); + Assert.False (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); + Assert.True (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.True (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); Assert.True (Application.RaiseKeyDownEvent (Key.E.NoShift)); Assert.True (Application.RaiseKeyDownEvent (Key.R.WithAlt)); Assert.False (cm.MenuBar.IsMenuOpen); @@ -1867,14 +1867,14 @@ public void HotKeys_With_ContextMenu_And_MenuBar () Assert.True (renameFile); Assert.Single (Application.Top!.Subviews); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); - Assert.True (menuBar.KeyBindings.Bindings.ContainsKey (Key.F.NoShift)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (menuBar.KeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (cm.MenuBar.KeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); + Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.F.NoShift)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); + Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); newFile = false; renameFile = false; diff --git a/UnitTests/Views/MenuBarTests.cs b/UnitTests/Views/MenuBarTests.cs index 534ac5085e..a332492e8d 100644 --- a/UnitTests/Views/MenuBarTests.cs +++ b/UnitTests/Views/MenuBarTests.cs @@ -19,8 +19,8 @@ public void AddMenuBarItem_RemoveMenuItem_Dynamically () menuBar.Menus = [menuBarItem]; Assert.Single (menuBar.Menus); Assert.Single (menuBar.Menus [0].Children!); - Assert.Contains (Key.N.WithAlt, menuBar.KeyBindings.Bindings); - Assert.DoesNotContain (Key.I, menuBar.KeyBindings.Bindings); + Assert.Contains (Key.N.WithAlt, menuBar.HotKeyBindings.Bindings); + Assert.DoesNotContain (Key.I, menuBar.HotKeyBindings.Bindings); var top = new Toplevel (); top.Add (menuBar); @@ -39,12 +39,12 @@ public void AddMenuBarItem_RemoveMenuItem_Dynamically () menuItem.RemoveMenuItem (); Assert.Single (menuBar.Menus); Assert.Null (menuBar.Menus [0].Children); - Assert.Contains (Key.N.WithAlt, menuBar.KeyBindings.Bindings); - Assert.DoesNotContain (Key.I, menuBar.KeyBindings.Bindings); + Assert.Contains (Key.N.WithAlt, menuBar.HotKeyBindings.Bindings); + Assert.DoesNotContain (Key.I, menuBar.HotKeyBindings.Bindings); menuBarItem.RemoveMenuItem (); Assert.Empty (menuBar.Menus); - Assert.DoesNotContain (Key.N.WithAlt, menuBar.KeyBindings.Bindings); + Assert.DoesNotContain (Key.N.WithAlt, menuBar.HotKeyBindings.Bindings); top.Dispose (); } @@ -2998,12 +2998,12 @@ public void Update_ShortcutKey_KeyBindings_Old_ShortcutKey_Is_Removed () ] }; - Assert.Contains (Key.A.WithCtrl, menuBar.KeyBindings.Bindings); + Assert.Contains (Key.A.WithCtrl, menuBar.HotKeyBindings.Bindings); menuBar.Menus [0].Children! [0].ShortcutKey = Key.B.WithCtrl; - Assert.DoesNotContain (Key.A.WithCtrl, menuBar.KeyBindings.Bindings); - Assert.Contains (Key.B.WithCtrl, menuBar.KeyBindings.Bindings); + Assert.DoesNotContain (Key.A.WithCtrl, menuBar.HotKeyBindings.Bindings); + Assert.Contains (Key.B.WithCtrl, menuBar.HotKeyBindings.Bindings); } [Fact] diff --git a/UnitTests/Views/RadioGroupTests.cs b/UnitTests/Views/RadioGroupTests.cs index 8c11b5b6e3..43f5756278 100644 --- a/UnitTests/Views/RadioGroupTests.cs +++ b/UnitTests/Views/RadioGroupTests.cs @@ -64,24 +64,28 @@ public void Initialize_SelectedItem_With_Minus_One () } [Fact] - public void KeyBindings_Are_Added_Correctly () + public void HotKeyBindings_Are_Added_Correctly () { var rg = new RadioGroup { RadioLabels = new [] { "_Left", "_Right" } }; - Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.L)); - Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.R)); + Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.L)); + Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.R)); - Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.L.WithShift)); - Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.L.WithAlt)); + Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.L.WithShift)); + Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.L.WithAlt)); - Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.R.WithShift)); - Assert.NotEmpty (rg.KeyBindings.GetCommands (Key.R.WithAlt)); + Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.R.WithShift)); + Assert.NotEmpty (rg.HotKeyBindings.GetCommands (Key.R.WithAlt)); } [Fact] public void Commands_HasFocus () { Application.Navigation = new (); - var rg = new RadioGroup { RadioLabels = new [] { "Test", "New Test" } }; + var rg = new RadioGroup + { + Id = "rg", + RadioLabels = ["Test", "New Test"] + }; Application.Top = new (); Application.Top.Add (rg); rg.SetFocus (); @@ -201,7 +205,7 @@ public void Commands_HasFocus () public void HotKey_HasFocus_False () { Application.Navigation = new (); - var rg = new RadioGroup { RadioLabels = new [] { "Test", "New Test" } }; + var rg = new RadioGroup { RadioLabels = ["Test", "New Test"] }; Application.Top = new (); // With !HasFocus @@ -278,7 +282,7 @@ public void HotKey_HasFocus_False () public void HotKeys_HasFocus_False_Does_Not_SetFocus_Selects () { Application.Navigation = new (); - var rg = new RadioGroup { RadioLabels = new [] { "Item _A", "Item _B" } }; + var rg = new RadioGroup { RadioLabels = ["Item _A", "Item _B"] }; Application.Top = new (); // With !HasFocus @@ -363,14 +367,14 @@ public void HotKeys_HasFocus_False_Does_Not_SetFocus_Selects () [Fact] public void HotKeys_HasFocus_True_Selects () { - var rg = new RadioGroup { RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" } }; + var rg = new RadioGroup { RadioLabels = ["_Left", "_Right", "Cen_tered", "_Justified"] }; Application.Top = new (); Application.Top.Add (rg); rg.SetFocus (); - Assert.NotEmpty (rg.KeyBindings.GetCommands (KeyCode.L)); - Assert.NotEmpty (rg.KeyBindings.GetCommands (KeyCode.L | KeyCode.ShiftMask)); - Assert.NotEmpty (rg.KeyBindings.GetCommands (KeyCode.L | KeyCode.AltMask)); + Assert.NotEmpty (rg.HotKeyBindings.GetCommands (KeyCode.L)); + Assert.NotEmpty (rg.HotKeyBindings.GetCommands (KeyCode.L | KeyCode.ShiftMask)); + Assert.NotEmpty (rg.HotKeyBindings.GetCommands (KeyCode.L | KeyCode.AltMask)); Assert.True (Application.RaiseKeyDownEvent (Key.T)); Assert.Equal (2, rg.SelectedItem); @@ -425,7 +429,7 @@ public void HotKey_SetsFocus () var group = new RadioGroup { Title = "Radio_Group", - RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" } + RadioLabels = ["_Left", "_Right", "Cen_tered", "_Justified"] }; superView.Add (group); @@ -450,7 +454,7 @@ public void HotKey_No_SelectedItem_Selects_First () var group = new RadioGroup { Title = "Radio_Group", - RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" } + RadioLabels = ["_Left", "_Right", "Cen_tered", "_Justified"] }; group.SelectedItem = -1; @@ -488,7 +492,7 @@ public void HotKeys_Does_Not_SetFocus () [Fact] public void HotKey_Command_Does_Not_Accept () { - var group = new RadioGroup { RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" } }; + var group = new RadioGroup { RadioLabels = ["_Left", "_Right", "Cen_tered", "_Justified"] }; var accepted = false; group.Accepting += OnAccept; @@ -504,7 +508,7 @@ public void HotKey_Command_Does_Not_Accept () [Fact] public void Accept_Command_Fires_Accept () { - var group = new RadioGroup { RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" } }; + var group = new RadioGroup { RadioLabels = ["_Left", "_Right", "Cen_tered", "_Justified"] }; var accepted = false; group.Accepting += OnAccept; @@ -521,7 +525,7 @@ public void Accept_Command_Fires_Accept () [AutoInitShutdown] public void Orientation_Width_Height_Vertical_Horizontal_Space () { - var rg = new RadioGroup { RadioLabels = new [] { "Test", "New Test 你" } }; + var rg = new RadioGroup { RadioLabels = ["Test", "New Test 你"] }; var win = new Window { Width = Dim.Fill (), Height = Dim.Fill () }; win.Add (rg); var top = new Toplevel (); @@ -597,7 +601,7 @@ public void SelectedItemChanged_Event () { int previousSelectedItem = -1; int selectedItem = -1; - var rg = new RadioGroup { RadioLabels = new [] { "Test", "New Test" } }; + var rg = new RadioGroup { RadioLabels = ["Test", "New Test"] }; rg.SelectedItemChanged += (s, e) => { diff --git a/UnitTests/Views/ShortcutTests.cs b/UnitTests/Views/ShortcutTests.cs index 760cb70f6f..7041913ddc 100644 --- a/UnitTests/Views/ShortcutTests.cs +++ b/UnitTests/Views/ShortcutTests.cs @@ -281,16 +281,16 @@ public void Key_Changing_Removes_Previous_Binding () var shortcut = new Shortcut (); shortcut.Key = Key.A; - Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys); + Assert.Contains (Key.A, shortcut.HotKeyBindings.Bindings.Keys); shortcut.Key = Key.B; - Assert.DoesNotContain (Key.A, shortcut.KeyBindings.Bindings.Keys); - Assert.Contains (Key.B, shortcut.KeyBindings.Bindings.Keys); + Assert.DoesNotContain (Key.A, shortcut.HotKeyBindings.Bindings.Keys); + Assert.Contains (Key.B, shortcut.HotKeyBindings.Bindings.Keys); } // Test Key gets bound correctly [Fact] - public void KeyBindingScope_Defaults_To_HotKey () + public void BindKeyToApplication_Defaults_To_HotKey () { var shortcut = new Shortcut (); @@ -298,7 +298,7 @@ public void KeyBindingScope_Defaults_To_HotKey () } [Fact] - public void KeyBindingScope_Can_Be_Set () + public void BindKeyToApplication_Can_Be_Set () { var shortcut = new Shortcut (); @@ -308,19 +308,19 @@ public void KeyBindingScope_Can_Be_Set () } [Fact] - public void KeyBindingScope_Changing_Adjusts_KeyBindings () + public void BindKeyToApplication_Changing_Adjusts_KeyBindings () { var shortcut = new Shortcut (); shortcut.Key = Key.A; - Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys); + Assert.Contains (Key.A, shortcut.HotKeyBindings.Bindings.Keys); shortcut.BindKeyToApplication = true; - Assert.DoesNotContain (Key.A, shortcut.KeyBindings.Bindings.Keys); + Assert.DoesNotContain (Key.A, shortcut.HotKeyBindings.Bindings.Keys); Assert.NotEmpty (Application.KeyBindings.GetBindings(Key.A)); shortcut.BindKeyToApplication = false; - Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys); + Assert.Contains (Key.A, shortcut.HotKeyBindings.Bindings.Keys); Assert.Empty (Application.KeyBindings.GetBindings (Key.A)); } From 5a0b3507a0d3d86f3f39c211f57bb697a8dd447e Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 7 Dec 2024 14:37:17 -0800 Subject: [PATCH 18/40] Merged v2_develop. Code cleanup --- .../AnsiEscapeSequenceRequest.cs | 2 +- Terminal.Gui/Input/ICommandContext.cs | 4 +- Terminal.Gui/Input/Keyboard/KeyBinding.cs | 1 - Terminal.Gui/Input/Keyboard/KeyBindings.cs | 10 ++-- Terminal.Gui/View/View.Keyboard.cs | 30 +++++++----- Terminal.Gui/Views/CharMap/CharMap.cs | 3 ++ Terminal.Gui/Views/Menu/Menu.cs | 2 +- Terminal.Gui/Views/Shortcut.cs | 2 +- .../ConsoleDrivers/AnsiResponseParserTests.cs | 2 - UnitTests/View/Keyboard/KeyboardEventTests.cs | 4 +- UnitTests/View/ViewCommandTests.cs | 48 ++++++++----------- 11 files changed, 53 insertions(+), 55 deletions(-) diff --git a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs index d2e70ae283..4b872ab626 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiEscapeSequenceRequest.cs @@ -21,7 +21,7 @@ public class AnsiEscapeSequenceRequest : AnsiEscapeSequence /// - /// Sends the to the raw output stream of the current . + /// Sends the to the raw output stream of the current . /// Only call this method from the main UI thread. You should use if /// sending many requests. /// diff --git a/Terminal.Gui/Input/ICommandContext.cs b/Terminal.Gui/Input/ICommandContext.cs index 740f059c9d..644029ca28 100644 --- a/Terminal.Gui/Input/ICommandContext.cs +++ b/Terminal.Gui/Input/ICommandContext.cs @@ -1,12 +1,14 @@ #nullable enable namespace Terminal.Gui; +#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// Describes the context in which a is being invoked. inherits from this interface. /// When a is invoked, /// a context object is passed to Command handlers as an reference. /// -/// . +/// . +#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved public interface ICommandContext { /// diff --git a/Terminal.Gui/Input/Keyboard/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs index 872429b9d0..7e05b7b452 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -24,7 +24,6 @@ public KeyBinding (Command [] commands, object? context = null) /// Initializes a new instance. /// The commands this key binding will invoke. - /// The scope of the . /// 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) diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index 5fcaa99bc4..a95073c9e7 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -48,17 +48,13 @@ public void Add (Key key, KeyBinding binding) Bindings.Add (new (key), binding); } +#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved /// /// /// 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 /// . /// @@ -74,6 +70,7 @@ public void Add (Key key, KeyBinding binding) /// consumed if any took effect. /// /// If is empty. +#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved public void Add (Key key, params Command [] commands) { Add (key, new KeyBinding (commands)); @@ -168,8 +165,7 @@ public IEnumerable GetKeysFromCommands (params Command [] commands) /// Removes a from the collection. /// - /// Optional View for bindings. - public void Remove (Key key, View? boundViewForAppScope = null) + public void Remove (Key key) { if (!TryGet (key, out KeyBinding _)) { diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index e4bca89b32..9c8917f951 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -299,13 +299,13 @@ public bool NewKeyDownEvent (Key key) // During (this is what can be cancelled) // TODO: NewKeyDownEvent returns bool. It should be bool? so state of InvokeCommands can be reflected up stack - if (InvokeCommandsBoundToKey (key) is true || key.Handled) + if (InvokeCommands (key) is true || key.Handled) { return true; } bool? handled = false; - if (InvokeCommandsBoundToHotKeyOnSubviews (key, ref handled)) + if (InvokeCommandsBoundToHotKey (key, ref handled)) { return true; } @@ -516,7 +516,7 @@ bool RaiseKeyUp (Key k) /// Gets the bindings for this view that will be invoked only if this view has focus. public KeyBindings KeyBindings { get; internal set; } = null!; - /// Gets the bindings for this view that will be invoked regardless of whehter this view has focus or not. + /// Gets the bindings for this view that will be invoked regardless of whether this view has focus or not. public KeyBindings HotKeyBindings { get; internal set; } = null!; /// @@ -531,7 +531,7 @@ bool RaiseKeyUp (Key k) /// if at least one command was invoked and handled (or /// cancelled); input processing should stop. /// - internal bool? InvokeCommandsBoundToKey (Key key) + internal bool? InvokeCommands (Key key) { // * If no key binding was found, `InvokeKeyBindings` returns `null`. // Continue passing the event (return `false` from `OnInvokeKeyBindings`). @@ -539,7 +539,7 @@ bool RaiseKeyUp (Key k) // `InvokeKeyBindings` returns `false`. Continue passing the event (return `false` from `OnInvokeKeyBindings`).. // * If key bindings were found, and any handled the key (at least one `Command` returned `true`), // `InvokeKeyBindings` returns `true`. Continue passing the event (return `false` from `OnInvokeKeyBindings`). - bool? handled = InvokeCommandsBoundToFocusedKey (key); + bool? handled = DoInvokeCommands (key); if (handled is true) { @@ -568,7 +568,7 @@ bool RaiseKeyUp (Key k) private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Key key, ref bool? handled) { - bool? adornmentHandled = adornment.InvokeCommandsBoundToKey (key); + bool? adornmentHandled = adornment.InvokeCommands (key); if (adornmentHandled is true) { @@ -582,7 +582,7 @@ private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Ke foreach (View subview in adornment.Subviews) { - bool? subViewHandled = subview.InvokeCommandsBoundToKey (key); + bool? subViewHandled = subview.InvokeCommands (key); if (subViewHandled is { }) { @@ -598,7 +598,14 @@ private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Ke return false; } - internal bool InvokeCommandsBoundToHotKeyOnSubviews (Key key, ref bool? handled, bool invoke = true) + // BUGBUG: This will miss any hotkeys in subviews of Adornments. + /// + /// Invokes any commands bound to on this view and subviews. + /// + /// + /// + /// + internal bool InvokeCommandsBoundToHotKey (Key key, ref bool? handled) { bool? weHandled = InvokeCommandsBoundToHotKey (key); if (weHandled is true) @@ -606,7 +613,7 @@ internal bool InvokeCommandsBoundToHotKeyOnSubviews (Key key, ref bool? handled, return true; } - // Now, process any key bindings in the subviews that are tagged to KeyBindingScope.HotKey. + // Now, process any HotKey bindings in the subviews foreach (View subview in Subviews) { if (subview == Focused) @@ -614,7 +621,7 @@ internal bool InvokeCommandsBoundToHotKeyOnSubviews (Key key, ref bool? handled, continue; } - bool recurse = subview.InvokeCommandsBoundToHotKeyOnSubviews (key, ref handled, invoke); + bool recurse = subview.InvokeCommandsBoundToHotKey (key, ref handled); if (recurse || (handled is { } && (bool)handled)) { @@ -669,7 +676,7 @@ public bool IsHotKeyBound (Key key, out View? boundView) /// if at least one command was invoked and handled (or cancelled); input processing should /// stop. /// - protected bool? InvokeCommandsBoundToFocusedKey (Key key) + protected bool? DoInvokeCommands (Key key) { if (!KeyBindings.TryGet (key, out KeyBinding binding)) { @@ -702,7 +709,6 @@ public bool IsHotKeyBound (Key key, out View? boundView) /// See for an overview of Terminal.Gui keyboard APIs. /// /// The key event passed. - /// The scope. /// /// if no command was invoked; input processing should continue. /// if at least one command was invoked and was not handled (or cancelled); input processing diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index 9f706bd514..b49c8c4e6f 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -20,9 +20,12 @@ public class CharMap : View, IDesignable private ContextMenu _contextMenu = new (); + /// /// Initializes a new instance. /// + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] public CharMap () { base.ColorScheme = Colors.ColorSchemes ["Dialog"]; diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 9c2ac63435..369bf88189 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -251,7 +251,7 @@ protected override bool OnKeyDownNotHandled (Key keyEvent) { // We didn't handle the key, pass it on to host bool? handled = null; - return _host.InvokeCommandsBoundToHotKeyOnSubviews (keyEvent, ref handled, true) == true; + return _host.InvokeCommandsBoundToHotKey (keyEvent, ref handled) == true; } protected override bool OnMouseEvent (MouseEventArgs me) diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 56129e0367..f52235da46 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -18,7 +18,7 @@ namespace Terminal.Gui; /// - Pressing the HotKey specified by . /// /// -/// If is , will invoke +/// If is , will invoke /// /// regardless of what View has focus, enabling an application-wide keyboard shortcut. /// diff --git a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs index cbbfd24d16..fbc87761f5 100644 --- a/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs +++ b/UnitTests/ConsoleDrivers/AnsiResponseParserTests.cs @@ -438,8 +438,6 @@ public void ShouldSwallowUnknownResponses_WhenDelegateSaysSo () [Fact] public void UnknownResponses_ParameterShouldMatch () { - int i = 0; - // Track unknown responses passed to the UnexpectedResponseHandler var unknownResponses = new List (); diff --git a/UnitTests/View/Keyboard/KeyboardEventTests.cs b/UnitTests/View/Keyboard/KeyboardEventTests.cs index 12cffb9a73..fa842d50fc 100644 --- a/UnitTests/View/Keyboard/KeyboardEventTests.cs +++ b/UnitTests/View/Keyboard/KeyboardEventTests.cs @@ -273,12 +273,12 @@ public void NewKeyUpEvent_KeyUp_Handled_True_Stops_Processing () [InlineData (null, null)] [InlineData (true, true)] [InlineData (false, false)] - public void InvokeCommandsBoundToKey_Returns_Nullable_Properly (bool? toReturn, bool? expected) + public void InvokeCommands_Returns_Nullable_Properly (bool? toReturn, bool? expected) { var view = new KeyBindingsTestView (); view.CommandReturns = toReturn; - bool? result = view.InvokeCommandsBoundToKey (Key.A); + bool? result = view.InvokeCommands (Key.A); Assert.Equal (expected, result); } diff --git a/UnitTests/View/ViewCommandTests.cs b/UnitTests/View/ViewCommandTests.cs index 3f4e89e2ed..ff42af6525 100644 --- a/UnitTests/View/ViewCommandTests.cs +++ b/UnitTests/View/ViewCommandTests.cs @@ -1,12 +1,9 @@ -using System.ComponentModel; -using System.Text; -using Xunit.Abstractions; - namespace Terminal.Gui.ViewTests; -public class ViewCommandTests (ITestOutputHelper output) +public class ViewCommandTests { #region OnAccept/Accept tests + [Fact] public void Accept_Command_Raises_NoFocus () { @@ -77,8 +74,8 @@ public void Accept_Command_Invokes_Accept_Event () [Fact] public void Accept_Command_Bubbles_Up_To_SuperView () { - var view = new ViewEventTester () { Id = "view" }; - var subview = new ViewEventTester () { Id = "subview" }; + var view = new ViewEventTester { Id = "view" }; + var subview = new ViewEventTester { Id = "subview" }; view.Add (subview); subview.InvokeCommand (Command.Accept); @@ -97,7 +94,7 @@ public void Accept_Command_Bubbles_Up_To_SuperView () Assert.Equal (1, view.OnAcceptedCount); // Add a super view to test deeper hierarchy - var superView = new ViewEventTester () { Id = "superView" }; + var superView = new ViewEventTester { Id = "superView" }; superView.Add (view); subview.InvokeCommand (Command.Accept); @@ -135,7 +132,7 @@ public void MouseClick_Does_Not_Invoke_Accept_Command () [CombinatorialData] public void Select_Command_Raises_SetsFocus (bool canFocus) { - var view = new ViewEventTester () + var view = new ViewEventTester { CanFocus = canFocus }; @@ -236,30 +233,29 @@ public ViewEventTester () CanFocus = true; Accepting += (s, a) => - { - a.Cancel = HandleAccepted; - AcceptedCount++; - }; + { + a.Cancel = HandleAccepted; + AcceptedCount++; + }; HandlingHotKey += (s, a) => - { - a.Cancel = HandleHandlingHotKey; - HandlingHotKeyCount++; - }; - + { + a.Cancel = HandleHandlingHotKey; + HandlingHotKeyCount++; + }; Selecting += (s, a) => - { - a.Cancel = HandleSelecting; - SelectingCount++; - }; + { + a.Cancel = HandleSelecting; + SelectingCount++; + }; } public int OnAcceptedCount { get; set; } public int AcceptedCount { get; set; } public bool HandleOnAccepted { get; set; } - /// + /// protected override bool OnAccepting (CommandEventArgs args) { OnAcceptedCount++; @@ -273,7 +269,7 @@ protected override bool OnAccepting (CommandEventArgs args) public int HandlingHotKeyCount { get; set; } public bool HandleOnHandlingHotKey { get; set; } - /// + /// protected override bool OnHandlingHotKey (CommandEventArgs args) { OnHandlingHotKeyCount++; @@ -283,12 +279,11 @@ protected override bool OnHandlingHotKey (CommandEventArgs args) public bool HandleHandlingHotKey { get; set; } - public int OnSelectingCount { get; set; } public int SelectingCount { get; set; } public bool HandleOnSelecting { get; set; } - /// + /// protected override bool OnSelecting (CommandEventArgs args) { OnSelectingCount++; @@ -297,6 +292,5 @@ protected override bool OnSelecting (CommandEventArgs args) } public bool HandleSelecting { get; set; } - } } From f673ef3b6c6a4f6a2f171a026415605e69e2ce4c Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 7 Dec 2024 22:12:27 -0800 Subject: [PATCH 19/40] Doc updates. Code cleanup --- .../KeyBindingScopeExtensions.cs | 93 ------------------- Terminal.Gui/Input/Keyboard/KeyBinding.cs | 2 +- .../Input/Keyboard/KeyBindingScope.cs | 26 ------ docfx/docs/keyboard.md | 47 +++++++--- 4 files changed, 37 insertions(+), 131 deletions(-) delete mode 100644 Terminal.Gui/EnumExtensions/KeyBindingScopeExtensions.cs delete mode 100644 Terminal.Gui/Input/Keyboard/KeyBindingScope.cs diff --git a/Terminal.Gui/EnumExtensions/KeyBindingScopeExtensions.cs b/Terminal.Gui/EnumExtensions/KeyBindingScopeExtensions.cs deleted file mode 100644 index 6f42f4c826..0000000000 --- a/Terminal.Gui/EnumExtensions/KeyBindingScopeExtensions.cs +++ /dev/null @@ -1,93 +0,0 @@ -#nullable enable - -using System.CodeDom.Compiler; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; - -namespace Terminal.Gui.EnumExtensions; - -/// Extension methods for the type. -[GeneratedCode ("Terminal.Gui.Analyzers.Internal", "1.0")] -[CompilerGenerated] -[DebuggerNonUserCode] -[ExcludeFromCodeCoverage (Justification = "Generated code is already tested.")] -[PublicAPI] -public static class KeyBindingScopeExtensions -{ - /// - /// Directly converts this value to an value with the - /// same binary representation. - /// - /// NO VALIDATION IS PERFORMED! - [MethodImpl (MethodImplOptions.AggressiveInlining)] - public static int AsInt32 (this KeyBindingScope e) => Unsafe.As (ref e); - - /// - /// Directly converts this value to a value with the - /// same binary representation. - /// - /// NO VALIDATION IS PERFORMED! - [MethodImpl (MethodImplOptions.AggressiveInlining)] - public static uint AsUInt32 (this KeyBindingScope e) => Unsafe.As (ref e); - - /// - /// Determines if the specified flags are set in the current value of this - /// . - /// - /// NO VALIDATION IS PERFORMED! - /// - /// True, if all flags present in are also present in the current value of the - /// .
Otherwise false. - ///
- [MethodImpl (MethodImplOptions.AggressiveInlining)] - public static bool FastHasFlags (this KeyBindingScope e, KeyBindingScope checkFlags) - { - ref uint enumCurrentValueRef = ref Unsafe.As (ref e); - ref uint checkFlagsValueRef = ref Unsafe.As (ref checkFlags); - - return (enumCurrentValueRef & checkFlagsValueRef) == checkFlagsValueRef; - } - - /// - /// Determines if the specified mask bits are set in the current value of this - /// . - /// - /// - /// The value to check against the - /// value. - /// - /// A mask to apply to the current value. - /// - /// True, if all bits set to 1 in the mask are also set to 1 in the current value of the - /// .
Otherwise false. - ///
- /// NO VALIDATION IS PERFORMED! - [MethodImpl (MethodImplOptions.AggressiveInlining)] - public static bool FastHasFlags (this KeyBindingScope e, int mask) - { - ref int enumCurrentValueRef = ref Unsafe.As (ref e); - - return (enumCurrentValueRef & mask) == mask; - } - - /// - /// Determines if the specified value is explicitly defined as a named value of the - /// type. - /// - /// - /// Only explicitly named values return true, as with IsDefined. Combined valid flag values of flags enums which are - /// not explicitly named will return false. - /// - public static bool FastIsDefined (this KeyBindingScope _, int value) - { - return value switch - { - 0 => true, - 1 => true, - 2 => true, - 4 => true, - _ => false - }; - } -} diff --git a/Terminal.Gui/Input/Keyboard/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs index 7e05b7b452..8ae7103ea1 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui; /// -/// Provides a collection of objects that are scoped to . +/// Provides a collection of objects stored in . /// /// /// diff --git a/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs b/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs deleted file mode 100644 index cfd31f6bca..0000000000 --- a/Terminal.Gui/Input/Keyboard/KeyBindingScope.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Terminal.Gui; - -/// -/// Defines the scope of a that has been bound to a key with -/// . -/// -/// -/// Key bindings are scoped to the most-focused view () by default. -/// -/// -/// -/// -[Flags] -public enum KeyBindingScope -{ - /// The key binding is disabled. - Disabled = 0, - - /// - /// The key binding is scoped to just the view that has focus. - /// - /// - /// - /// - Focused = 1 -} diff --git a/docfx/docs/keyboard.md b/docfx/docs/keyboard.md index ddace3282c..4abd704259 100644 --- a/docfx/docs/keyboard.md +++ b/docfx/docs/keyboard.md @@ -1,4 +1,4 @@ -# Keyboard Events +# Keyboard Deep Dive ## Tenets for Terminal.Gui Keyboard Handling (Unless you know better ones...) @@ -14,19 +14,34 @@ Tenets higher in the list have precedence over tenets lower in the list. * **If It's Hot, It Works** - If a View with a @Terminal.Gui.View.HotKey is visible, and the HotKey is visible, the user should be able to press that HotKey and whatever behavior is defined for it should work. For example, in v1, when a Modal view was active, the HotKeys on MenuBar continued to show "hot". In v2 we strive to ensure this doesn't happen. - ## Keyboard APIs *Terminal.Gui* provides the following APIs for handling keyboard input: -### **[Key](~/api/Terminal.Gui.Key.yml)** - -The `Key` class provides a platform-independent abstraction for common keyboard operations. It is used for processing keyboard input and raising keyboard events. This class provides a high-level abstraction with helper methods and properties for common keyboard operations. Use this class instead of the low-level `KeyCode` enum when possible. +* **Key** - @Terminal.Gui.Key provides a platform-independent abstraction for common keyboard operations. It is used for processing keyboard input and raising keyboard events. This class provides a high-level abstraction with helper methods and properties for common keyboard operations. Use this class instead of the low-level `KeyCode` enum when possible. +* **Key Bindings** - Key Bindings provide a declarative method for handling keyboard input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.KeyBindings to indicate which key presses will invoke the command. +* **Key Events** - The Key Bindings API is rich enough to support the vast majority of use-cases. However, in some cases subscribing directly to key events is needed (e.g. when capturing arbitrary typing by a user). Use @Terminal.Gui.View.KeyDown and related events in these cases. -See [Key](~/api/Terminal.Gui.Key.yml) for more details. +Each of these APIs are described more fully below. ### **[Key Bindings](~/api/Terminal.Gui.KeyBindings.yml)** +Key Bindings is the preferred way of handling keyboard input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.KeyBindings to indicate which key presses will invoke the command. For example, if a View wants to respond to the user pressing the up arrow key to scroll up it would do this + +```cs +public MyView : View +{ + AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); + KeyBindings.Add (Key.CursorUp, Command.ScrollUp); +} +``` + +The `Character Map` Scenario includes a View called `CharMap` that is a good example of the Key Bindings API. + +The [Command](~/api/Terminal.Gui.Command.yml) enum lists generic operations that are implemented by views. For example `Command.Accept` in a `Button` results in the `Accepting` event +firing while in `TableView` it is bound to `CellActivated`. Not all commands +are implemented by all views (e.g. you cannot scroll in a `Button`). Use the @Terminal.Gui.View.GetSupportedCommands() method to determine which commands are implemented by a `View`. + The default key for activating a button is `Space`. You can change this using `KeyBindings.ReplaceKey()`: @@ -35,11 +50,21 @@ var btn = new Button () { Title = "Press me" }; btn.KeyBindings.ReplaceKey (btn.KeyBindings.GetKeyFromCommands (Command.Accept)); ``` -The [Command](~/api/Terminal.Gui.Command.yml) enum lists generic operations that are implemented by views. For example `Command.Accept` in a `Button` results in the `Clicked` event -firing while in `TableView` it is bound to `CellActivated`. Not all commands -are implemented by all views (e.g. you cannot scroll in a `Button`). Use the @Terminal.Gui.View.GetSupportedCommands() method to determine which commands are implemented by a `View`. +Key Bindings can be added at the `Application` or `View` level. + +For **Application-scoped Key Bindings** there are two categories of Application-scoped Key Bindings: + +1) **Application Command Key Bindings** - Bindings for `Command`s supported by @Terminal.Gui.Application. For example, @Terminal.Gui.Application.QuitKey, which is bound to `Command.Quit` and results in @Terminal.Gui.Application.RequestStop being called. +2) **Application Key Bindings** - Bindings for `Command`s supported on arbitrary `Views` that are meant to be invoked regardless of which part of the application is visible/active. + +Use @Terminal.Gui.Application.KeyBindings to add or modify Application-scoped Key Bindings. + +**View-scoped Key Bindings** also have two categories: + +1) **HotKey Bindings** - These bind to `Command`s that will be invoked regardless of whether the View has focus or not. The most common use-case for `HotKey` bindings is @Terminal.Gui.View.HotKey. For example, a `Button` with a `Title` of `_OK`, the user can press `Alt-O` and the button will be accepted regardless of whether it has focus or not. Add and modify HotKey bindings with @Terminal.Gui.View.HotKeyBindings. +2) **Focused Bindings** - These bind to `Command`s that will be invoked only when the View has focus. Focused Key Bindings are the easiest way to enable a View to support responding to key events. Add and modify Focused bindings with @Terminal.Gui.View.KeyBindings. -Key Bindings can be added at the `Application` or `View` level. For Application-scoped Key Bindings see @Terminal.Gui.Application.Navigation. For View-scoped Key Bindings see @Terminal.Gui.View.KeyBindings. +**Application-Scoped** Key Bindings ### **@"Terminal.Gui.View.HotKey"** @@ -57,7 +82,7 @@ The Command can be invoked even if the `View` that defines them is not focused o [MenuBar](~/api/Terminal.Gui.MenuBar.yml), [ContextMenu](~/api/Terminal.Gui.ContextMenu.yml), and [StatusBar](~/api/Terminal.Gui.StatusBar.yml) support `Shortcut`s. -### **Handling Keyboard Events** +### **Key Events** Keyboard events are retrieved from [Console Drivers](drivers.md) each iteration of the [Application](~/api/Terminal.Gui.Application.yml) [Main Loop](mainloop.md). The console driver raises the @Terminal.Gui.ConsoleDriver.KeyDown and @Terminal.Gui.ConsoleDriver.KeyUp events which invoke @Terminal.Gui.Application.RaiseKeyDown(Terminal.Gui.Key) and @Terminal.Gui.Application.RaiseKeyUp(Terminal.Gui.Key) respectively. From 0b72db27858eccaf72b849df862cf82828574b1a Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 7 Dec 2024 22:22:17 -0800 Subject: [PATCH 20/40] Doc updates. --- docfx/docs/View.md | 4 ++++ docfx/docs/keyboard.md | 6 +++--- docfx/docs/mouse.md | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/docfx/docs/View.md b/docfx/docs/View.md index 332ef414dd..e91d7eac1a 100644 --- a/docfx/docs/View.md +++ b/docfx/docs/View.md @@ -14,6 +14,10 @@ * *Parent View* - A view that holds a reference to another view in a parent/child relationship, but is NOT a SuperView of the child. Terminal.Gui uses the terms "Child" and "Parent" sparingly. Generally Subview/SuperView is preferred. +### Input + +See the [Keyboard Deep Dive](keyboard.md) and [Mouse Deep Dive](mouse.md). + ### Layout and Arrangement See the [Layout Deep Dive](layout.md) and the [Arrangement Deep Dive](arrangement.md). diff --git a/docfx/docs/keyboard.md b/docfx/docs/keyboard.md index 4abd704259..2a6abd76ed 100644 --- a/docfx/docs/keyboard.md +++ b/docfx/docs/keyboard.md @@ -66,13 +66,13 @@ Use @Terminal.Gui.Application.KeyBindings to add or modify Application-scoped Ke **Application-Scoped** Key Bindings -### **@"Terminal.Gui.View.HotKey"** +### HotKey A **HotKey** is a key press that selects a visible UI item. For selecting items across `View`s (e.g. a `Button` in a `Dialog`) the key press must have the `Alt` modifier. For selecting items within a `View` that are not `View`s themselves, the key press can be key without the `Alt` modifier. For example, in a `Dialog`, a `Button` with the text of "_Text" can be selected with `Alt+T`. Or, in a `Menu` with "_File _Edit", `Alt+F` will select (show) the "_File" menu. If the "_File" menu has a sub-menu of "_New" `Alt+N` or `N` will ONLY select the "_New" sub-menu if the "_File" menu is already opened. By default, the `Text` of a `View` is used to determine the `HotKey` by looking for the first occurrence of the @Terminal.Gui.View.HotKeySpecifier (which is underscore (`_`) by default). The character following the underscore is the `HotKey`. If the `HotKeySpecifier` is not found in `Text`, the first character of `Text` is used as the `HotKey`. The `Text` of a `View` can be changed at runtime, and the `HotKey` will be updated accordingly. @"Terminal.Gui.View.HotKey" is `virtual` enabling this behavior to be customized. -### **[Shortcut](~/api/Terminal.Gui.Shortcut.yml)** +### **Shortcut** A **Shortcut** is an opinionated (visually & API) View for displaying a command, help text, key key press that invokes a [Command](~/api/Terminal.Gui.Command.yml). @@ -141,7 +141,7 @@ To define application key handling logic for an entire application in cases wher ## View -* Implements support for `KeyBindingScope.View` and `KeyBindingScope.HotKey`. +* Implements support for `KeyBindings` and `HotKeyBindings`. * Exposes cancelable non-virtual methods for a new key event: `NewKeyDownEvent` and `NewKeyUpEvent`. These methods are called by `Application` can be called to simulate keyboard input. * Exposes cancelable virtual methods for a new key event: `OnKeyDown` and `OnKeyUp`. These methods are called by `NewKeyDownEvent` and `NewKeyUpEvent` and can be overridden to handle keyboard input. diff --git a/docfx/docs/mouse.md b/docfx/docs/mouse.md index 18ffd2cb0e..ffc136be5d 100644 --- a/docfx/docs/mouse.md +++ b/docfx/docs/mouse.md @@ -10,6 +10,30 @@ Tenets higher in the list have precedence over tenets lower in the list. ## Mouse APIs +*Terminal.Gui* provides the following APIs for handling mouse input: + +* **MouseEventArgs** - @Terminal.Gui.MouseEventArgs provides a platform-independent abstraction for common mouse operations. It is used for processing mouse input and raising mouse events. +* **Mouse Bindings** - Mouse Bindings provide a declarative method for handling mouse input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.MouseBindings to indicate which mouse events will invoke the command. +* **Mouse Events** - The Mouse Bindings API is rich enough to support the majority of use-cases. However, in some cases subscribing directly to key events is needed (e.g. drag & drop). Use @Terminal.Gui.View.MouseEvent and related events in these cases. + +Each of these APIs are described more fully below. + +## Mouse Bindings + +Mouse Bindings is the preferred way of handling mouse input in View implementations. The View calls @Terminal.Gui.AddCommand to declare it supports a particular command and then uses @Terminal.Gui.MouseBindings to indicate which mouse events will invoke the command. For example, if a View wants to respond to the user using the mouse wheel to scroll up, it would do this: + +```cs +public MyView : View +{ + AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); + MouseBindings.Add (MouseFlags.Button1DoubleClick, Command.ScrollUp); +} +``` + +The [Command](~/api/Terminal.Gui.Command.yml) enum lists generic operations that are implemented by views. + +## Mouse Events + At the core of *Terminal.Gui*'s mouse API is the @Terminal.Gui.MouseEventArgs class. The @Terminal.Gui.MouseEventArgs class provides a platform-independent abstraction for common mouse events. Every mouse event can be fully described in a @Terminal.Gui.MouseEventArgs instance, and most of the mouse-related APIs are simply helper functions for decoding a @Terminal.Gui.MouseEventArgs. When the user does something with the mouse, the `ConsoleDriver` maps the platform-specific mouse event into a `MouseEventArgs` and calls `Application.RaiseMouseEvent`. Then, `Application.RaiseMouseEvent` determines which `View` the event should go to. The `View.OnMouseEvent` method can be overridden or the `View.MouseEvent` event can be subscribed to, to handle the low-level mouse event. If the low-level event is not handled by a view, `Application` will then call the appropriate high-level helper APIs. For example, if the user double-clicks the mouse, `View.OnMouseClick` will be called/`View.MouseClick` will be raised with the event arguments indicating which mouse button was double-clicked. From 0f137579c865c0dd4c21ae00a4caa10321f990b5 Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 7 Dec 2024 23:42:38 -0800 Subject: [PATCH 21/40] Combined KeyBinding classes --- .../Application/Application.Keyboard.cs | 10 +- Terminal.Gui/Application/Application.Mouse.cs | 2 +- .../WindowsDriver/WindowsDriver.cs | 2 +- .../Input/Keyboard/ApplicationKeyBinding.cs | 41 --- .../Input/Keyboard/ApplicationKeyBindings.cs | 252 ------------------ Terminal.Gui/Input/Keyboard/KeyBinding.cs | 8 +- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 70 ++++- Terminal.Gui/Views/MessageBox.cs | 2 +- Terminal.Gui/Views/RadioGroup.cs | 4 +- UnitTests/Application/KeyboardTests.cs | 4 +- UnitTests/Input/Keyboard/KeyBindingsTests.cs | 9 +- 11 files changed, 78 insertions(+), 326 deletions(-) delete mode 100644 Terminal.Gui/Input/Keyboard/ApplicationKeyBinding.cs delete mode 100644 Terminal.Gui/Input/Keyboard/ApplicationKeyBindings.cs diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 48bb76837d..2ef614f682 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -45,7 +45,7 @@ public static bool RaiseKeyDownEvent (Key key) // 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 bool RaiseKeyDownEvent (Key key) } else { - if (!KeyBindings.TryGet (key, out ApplicationKeyBinding appBinding)) + if (!KeyBindings.TryGet (key, out KeyBinding appBinding)) { continue; } @@ -81,7 +81,7 @@ public static bool RaiseKeyDownEvent (Key key) 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 bool RaiseKeyDownEvent (Key key) 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 bool RaiseKeyUpEvent (Key key) 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 a944c4c376..11b1391fbe 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -184,7 +184,7 @@ internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) } // 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 a96814208d..02d8a03ff7 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver/WindowsDriver.cs @@ -509,7 +509,7 @@ internal void ProcessInputAfterParsing (WindowsConsole.InputRecord inputEvent) 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 f29e57cd59..0000000000 --- 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 b0cd604bc6..0000000000 --- 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 8ae7103ea1..dce67950fe 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -24,12 +24,12 @@ public KeyBinding (Command [] commands, object? context = null) /// 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 KeyBinding (Command [] commands, View? boundView, object? data = null) 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 a95073c9e7..5efe34f24a 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 void Add (Key key, KeyBinding binding) 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 void Add (Key key, KeyBinding binding) //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 void Add (Key key, params Command [] commands) 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 void Add (Key key, params Command [] commands) /// /// 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 void ReplaceCommands (Key key, params Command [] newCommands) /// The new key to be used. If no action will be taken. public void ReplaceKey (Key oldKey, Key newKey) { - if (!TryGet (oldKey, out KeyBinding _)) + if (!newKey.IsValid) { - throw new InvalidOperationException ($"Key {oldKey} is not bound."); + throw new InvalidOperationException ($"Key {newKey} is is not valid."); } - if (!newKey.IsValid) + if (newKey == Key.Empty) { - throw new InvalidOperationException ($"Key {newKey} is is not valid."); + Remove (oldKey); + return; } - KeyBinding value = Bindings [oldKey]; - Remove (oldKey); - Add (newKey, value); + + 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 eff0b62c3a..cd14dfc4fa 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -370,7 +370,7 @@ params string [] buttons { 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 aa3e7b9fb1..32b51cc5f9 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -69,7 +69,7 @@ public RadioGroup () 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 @@ private void RadioGroup_MouseClick (object? sender, MouseEventArgs e) 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 a164b8842d..970d6a5b87 100644 --- a/UnitTests/Application/KeyboardTests.cs +++ b/UnitTests/Application/KeyboardTests.cs @@ -140,12 +140,12 @@ public void KeyBindings_Add_Adds () 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 5df3a6a143..cce65c6d55 100644 --- a/UnitTests/Input/Keyboard/KeyBindingsTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -58,7 +58,7 @@ public void Add_Single_Adds () 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 void Defaults () 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 void ReplaceKey_Replaces_Leaves_Old_Binding () } [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] From 921d125aa03f7c4ced2095f694b574173ee4be6b Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 7 Dec 2024 23:49:34 -0800 Subject: [PATCH 22/40] TOOD --- Terminal.Gui/Input/Mouse/MouseFlagsChangedEventArgs.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Terminal.Gui/Input/Mouse/MouseFlagsChangedEventArgs.cs b/Terminal.Gui/Input/Mouse/MouseFlagsChangedEventArgs.cs index b71ad51df0..0af4c996a0 100644 --- a/Terminal.Gui/Input/Mouse/MouseFlagsChangedEventArgs.cs +++ b/Terminal.Gui/Input/Mouse/MouseFlagsChangedEventArgs.cs @@ -1,5 +1,6 @@ namespace Terminal.Gui; +// TODO: This class is unnecessary. Replace it with CancelEventArgs from Terminal.Gui.View\CancelEventArgs.cs /// Args for events that describe a change in public class MouseFlagsChangedEventArgs : EventArgs { From 7e289f02e9e568618ee2d54d54e8cc3bf41f7589 Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 8 Dec 2024 07:19:48 -0800 Subject: [PATCH 23/40] KeyBindings cleanup --- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 29 ++-- UICatalog/Scenarios/KeyBindings.cs | 8 +- UnitTests/Input/Keyboard/KeyBindingsTests.cs | 5 +- UnitTests/View/Keyboard/HotKeyTests.cs | 6 +- UnitTests/Views/ContextMenuTests.cs | 158 +++++++++---------- UnitTests/Views/MenuBarTests.cs | 17 +- UnitTests/Views/ShortcutTests.cs | 12 +- 7 files changed, 118 insertions(+), 117 deletions(-) diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index 5efe34f24a..e24914f80c 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -45,7 +45,7 @@ public void Add (Key key, KeyBinding binding) // 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); + _bindings.Add (new (key), binding); } #pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved @@ -102,26 +102,27 @@ public void Add (Key key, View? target, params Command [] commands) 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 ()); + private readonly Dictionary _bindings = new (new KeyEqualityComparer ()); /// /// Gets the bindings bound to . /// /// /// - public IEnumerable> GetBindings (Key key) + public IEnumerable> GetBindings (Key? key = null) { - return Bindings.Where (b => b.Key == key.KeyCode); + if (key is null) + { + return _bindings; + } + return _bindings.Where (b => b.Key == key.KeyCode); } /// /// Gets the keys that are bound. /// /// - public IEnumerable GetBoundKeys () { return Bindings.Keys; } + public IEnumerable GetBoundKeys () { return _bindings.Keys; } /// /// The view that the are bound to. @@ -132,7 +133,7 @@ public IEnumerable> GetBindings (Key key) public View? Target { get; init; } /// Removes all objects from the collection. - public void Clear () { Bindings.Clear (); } + public void Clear () { _bindings.Clear (); } /// /// Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to @@ -141,7 +142,7 @@ public IEnumerable> GetBindings (Key key) /// public void Clear (params Command [] command) { - KeyValuePair [] kvps = Bindings + KeyValuePair [] kvps = _bindings .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) .ToArray (); @@ -186,7 +187,7 @@ public Command [] GetCommands (Key key) /// 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; } + 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. @@ -196,7 +197,7 @@ public Command [] GetCommands (Key key) /// public IEnumerable GetKeysFromCommands (params Command [] commands) { - return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); + return _bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); } /// Removes a from the collection. @@ -208,7 +209,7 @@ public void Remove (Key key) return; } - Bindings.Remove (key); + _bindings.Remove (key); } /// Replaces the commands already bound to a key. @@ -275,7 +276,7 @@ public bool TryGet (Key key, out KeyBinding binding) if (key.IsValid) { - return Bindings.TryGetValue (key, out binding); + return _bindings.TryGetValue (key, out binding); } return false; diff --git a/UICatalog/Scenarios/KeyBindings.cs b/UICatalog/Scenarios/KeyBindings.cs index d09c8850c0..54862b8c76 100644 --- a/UICatalog/Scenarios/KeyBindings.cs +++ b/UICatalog/Scenarios/KeyBindings.cs @@ -80,7 +80,7 @@ Pressing Esc or {Application.QuitKey} will cause it to quit the app. }; appWindow.Add (appBindingsListView); - foreach (var key in Application.KeyBindings.GetBoundKeys()) + foreach (var key in Application.KeyBindings.GetBoundKeys ()) { var binding = Application.KeyBindings.Get (key); appBindings.Add ($"{key} -> {binding.Target?.GetType ().Name} - {binding.Commands [0]}"); @@ -104,7 +104,7 @@ Pressing Esc or {Application.QuitKey} will cause it to quit the app. foreach (var subview in appWindow.Subviews) { - foreach (var binding in subview.HotKeyBindings.Bindings) + foreach (KeyValuePair binding in subview.HotKeyBindings.GetBindings ()) { hotkeyBindings.Add ($"{binding.Key} -> {subview.GetType ().Name} - {binding.Value.Commands [0]}"); } @@ -148,8 +148,8 @@ private void Application_HasFocusChanged (object sender, EventArgs e) _focusedBindingsListView.Title = $"_Focused ({focused?.GetType ().Name}) Bindings"; - _focusedBindings.Clear(); - foreach (var binding in focused?.KeyBindings!.Bindings) + _focusedBindings.Clear (); + foreach (var binding in focused?.KeyBindings!.GetBindings ()) { _focusedBindings.Add ($"{binding.Key} -> {binding.Value.Commands [0]}"); } diff --git a/UnitTests/Input/Keyboard/KeyBindingsTests.cs b/UnitTests/Input/Keyboard/KeyBindingsTests.cs index cce65c6d55..d2bdd64850 100644 --- a/UnitTests/Input/Keyboard/KeyBindingsTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -103,7 +103,7 @@ public void Clear_Clears () public void Defaults () { var keyBindings = new KeyBindings (new ()); - Assert.Empty (keyBindings.Bindings); + Assert.Empty (keyBindings.GetBindings ()); Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept)); Assert.NotNull (keyBindings.Target); } @@ -305,9 +305,6 @@ public void TryGet_Succeeds () var key = new Key (Key.Q.WithCtrl); bool result = keyBindings.TryGet (key, out KeyBinding _); Assert.True (result); ; - - result = keyBindings.Bindings.TryGetValue (key, out KeyBinding _); - Assert.True (result); } [Fact] diff --git a/UnitTests/View/Keyboard/HotKeyTests.cs b/UnitTests/View/Keyboard/HotKeyTests.cs index 4a70ff3487..1c7a34e85b 100644 --- a/UnitTests/View/Keyboard/HotKeyTests.cs +++ b/UnitTests/View/Keyboard/HotKeyTests.cs @@ -72,7 +72,8 @@ public void AddKeyBindingsForHotKey_SetsBinding_Key () Assert.Equal (KeyCode.Z, view.HotKey); view.AddKeyBindingsForHotKey (view.HotKey, Key.A); - Assert.Equal (Key.A, view.HotKeyBindings.Bindings [Key.A].Key); + view.HotKeyBindings.TryGet (Key.A, out var binding); + Assert.Equal (Key.A, binding.Key); } [Fact] @@ -83,7 +84,8 @@ public void AddKeyBindingsForHotKey_SetsBinding_Data () Assert.Equal (KeyCode.Z, view.HotKey); view.AddKeyBindingsForHotKey (view.HotKey, Key.A, "data"); - Assert.Equal ("data", view.HotKeyBindings.Bindings [Key.A].Data); + view.HotKeyBindings.TryGet (Key.A, out var binding); + Assert.Equal ("data", binding.Data); } [Fact] diff --git a/UnitTests/Views/ContextMenuTests.cs b/UnitTests/Views/ContextMenuTests.cs index 066848a501..8fa24b729d 100644 --- a/UnitTests/Views/ContextMenuTests.cs +++ b/UnitTests/Views/ContextMenuTests.cs @@ -1483,9 +1483,9 @@ public void KeyBindings_Removed_On_Close_ContextMenu () Assert.False (deleteFile); cm.Show (menuItems); - Assert.True (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.True (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); - Assert.True (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.WithCtrl)); + Assert.True (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); + Assert.True (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); + Assert.True (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithCtrl, out _)); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); Application.MainLoop!.RunIteration (); @@ -1502,9 +1502,9 @@ public void KeyBindings_Removed_On_Close_ContextMenu () Assert.True (deleteFile); Assert.False (cm.MenuBar.IsMenuOpen); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.WithCtrl)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithCtrl, out _)); newFile = false; renameFile = false; @@ -1555,8 +1555,8 @@ public void KeyBindings_With_ContextMenu_And_MenuBar () top.Add (menuBar); Application.Begin (top); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); Assert.Null (cm.MenuBar); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); @@ -1568,10 +1568,10 @@ public void KeyBindings_With_ContextMenu_And_MenuBar () newFile = false; cm.Show (menuItems); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); - Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.True (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); + Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); + Assert.True (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); Assert.True (cm.MenuBar.IsMenuOpen); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); @@ -1584,10 +1584,10 @@ public void KeyBindings_With_ContextMenu_And_MenuBar () Assert.True (renameFile); Assert.False (cm.MenuBar.IsMenuOpen); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); newFile = false; renameFile = false; @@ -1634,7 +1634,7 @@ public void KeyBindings_With_Same_Shortcut_ContextMenu_And_MenuBar () top.Add (menuBar); Application.Begin (top); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); Assert.Null (cm.MenuBar); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); @@ -1645,8 +1645,8 @@ public void KeyBindings_With_Same_Shortcut_ContextMenu_And_MenuBar () newMenuBar = false; cm.Show (menuItems); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.True (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); + Assert.True (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); Assert.True (cm.MenuBar.IsMenuOpen); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); @@ -1657,8 +1657,8 @@ public void KeyBindings_With_Same_Shortcut_ContextMenu_And_MenuBar () Assert.True (newContextMenu); Assert.False (cm.MenuBar!.IsMenuOpen); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); newMenuBar = false; newContextMenu = false; @@ -1704,20 +1704,20 @@ public void HotKeys_Removed_On_Close_ContextMenu () cm.Show (menuItems); Assert.True (cm.MenuBar!.IsMenuOpen); - Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.WithAlt)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.NoShift)); + Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.NoShift, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithAlt, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.NoShift, out _)); Assert.Single (Application.Top!.Subviews); View [] menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray (); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.D.WithAlt)); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.D.NoShift)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.NoShift, out _)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.R.WithAlt, out _)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.R.NoShift, out _)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.D.WithAlt, out _)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.D.NoShift, out _)); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithAlt)); Assert.False (cm.MenuBar!.IsMenuOpen); @@ -1734,12 +1734,12 @@ public void HotKeys_Removed_On_Close_ContextMenu () Application.MainLoop!.RunIteration (); Assert.True (deleteFile); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.WithAlt)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.D.NoShift)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.NoShift, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithAlt, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.NoShift, out _)); newFile = false; renameFile = false; @@ -1796,9 +1796,9 @@ public void HotKeys_With_ContextMenu_And_MenuBar () top.Add (menuBar); Application.Begin (top); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); View [] menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == menuBar).ToArray (); Assert.Empty (menus); Assert.Null (cm.MenuBar); @@ -1807,7 +1807,7 @@ public void HotKeys_With_ContextMenu_And_MenuBar () Assert.True (menuBar.IsMenuOpen); Assert.Equal (2, Application.Top!.Subviews.Count); menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == menuBar).ToArray (); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.WithAlt, out _)); Assert.True (Application.RaiseKeyDownEvent (Key.N.WithAlt)); Assert.False (menuBar.IsMenuOpen); Assert.False (Application.RaiseKeyDownEvent (Key.R.WithAlt)); @@ -1818,29 +1818,29 @@ public void HotKeys_With_ContextMenu_And_MenuBar () newFile = false; cm.Show (menuItems); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.F.NoShift)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.WithAlt, out _)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.NoShift, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.NoShift, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.E.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.E.NoShift, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _)); Assert.True (cm.MenuBar!.IsMenuOpen); - Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); - Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.F.NoShift)); - Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.False (cm.MenuBar!.HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.F.WithAlt, out _)); + Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.F.NoShift, out _)); + Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.NoShift, out _)); + Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.E.WithAlt, out _)); + Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.E.NoShift, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _)); Assert.Equal (3, Application.Top!.Subviews.Count); menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray (); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.True (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.True (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.WithAlt, out _)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.NoShift, out _)); + Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.WithAlt, out _)); + Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.NoShift, out _)); Assert.True (cm.MenuBar.IsMenuOpen); Assert.True (Application.RaiseKeyDownEvent (Key.F.WithAlt)); Assert.False (cm.MenuBar.IsMenuOpen); @@ -1852,14 +1852,14 @@ public void HotKeys_With_ContextMenu_And_MenuBar () Assert.True (cm.MenuBar.IsMenuOpen); Assert.Equal (3, Application.Top!.Subviews.Count); menus = Application.Top!.Subviews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray (); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.True (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.False (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (menus [0].HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); - Assert.False (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.False (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.True (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.True (menus [1].HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.WithAlt, out _)); + Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.NoShift, out _)); + Assert.False (menus [0].HotKeyBindings.TryGet (Key.R.WithAlt, out _)); + Assert.False (menus [0].HotKeyBindings.TryGet (Key.R.NoShift, out _)); + Assert.False (menus [1].HotKeyBindings.TryGet (Key.E.WithAlt, out _)); + Assert.False (menus [1].HotKeyBindings.TryGet (Key.E.NoShift, out _)); + Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.WithAlt, out _)); + Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.NoShift, out _)); Assert.True (Application.RaiseKeyDownEvent (Key.E.NoShift)); Assert.True (Application.RaiseKeyDownEvent (Key.R.WithAlt)); Assert.False (cm.MenuBar.IsMenuOpen); @@ -1867,14 +1867,14 @@ public void HotKeys_With_ContextMenu_And_MenuBar () Assert.True (renameFile); Assert.Single (Application.Top!.Subviews); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.F.WithAlt)); - Assert.True (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.F.NoShift)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.WithAlt)); - Assert.False (menuBar.HotKeyBindings.Bindings.ContainsKey (Key.N.NoShift)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.E.WithAlt)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.E.NoShift)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.WithAlt)); - Assert.False (cm.MenuBar.HotKeyBindings.Bindings.ContainsKey (Key.R.NoShift)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.WithAlt, out _)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.NoShift, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.NoShift, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.E.WithAlt, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.E.NoShift, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); + Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _)); newFile = false; renameFile = false; diff --git a/UnitTests/Views/MenuBarTests.cs b/UnitTests/Views/MenuBarTests.cs index a332492e8d..34fac896e8 100644 --- a/UnitTests/Views/MenuBarTests.cs +++ b/UnitTests/Views/MenuBarTests.cs @@ -19,8 +19,9 @@ public void AddMenuBarItem_RemoveMenuItem_Dynamically () menuBar.Menus = [menuBarItem]; Assert.Single (menuBar.Menus); Assert.Single (menuBar.Menus [0].Children!); - Assert.Contains (Key.N.WithAlt, menuBar.HotKeyBindings.Bindings); - Assert.DoesNotContain (Key.I, menuBar.HotKeyBindings.Bindings); + + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); var top = new Toplevel (); top.Add (menuBar); @@ -39,12 +40,12 @@ public void AddMenuBarItem_RemoveMenuItem_Dynamically () menuItem.RemoveMenuItem (); Assert.Single (menuBar.Menus); Assert.Null (menuBar.Menus [0].Children); - Assert.Contains (Key.N.WithAlt, menuBar.HotKeyBindings.Bindings); - Assert.DoesNotContain (Key.I, menuBar.HotKeyBindings.Bindings); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); menuBarItem.RemoveMenuItem (); Assert.Empty (menuBar.Menus); - Assert.DoesNotContain (Key.N.WithAlt, menuBar.HotKeyBindings.Bindings); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); top.Dispose (); } @@ -2998,12 +2999,12 @@ public void Update_ShortcutKey_KeyBindings_Old_ShortcutKey_Is_Removed () ] }; - Assert.Contains (Key.A.WithCtrl, menuBar.HotKeyBindings.Bindings); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); menuBar.Menus [0].Children! [0].ShortcutKey = Key.B.WithCtrl; - Assert.DoesNotContain (Key.A.WithCtrl, menuBar.HotKeyBindings.Bindings); - Assert.Contains (Key.B.WithCtrl, menuBar.HotKeyBindings.Bindings); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.B.WithCtrl, out _)); } [Fact] diff --git a/UnitTests/Views/ShortcutTests.cs b/UnitTests/Views/ShortcutTests.cs index 7041913ddc..eae2e7ad40 100644 --- a/UnitTests/Views/ShortcutTests.cs +++ b/UnitTests/Views/ShortcutTests.cs @@ -281,11 +281,11 @@ public void Key_Changing_Removes_Previous_Binding () var shortcut = new Shortcut (); shortcut.Key = Key.A; - Assert.Contains (Key.A, shortcut.HotKeyBindings.Bindings.Keys); + Assert.True(shortcut.HotKeyBindings.TryGet(Key.A, out _)); shortcut.Key = Key.B; - Assert.DoesNotContain (Key.A, shortcut.HotKeyBindings.Bindings.Keys); - Assert.Contains (Key.B, shortcut.HotKeyBindings.Bindings.Keys); + Assert.False (shortcut.HotKeyBindings.TryGet (Key.A, out _)); + Assert.True (shortcut.HotKeyBindings.TryGet (Key.B, out _)); } // Test Key gets bound correctly @@ -313,14 +313,14 @@ public void BindKeyToApplication_Changing_Adjusts_KeyBindings () var shortcut = new Shortcut (); shortcut.Key = Key.A; - Assert.Contains (Key.A, shortcut.HotKeyBindings.Bindings.Keys); + Assert.True (shortcut.HotKeyBindings.TryGet(Key.A, out _)); shortcut.BindKeyToApplication = true; - Assert.DoesNotContain (Key.A, shortcut.HotKeyBindings.Bindings.Keys); + Assert.False (shortcut.HotKeyBindings.TryGet (Key.A, out _)); Assert.NotEmpty (Application.KeyBindings.GetBindings(Key.A)); shortcut.BindKeyToApplication = false; - Assert.Contains (Key.A, shortcut.HotKeyBindings.Bindings.Keys); + Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _)); Assert.Empty (Application.KeyBindings.GetBindings (Key.A)); } From 98244b71310f9a99b1a22f6984534f47223e13ea Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 8 Dec 2024 07:23:38 -0800 Subject: [PATCH 24/40] KeyBindings cleanup --- Terminal.Gui/View/View.Keyboard.cs | 36 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 9c8917f951..323d36193d 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -705,10 +705,10 @@ public bool IsHotKeyBound (Key key, out View? boundView) /// - /// Invokes the Commands bound to . + /// Invokes the Commands bound to . /// See for an overview of Terminal.Gui keyboard APIs. /// - /// The key event passed. + /// The hot key event passed. /// /// if no command was invoked; input processing should continue. /// if at least one command was invoked and was not handled (or cancelled); input processing @@ -716,30 +716,30 @@ public bool IsHotKeyBound (Key key, out View? boundView) /// if at least one command was invoked and handled (or cancelled); input processing should /// stop. /// - protected bool? InvokeCommandsBoundToHotKey (Key key) + protected bool? InvokeCommandsBoundToHotKey (Key hotKey) { - if (!HotKeyBindings.TryGet (key, out KeyBinding binding)) + if (!HotKeyBindings.TryGet (hotKey, out KeyBinding binding)) { return null; } -#if DEBUG +//#if DEBUG - //if (Application.KeyBindings.TryGet (key, out KeyBinding b)) - //{ - // Debug.WriteLine ( - // $"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command."); - //} +// //if (Application.KeyBindings.TryGet (key, out KeyBinding b)) +// //{ +// // Debug.WriteLine ( +// // $"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command."); +// //} - // TODO: This is a "prototype" debug check. It may be too annoying vs. useful. - // Scour the bindings up our View hierarchy - // to ensure that the key is not already bound to a different set of commands. - if (SuperView?.IsHotKeyBound (key, out View? previouslyBoundView) ?? false) - { - Debug.WriteLine ($"WARNING: InvokeKeyBindings ({key}) - A subview or peer has bound this Key and will not see it: {previouslyBoundView}."); - } +// // TODO: This is a "prototype" debug check. It may be too annoying vs. useful. +// // Scour the bindings up our View hierarchy +// // to ensure that the key is not already bound to a different set of commands. +// if (SuperView?.IsHotKeyBound (hotKey, out View? previouslyBoundView) ?? false) +// { +// Debug.WriteLine ($"WARNING: InvokeKeyBindings ({hotKey}) - A subview or peer has bound this Key and will not see it: {previouslyBoundView}."); +// } -#endif +//#endif return InvokeCommands (binding.Commands, binding); } From e502a13402a79fe1c80928018a1841e4005595bf Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 8 Dec 2024 13:40:32 -0800 Subject: [PATCH 25/40] MouseBindings tests --- .../Application/Application.Keyboard.cs | 11 +- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 11 +- Terminal.Gui/Input/Mouse/MouseBindings.cs | 86 +++-- Terminal.Gui/View/View.Mouse.cs | 9 +- UnitTests/Application/ApplicationTests.cs | 2 +- UnitTests/Input/Keyboard/KeyBindingsTests.cs | 59 +-- UnitTests/Input/Mouse/MouseBindingTests.cs | 6 + UnitTests/Input/Mouse/MouseBindingsTests.cs | 346 ++++++++++++++++++ UnitTests/Input/Mouse/MouseEventArgsTest.cs | 18 + UnitTests/Views/ShortcutTests.cs | 10 +- 10 files changed, 475 insertions(+), 83 deletions(-) create mode 100644 UnitTests/Input/Mouse/MouseBindingTests.cs create mode 100644 UnitTests/Input/Mouse/MouseBindingsTests.cs create mode 100644 UnitTests/Input/Mouse/MouseEventArgsTest.cs diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 2ef614f682..1c8e21b4a9 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -45,16 +45,17 @@ public static bool RaiseKeyDownEvent (Key key) // 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 (KeyBindings.TryGet (key, out KeyBinding binding)) { - if (binding.Value.Target is { }) + if (binding.Target is { }) { - if (!binding.Value.Target.Enabled) + if (!binding.Target.Enabled) { return false; } - bool? handled = binding.Value.Target?.InvokeCommands (binding.Value.Commands, binding.Value); + bool? handled = binding.Target?.InvokeCommands (binding.Commands, binding); if (handled != null && (bool)handled) { @@ -65,7 +66,7 @@ public static bool RaiseKeyDownEvent (Key key) { if (!KeyBindings.TryGet (key, out KeyBinding appBinding)) { - continue; + return false; } bool? toReturn = null; diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index e24914f80c..1bb3e1ab56 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -105,17 +105,12 @@ public void Add (Key key, View? target, params Command [] commands) private readonly Dictionary _bindings = new (new KeyEqualityComparer ()); /// - /// Gets the bindings bound to . + /// Gets the bindings. /// - /// /// - public IEnumerable> GetBindings (Key? key = null) + public IEnumerable> GetBindings () { - if (key is null) - { - return _bindings; - } - return _bindings.Where (b => b.Key == key.KeyCode); + return _bindings; } /// diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index cdddf5b9eb..8b3f9814ba 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -28,7 +28,7 @@ public void Add (MouseFlags mouseEventArgs, MouseBinding binding) // 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 mouseEventArgs, but the old mouseEventArgs will still be in the dictionary. // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. - Bindings.Add (mouseEventArgs, binding); + _bindings.Add (mouseEventArgs, binding); } /// @@ -42,16 +42,16 @@ public void Add (MouseFlags mouseEventArgs, MouseBinding binding) /// 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 mouse flags to check. + /// The mouse flags to check. /// - /// The command to invoked on the when is received. When - /// multiple commands are provided,they will be applied in sequence. The bound event + /// The command to invoked on the when is received. When + /// multiple commands are provided,they will be applied in sequence. The bound event /// will be /// consumed if any took effect. /// - public void Add (MouseFlags mouseEventArgs, params Command [] commands) + public void Add (MouseFlags mouseFlags, params Command [] commands) { - if (mouseEventArgs == MouseFlags.None) + if (!Enum.IsDefined (typeof (MouseFlags), mouseFlags) || mouseFlags == MouseFlags.None) { throw new ArgumentException (@"Invalid MouseFlag", nameof (commands)); } @@ -61,21 +61,27 @@ public void Add (MouseFlags mouseEventArgs, params Command [] commands) throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } - if (TryGet (mouseEventArgs, out MouseBinding binding)) + if (TryGet (mouseFlags, out MouseBinding binding)) { - throw new InvalidOperationException (@$"A binding for {mouseEventArgs} exists ({binding})."); + throw new InvalidOperationException (@$"A binding for {mouseFlags} exists ({binding})."); } - Add (mouseEventArgs, new MouseBinding (commands, mouseEventArgs)); + Add (mouseFlags, new MouseBinding (commands, mouseFlags)); } - // TODO: Add a dictionary comparer that ignores Scope - // TODO: This should not be public! - /// The collection of objects. - public Dictionary Bindings { get; } = new (); + private readonly Dictionary _bindings = new (); + + /// + /// Gets the bindings. + /// + /// + public IEnumerable> GetBindings () + { + return _bindings; + } /// Removes all objects from the collection. - public void Clear () { Bindings.Clear (); } + public void Clear () { _bindings.Clear (); } /// /// Removes all bindings that trigger the given command set. Views can have multiple different events bound to @@ -84,7 +90,7 @@ public void Add (MouseFlags mouseEventArgs, params Command [] commands) /// public void Clear (params Command [] command) { - KeyValuePair [] kvps = Bindings + KeyValuePair [] kvps = _bindings .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) .ToArray (); @@ -118,25 +124,25 @@ public MouseBinding Get (MouseFlags mouseEventArgs) /// public IEnumerable GetAllMouseFlagsFromCommands (params Command [] commands) { - return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); + return _bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); } /// /// Gets the that are bound. /// /// - public IEnumerable GetBoundMouseFlags () { return Bindings.Keys; } + public IEnumerable GetBoundMouseFlags () { return _bindings.Keys; } - /// Gets the array of s bound to if it exists. - /// The key to check. + /// Gets the array of s bound to if it exists. + /// The key to check. /// - /// The array of s if is bound. An empty + /// The array of s if is bound. An empty /// array /// if not. /// - public Command [] GetCommands (MouseFlags mouseEventArgs) + public Command [] GetCommands (MouseFlags mouseFlags) { - if (TryGet (mouseEventArgs, out MouseBinding bindings)) + if (TryGet (mouseFlags, out MouseBinding bindings)) { return bindings.Commands; } @@ -153,9 +159,9 @@ public Command [] GetCommands (MouseFlags mouseEventArgs) /// The first combination of bound to the set of commands specified by /// . if the set of caommands was not found. /// - public MouseFlags? GetMouseFlagsFromCommands (params Command [] commands) + public MouseFlags GetMouseFlagsFromCommands (params Command [] commands) { - return Bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; + return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } /// Removes a from the collection. @@ -167,7 +173,7 @@ public void Remove (MouseFlags mouseEventArgs) return; } - Bindings.Remove (mouseEventArgs); + _bindings.Remove (mouseEventArgs); } /// Replaces the commands already bound to a combination of . @@ -177,16 +183,17 @@ public void Remove (MouseFlags mouseEventArgs) /// /// /// The combination of bound to the command to be replaced. - /// The set of commands to replace the old ones with. - public void ReplaceCommands (MouseFlags mouseEventArgs, params Command [] commands) + /// The set of commands to replace the old ones with. + public void ReplaceCommands (MouseFlags mouseEventArgs, params Command [] newCommands) { if (TryGet (mouseEventArgs, out MouseBinding binding)) { - binding.Commands = commands; + Remove (mouseEventArgs); + Add (mouseEventArgs, newCommands); } else { - Add (mouseEventArgs, commands); + Add (mouseEventArgs, newCommands); } } @@ -197,16 +204,23 @@ public void ReplaceCommands (MouseFlags mouseEventArgs, params Command [] comman /// The new to be used. If no action /// will be taken. /// - public void ReplaceKey (MouseFlags oldMouseFlags, MouseFlags newMouseFlags) + public void ReplaceMouseFlag (MouseFlags oldMouseFlags, MouseFlags newMouseFlags) { - if (!TryGet (oldMouseFlags, out MouseBinding _)) + if (newMouseFlags == MouseFlags.None) { - throw new InvalidOperationException ($"Key {oldMouseFlags} is not bound."); + throw new ArgumentException (@"Invalid MouseFlag", nameof (newMouseFlags)); } - MouseBinding value = Bindings [oldMouseFlags]; - Remove (oldMouseFlags); - Add (newMouseFlags, value); + + if (TryGet (oldMouseFlags, out MouseBinding binding)) + { + Remove (oldMouseFlags); + Add (newMouseFlags, binding); + } + else + { + Add (newMouseFlags, binding); + } } /// Gets the commands bound with the specified . @@ -221,6 +235,6 @@ public bool TryGet (MouseFlags mouseEventArgs, out MouseBinding binding) { binding = new ([], mouseEventArgs); - return Bindings.TryGetValue (mouseEventArgs, out binding); + return _bindings.TryGetValue (mouseEventArgs, out binding); } } diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index 1f52b41f6f..03cb167c40 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -595,8 +595,6 @@ protected bool RaiseMouseWheelEvent (MouseEventArgs args) return true; } - // Post-conditions - args.Handled = InvokeCommandsBoundToMouse (args) == true; return args.Handled; @@ -643,6 +641,13 @@ private bool RaiseHighlight (CancelEventArgs args) Highlight?.Invoke (this, args); + //if (args.Cancel) + //{ + // return true; + //} + + //args.Cancel = InvokeCommandsBoundToMouse (args) == true; + return args.Cancel; } diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 82e6846e10..e3528e3764 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -552,7 +552,7 @@ public void Init_KeyBindings_Set_To_Custom () Assert.Equal (Key.Q.WithCtrl, Application.QuitKey); - Assert.NotEmpty (Application.KeyBindings.GetBindings (Key.Q.WithCtrl)); + Assert.True (Application.KeyBindings.TryGet (Key.Q.WithCtrl, out _)); Application.Shutdown (); Locations = ConfigLocations.Default; diff --git a/UnitTests/Input/Keyboard/KeyBindingsTests.cs b/UnitTests/Input/Keyboard/KeyBindingsTests.cs index d2bdd64850..8e39cb96d0 100644 --- a/UnitTests/Input/Keyboard/KeyBindingsTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -6,6 +6,27 @@ namespace Terminal.Gui.InputTests; public class KeyBindingsTests () { + [Fact] + public void Add_Adds () + { + var keyBindings = new KeyBindings (new ()); + Command [] commands = { Command.Right, Command.Left }; + + var key = new Key (Key.A); + keyBindings.Add (Key.A, commands); + KeyBinding binding = keyBindings.Get (key); + Assert.Contains (Command.Right, binding.Commands); + Assert.Contains (Command.Left, binding.Commands); + + binding = keyBindings.Get (key); + Assert.Contains (Command.Right, binding.Commands); + Assert.Contains (Command.Left, binding.Commands); + + Command [] resultCommands = keyBindings.GetCommands (key); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + } + [Fact] public void Add_Invalid_Key_Throws () { @@ -15,7 +36,7 @@ public void Add_Invalid_Key_Throws () } [Fact] - public void Add_Multiple_Adds () + public void Add_Multiple_Commands_Adds () { var keyBindings = new KeyBindings (new ()); Command [] commands = [Command.Right, Command.Left]; @@ -40,7 +61,7 @@ public void Add_No_Commands_Throws () } [Fact] - public void Add_Single_Adds () + public void Add_Single_Command_Adds () { var keyBindings = new KeyBindings (new ()); keyBindings.Add (Key.A, Command.HotKey); @@ -55,7 +76,7 @@ public void Add_Single_Adds () // Add should not allow duplicates [Fact] - public void Add_With_Throws_If_Exists () + public void Add_Throws_If_Exists () { var keyBindings = new KeyBindings (new View ()); keyBindings.Add (Key.A, Command.HotKey); @@ -79,7 +100,14 @@ public void Add_With_Throws_If_Exists () Assert.Contains (Command.HotKey, resultCommands); keyBindings = new (new View ()); - keyBindings.Add (Key.A, new KeyBinding (new [] { Command.HotKey })); + keyBindings.Add (Key.A, Command.Accept); + Assert.Throws (() => keyBindings.Add (Key.A, Command.ScrollDown)); + + resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.Accept, resultCommands); + + keyBindings = new (new View ()); + keyBindings.Add (Key.A, new KeyBinding ([Command.HotKey])); Assert.Throws (() => keyBindings.Add (Key.A, new KeyBinding (new [] { Command.Accept }))); resultCommands = keyBindings.GetCommands (Key.A); @@ -247,7 +275,7 @@ public void ReplaceKey_Adds_If_DoesNotContain_Old () { var keyBindings = new KeyBindings (new ()); keyBindings.ReplaceKey (Key.A, Key.B); - Assert.NotEmpty (keyBindings.GetBindings (Key.B)); + Assert.True (keyBindings.TryGet (Key.B, out _)); } [Fact] @@ -258,27 +286,6 @@ public void ReplaceKey_Throws_If_New_Is_Empty () Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.Empty)); } - [Fact] - public void Add_Adds () - { - var keyBindings = new KeyBindings (new ()); - Command [] commands = { Command.Right, Command.Left }; - - var key = new Key (Key.A); - keyBindings.Add (Key.A, commands); - KeyBinding binding = keyBindings.Get (key); - Assert.Contains (Command.Right, binding.Commands); - Assert.Contains (Command.Left, binding.Commands); - - binding = keyBindings.Get (key); - Assert.Contains (Command.Right, binding.Commands); - Assert.Contains (Command.Left, binding.Commands); - - Command [] resultCommands = keyBindings.GetCommands (key); - Assert.Contains (Command.Right, resultCommands); - Assert.Contains (Command.Left, resultCommands); - } - [Fact] public void Get_Gets () { diff --git a/UnitTests/Input/Mouse/MouseBindingTests.cs b/UnitTests/Input/Mouse/MouseBindingTests.cs new file mode 100644 index 0000000000..1d7e7e3a41 --- /dev/null +++ b/UnitTests/Input/Mouse/MouseBindingTests.cs @@ -0,0 +1,6 @@ +namespace Terminal.Gui.InputTests; + +public class MouseBindingTests +{ + // TODO: Add tests for MouseBinding +} diff --git a/UnitTests/Input/Mouse/MouseBindingsTests.cs b/UnitTests/Input/Mouse/MouseBindingsTests.cs new file mode 100644 index 0000000000..692710196a --- /dev/null +++ b/UnitTests/Input/Mouse/MouseBindingsTests.cs @@ -0,0 +1,346 @@ +namespace Terminal.Gui.InputTests; + +public class MouseBindingsTests +{ + [Fact] + public void Add_Adds () + { + var mouseBindings = new MouseBindings (); + Command [] commands = [Command.Right, Command.Left]; + + var flags = MouseFlags.AllEvents; + mouseBindings.Add (flags, commands); + MouseBinding binding = mouseBindings.Get (flags); + Assert.Contains (Command.Right, binding.Commands); + Assert.Contains (Command.Left, binding.Commands); + + binding = mouseBindings.Get (flags); + Assert.Contains (Command.Right, binding.Commands); + Assert.Contains (Command.Left, binding.Commands); + + Command [] resultCommands = mouseBindings.GetCommands (flags); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + } + + [Fact] + public void Add_Invalid_Flag_Throws () + { + var mouseBindings = new MouseBindings (); + List commands = new (); + Assert.Throws (() => mouseBindings.Add (MouseFlags.None, Command.Accept)); + + Assert.Throws (() => mouseBindings.Add ((MouseFlags)0x8ffffff, Command.Accept)); + } + + [Fact] + public void Add_Multiple_Commands_Adds () + { + var mouseBindings = new MouseBindings (); + Command [] commands = [Command.Right, Command.Left]; + + mouseBindings.Add (MouseFlags.Button1Clicked, commands); + Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + + mouseBindings.Add (MouseFlags.Button2Clicked, commands); + resultCommands = mouseBindings.GetCommands (MouseFlags.Button2Clicked); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + } + + [Fact] + public void Add_No_Commands_Throws () + { + var mouseBindings = new MouseBindings (); + List commands = new (); + Assert.Throws (() => mouseBindings.Add (MouseFlags.Button1Clicked, commands.ToArray ())); + } + + [Fact] + public void Add_Single_Command_Adds () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Contains (Command.HotKey, resultCommands); + + mouseBindings.Add (MouseFlags.Button2Clicked, Command.HotKey); + resultCommands = mouseBindings.GetCommands (MouseFlags.Button2Clicked); + Assert.Contains (Command.HotKey, resultCommands); + } + + // Add should not allow duplicates + [Fact] + public void Add_Throws_If_Exists () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + Assert.Throws (() => mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept)); + + Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Contains (Command.HotKey, resultCommands); + + mouseBindings = new (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + Assert.Throws (() => mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept)); + + resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Contains (Command.HotKey, resultCommands); + + mouseBindings = new (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + Assert.Throws (() => mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept)); + + resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Contains (Command.HotKey, resultCommands); + + mouseBindings = new (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept); + Assert.Throws (() => mouseBindings.Add (MouseFlags.Button1Clicked, Command.ScrollDown)); + + resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Contains (Command.Accept, resultCommands); + + mouseBindings = new (); + mouseBindings.Add (MouseFlags.Button1Clicked, new MouseBinding ([Command.HotKey], MouseFlags.Button1Clicked)); + + Assert.Throws ( + () => mouseBindings.Add ( + MouseFlags.Button1Clicked, + new MouseBinding ([Command.Accept], MouseFlags.Button1Clicked))); + + resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Contains (Command.HotKey, resultCommands); + } + + // Clear + [Fact] + public void Clear_Clears () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + mouseBindings.Clear (); + Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Empty (resultCommands); + resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Empty (resultCommands); + } + + [Fact] + public void Defaults () + { + var mouseBindings = new MouseBindings (); + Assert.Empty (mouseBindings.GetBindings ()); + Assert.Equal (MouseFlags.None, mouseBindings.GetMouseFlagsFromCommands (Command.Accept)); + } + + [Fact] + public void Get_Binding_Not_Found_Throws () + { + var mouseBindings = new MouseBindings (); + Assert.Throws (() => mouseBindings.Get (MouseFlags.Button1Clicked)); + Assert.Throws (() => mouseBindings.Get (MouseFlags.AllEvents)); + } + + // GetCommands + [Fact] + public void GetCommands_Unknown_ReturnsEmpty () + { + var mouseBindings = new MouseBindings (); + Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Empty (resultCommands); + } + + [Fact] + public void GetCommands_WithCommands_ReturnsCommands () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Contains (Command.HotKey, resultCommands); + } + + [Fact] + public void GetCommands_WithMultipleBindings_ReturnsCommands () + { + var mouseBindings = new MouseBindings (); + Command [] commands = [Command.Right, Command.Left]; + mouseBindings.Add (MouseFlags.Button1Clicked, commands); + mouseBindings.Add (MouseFlags.Button2Clicked, commands); + Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + resultCommands = mouseBindings.GetCommands (MouseFlags.Button2Clicked); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + } + + [Fact] + public void GetCommands_WithMultipleCommands_ReturnsCommands () + { + var mouseBindings = new MouseBindings (); + Command [] commands = [Command.Right, Command.Left]; + mouseBindings.Add (MouseFlags.Button1Clicked, commands); + Command [] resultCommands = mouseBindings.GetCommands (MouseFlags.Button1Clicked); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + } + + [Fact] + public void GetMouseFlagsFromCommands_MultipleCommands () + { + var mouseBindings = new MouseBindings (); + Command [] commands1 = [Command.Right, Command.Left]; + mouseBindings.Add (MouseFlags.Button1Clicked, commands1); + + Command [] commands2 = { Command.Up, Command.Down }; + mouseBindings.Add (MouseFlags.Button2Clicked, commands2); + + MouseFlags mouseFlags = mouseBindings.GetMouseFlagsFromCommands (commands1); + Assert.Equal (MouseFlags.Button1Clicked, mouseFlags); + + mouseFlags = mouseBindings.GetMouseFlagsFromCommands (commands2); + Assert.Equal (MouseFlags.Button2Clicked, mouseFlags); + } + + [Fact] + public void GetMouseFlagsFromCommands_OneCommand () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.Right); + + MouseFlags mouseFlags = mouseBindings.GetMouseFlagsFromCommands (Command.Right); + Assert.Equal (MouseFlags.Button1Clicked, mouseFlags); + } + + // GetMouseFlagsFromCommands + [Fact] + public void GetMouseFlagsFromCommands_Unknown_Returns_Key_Empty () + { + var mouseBindings = new MouseBindings (); + Assert.Equal (MouseFlags.None, mouseBindings.GetMouseFlagsFromCommands (Command.Accept)); + } + + [Fact] + public void GetMouseFlagsFromCommands_WithCommands_ReturnsKey () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + MouseFlags mouseFlags = mouseBindings.GetMouseFlagsFromCommands (Command.HotKey); + Assert.Equal (MouseFlags.Button1Clicked, mouseFlags); + } + + [Fact] + public void ReplaceMouseFlags_Replaces () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + mouseBindings.Add (MouseFlags.Button2Clicked, Command.HotKey); + mouseBindings.Add (MouseFlags.Button3Clicked, Command.HotKey); + mouseBindings.Add (MouseFlags.Button4Clicked, Command.HotKey); + + mouseBindings.ReplaceMouseFlag (MouseFlags.Button1Clicked, MouseFlags.Button1DoubleClicked); + Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button1Clicked)); + Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button1DoubleClicked)); + + mouseBindings.ReplaceMouseFlag (MouseFlags.Button2Clicked, MouseFlags.Button2DoubleClicked); + Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button2Clicked)); + Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button2DoubleClicked)); + + mouseBindings.ReplaceMouseFlag (MouseFlags.Button3Clicked, MouseFlags.Button3DoubleClicked); + Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button3Clicked)); + Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button3DoubleClicked)); + + mouseBindings.ReplaceMouseFlag (MouseFlags.Button4Clicked, MouseFlags.Button4DoubleClicked); + Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button4Clicked)); + Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button4DoubleClicked)); + } + + [Fact] + public void ReplaceMouseFlags_Replaces_Leaves_Old_Binding () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept); + mouseBindings.Add (MouseFlags.Button2Clicked, Command.HotKey); + + mouseBindings.ReplaceMouseFlag (mouseBindings.GetMouseFlagsFromCommands (Command.Accept), MouseFlags.Button3Clicked); + Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button1Clicked)); + Assert.Contains (Command.Accept, mouseBindings.GetCommands (MouseFlags.Button3Clicked)); + } + + [Fact] + public void ReplaceMouseFlags_Adds_If_DoesNotContain_Old () + { + var mouseBindings = new MouseBindings (); + mouseBindings.ReplaceMouseFlag (MouseFlags.Button1Clicked, MouseFlags.Button2Clicked); + Assert.True (mouseBindings.TryGet (MouseFlags.Button2Clicked, out _)); + } + + [Fact] + public void ReplaceMouseFlags_Throws_If_New_Is_None () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + Assert.Throws (() => mouseBindings.ReplaceMouseFlag (MouseFlags.Button1Clicked, MouseFlags.None)); + } + + [Fact] + public void Get_Gets () + { + var mouseBindings = new MouseBindings (); + Command [] commands = [Command.Right, Command.Left]; + + mouseBindings.Add (MouseFlags.Button1Clicked, commands); + MouseBinding binding = mouseBindings.Get (MouseFlags.Button1Clicked); + Assert.Contains (Command.Right, binding.Commands); + Assert.Contains (Command.Left, binding.Commands); + + binding = mouseBindings.Get (MouseFlags.Button1Clicked); + Assert.Contains (Command.Right, binding.Commands); + Assert.Contains (Command.Left, binding.Commands); + } + + // TryGet + [Fact] + public void TryGet_Succeeds () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + bool result = mouseBindings.TryGet (MouseFlags.Button1Clicked, out MouseBinding _); + Assert.True (result); + ; + } + + [Fact] + public void TryGet_Unknown_ReturnsFalse () + { + var mouseBindings = new MouseBindings (); + bool result = mouseBindings.TryGet (MouseFlags.Button1Clicked, out MouseBinding _); + Assert.False (result); + } + + [Fact] + public void TryGet_WithCommands_ReturnsTrue () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + bool result = mouseBindings.TryGet (MouseFlags.Button1Clicked, out MouseBinding bindings); + Assert.True (result); + Assert.Contains (Command.HotKey, bindings.Commands); + } + + [Fact] + public void ReplaceCommands_Replaces () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept); + + mouseBindings.ReplaceCommands (MouseFlags.Button1Clicked, Command.Refresh); + + bool result = mouseBindings.TryGet (MouseFlags.Button1Clicked, out MouseBinding bindings); + Assert.True (result); + Assert.Contains (Command.Refresh, bindings.Commands); + } +} diff --git a/UnitTests/Input/Mouse/MouseEventArgsTest.cs b/UnitTests/Input/Mouse/MouseEventArgsTest.cs new file mode 100644 index 0000000000..78124211b7 --- /dev/null +++ b/UnitTests/Input/Mouse/MouseEventArgsTest.cs @@ -0,0 +1,18 @@ +namespace Terminal.Gui.InputTests; + +public class MouseEventArgsTests +{ + [Fact] + public void Constructor_Default_ShouldSetFlagsToNone () + { + var eventArgs = new MouseEventArgs (); + Assert.Equal (MouseFlags.None, eventArgs.Flags); + } + + [Fact] + public void HandledProperty_ShouldBeFalseByDefault () + { + var eventArgs = new MouseEventArgs (); + Assert.False (eventArgs.Handled); + } +} diff --git a/UnitTests/Views/ShortcutTests.cs b/UnitTests/Views/ShortcutTests.cs index eae2e7ad40..bc2ba9645d 100644 --- a/UnitTests/Views/ShortcutTests.cs +++ b/UnitTests/Views/ShortcutTests.cs @@ -100,7 +100,7 @@ public void NaturalSize (string command, string help, KeyCode key, int expectedW Title = command, }; - shortcut.Layout(); + shortcut.Layout (); // |0123456789 // | C H K | @@ -281,7 +281,7 @@ public void Key_Changing_Removes_Previous_Binding () var shortcut = new Shortcut (); shortcut.Key = Key.A; - Assert.True(shortcut.HotKeyBindings.TryGet(Key.A, out _)); + Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _)); shortcut.Key = Key.B; Assert.False (shortcut.HotKeyBindings.TryGet (Key.A, out _)); @@ -313,15 +313,15 @@ public void BindKeyToApplication_Changing_Adjusts_KeyBindings () var shortcut = new Shortcut (); shortcut.Key = Key.A; - Assert.True (shortcut.HotKeyBindings.TryGet(Key.A, out _)); + Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _)); shortcut.BindKeyToApplication = true; Assert.False (shortcut.HotKeyBindings.TryGet (Key.A, out _)); - Assert.NotEmpty (Application.KeyBindings.GetBindings(Key.A)); + Assert.True (Application.KeyBindings.TryGet (Key.A, out _)); shortcut.BindKeyToApplication = false; Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _)); - Assert.Empty (Application.KeyBindings.GetBindings (Key.A)); + Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); } [Theory] From babc6fead95a446690218b649fe6a8c2e3ec1442 Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 8 Dec 2024 17:10:08 -0800 Subject: [PATCH 26/40] Tweaked ColorPicker16 --- Terminal.Gui/Views/ColorPicker.16.cs | 46 +++++++++++++--------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/Terminal.Gui/Views/ColorPicker.16.cs b/Terminal.Gui/Views/ColorPicker.16.cs index c70565a445..20420d26fc 100644 --- a/Terminal.Gui/Views/ColorPicker.16.cs +++ b/Terminal.Gui/Views/ColorPicker.16.cs @@ -1,4 +1,5 @@ -namespace Terminal.Gui; +#nullable enable +namespace Terminal.Gui; /// The Color picker. public class ColorPicker16 : View @@ -7,10 +8,10 @@ public class ColorPicker16 : View public ColorPicker16 () { SetInitialProperties (); } /// Columns of color boxes - private readonly int _cols = 8; + private const int COLS = 8; /// Rows of color boxes - private readonly int _rows = 2; + private const int ROWS = 2; private int _boxHeight = 2; private int _boxWidth = 4; @@ -25,9 +26,9 @@ public int BoxHeight if (_boxHeight != value) { _boxHeight = value; - Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); - Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); - SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); + Width = Dim.Auto (minimumContentDim: _boxWidth * COLS); + Height = Dim.Auto (minimumContentDim: _boxHeight * ROWS); + SetContentSize (new (_boxWidth * COLS, _boxHeight * ROWS)); SetNeedsLayout (); } } @@ -42,25 +43,24 @@ public int BoxWidth if (_boxWidth != value) { _boxWidth = value; - Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); - Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); - SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); + Width = Dim.Auto (minimumContentDim: _boxWidth * COLS); + Height = Dim.Auto (minimumContentDim: _boxHeight * ROWS); + SetContentSize (new (_boxWidth * COLS, _boxHeight * ROWS)); SetNeedsLayout (); } } } /// Fired when a color is picked. - [CanBeNull] - public event EventHandler ColorChanged; + public event EventHandler? ColorChanged; /// Cursor for the selected color. public Point Cursor { - get => new (_selectColorIndex % _cols, _selectColorIndex / _cols); + get => new (_selectColorIndex % COLS, _selectColorIndex / COLS); set { - int colorIndex = value.Y * _cols + value.X; + int colorIndex = value.Y * COLS + value.X; SelectedColor = (ColorName16)colorIndex; } } @@ -73,9 +73,9 @@ private bool MoveDown (ICommandContext commandContext) { return true; } - if (Cursor.Y < _rows - 1) + if (Cursor.Y < ROWS - 1) { - SelectedColor += _cols; + SelectedColor += COLS; } return true; @@ -106,7 +106,7 @@ private bool MoveRight (ICommandContext commandContext) { return true; } - if (Cursor.X < _cols - 1) + if (Cursor.X < COLS - 1) { SelectedColor++; } @@ -124,7 +124,7 @@ private bool MoveUp (ICommandContext commandContext) } if (Cursor.Y > 0) { - SelectedColor -= _cols; + SelectedColor -= COLS; } return true; @@ -140,7 +140,7 @@ protected override bool OnDrawingContent () { for (var x = 0; x < Math.Max (8, Viewport.Width / BoxWidth); x++) { - int foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols; + int foregroundColorIndex = y == 0 ? colorIndex + COLS : colorIndex - COLS; if (foregroundColorIndex > 15 || colorIndex > 15) { @@ -210,21 +210,17 @@ private void AddKeyBindings () // TODO: Decouple Cursor from SelectedColor so that mouse press-and-hold can show the color under the cursor. - /// Draw a box for one color. /// X location. /// Y location /// private void DrawColorBox (int x, int y, bool selected) { - var index = 0; - for (var zoomedY = 0; zoomedY < BoxHeight; zoomedY++) { for (var zoomedX = 0; zoomedX < BoxWidth; zoomedX++) { AddRune (x * BoxWidth + zoomedX, y * BoxHeight + zoomedY, (Rune)' '); - index++; } } @@ -281,8 +277,8 @@ private void SetInitialProperties () AddCommands (); AddKeyBindings (); - Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); - Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); - SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); + Width = Dim.Auto (minimumContentDim: _boxWidth * COLS); + Height = Dim.Auto (minimumContentDim: _boxHeight * ROWS); + SetContentSize (new (_boxWidth * COLS, _boxHeight * ROWS)); } } From e6aeada48004cad0d00242faa7dfc25fbe85c265 Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 8 Dec 2024 17:37:28 -0800 Subject: [PATCH 27/40] Tweaked HexView --- Terminal.Gui/Input/Mouse/MouseBindings.cs | 4 +- Terminal.Gui/Views/ColorPicker.16.cs | 2 +- Terminal.Gui/Views/HexView.cs | 59 +++++++++-------------- 3 files changed, 25 insertions(+), 40 deletions(-) diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index 8b3f9814ba..5dd5df04ce 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -51,9 +51,9 @@ public void Add (MouseFlags mouseEventArgs, MouseBinding binding) /// public void Add (MouseFlags mouseFlags, params Command [] commands) { - if (!Enum.IsDefined (typeof (MouseFlags), mouseFlags) || mouseFlags == MouseFlags.None) + if (mouseFlags == MouseFlags.None) { - throw new ArgumentException (@"Invalid MouseFlag", nameof (commands)); + throw new ArgumentException (@"Invalid MouseFlag", nameof (mouseFlags)); } if (commands.Length == 0) diff --git a/Terminal.Gui/Views/ColorPicker.16.cs b/Terminal.Gui/Views/ColorPicker.16.cs index 20420d26fc..555ac29f7e 100644 --- a/Terminal.Gui/Views/ColorPicker.16.cs +++ b/Terminal.Gui/Views/ColorPicker.16.cs @@ -188,7 +188,7 @@ private void AddCommands () AddCommand (Command.Select, (ctx) => { - bool set = false; + var set = false; if (ctx is CommandContext { Binding.MouseEventArgs: { } } mouseCommandContext) { diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index 1e47f20262..c2efbb7de0 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -58,9 +58,7 @@ public HexView (Stream? source) _leftSideHasFocus = true; _firstNibble = true; - // PERF: Closure capture of 'this' creates a lot of overhead. - // BUG: Closure capture of 'this' may have unexpected results depending on how this is called. - // The above two comments apply to all the lambdas passed to all calls to AddCommand below. + AddCommand (Command.Select, HandleMouseClick); AddCommand (Command.Left, () => MoveLeft ()); AddCommand (Command.Right, () => MoveRight ()); AddCommand (Command.Down, () => MoveDown (BytesPerLine)); @@ -76,6 +74,8 @@ public HexView (Stream? source) Command.EndOfPage, () => MoveDown (BytesPerLine * (Viewport.Height - 1 - (int)(Address - Viewport.Y) / BytesPerLine)) ); + AddCommand (Command.ScrollDown, () => ScrollVertical (1)); + AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); AddCommand (Command.DeleteCharLeft, () => true); AddCommand (Command.DeleteCharRight, () => true); AddCommand (Command.Insert, () => true); @@ -84,11 +84,8 @@ public HexView (Stream? source) KeyBindings.Add (Key.CursorRight, Command.Right); KeyBindings.Add (Key.CursorDown, Command.Down); KeyBindings.Add (Key.CursorUp, Command.Up); - KeyBindings.Add (Key.PageUp, Command.PageUp); - KeyBindings.Add (Key.PageDown, Command.PageDown); - KeyBindings.Add (Key.Home, Command.Start); KeyBindings.Add (Key.End, Command.End); KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.LeftStart); @@ -103,6 +100,12 @@ public HexView (Stream? source) KeyBindings.Remove (Key.Space); KeyBindings.Remove (Key.Enter); + // The Select handler deals with both single and double clicks + MouseBindings.ReplaceCommands (MouseFlags.Button1Clicked, Command.Select); + MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Select); + MouseBindings.Add (MouseFlags.WheeledUp, Command.ScrollUp); + MouseBindings.Add (MouseFlags.WheeledDown, Command.ScrollDown); + SubviewsLaidOut += HexViewSubviewsLaidOut; } @@ -173,7 +176,7 @@ private void ScrollToMakeCursorVisible (Point offsetToNewCursor) if (offsetToNewCursor.X < 1) { - ScrollHorizontal(offsetToNewCursor.X); + ScrollHorizontal (offsetToNewCursor.X); } else if (offsetToNewCursor.X >= Viewport.Width) { @@ -347,20 +350,16 @@ public int AddressWidth private int GetLeftSideStartColumn () { return AddressWidth == 0 ? 0 : AddressWidth + 1; } - /// - protected override bool OnMouseEvent (MouseEventArgs me) + private bool? HandleMouseClick (ICommandContext? commandContext) { - if (_source is null) + if (commandContext is not CommandContext { Binding.MouseEventArgs: { } } mouseCommandContext) { return false; } - if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) - && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) - && !me.Flags.HasFlag (MouseFlags.WheeledDown) - && !me.Flags.HasFlag (MouseFlags.WheeledUp)) + if (RaiseSelecting (commandContext) is true) { - return false; + return true; } if (!HasFocus) @@ -368,21 +367,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) SetFocus (); } - if (me.Flags == MouseFlags.WheeledDown) - { - ScrollVertical (1); - - return true; - } - - if (me.Flags == MouseFlags.WheeledUp) - { - ScrollVertical (-1); - - return true; - } - - if (me.Position.X < GetLeftSideStartColumn ()) + if (mouseCommandContext.Binding.MouseEventArgs.Position.X < GetLeftSideStartColumn ()) { return true; } @@ -391,14 +376,14 @@ protected override bool OnMouseEvent (MouseEventArgs me) int blocksSize = blocks * HEX_COLUMN_WIDTH; int blocksRightOffset = GetLeftSideStartColumn () + blocksSize - 1; - if (me.Position.X > blocksRightOffset + BytesPerLine - 1) + if (mouseCommandContext.Binding.MouseEventArgs.Position.X > blocksRightOffset + BytesPerLine - 1) { return true; } - bool clickIsOnLeftSide = me.Position.X >= blocksRightOffset; - long lineStart = me.Position.Y * BytesPerLine + Viewport.Y * BytesPerLine; - int x = me.Position.X - GetLeftSideStartColumn () + 1; + bool clickIsOnLeftSide = mouseCommandContext.Binding.MouseEventArgs.Position.X >= blocksRightOffset; + long lineStart = mouseCommandContext.Binding.MouseEventArgs.Position.Y * BytesPerLine + Viewport.Y * BytesPerLine; + int x = mouseCommandContext.Binding.MouseEventArgs.Position.X - GetLeftSideStartColumn () + 1; int block = x / HEX_COLUMN_WIDTH; x -= block * 2; int empty = x % 3; @@ -413,14 +398,14 @@ protected override bool OnMouseEvent (MouseEventArgs me) if (clickIsOnLeftSide) { - Address = Math.Min (lineStart + me.Position.X - blocksRightOffset, GetEditedSize ()); + Address = Math.Min (lineStart + mouseCommandContext.Binding.MouseEventArgs.Position.X - blocksRightOffset, GetEditedSize ()); } else { Address = Math.Min (lineStart + item, GetEditedSize ()); } - if (me.Flags == MouseFlags.Button1DoubleClicked) + if (mouseCommandContext.Binding.MouseEventArgs.Flags == MouseFlags.Button1DoubleClicked) { _leftSideHasFocus = !clickIsOnLeftSide; @@ -435,7 +420,7 @@ protected override bool OnMouseEvent (MouseEventArgs me) SetNeedsDraw (); } - return true; + return false; } /// From ed67edad415fd8435b955add52b61fbeb3114d0e Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 8 Dec 2024 18:30:03 -0800 Subject: [PATCH 28/40] Doc tweak --- docfx/docs/keyboard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docfx/docs/keyboard.md b/docfx/docs/keyboard.md index 2a6abd76ed..bd03900cec 100644 --- a/docfx/docs/keyboard.md +++ b/docfx/docs/keyboard.md @@ -40,7 +40,7 @@ The `Character Map` Scenario includes a View called `CharMap` that is a good exa The [Command](~/api/Terminal.Gui.Command.yml) enum lists generic operations that are implemented by views. For example `Command.Accept` in a `Button` results in the `Accepting` event firing while in `TableView` it is bound to `CellActivated`. Not all commands -are implemented by all views (e.g. you cannot scroll in a `Button`). Use the @Terminal.Gui.View.GetSupportedCommands() method to determine which commands are implemented by a `View`. +are implemented by all views (e.g. you cannot scroll in a `Button`). Use the @Terminal.Gui.View.GetSupportedCommands method to determine which commands are implemented by a `View`. The default key for activating a button is `Space`. You can change this using `KeyBindings.ReplaceKey()`: From 9f4d30db2b2facf1aae6eceee1cd9839a74522be Mon Sep 17 00:00:00 2001 From: Tig Date: Sun, 8 Dec 2024 18:34:39 -0800 Subject: [PATCH 29/40] Unit test tweak --- UnitTests/Input/Mouse/MouseBindingsTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/UnitTests/Input/Mouse/MouseBindingsTests.cs b/UnitTests/Input/Mouse/MouseBindingsTests.cs index 692710196a..9db8583ea8 100644 --- a/UnitTests/Input/Mouse/MouseBindingsTests.cs +++ b/UnitTests/Input/Mouse/MouseBindingsTests.cs @@ -29,8 +29,6 @@ public void Add_Invalid_Flag_Throws () var mouseBindings = new MouseBindings (); List commands = new (); Assert.Throws (() => mouseBindings.Add (MouseFlags.None, Command.Accept)); - - Assert.Throws (() => mouseBindings.Add ((MouseFlags)0x8ffffff, Command.Accept)); } [Fact] From bf8879537c36fb01e1a39c07aca409b66c52aee1 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 9 Dec 2024 16:59:17 +0000 Subject: [PATCH 30/40] Super rough sketch of what generics solution would look like --- Terminal.Gui/Input/Keyboard/KeyBinding.cs | 2 +- Terminal.Gui/Input/Mouse/IInputBinding.cs | 7 +++++ Terminal.Gui/Input/Mouse/MouseBinding.cs | 2 +- Terminal.Gui/Input/Mouse/MouseBindings.cs | 38 ++++++++++++++++++++++- 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 Terminal.Gui/Input/Mouse/IInputBinding.cs diff --git a/Terminal.Gui/Input/Keyboard/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs index dce67950fe..eb87b33813 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -11,7 +11,7 @@ namespace Terminal.Gui; /// /// /// -public record struct KeyBinding +public record struct KeyBinding : IInputBinding { /// Initializes a new instance. /// The commands this key binding will invoke. diff --git a/Terminal.Gui/Input/Mouse/IInputBinding.cs b/Terminal.Gui/Input/Mouse/IInputBinding.cs new file mode 100644 index 0000000000..7fd6bcf0f5 --- /dev/null +++ b/Terminal.Gui/Input/Mouse/IInputBinding.cs @@ -0,0 +1,7 @@ +#nullable enable +namespace Terminal.Gui; + +public interface IInputBinding +{ + Command [] Commands { get; set; } +} diff --git a/Terminal.Gui/Input/Mouse/MouseBinding.cs b/Terminal.Gui/Input/Mouse/MouseBinding.cs index 03238a53df..5589ee2dfc 100644 --- a/Terminal.Gui/Input/Mouse/MouseBinding.cs +++ b/Terminal.Gui/Input/Mouse/MouseBinding.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui; /// Provides a collection of objects for mouse events. /// /// -public record struct MouseBinding +public record struct MouseBinding : IInputBinding { /// Initializes a new instance. /// The commands this mouse binding will invoke. diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index 5dd5df04ce..a985d503d5 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -1,12 +1,48 @@ #nullable enable namespace Terminal.Gui; +public abstract class Bindings where TKey: Enum where TBind : IInputBinding, new() +{ + private readonly Dictionary _bindings = new (); + + /// Adds a to the collection. + /// + /// + public void Add (TKey mouseEventArgs, TBind binding) + { + if (TryGet (mouseEventArgs, out TBind _)) + { + throw new InvalidOperationException (@$"A binding for {mouseEventArgs} exists ({binding})."); + } + + // IMPORTANT: Add a COPY of the mouseEventArgs. 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 mouseEventArgs, but the old mouseEventArgs will still be in the dictionary. + // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. + _bindings.Add (mouseEventArgs, binding); + } + + + /// Gets the commands bound with the specified . + /// + /// The key to check. + /// + /// When this method returns, contains the commands bound with the specified mouse flags, if the mouse flags are + /// found; otherwise, null. This parameter is passed uninitialized. + /// + /// if the mouse flags are bound; otherwise . + public bool TryGet (TKey mouseEventArgs, out TBind? binding) + { + return _bindings.TryGetValue (mouseEventArgs, out binding); + } +} + /// /// Provides a collection of objects bound to a combination of . /// /// /// -public class MouseBindings +public class MouseBindings : Bindings { /// /// Initializes a new instance. This constructor is used when the are not bound to a From 9002acf94255ab6b06982bfb21e2dd247c162d2e Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 9 Dec 2024 20:29:54 +0000 Subject: [PATCH 31/40] WIP move more to bind base class --- Terminal.Gui/Input/Mouse/MouseBindings.cs | 105 +++++++++++----------- 1 file changed, 50 insertions(+), 55 deletions(-) diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index a985d503d5..d2fe089df1 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -1,9 +1,17 @@ #nullable enable +using System.Collections.Generic; + namespace Terminal.Gui; public abstract class Bindings where TKey: Enum where TBind : IInputBinding, new() { - private readonly Dictionary _bindings = new (); + protected readonly Dictionary _bindings = new (); + private readonly Func _constructBinding; + + protected Bindings (Func constructBinding) + { + _constructBinding = constructBinding; + } /// Adds a to the collection. /// @@ -35,37 +43,7 @@ public bool TryGet (TKey mouseEventArgs, out TBind? binding) { return _bindings.TryGetValue (mouseEventArgs, out binding); } -} - -/// -/// Provides a collection of objects bound to a combination of . -/// -/// -/// -public class MouseBindings : Bindings -{ - /// - /// Initializes a new instance. This constructor is used when the are not bound to a - /// . This is used for Application.MouseBindings and unit tests. - /// - public MouseBindings () { } - /// Adds a to the collection. - /// - /// - public void Add (MouseFlags mouseEventArgs, MouseBinding binding) - { - if (TryGet (mouseEventArgs, out MouseBinding _)) - { - throw new InvalidOperationException (@$"A binding for {mouseEventArgs} exists ({binding})."); - } - - // IMPORTANT: Add a COPY of the mouseEventArgs. 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 mouseEventArgs, but the old mouseEventArgs will still be in the dictionary. - // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. - _bindings.Add (mouseEventArgs, binding); - } /// /// Adds a new mouse flag combination that will trigger the commands in . @@ -85,9 +63,9 @@ public void Add (MouseFlags mouseEventArgs, MouseBinding binding) /// will be /// consumed if any took effect. /// - public void Add (MouseFlags mouseFlags, params Command [] commands) + public void Add (TKey mouseFlags, params Command [] commands) { - if (mouseFlags == MouseFlags.None) + if (EqualityComparer.Default.Equals (mouseFlags, default)) { throw new ArgumentException (@"Invalid MouseFlag", nameof (mouseFlags)); } @@ -97,21 +75,19 @@ public void Add (MouseFlags mouseFlags, params Command [] commands) throw new ArgumentException (@"At least one command must be specified", nameof (commands)); } - if (TryGet (mouseFlags, out MouseBinding binding)) + if (TryGet (mouseFlags, out var binding)) { throw new InvalidOperationException (@$"A binding for {mouseFlags} exists ({binding})."); } - Add (mouseFlags, new MouseBinding (commands, mouseFlags)); + Add (mouseFlags, _constructBinding(commands,mouseFlags)); } - private readonly Dictionary _bindings = new (); - /// /// Gets the bindings. /// /// - public IEnumerable> GetBindings () + public IEnumerable> GetBindings () { return _bindings; } @@ -126,11 +102,11 @@ public IEnumerable> GetBindings () /// public void Clear (params Command [] command) { - KeyValuePair [] kvps = _bindings - .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) - .ToArray (); + KeyValuePair [] kvps = _bindings + .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) + .ToArray (); - foreach (KeyValuePair kvp in kvps) + foreach (KeyValuePair kvp in kvps) { Remove (kvp.Key); } @@ -139,9 +115,9 @@ public void Clear (params Command [] command) /// Gets the for the specified combination of . /// /// - public MouseBinding Get (MouseFlags mouseEventArgs) + public TBind? Get (TKey mouseEventArgs) { - if (TryGet (mouseEventArgs, out MouseBinding binding)) + if (TryGet (mouseEventArgs, out var binding)) { return binding; } @@ -149,6 +125,36 @@ public MouseBinding Get (MouseFlags mouseEventArgs) throw new InvalidOperationException ($"{mouseEventArgs} is not bound."); } + + /// Removes a from the collection. + /// + public void Remove (TKey mouseEventArgs) + { + if (!TryGet (mouseEventArgs, out var _)) + { + return; + } + + _bindings.Remove (mouseEventArgs); + } +} + +/// +/// Provides a collection of objects bound to a combination of . +/// +/// +/// +public class MouseBindings : Bindings +{ + /// + /// Initializes a new instance. This constructor is used when the are not bound to a + /// . This is used for Application.MouseBindings and unit tests. + /// + public MouseBindings ():base( + (commands, flags)=> new MouseBinding (commands, flags)) { } + + + /// /// Gets combination of bound to the set of commands specified by /// . @@ -200,17 +206,6 @@ public MouseFlags GetMouseFlagsFromCommands (params Command [] commands) return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } - /// Removes a from the collection. - /// - public void Remove (MouseFlags mouseEventArgs) - { - if (!TryGet (mouseEventArgs, out MouseBinding _)) - { - return; - } - - _bindings.Remove (mouseEventArgs); - } /// Replaces the commands already bound to a combination of . /// From c18cff280e90b6415fa2e1afdbbdc8f6426e67c4 Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 9 Dec 2024 20:43:32 +0000 Subject: [PATCH 32/40] make keybindings share base --- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 4 ++-- Terminal.Gui/Input/Mouse/MouseBindings.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index 1bb3e1ab56..86aa655a71 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -7,10 +7,10 @@ namespace Terminal.Gui; /// /// /// -public class KeyBindings +public class KeyBindings : Bindings { /// Initializes a new instance bound to . - public KeyBindings (View? target) { Target = target; } + public KeyBindings (View? target) :base((commands,key)=> new KeyBinding (commands)) { Target = target; } /// Adds a to the collection. /// diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index d2fe089df1..37c11e63cb 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -3,7 +3,7 @@ namespace Terminal.Gui; -public abstract class Bindings where TKey: Enum where TBind : IInputBinding, new() +public abstract class Bindings where TBind : IInputBinding, new() { protected readonly Dictionary _bindings = new (); private readonly Func _constructBinding; From d15b3021d115a06f343e71d331929217c414017a Mon Sep 17 00:00:00 2001 From: tznind Date: Mon, 9 Dec 2024 20:49:01 +0000 Subject: [PATCH 33/40] Equality comparer support --- Terminal.Gui/Input/Keyboard/KeyBindings.cs | 69 +--------------------- Terminal.Gui/Input/Mouse/MouseBindings.cs | 9 ++- 2 files changed, 9 insertions(+), 69 deletions(-) diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index 86aa655a71..ed3585c57a 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -10,71 +10,10 @@ namespace Terminal.Gui; public class KeyBindings : Bindings { /// Initializes a new instance bound to . - public KeyBindings (View? target) :base((commands,key)=> new KeyBinding (commands)) { Target = target; } + public KeyBindings (View? target) :base( + (commands,key)=> new KeyBinding (commands), + new KeyEqualityComparer ()) { Target = target; } - /// Adds a to the collection. - /// - /// - /// If has no Commands or is invalid. - public void Add (Key key, KeyBinding binding) - { - - if (!key.IsValid) - { - throw new ArgumentException (nameof (key)); - } - - if (binding.Commands is { Length: 0 }) - { - throw new ArgumentException (nameof (binding)); - } - - if (TryGet (key, out KeyBinding _)) - { - throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); - - //Bindings [key] = binding; - } - - if (Target is { }) - { - binding.Target = Target; - } - - // 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); - } - -#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved - /// - /// - /// Adds a new key combination that will trigger the commands in (if supported by the - /// View - see ). - /// - /// - /// 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. - /// - /// If is empty. -#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved - public void Add (Key key, params Command [] commands) - { - Add (key, new KeyBinding (commands)); - } /// @@ -102,8 +41,6 @@ public void Add (Key key, View? target, params Command [] commands) Add (key, binding); } - private readonly Dictionary _bindings = new (new KeyEqualityComparer ()); - /// /// Gets the bindings. /// diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index 37c11e63cb..46ca727263 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -1,16 +1,18 @@ #nullable enable +using System.Collections; using System.Collections.Generic; namespace Terminal.Gui; public abstract class Bindings where TBind : IInputBinding, new() { - protected readonly Dictionary _bindings = new (); + protected readonly Dictionary _bindings; private readonly Func _constructBinding; - protected Bindings (Func constructBinding) + protected Bindings (Func constructBinding, IEqualityComparer equalityComparer) { _constructBinding = constructBinding; + _bindings = new (equalityComparer); } /// Adds a to the collection. @@ -151,7 +153,8 @@ public class MouseBindings : Bindings /// . This is used for Application.MouseBindings and unit tests. /// public MouseBindings ():base( - (commands, flags)=> new MouseBinding (commands, flags)) { } + (commands, flags)=> new MouseBinding (commands, flags), + EqualityComparer.Default) { } From 23344baca736ca068d6e1066b9070bb2e9894214 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 9 Dec 2024 16:53:58 -0800 Subject: [PATCH 34/40] Incorporated tznind's stuff --- Terminal.Gui/Input/Bindings.cs | 150 +++++++++++++++++ Terminal.Gui/Input/CommandContext.cs | 6 +- .../Input/{Mouse => }/IInputBinding.cs | 0 Terminal.Gui/Input/Keyboard/Key.cs | 2 + Terminal.Gui/Input/Keyboard/KeyBindings.cs | 27 +-- Terminal.Gui/Input/Mouse/MouseBindings.cs | 156 +----------------- Terminal.Gui/Views/RadioGroup.cs | 4 +- UnitTests/Input/Keyboard/KeyBindingsTests.cs | 14 +- UnitTests/Input/Mouse/MouseBindingsTests.cs | 14 +- 9 files changed, 191 insertions(+), 182 deletions(-) create mode 100644 Terminal.Gui/Input/Bindings.cs rename Terminal.Gui/Input/{Mouse => }/IInputBinding.cs (100%) diff --git a/Terminal.Gui/Input/Bindings.cs b/Terminal.Gui/Input/Bindings.cs new file mode 100644 index 0000000000..6894ebfd42 --- /dev/null +++ b/Terminal.Gui/Input/Bindings.cs @@ -0,0 +1,150 @@ +#nullable enable +using System; + +namespace Terminal.Gui; + +/// +/// Abstract base class for and . +/// +/// The type of the event (e.g. or ). +/// The binding type (e.g. ). +public abstract class Bindings where TBinding : IInputBinding, new() where TEvent : notnull +{ + /// + /// The bindings. + /// + protected readonly Dictionary _bindings; + + private readonly Func _constructBinding; + + /// + /// Initializes a new instance. + /// + /// + /// + protected Bindings (Func constructBinding, IEqualityComparer equalityComparer) + { + _constructBinding = constructBinding; + _bindings = new (equalityComparer); + } + + /// Adds a bound to to the collection. + /// + /// + public void Add (TEvent eventArgs, TBinding binding) + { + if (TryGet (eventArgs, out TBinding _)) + { + throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); + } + + // IMPORTANT: Add a COPY of the mouseEventArgs. 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 mouseEventArgs, but the old mouseEventArgs will still be in the dictionary. + // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. + _bindings.Add (eventArgs, binding); + } + + + /// Gets the commands bound with the specified . + /// + /// The args to check. + /// + /// When this method returns, contains the commands bound with the specified mouse flags, if the mouse flags are + /// found; otherwise, null. This parameter is passed uninitialized. + /// + /// if the mouse flags are bound; otherwise . + public bool TryGet (TEvent eventArgs, out TBinding? binding) + { + return _bindings.TryGetValue (eventArgs, out binding); + } + + + /// + /// Adds a new mouse flag 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 mouse flags to check. + /// + /// The command to invoked on the when is received. When + /// multiple commands are provided,they will be applied in sequence. The bound event + /// will be + /// consumed if any took effect. + /// + public void Add (TEvent eventArgs, params Command [] commands) + { + if (commands.Length == 0) + { + throw new ArgumentException (@"At least one command must be specified", nameof (commands)); + } + + if (TryGet (eventArgs, out var binding)) + { + throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); + } + + Add (eventArgs, _constructBinding(commands,eventArgs)); + } + + /// + /// Gets the bindings. + /// + /// + public IEnumerable> GetBindings () + { + return _bindings; + } + + /// Removes all objects from the collection. + public void Clear () { _bindings.Clear (); } + + /// + /// Removes all bindings that trigger the given command set. Views can have multiple different events 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 TBinding? Get (TEvent eventArgs) + { + if (TryGet (eventArgs, out var binding)) + { + return binding; + } + + throw new InvalidOperationException ($"{eventArgs} is not bound."); + } + + + /// Removes a from the collection. + /// + public void Remove (TEvent mouseEventArgs) + { + if (!TryGet (mouseEventArgs, out var _)) + { + return; + } + + _bindings.Remove (mouseEventArgs); + } +} diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs index 61c21f4e68..e4fdfba680 100644 --- a/Terminal.Gui/Input/CommandContext.cs +++ b/Terminal.Gui/Input/CommandContext.cs @@ -7,14 +7,14 @@ namespace Terminal.Gui; /// /// . #pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved -public record struct CommandContext : ICommandContext +public record struct CommandContext : ICommandContext { /// /// Initializes a new instance with the specified , /// /// /// - public CommandContext (Command command, TBindingType? binding) + public CommandContext (Command command, TBinding? binding) { Command = command; Binding = binding; @@ -26,5 +26,5 @@ public CommandContext (Command command, TBindingType? binding) /// /// The keyboard or mouse minding that was used to invoke the , if any. /// - public TBindingType? Binding { get; set; } + public TBinding? Binding { get; set; } } \ No newline at end of file diff --git a/Terminal.Gui/Input/Mouse/IInputBinding.cs b/Terminal.Gui/Input/IInputBinding.cs similarity index 100% rename from Terminal.Gui/Input/Mouse/IInputBinding.cs rename to Terminal.Gui/Input/IInputBinding.cs diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs index 84f42ee3fd..3fece5c1a8 100644 --- a/Terminal.Gui/Input/Keyboard/Key.cs +++ b/Terminal.Gui/Input/Keyboard/Key.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -69,6 +70,7 @@ namespace Terminal.Gui; /// /// /// +[DefaultValue(KeyCode.Null)] public class Key : EventArgs, IEquatable { /// Constructs a new diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index ed3585c57a..7b68e1b43a 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -7,14 +7,15 @@ namespace Terminal.Gui; /// /// /// -public class KeyBindings : Bindings +public class KeyBindings : Bindings { /// Initializes a new instance bound to . - public KeyBindings (View? target) :base( - (commands,key)=> new KeyBinding (commands), - new KeyEqualityComparer ()) { Target = target; } - - + public KeyBindings (View? target) : base ( + (commands, key) => new (commands), + new KeyEqualityComparer ()) + { + Target = target; + } /// /// @@ -29,11 +30,14 @@ public KeyBindings (View? target) :base( /// /// /// The key to check. - /// The View the commands will be invoked on. If , the key will be bound to . + /// + /// 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. + /// consumed if any took effect. /// public void Add (Key key, View? target, params Command [] commands) { @@ -45,10 +49,7 @@ public void Add (Key key, View? target, params Command [] commands) /// Gets the bindings. /// /// - public IEnumerable> GetBindings () - { - return _bindings; - } + public IEnumerable> GetBindings () { return _bindings; } /// /// Gets the keys that are bound. @@ -179,10 +180,10 @@ public void ReplaceKey (Key oldKey, Key newKey) if (newKey == Key.Empty) { Remove (oldKey); + return; } - if (TryGet (oldKey, out KeyBinding binding)) { Remove (oldKey); diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index 46ca727263..5300fccfdc 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -1,162 +1,20 @@ #nullable enable -using System.Collections; -using System.Collections.Generic; - namespace Terminal.Gui; -public abstract class Bindings where TBind : IInputBinding, new() -{ - protected readonly Dictionary _bindings; - private readonly Func _constructBinding; - - protected Bindings (Func constructBinding, IEqualityComparer equalityComparer) - { - _constructBinding = constructBinding; - _bindings = new (equalityComparer); - } - - /// Adds a to the collection. - /// - /// - public void Add (TKey mouseEventArgs, TBind binding) - { - if (TryGet (mouseEventArgs, out TBind _)) - { - throw new InvalidOperationException (@$"A binding for {mouseEventArgs} exists ({binding})."); - } - - // IMPORTANT: Add a COPY of the mouseEventArgs. 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 mouseEventArgs, but the old mouseEventArgs will still be in the dictionary. - // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. - _bindings.Add (mouseEventArgs, binding); - } - - - /// Gets the commands bound with the specified . - /// - /// The key to check. - /// - /// When this method returns, contains the commands bound with the specified mouse flags, if the mouse flags are - /// found; otherwise, null. This parameter is passed uninitialized. - /// - /// if the mouse flags are bound; otherwise . - public bool TryGet (TKey mouseEventArgs, out TBind? binding) - { - return _bindings.TryGetValue (mouseEventArgs, out binding); - } - - - /// - /// Adds a new mouse flag 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 mouse flags to check. - /// - /// The command to invoked on the when is received. When - /// multiple commands are provided,they will be applied in sequence. The bound event - /// will be - /// consumed if any took effect. - /// - public void Add (TKey mouseFlags, params Command [] commands) - { - if (EqualityComparer.Default.Equals (mouseFlags, default)) - { - throw new ArgumentException (@"Invalid MouseFlag", nameof (mouseFlags)); - } - - if (commands.Length == 0) - { - throw new ArgumentException (@"At least one command must be specified", nameof (commands)); - } - - if (TryGet (mouseFlags, out var binding)) - { - throw new InvalidOperationException (@$"A binding for {mouseFlags} exists ({binding})."); - } - - Add (mouseFlags, _constructBinding(commands,mouseFlags)); - } - - /// - /// Gets the bindings. - /// - /// - public IEnumerable> GetBindings () - { - return _bindings; - } - - /// Removes all objects from the collection. - public void Clear () { _bindings.Clear (); } - - /// - /// Removes all bindings that trigger the given command set. Views can have multiple different events 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 combination of . - /// - /// - public TBind? Get (TKey mouseEventArgs) - { - if (TryGet (mouseEventArgs, out var binding)) - { - return binding; - } - - throw new InvalidOperationException ($"{mouseEventArgs} is not bound."); - } - - - /// Removes a from the collection. - /// - public void Remove (TKey mouseEventArgs) - { - if (!TryGet (mouseEventArgs, out var _)) - { - return; - } - - _bindings.Remove (mouseEventArgs); - } -} - /// /// Provides a collection of objects bound to a combination of . /// /// /// -public class MouseBindings : Bindings +public class MouseBindings : Bindings { /// - /// Initializes a new instance. This constructor is used when the are not bound to a - /// . This is used for Application.MouseBindings and unit tests. + /// Initializes a new instance. /// - public MouseBindings ():base( - (commands, flags)=> new MouseBinding (commands, flags), - EqualityComparer.Default) { } - - + public MouseBindings () : base ( + (commands, flags) => new (commands, flags), + EqualityComparer.Default) + { } /// /// Gets combination of bound to the set of commands specified by @@ -209,7 +67,6 @@ public MouseFlags GetMouseFlagsFromCommands (params Command [] commands) return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } - /// Replaces the commands already bound to a combination of . /// /// @@ -245,7 +102,6 @@ public void ReplaceMouseFlag (MouseFlags oldMouseFlags, MouseFlags newMouseFlags throw new ArgumentException (@"Invalid MouseFlag", nameof (newMouseFlags)); } - if (TryGet (oldMouseFlags, out MouseBinding binding)) { Remove (oldMouseFlags); diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 32b51cc5f9..623ccc4b83 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -59,6 +59,7 @@ public RadioGroup () AddCommand (Command.HotKey, ctx => { + // If the command did not come from a keyboard event, ignore it if (ctx is not CommandContext keyCommandContext) { return false; @@ -66,10 +67,9 @@ public RadioGroup () var item = keyCommandContext.Binding.Data as int?; - if (HasFocus) { - if (keyCommandContext is { Binding : { } } && (keyCommandContext.Binding.Target != this || HotKey == keyCommandContext.Binding.Key?.NoAlt.NoCtrl.NoShift)) + if (keyCommandContext is { Binding : { } } && (item is null || 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); diff --git a/UnitTests/Input/Keyboard/KeyBindingsTests.cs b/UnitTests/Input/Keyboard/KeyBindingsTests.cs index 8e39cb96d0..70be3dc11e 100644 --- a/UnitTests/Input/Keyboard/KeyBindingsTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -27,13 +27,13 @@ public void Add_Adds () Assert.Contains (Command.Left, resultCommands); } - [Fact] - public void Add_Invalid_Key_Throws () - { - var keyBindings = new KeyBindings (new View ()); - List commands = new (); - Assert.Throws (() => keyBindings.Add (Key.Empty, Command.Accept)); - } + //[Fact] + //public void Add_Invalid_Key_Throws () + //{ + // var keyBindings = new KeyBindings (new View ()); + // List commands = new (); + // Assert.Throws (() => keyBindings.Add (Key.Empty, Command.Accept)); + //} [Fact] public void Add_Multiple_Commands_Adds () diff --git a/UnitTests/Input/Mouse/MouseBindingsTests.cs b/UnitTests/Input/Mouse/MouseBindingsTests.cs index 9db8583ea8..d8efd8f6a9 100644 --- a/UnitTests/Input/Mouse/MouseBindingsTests.cs +++ b/UnitTests/Input/Mouse/MouseBindingsTests.cs @@ -23,13 +23,13 @@ public void Add_Adds () Assert.Contains (Command.Left, resultCommands); } - [Fact] - public void Add_Invalid_Flag_Throws () - { - var mouseBindings = new MouseBindings (); - List commands = new (); - Assert.Throws (() => mouseBindings.Add (MouseFlags.None, Command.Accept)); - } + //[Fact] + //public void Add_Invalid_Flag_Throws () + //{ + // var mouseBindings = new MouseBindings (); + // List commands = new (); + // Assert.Throws (() => mouseBindings.Add (MouseFlags.None, Command.Accept)); + //} [Fact] public void Add_Multiple_Commands_Adds () From 2b8884a8818fcebce4d76051b1935e790f07aa40 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 9 Dec 2024 22:03:40 -0800 Subject: [PATCH 35/40] Finished integrated tznind's work. --- .../Application/Application.Keyboard.cs | 11 +- .../Application/Application.Navigation.cs | 8 +- Terminal.Gui/Application/Application.Run.cs | 4 +- Terminal.Gui/Input/Bindings.cs | 150 ----------- Terminal.Gui/Input/IInputBinding.cs | 6 + Terminal.Gui/Input/InputBindings.cs | 233 ++++++++++++++++++ Terminal.Gui/Input/Keyboard/KeyBindings.cs | 173 +------------ Terminal.Gui/Input/Mouse/MouseBinding.cs | 5 +- Terminal.Gui/Input/Mouse/MouseBindings.cs | 115 +-------- Terminal.Gui/View/View.Keyboard.cs | 12 - Terminal.Gui/Views/ColorPicker.16.cs | 8 +- Terminal.Gui/Views/ListView.cs | 4 +- Terminal.Gui/Views/Shortcut.cs | 2 +- Terminal.Gui/Views/TableView/TableView.cs | 4 +- Terminal.Gui/Views/TextField.cs | 16 +- Terminal.Gui/Views/TextView.cs | 18 +- Terminal.Gui/Views/TreeView/TreeView.cs | 2 +- UICatalog/Scenarios/KeyBindings.cs | 2 +- UICatalog/Scenarios/Text.cs | 4 +- UnitTests/Input/Keyboard/KeyBindingsTests.cs | 40 +-- UnitTests/Input/Mouse/MouseBindingsTests.cs | 40 +-- UnitTests/View/Keyboard/HotKeyTests.cs | 2 +- 22 files changed, 334 insertions(+), 525 deletions(-) delete mode 100644 Terminal.Gui/Input/Bindings.cs create mode 100644 Terminal.Gui/Input/InputBindings.cs diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 1c8e21b4a9..625b5b15f2 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -64,16 +64,17 @@ public static bool RaiseKeyDownEvent (Key key) } else { - if (!KeyBindings.TryGet (key, out KeyBinding appBinding)) + // BUGBUG: this seems unneeded. + if (!KeyBindings.TryGet (key, out KeyBinding keybinding)) { return false; } bool? toReturn = null; - foreach (Command command in appBinding.Commands) + foreach (Command command in keybinding.Commands) { - toReturn = InvokeCommand (command, key, appBinding); + toReturn = InvokeCommand (command, key, keybinding); } return toReturn ?? true; @@ -82,7 +83,7 @@ public static bool RaiseKeyDownEvent (Key key) return false; - static bool? InvokeCommand (Command command, Key key, KeyBinding appBinding) + static bool? InvokeCommand (Command command, Key key, KeyBinding binding) { if (!_commandImplementations!.ContainsKey (command)) { @@ -93,7 +94,7 @@ public static bool RaiseKeyDownEvent (Key key) if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation)) { - CommandContext context = new (command, appBinding); // Create the context here + CommandContext context = new (command, binding); // Create the context here return implementation (context); } diff --git a/Terminal.Gui/Application/Application.Navigation.cs b/Terminal.Gui/Application/Application.Navigation.cs index 893b06afb6..1d3aca97cb 100644 --- a/Terminal.Gui/Application/Application.Navigation.cs +++ b/Terminal.Gui/Application/Application.Navigation.cs @@ -22,7 +22,7 @@ public static Key NextTabGroupKey { if (_nextTabGroupKey != value) { - KeyBindings.ReplaceKey (_nextTabGroupKey, value); + KeyBindings.Replace (_nextTabGroupKey, value); _nextTabGroupKey = value; } } @@ -37,7 +37,7 @@ public static Key NextTabKey { if (_nextTabKey != value) { - KeyBindings.ReplaceKey (_nextTabKey, value); + KeyBindings.Replace (_nextTabKey, value); _nextTabKey = value; } } @@ -66,7 +66,7 @@ public static Key PrevTabGroupKey { if (_prevTabGroupKey != value) { - KeyBindings.ReplaceKey (_prevTabGroupKey, value); + KeyBindings.Replace (_prevTabGroupKey, value); _prevTabGroupKey = value; } } @@ -81,7 +81,7 @@ public static Key PrevTabKey { if (_prevTabKey != value) { - KeyBindings.ReplaceKey (_prevTabKey, value); + KeyBindings.Replace (_prevTabKey, value); _prevTabKey = value; } } diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 051c81223c..9e4d43b705 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -19,7 +19,7 @@ public static Key QuitKey { if (_quitKey != value) { - KeyBindings.ReplaceKey (_quitKey, value); + KeyBindings.Replace (_quitKey, value); _quitKey = value; } } @@ -37,7 +37,7 @@ public static Key ArrangeKey { if (_arrangeKey != value) { - KeyBindings.ReplaceKey (_arrangeKey, value); + KeyBindings.Replace (_arrangeKey, value); _arrangeKey = value; } } diff --git a/Terminal.Gui/Input/Bindings.cs b/Terminal.Gui/Input/Bindings.cs deleted file mode 100644 index 6894ebfd42..0000000000 --- a/Terminal.Gui/Input/Bindings.cs +++ /dev/null @@ -1,150 +0,0 @@ -#nullable enable -using System; - -namespace Terminal.Gui; - -/// -/// Abstract base class for and . -/// -/// The type of the event (e.g. or ). -/// The binding type (e.g. ). -public abstract class Bindings where TBinding : IInputBinding, new() where TEvent : notnull -{ - /// - /// The bindings. - /// - protected readonly Dictionary _bindings; - - private readonly Func _constructBinding; - - /// - /// Initializes a new instance. - /// - /// - /// - protected Bindings (Func constructBinding, IEqualityComparer equalityComparer) - { - _constructBinding = constructBinding; - _bindings = new (equalityComparer); - } - - /// Adds a bound to to the collection. - /// - /// - public void Add (TEvent eventArgs, TBinding binding) - { - if (TryGet (eventArgs, out TBinding _)) - { - throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); - } - - // IMPORTANT: Add a COPY of the mouseEventArgs. 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 mouseEventArgs, but the old mouseEventArgs will still be in the dictionary. - // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. - _bindings.Add (eventArgs, binding); - } - - - /// Gets the commands bound with the specified . - /// - /// The args to check. - /// - /// When this method returns, contains the commands bound with the specified mouse flags, if the mouse flags are - /// found; otherwise, null. This parameter is passed uninitialized. - /// - /// if the mouse flags are bound; otherwise . - public bool TryGet (TEvent eventArgs, out TBinding? binding) - { - return _bindings.TryGetValue (eventArgs, out binding); - } - - - /// - /// Adds a new mouse flag 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 mouse flags to check. - /// - /// The command to invoked on the when is received. When - /// multiple commands are provided,they will be applied in sequence. The bound event - /// will be - /// consumed if any took effect. - /// - public void Add (TEvent eventArgs, params Command [] commands) - { - if (commands.Length == 0) - { - throw new ArgumentException (@"At least one command must be specified", nameof (commands)); - } - - if (TryGet (eventArgs, out var binding)) - { - throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); - } - - Add (eventArgs, _constructBinding(commands,eventArgs)); - } - - /// - /// Gets the bindings. - /// - /// - public IEnumerable> GetBindings () - { - return _bindings; - } - - /// Removes all objects from the collection. - public void Clear () { _bindings.Clear (); } - - /// - /// Removes all bindings that trigger the given command set. Views can have multiple different events 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 TBinding? Get (TEvent eventArgs) - { - if (TryGet (eventArgs, out var binding)) - { - return binding; - } - - throw new InvalidOperationException ($"{eventArgs} is not bound."); - } - - - /// Removes a from the collection. - /// - public void Remove (TEvent mouseEventArgs) - { - if (!TryGet (mouseEventArgs, out var _)) - { - return; - } - - _bindings.Remove (mouseEventArgs); - } -} diff --git a/Terminal.Gui/Input/IInputBinding.cs b/Terminal.Gui/Input/IInputBinding.cs index 7fd6bcf0f5..2ce2bec8bc 100644 --- a/Terminal.Gui/Input/IInputBinding.cs +++ b/Terminal.Gui/Input/IInputBinding.cs @@ -1,7 +1,13 @@ #nullable enable namespace Terminal.Gui; +/// +/// Describes an input binding. Used to bind a set of objects to a specific input event. +/// public interface IInputBinding { + /// + /// Gets or sets the commands this input binding will invoke. + /// Command [] Commands { get; set; } } diff --git a/Terminal.Gui/Input/InputBindings.cs b/Terminal.Gui/Input/InputBindings.cs new file mode 100644 index 0000000000..ec970a4dfa --- /dev/null +++ b/Terminal.Gui/Input/InputBindings.cs @@ -0,0 +1,233 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Abstract base class for and . +/// +/// The type of the event (e.g. or ). +/// The binding type (e.g. ). +public abstract class InputBindings where TBinding : IInputBinding, new () where TEvent : notnull +{ + /// + /// The bindings. + /// + private readonly Dictionary _bindings; + + private readonly Func _constructBinding; + + /// + /// Initializes a new instance. + /// + /// + /// + protected InputBindings (Func constructBinding, IEqualityComparer equalityComparer) + { + _constructBinding = constructBinding; + _bindings = new (equalityComparer); + } + + /// + /// Tests whether is valid or not. + /// + /// + /// + public abstract bool IsValid (TEvent eventArgs); + + /// Adds a bound to to the collection. + /// + /// + public void Add (TEvent eventArgs, TBinding binding) + { + if (!IsValid (eventArgs)) + { + throw new ArgumentException (@"Invalid newEventArgs", nameof (eventArgs)); + } + + if (TryGet (eventArgs, out TBinding _)) + { + throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); + } + + // IMPORTANT: Add a COPY of the eventArgs. 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 eventArgs, but the old eventArgs will still be in the dictionary. + // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details. + _bindings.Add (eventArgs, binding); + } + + /// + /// Adds a new that will trigger the commands in . + /// + /// If the is already bound to a different set of s it will be rebound + /// . + /// + /// + /// The event to check. + /// + /// The command to invoked on the when is received. When + /// multiple commands are provided,they will be applied in sequence. The bound event + /// will be + /// consumed if any took effect. + /// + public void Add (TEvent eventArgs, params Command [] commands) + { + if (commands.Length == 0) + { + throw new ArgumentException (@"At least one command must be specified", nameof (commands)); + } + + if (TryGet (eventArgs, out TBinding? binding)) + { + throw new InvalidOperationException (@$"A binding for {eventArgs} exists ({binding})."); + } + + Add (eventArgs, _constructBinding (commands, eventArgs)); + } + + /// + /// Gets the bindings. + /// + /// + public IEnumerable> GetBindings () { return _bindings; } + + /// Removes all objects from the collection. + public void Clear () { _bindings.Clear (); } + + /// + /// Removes all bindings that trigger the given command set. Views can have multiple different + /// 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 TBinding? Get (TEvent eventArgs) + { + if (TryGet (eventArgs, out TBinding? binding)) + { + return binding; + } + + throw new InvalidOperationException ($"{eventArgs} is not bound."); + } + + /// Gets the commands bound with the specified . + /// + /// The to check. + /// + /// When this method returns, contains the commands bound with the , if the is + /// not + /// found; otherwise, null. This parameter is passed uninitialized. + /// + /// if the is bound; otherwise . + public bool TryGet (TEvent eventArgs, out TBinding? binding) { return _bindings.TryGetValue (eventArgs, out binding); } + + /// 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 (TEvent eventArgs) + { + if (TryGet (eventArgs, out TBinding? bindings)) + { + return bindings!.Commands; + } + + return []; + } + + /// + /// Gets the first matching bound to the set of commands specified by + /// . + /// + /// The set of commands to search. + /// + /// The first matching bound to the set of commands specified by + /// . if the set of caommands was not found. + /// + public TEvent GetFirstFromCommands (params Command [] commands) { return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } + + /// Gets all 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 GetAllFromCommands (params Command [] commands) + { + return _bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); + } + + /// Replaces a combination already bound to a set of s. + /// + /// The to be replaced. + /// + /// The new to be used. + /// + public void Replace (TEvent oldEventArgs, TEvent newEventArgs) + { + if (!IsValid (newEventArgs)) + { + throw new ArgumentException (@"Invalid newEventArgs", nameof (newEventArgs)); + } + + if (TryGet (oldEventArgs, out TBinding? binding)) + { + Remove (oldEventArgs); + Add (newEventArgs, binding!); + } + else + { + Add (newEventArgs, binding!); + } + } + + /// Replaces the commands already bound to a combination of . + /// + /// + /// If the of is not already bound, it will be added. + /// + /// + /// The combination of bound to the command to be replaced. + /// The set of commands to replace the old ones with. + public void ReplaceCommands (TEvent eventArgs, params Command [] newCommands) + { + if (TryGet (eventArgs, out TBinding _)) + { + Remove (eventArgs); + Add (eventArgs, newCommands); + } + else + { + Add (eventArgs, newCommands); + } + } + + /// Removes a from the collection. + /// + public void Remove (TEvent eventArgs) + { + if (!TryGet (eventArgs, out _)) + { + return; + } + + _bindings.Remove (eventArgs); + } +} diff --git a/Terminal.Gui/Input/Keyboard/KeyBindings.cs b/Terminal.Gui/Input/Keyboard/KeyBindings.cs index 7b68e1b43a..7b7590729d 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBindings.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -2,21 +2,22 @@ namespace Terminal.Gui; /// -/// Provides a collection of objects bound to a . +/// Provides a collection of s bound to s. /// /// /// /// -public class KeyBindings : Bindings +public class KeyBindings : InputBindings { /// Initializes a new instance bound to . - public KeyBindings (View? target) : base ( - (commands, key) => new (commands), - new KeyEqualityComparer ()) + public KeyBindings (View? target) : base ((commands, key) => new (commands), new KeyEqualityComparer ()) { Target = target; } + /// + public override bool IsValid (Key eventArgs) { return eventArgs.IsValid; } + /// /// /// Adds a new key combination that will trigger the commands in on the View @@ -45,18 +46,6 @@ public void Add (Key key, View? target, params Command [] commands) Add (key, binding); } - /// - /// Gets the bindings. - /// - /// - public IEnumerable> GetBindings () { return _bindings; } - - /// - /// Gets the keys that are bound. - /// - /// - public IEnumerable GetBoundKeys () { return _bindings.Keys; } - /// /// The view that the are bound to. /// @@ -64,154 +53,4 @@ public void Add (Key key, View? target, params Command [] commands) /// If the KeyBindings object is being used for Application.KeyBindings. /// public View? Target { get; init; } - - /// 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 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 []; - } - - /// 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 KeyBinding _)) - { - 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 KeyBinding binding)) - { - Remove (key); - Add (key, newCommands); - } - else - { - Add (key, newCommands); - } - } - - /// Replaces a key combination already bound to a set of s. - /// - /// The key to be replaced. - /// The new key to be used. If no action will be taken. - 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 KeyBinding 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 KeyBinding binding) - { - binding = new ([], null); - - if (key.IsValid) - { - return _bindings.TryGetValue (key, out binding); - } - - return false; - } } diff --git a/Terminal.Gui/Input/Mouse/MouseBinding.cs b/Terminal.Gui/Input/Mouse/MouseBinding.cs index 5589ee2dfc..11689719a7 100644 --- a/Terminal.Gui/Input/Mouse/MouseBinding.cs +++ b/Terminal.Gui/Input/Mouse/MouseBinding.cs @@ -3,8 +3,9 @@ namespace Terminal.Gui; /// -/// Provides a collection of objects for mouse events. +/// Provides a collection of bound to s. /// +/// /// public record struct MouseBinding : IInputBinding { @@ -21,7 +22,7 @@ public MouseBinding (Command [] commands, MouseFlags mouseFlags) }; } - /// The commands this key binding will invoke. + /// The commands this binding will invoke. public Command [] Commands { get; set; } /// diff --git a/Terminal.Gui/Input/Mouse/MouseBindings.cs b/Terminal.Gui/Input/Mouse/MouseBindings.cs index 5300fccfdc..afcab50dae 100644 --- a/Terminal.Gui/Input/Mouse/MouseBindings.cs +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui; /// /// /// -public class MouseBindings : Bindings +public class MouseBindings : InputBindings { /// /// Initializes a new instance. @@ -16,115 +16,6 @@ public MouseBindings () : base ( EqualityComparer.Default) { } - /// - /// Gets combination of bound to the set of commands specified by - /// . - /// - /// The set of commands to search. - /// - /// The combination of bound to the set of commands specified by - /// . An empty list if the set of caommands was not found. - /// - public IEnumerable GetAllMouseFlagsFromCommands (params Command [] commands) - { - return _bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key); - } - - /// - /// Gets the that are bound. - /// - /// - public IEnumerable GetBoundMouseFlags () { return _bindings.Keys; } - - /// 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 (MouseFlags mouseFlags) - { - if (TryGet (mouseFlags, out MouseBinding bindings)) - { - return bindings.Commands; - } - - return []; - } - - /// - /// Gets the first combination of bound to the set of commands specified by - /// . - /// - /// The set of commands to search. - /// - /// The first combination of bound to the set of commands specified by - /// . if the set of caommands was not found. - /// - public MouseFlags GetMouseFlagsFromCommands (params Command [] commands) - { - return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; - } - - /// Replaces the commands already bound to a combination of . - /// - /// - /// If the combination of is not already bound, it will be added. - /// - /// - /// The combination of bound to the command to be replaced. - /// The set of commands to replace the old ones with. - public void ReplaceCommands (MouseFlags mouseEventArgs, params Command [] newCommands) - { - if (TryGet (mouseEventArgs, out MouseBinding binding)) - { - Remove (mouseEventArgs); - Add (mouseEventArgs, newCommands); - } - else - { - Add (mouseEventArgs, newCommands); - } - } - - /// Replaces a combination already bound to a set of s. - /// - /// The to be replaced. - /// - /// The new to be used. If no action - /// will be taken. - /// - public void ReplaceMouseFlag (MouseFlags oldMouseFlags, MouseFlags newMouseFlags) - { - if (newMouseFlags == MouseFlags.None) - { - throw new ArgumentException (@"Invalid MouseFlag", nameof (newMouseFlags)); - } - - if (TryGet (oldMouseFlags, out MouseBinding binding)) - { - Remove (oldMouseFlags); - Add (newMouseFlags, binding); - } - else - { - Add (newMouseFlags, binding); - } - } - - /// Gets the commands bound with the specified . - /// - /// The key to check. - /// - /// When this method returns, contains the commands bound with the specified mouse flags, if the mouse flags are - /// found; otherwise, null. This parameter is passed uninitialized. - /// - /// if the mouse flags are bound; otherwise . - public bool TryGet (MouseFlags mouseEventArgs, out MouseBinding binding) - { - binding = new ([], mouseEventArgs); - - return _bindings.TryGetValue (mouseEventArgs, out binding); - } + /// + public override bool IsValid (MouseFlags eventArgs) { return eventArgs != MouseFlags.None; } } diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 323d36193d..700729509e 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -358,10 +358,6 @@ bool RaiseKeyDownNotHandled (Key k) /// and processing should stop. /// /// - /// - /// For processing s and commands, use and - /// instead. - /// /// Fires the event. /// protected virtual bool OnKeyDown (Key key) { return false; } @@ -387,10 +383,6 @@ bool RaiseKeyDownNotHandled (Key k) /// /// /// - /// For processing s and commands, use and - /// instead. - /// - /// /// Not all terminals support distinct key up notifications; applications should avoid depending on distinct /// KeyUp events. /// @@ -407,10 +399,6 @@ bool RaiseKeyDownNotHandled (Key k) /// /// /// - /// For processing s and commands, use and - /// instead. - /// - /// /// SubViews can use the of their super view override the default behavior of when /// key bindings are invoked. /// diff --git a/Terminal.Gui/Views/ColorPicker.16.cs b/Terminal.Gui/Views/ColorPicker.16.cs index 555ac29f7e..1c6808bf79 100644 --- a/Terminal.Gui/Views/ColorPicker.16.cs +++ b/Terminal.Gui/Views/ColorPicker.16.cs @@ -67,7 +67,7 @@ public Point Cursor /// Moves the selected item index to the next row. /// - private bool MoveDown (ICommandContext commandContext) + private bool MoveDown (ICommandContext? commandContext) { if (RaiseSelecting (commandContext) == true) { @@ -83,7 +83,7 @@ private bool MoveDown (ICommandContext commandContext) /// Moves the selected item index to the previous column. /// - private bool MoveLeft (ICommandContext commandContext) + private bool MoveLeft (ICommandContext? commandContext) { if (RaiseSelecting (commandContext) == true) { @@ -100,7 +100,7 @@ private bool MoveLeft (ICommandContext commandContext) /// Moves the selected item index to the next column. /// - private bool MoveRight (ICommandContext commandContext) + private bool MoveRight (ICommandContext? commandContext) { if (RaiseSelecting (commandContext) == true) { @@ -116,7 +116,7 @@ private bool MoveRight (ICommandContext commandContext) /// Moves the selected item index to the previous row. /// - private bool MoveUp (ICommandContext commandContext) + private bool MoveUp (ICommandContext? commandContext) { if (RaiseSelecting (commandContext) == true) { diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 7bb79a530a..772a9775cc 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -813,14 +813,14 @@ protected override bool OnKeyDown (Key a) // at it if (AllowsMarking) { - var keys = KeyBindings.GetKeysFromCommands (Command.Select); + var keys = KeyBindings.GetAllFromCommands (Command.Select); if (keys.Contains (a)) { return false; } - keys = KeyBindings.GetKeysFromCommands ([Command.Select, Command.Down]); + keys = KeyBindings.GetAllFromCommands ([Command.Select, Command.Down]); if (keys.Contains (a)) { diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index f52235da46..9cb02ca3cf 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -69,7 +69,7 @@ public Shortcut () : this (Key.Empty, null, null, null) { } /// The help text to display. public Shortcut (View targetView, Command command, string commandText, string? helpText = null) : this ( - targetView?.HotKeyBindings.GetKeyFromCommands (command)!, + targetView?.HotKeyBindings.GetFirstFromCommands (command)!, commandText, null, helpText) diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 68c5a037e0..9c8c5b0d76 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -293,7 +293,7 @@ public KeyCode CellActivationKey { if (KeyBindings.TryGet (cellActivationKey, out _)) { - KeyBindings.ReplaceKey (cellActivationKey, value); + KeyBindings.Replace (cellActivationKey, value); } else { @@ -1013,7 +1013,7 @@ protected override bool OnKeyDown (Key key) if (CollectionNavigator != null && HasFocus && Table.Rows != 0 - && key != KeyBindings.GetKeyFromCommands (Command.Accept) + && key != KeyBindings.GetFirstFromCommands (Command.Accept) && key != CellActivationKey && CollectionNavigatorBase.IsCompatibleKey (key) && !key.KeyCode.HasFlag (KeyCode.CtrlMask) diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 6b9b1a3aee..5bb386ca72 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -1236,7 +1236,7 @@ private MenuBarItem BuildContextMenuBarItem () () => SelectAll (), null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.SelectAll) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.SelectAll) ), new ( Strings.ctxDeleteAll, @@ -1244,7 +1244,7 @@ private MenuBarItem BuildContextMenuBarItem () () => DeleteAll (), null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.DeleteAll) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.DeleteAll) ), new ( Strings.ctxCopy, @@ -1252,7 +1252,7 @@ private MenuBarItem BuildContextMenuBarItem () () => Copy (), null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Copy) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Copy) ), new ( Strings.ctxCut, @@ -1260,7 +1260,7 @@ private MenuBarItem BuildContextMenuBarItem () () => Cut (), null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Cut) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Cut) ), new ( Strings.ctxPaste, @@ -1268,7 +1268,7 @@ private MenuBarItem BuildContextMenuBarItem () () => Paste (), null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Paste) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Paste) ), new ( Strings.ctxUndo, @@ -1276,7 +1276,7 @@ private MenuBarItem BuildContextMenuBarItem () () => Undo (), null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Undo) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Undo) ), new ( Strings.ctxRedo, @@ -1284,13 +1284,13 @@ private MenuBarItem BuildContextMenuBarItem () () => Redo (), null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Redo) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Redo) ) } ); } - private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.ReplaceKey (e.OldKey.KeyCode, e.NewKey.KeyCode); } + private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey.KeyCode, e.NewKey.KeyCode); } private List DeleteSelectedText () { diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 7773f882d3..0dbd1f960f 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -4163,7 +4163,7 @@ private void Adjust () SelectAll, null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.SelectAll) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.SelectAll) ), new ( Strings.ctxDeleteAll, @@ -4171,7 +4171,7 @@ private void Adjust () DeleteAll, null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.DeleteAll) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.DeleteAll) ), new ( Strings.ctxCopy, @@ -4179,7 +4179,7 @@ private void Adjust () Copy, null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Copy) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Copy) ), new ( Strings.ctxCut, @@ -4187,7 +4187,7 @@ private void Adjust () Cut, null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Cut) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Cut) ), new ( Strings.ctxPaste, @@ -4195,7 +4195,7 @@ private void Adjust () Paste, null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Paste) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Paste) ), new ( Strings.ctxUndo, @@ -4203,7 +4203,7 @@ private void Adjust () Undo, null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Undo) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Undo) ), new ( Strings.ctxRedo, @@ -4211,7 +4211,7 @@ private void Adjust () Redo, null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Redo) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Redo) ), new ( Strings.ctxColors, @@ -4219,7 +4219,7 @@ private void Adjust () () => PromptForColors (), null, null, - (KeyCode)KeyBindings.GetKeyFromCommands (Command.Open) + (KeyCode)KeyBindings.GetFirstFromCommands (Command.Open) ) } ); @@ -4333,7 +4333,7 @@ private void ClearSelectedRegion () DoNeededAction (); } - private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.ReplaceKey (e.OldKey, e.NewKey); } + private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey, e.NewKey); } private bool DeleteTextBackwards () { diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index 74de1047b1..4ff0a3c89f 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -375,7 +375,7 @@ public KeyCode ObjectActivationKey { if (objectActivationKey != value) { - KeyBindings.ReplaceKey (ObjectActivationKey, value); + KeyBindings.Replace (ObjectActivationKey, value); objectActivationKey = value; SetNeedsDraw (); } diff --git a/UICatalog/Scenarios/KeyBindings.cs b/UICatalog/Scenarios/KeyBindings.cs index 54862b8c76..2e6165c27b 100644 --- a/UICatalog/Scenarios/KeyBindings.cs +++ b/UICatalog/Scenarios/KeyBindings.cs @@ -80,7 +80,7 @@ Pressing Esc or {Application.QuitKey} will cause it to quit the app. }; appWindow.Add (appBindingsListView); - foreach (var key in Application.KeyBindings.GetBoundKeys ()) + foreach (Key key in Application.KeyBindings.GetBindings().ToDictionary().Keys) { var binding = Application.KeyBindings.Get (key); appBindings.Add ($"{key} -> {binding.Target?.GetType ().Name} - {binding.Commands [0]}"); diff --git a/UICatalog/Scenarios/Text.cs b/UICatalog/Scenarios/Text.cs index 5e60530a04..eafae77eb6 100644 --- a/UICatalog/Scenarios/Text.cs +++ b/UICatalog/Scenarios/Text.cs @@ -148,8 +148,8 @@ void TextView_DrawContent (object sender, DrawEventArgs e) } }; - Key keyTab = textView.KeyBindings.GetKeyFromCommands (Command.Tab); - Key keyBackTab = textView.KeyBindings.GetKeyFromCommands (Command.BackTab); + Key keyTab = textView.KeyBindings.GetFirstFromCommands (Command.Tab); + Key keyBackTab = textView.KeyBindings.GetFirstFromCommands (Command.BackTab); chxCaptureTabs.CheckedStateChanging += (s, e) => { diff --git a/UnitTests/Input/Keyboard/KeyBindingsTests.cs b/UnitTests/Input/Keyboard/KeyBindingsTests.cs index 70be3dc11e..05147e6152 100644 --- a/UnitTests/Input/Keyboard/KeyBindingsTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -27,13 +27,13 @@ public void Add_Adds () Assert.Contains (Command.Left, resultCommands); } - //[Fact] - //public void Add_Invalid_Key_Throws () - //{ - // var keyBindings = new KeyBindings (new View ()); - // List commands = new (); - // Assert.Throws (() => keyBindings.Add (Key.Empty, Command.Accept)); - //} + [Fact] + public void Add_Invalid_Key_Throws () + { + var keyBindings = new KeyBindings (new View ()); + List commands = new (); + Assert.Throws (() => keyBindings.Add (Key.Empty, Command.Accept)); + } [Fact] public void Add_Multiple_Commands_Adds () @@ -132,7 +132,7 @@ public void Defaults () { var keyBindings = new KeyBindings (new ()); Assert.Empty (keyBindings.GetBindings ()); - Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept)); + Assert.Null (keyBindings.GetFirstFromCommands (Command.Accept)); Assert.NotNull (keyBindings.Target); } @@ -198,10 +198,10 @@ public void GetKeyFromCommands_MultipleCommands () Command [] commands2 = { Command.Up, Command.Down }; keyBindings.Add (Key.B, commands2); - Key key = keyBindings.GetKeyFromCommands (commands1); + Key key = keyBindings.GetFirstFromCommands (commands1); Assert.Equal (Key.A, key); - key = keyBindings.GetKeyFromCommands (commands2); + key = keyBindings.GetFirstFromCommands (commands2); Assert.Equal (Key.B, key); } @@ -211,7 +211,7 @@ public void GetKeyFromCommands_OneCommand () var keyBindings = new KeyBindings (new ()); keyBindings.Add (Key.A, Command.Right); - Key key = keyBindings.GetKeyFromCommands (Command.Right); + Key key = keyBindings.GetFirstFromCommands (Command.Right); Assert.Equal (Key.A, key); } @@ -220,7 +220,7 @@ public void GetKeyFromCommands_OneCommand () public void GetKeyFromCommands_Unknown_Returns_Key_Empty () { var keyBindings = new KeyBindings (new ()); - Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept)); + Assert.Null (keyBindings.GetFirstFromCommands (Command.Accept)); } [Fact] @@ -228,7 +228,7 @@ public void GetKeyFromCommands_WithCommands_ReturnsKey () { var keyBindings = new KeyBindings (new ()); keyBindings.Add (Key.A, Command.HotKey); - Key resultKey = keyBindings.GetKeyFromCommands (Command.HotKey); + Key resultKey = keyBindings.GetFirstFromCommands (Command.HotKey); Assert.Equal (Key.A, resultKey); } @@ -241,19 +241,19 @@ public void ReplaceKey_Replaces () keyBindings.Add (Key.C, Command.HotKey); keyBindings.Add (Key.D, Command.HotKey); - keyBindings.ReplaceKey (Key.A, Key.E); + keyBindings.Replace (Key.A, Key.E); Assert.Empty (keyBindings.GetCommands (Key.A)); Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.E)); - keyBindings.ReplaceKey (Key.B, Key.F); + keyBindings.Replace (Key.B, Key.F); Assert.Empty (keyBindings.GetCommands (Key.B)); Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.F)); - keyBindings.ReplaceKey (Key.C, Key.G); + keyBindings.Replace (Key.C, Key.G); Assert.Empty (keyBindings.GetCommands (Key.C)); Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.G)); - keyBindings.ReplaceKey (Key.D, Key.H); + keyBindings.Replace (Key.D, Key.H); Assert.Empty (keyBindings.GetCommands (Key.D)); Assert.Contains (Command.HotKey, keyBindings.GetCommands (Key.H)); } @@ -265,7 +265,7 @@ public void ReplaceKey_Replaces_Leaves_Old_Binding () keyBindings.Add (Key.A, Command.Accept); keyBindings.Add (Key.B, Command.HotKey); - keyBindings.ReplaceKey (keyBindings.GetKeyFromCommands (Command.Accept), Key.C); + keyBindings.Replace (keyBindings.GetFirstFromCommands (Command.Accept), Key.C); Assert.Empty (keyBindings.GetCommands (Key.A)); Assert.Contains (Command.Accept, keyBindings.GetCommands (Key.C)); } @@ -274,7 +274,7 @@ public void ReplaceKey_Replaces_Leaves_Old_Binding () public void ReplaceKey_Adds_If_DoesNotContain_Old () { var keyBindings = new KeyBindings (new ()); - keyBindings.ReplaceKey (Key.A, Key.B); + keyBindings.Replace (Key.A, Key.B); Assert.True (keyBindings.TryGet (Key.B, out _)); } @@ -283,7 +283,7 @@ public void ReplaceKey_Throws_If_New_Is_Empty () { var keyBindings = new KeyBindings (new ()); keyBindings.Add (Key.A, Command.HotKey); - Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.Empty)); + Assert.Throws (() => keyBindings.Replace (Key.A, Key.Empty)); } [Fact] diff --git a/UnitTests/Input/Mouse/MouseBindingsTests.cs b/UnitTests/Input/Mouse/MouseBindingsTests.cs index d8efd8f6a9..3cc86d259f 100644 --- a/UnitTests/Input/Mouse/MouseBindingsTests.cs +++ b/UnitTests/Input/Mouse/MouseBindingsTests.cs @@ -23,13 +23,13 @@ public void Add_Adds () Assert.Contains (Command.Left, resultCommands); } - //[Fact] - //public void Add_Invalid_Flag_Throws () - //{ - // var mouseBindings = new MouseBindings (); - // List commands = new (); - // Assert.Throws (() => mouseBindings.Add (MouseFlags.None, Command.Accept)); - //} + [Fact] + public void Add_Invalid_Flag_Throws () + { + var mouseBindings = new MouseBindings (); + List commands = new (); + Assert.Throws (() => mouseBindings.Add (MouseFlags.None, Command.Accept)); + } [Fact] public void Add_Multiple_Commands_Adds () @@ -131,7 +131,7 @@ public void Defaults () { var mouseBindings = new MouseBindings (); Assert.Empty (mouseBindings.GetBindings ()); - Assert.Equal (MouseFlags.None, mouseBindings.GetMouseFlagsFromCommands (Command.Accept)); + Assert.Equal (MouseFlags.None, mouseBindings.GetFirstFromCommands (Command.Accept)); } [Fact] @@ -196,10 +196,10 @@ public void GetMouseFlagsFromCommands_MultipleCommands () Command [] commands2 = { Command.Up, Command.Down }; mouseBindings.Add (MouseFlags.Button2Clicked, commands2); - MouseFlags mouseFlags = mouseBindings.GetMouseFlagsFromCommands (commands1); + MouseFlags mouseFlags = mouseBindings.GetFirstFromCommands (commands1); Assert.Equal (MouseFlags.Button1Clicked, mouseFlags); - mouseFlags = mouseBindings.GetMouseFlagsFromCommands (commands2); + mouseFlags = mouseBindings.GetFirstFromCommands (commands2); Assert.Equal (MouseFlags.Button2Clicked, mouseFlags); } @@ -209,7 +209,7 @@ public void GetMouseFlagsFromCommands_OneCommand () var mouseBindings = new MouseBindings (); mouseBindings.Add (MouseFlags.Button1Clicked, Command.Right); - MouseFlags mouseFlags = mouseBindings.GetMouseFlagsFromCommands (Command.Right); + MouseFlags mouseFlags = mouseBindings.GetFirstFromCommands (Command.Right); Assert.Equal (MouseFlags.Button1Clicked, mouseFlags); } @@ -218,7 +218,7 @@ public void GetMouseFlagsFromCommands_OneCommand () public void GetMouseFlagsFromCommands_Unknown_Returns_Key_Empty () { var mouseBindings = new MouseBindings (); - Assert.Equal (MouseFlags.None, mouseBindings.GetMouseFlagsFromCommands (Command.Accept)); + Assert.Equal (MouseFlags.None, mouseBindings.GetFirstFromCommands (Command.Accept)); } [Fact] @@ -226,7 +226,7 @@ public void GetMouseFlagsFromCommands_WithCommands_ReturnsKey () { var mouseBindings = new MouseBindings (); mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); - MouseFlags mouseFlags = mouseBindings.GetMouseFlagsFromCommands (Command.HotKey); + MouseFlags mouseFlags = mouseBindings.GetFirstFromCommands (Command.HotKey); Assert.Equal (MouseFlags.Button1Clicked, mouseFlags); } @@ -239,19 +239,19 @@ public void ReplaceMouseFlags_Replaces () mouseBindings.Add (MouseFlags.Button3Clicked, Command.HotKey); mouseBindings.Add (MouseFlags.Button4Clicked, Command.HotKey); - mouseBindings.ReplaceMouseFlag (MouseFlags.Button1Clicked, MouseFlags.Button1DoubleClicked); + mouseBindings.Replace (MouseFlags.Button1Clicked, MouseFlags.Button1DoubleClicked); Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button1Clicked)); Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button1DoubleClicked)); - mouseBindings.ReplaceMouseFlag (MouseFlags.Button2Clicked, MouseFlags.Button2DoubleClicked); + mouseBindings.Replace (MouseFlags.Button2Clicked, MouseFlags.Button2DoubleClicked); Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button2Clicked)); Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button2DoubleClicked)); - mouseBindings.ReplaceMouseFlag (MouseFlags.Button3Clicked, MouseFlags.Button3DoubleClicked); + mouseBindings.Replace (MouseFlags.Button3Clicked, MouseFlags.Button3DoubleClicked); Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button3Clicked)); Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button3DoubleClicked)); - mouseBindings.ReplaceMouseFlag (MouseFlags.Button4Clicked, MouseFlags.Button4DoubleClicked); + mouseBindings.Replace (MouseFlags.Button4Clicked, MouseFlags.Button4DoubleClicked); Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button4Clicked)); Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button4DoubleClicked)); } @@ -263,7 +263,7 @@ public void ReplaceMouseFlags_Replaces_Leaves_Old_Binding () mouseBindings.Add (MouseFlags.Button1Clicked, Command.Accept); mouseBindings.Add (MouseFlags.Button2Clicked, Command.HotKey); - mouseBindings.ReplaceMouseFlag (mouseBindings.GetMouseFlagsFromCommands (Command.Accept), MouseFlags.Button3Clicked); + mouseBindings.Replace (mouseBindings.GetFirstFromCommands (Command.Accept), MouseFlags.Button3Clicked); Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button1Clicked)); Assert.Contains (Command.Accept, mouseBindings.GetCommands (MouseFlags.Button3Clicked)); } @@ -272,7 +272,7 @@ public void ReplaceMouseFlags_Replaces_Leaves_Old_Binding () public void ReplaceMouseFlags_Adds_If_DoesNotContain_Old () { var mouseBindings = new MouseBindings (); - mouseBindings.ReplaceMouseFlag (MouseFlags.Button1Clicked, MouseFlags.Button2Clicked); + mouseBindings.Replace (MouseFlags.Button1Clicked, MouseFlags.Button2Clicked); Assert.True (mouseBindings.TryGet (MouseFlags.Button2Clicked, out _)); } @@ -281,7 +281,7 @@ public void ReplaceMouseFlags_Throws_If_New_Is_None () { var mouseBindings = new MouseBindings (); mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); - Assert.Throws (() => mouseBindings.ReplaceMouseFlag (MouseFlags.Button1Clicked, MouseFlags.None)); + Assert.Throws (() => mouseBindings.Replace (MouseFlags.Button1Clicked, MouseFlags.None)); } [Fact] diff --git a/UnitTests/View/Keyboard/HotKeyTests.cs b/UnitTests/View/Keyboard/HotKeyTests.cs index 1c7a34e85b..1127ca0ff4 100644 --- a/UnitTests/View/Keyboard/HotKeyTests.cs +++ b/UnitTests/View/Keyboard/HotKeyTests.cs @@ -102,7 +102,7 @@ public void Defaults () commands = view.HotKeyBindings.GetCommands (KeyCode.Null); Assert.Empty (commands); - Assert.Empty (view.HotKeyBindings.GetBoundKeys ()); + Assert.Empty (view.HotKeyBindings.GetBindings ()); } [Theory] From 1180b2fe31b51c0edf22ca3797568e889f5c33ed Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 9 Dec 2024 22:18:25 -0800 Subject: [PATCH 36/40] Code cleanup --- Terminal.Gui/Input/InputBindings.cs | 4 +- Terminal.Gui/View/View.Keyboard.cs | 97 +++++------------------------ Terminal.Gui/View/View.Mouse.cs | 54 ++++++++-------- 3 files changed, 46 insertions(+), 109 deletions(-) diff --git a/Terminal.Gui/Input/InputBindings.cs b/Terminal.Gui/Input/InputBindings.cs index ec970a4dfa..3812cdfd49 100644 --- a/Terminal.Gui/Input/InputBindings.cs +++ b/Terminal.Gui/Input/InputBindings.cs @@ -2,7 +2,7 @@ namespace Terminal.Gui; /// -/// Abstract base class for and . +/// Abstract class for and . /// /// The type of the event (e.g. or ). /// The binding type (e.g. ). @@ -136,7 +136,7 @@ public void Clear (params Command [] command) public bool TryGet (TEvent eventArgs, out TBinding? binding) { return _bindings.TryGetValue (eventArgs, out binding); } /// Gets the array of s bound to if it exists. - /// The key to check. + /// The to check. /// /// The array of s if is bound. An empty array /// if not. diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 700729509e..cc21f6203c 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -1,7 +1,4 @@ #nullable enable -using System.Diagnostics; -using System.Reflection.Metadata; - namespace Terminal.Gui; public partial class View // Keyboard APIs @@ -188,7 +185,7 @@ public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, object? { Commands = [Command.HotKey], Key = newKey, - Data = context, + Data = context }; // Add the base and Alt key @@ -263,7 +260,8 @@ private void SetHotKeyFromTitle () /// /// If a more focused subview does not handle the key press, this method raises / /// to allow the - /// view to pre-process the key press. If / is not handled any commands bound to the key will be invoked. + /// view to pre-process the key press. If / is not handled any commands + /// bound to the key will be invoked. /// Then, only if no key bindings are /// handled, / will be raised allowing the view to /// process the key press. @@ -305,6 +303,7 @@ public bool NewKeyDownEvent (Key key) } bool? handled = false; + if (InvokeCommandsBoundToHotKey (key, ref handled)) { return true; @@ -588,17 +587,20 @@ private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Ke // BUGBUG: This will miss any hotkeys in subviews of Adornments. /// - /// Invokes any commands bound to on this view and subviews. + /// Invokes any commands bound to on this view and subviews. /// - /// + /// /// /// - internal bool InvokeCommandsBoundToHotKey (Key key, ref bool? handled) + internal bool InvokeCommandsBoundToHotKey (Key hotKey, ref bool? handled) { - bool? weHandled = InvokeCommandsBoundToHotKey (key); - if (weHandled is true) + // Process this View + if (HotKeyBindings.TryGet (hotKey, out KeyBinding binding)) { - return true; + if (InvokeCommands (binding.Commands, binding) is true) + { + return true; + } } // Now, process any HotKey bindings in the subviews @@ -609,7 +611,7 @@ internal bool InvokeCommandsBoundToHotKey (Key key, ref bool? handled) continue; } - bool recurse = subview.InvokeCommandsBoundToHotKey (key, ref handled); + bool recurse = subview.InvokeCommandsBoundToHotKey (hotKey, ref handled); if (recurse || (handled is { } && (bool)handled)) { @@ -620,38 +622,6 @@ internal bool InvokeCommandsBoundToHotKey (Key key, ref bool? handled) return false; } - // TODO: This is a "prototype" debug check. It may be too annoying vs. useful. - // TODO: A better approach would be to have Application hold a list of bound Hotkeys, similar to - // TODO: how Application holds a list of Application Scoped key bindings and then check that list. - /// - /// Returns true if Key is bound in this view hierarchy. For debugging - /// - /// The key to test. - /// Returns the view the key is bound to. - /// - public bool IsHotKeyBound (Key key, out View? boundView) - { - // recurse through the subviews to find the views that has the key bound - boundView = null; - - foreach (View subview in Subviews) - { - if (subview.HotKeyBindings.TryGet (key, out _)) - { - boundView = subview; - - return true; - } - - if (subview.IsHotKeyBound (key, out boundView)) - { - return true; - } - } - - return false; - } - /// /// Invokes the Commands bound to . /// See for an overview of Terminal.Gui keyboard APIs. @@ -671,27 +641,9 @@ public bool IsHotKeyBound (Key key, out View? boundView) return null; } -#if DEBUG - - //if (Application.KeyBindings.TryGet (key, out KeyBinding b)) - //{ - // Debug.WriteLine ( - // $"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command."); - //} - - // TODO: This is a "prototype" debug check. It may be too annoying vs. useful. - // Scour the bindings up our View hierarchy - // to ensure that the key is not already bound to a different set of commands. - if (SuperView?.IsHotKeyBound (key, out View? previouslyBoundView) ?? false) - { - Debug.WriteLine ($"WARNING: InvokeKeyBindings ({key}) - A subview or peer has bound this Key and will not see it: {previouslyBoundView}."); - } - -#endif - return InvokeCommands (binding.Commands, binding); + return InvokeCommands (binding.Commands, binding); } - /// /// Invokes the Commands bound to . /// See for an overview of Terminal.Gui keyboard APIs. @@ -711,24 +663,7 @@ public bool IsHotKeyBound (Key key, out View? boundView) return null; } -//#if DEBUG - -// //if (Application.KeyBindings.TryGet (key, out KeyBinding b)) -// //{ -// // Debug.WriteLine ( -// // $"WARNING: InvokeKeyBindings ({key}) - An Application scope binding exists for this key. The registered view will not invoke Command."); -// //} - -// // TODO: This is a "prototype" debug check. It may be too annoying vs. useful. -// // Scour the bindings up our View hierarchy -// // to ensure that the key is not already bound to a different set of commands. -// if (SuperView?.IsHotKeyBound (hotKey, out View? previouslyBoundView) ?? false) -// { -// Debug.WriteLine ($"WARNING: InvokeKeyBindings ({hotKey}) - A subview or peer has bound this Key and will not see it: {previouslyBoundView}."); -// } - -//#endif - return InvokeCommands (binding.Commands, binding); + return InvokeCommands (binding.Commands, binding); } #endregion Key Bindings diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index 03cb167c40..41f68a3202 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -1,6 +1,5 @@ #nullable enable using System.ComponentModel; -using System.Diagnostics; namespace Terminal.Gui; @@ -21,7 +20,6 @@ private void SetupMouse () MouseBindings.Add (MouseFlags.Button1Clicked | MouseFlags.ButtonCtrl, Command.Select); } - /// /// Invokes the Commands bound to the MouseFlags specified by . /// See for an overview of Terminal.Gui mouse APIs. @@ -43,7 +41,7 @@ private void SetupMouse () binding.MouseEventArgs = mouseEventArgs; - return InvokeCommands (binding.Commands, binding); + return InvokeCommands (binding.Commands, binding); } #region MouseEnterLeave @@ -52,7 +50,8 @@ private void SetupMouse () private ColorScheme? _savedNonHoverColorScheme; /// - /// INTERNAL Called by when the mouse moves over the View's . + /// INTERNAL Called by when the mouse moves over the View's + /// . /// will /// be raised when the mouse is no longer over the . If another View occludes this View, the /// that View will also receive MouseEnter/Leave events. @@ -167,7 +166,8 @@ private void SetupMouse () public event EventHandler? MouseEnter; /// - /// INTERNAL Called by when the mouse leaves , or is occluded + /// INTERNAL Called by when the mouse leaves , or is + /// occluded /// by another non-SubView. /// /// @@ -245,7 +245,8 @@ protected virtual void OnMouseLeave () { } public bool WantMousePositionReports { get; set; } /// - /// Processes a new . This method is called by when a mouse + /// Processes a new . This method is called by when a + /// mouse /// event occurs. /// /// @@ -260,8 +261,10 @@ protected virtual void OnMouseLeave () { } /// See for more information. /// /// - /// If is , the / event - /// will be raised on any new mouse event where indicates a button is pressed. + /// If is , the / + /// event + /// will be raised on any new mouse event where indicates a button + /// is pressed. /// /// /// @@ -332,7 +335,7 @@ protected virtual void OnMouseLeave () { } /// , if the event was handled, otherwise. public bool RaiseMouseEvent (MouseEventArgs mouseEvent) { - if (OnMouseEvent (mouseEvent) || mouseEvent.Handled == true) + if (OnMouseEvent (mouseEvent) || mouseEvent.Handled) { return true; } @@ -350,10 +353,7 @@ public bool RaiseMouseEvent (MouseEventArgs mouseEvent) /// /// /// , if the event was handled, otherwise. - protected virtual bool OnMouseEvent (MouseEventArgs mouseEvent) - { - return false; - } + protected virtual bool OnMouseEvent (MouseEventArgs mouseEvent) { return false; } /// Raised when a mouse event occurs. /// @@ -368,7 +368,8 @@ protected virtual bool OnMouseEvent (MouseEventArgs mouseEvent) #region Mouse Pressed Events /// - /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically + /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event + /// (typically /// when or are set). /// /// @@ -394,7 +395,8 @@ internal bool WhenGrabbedHandleReleased (MouseEventArgs mouseEvent) } /// - /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically + /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event + /// (typically /// when or are set). /// /// @@ -463,7 +465,8 @@ private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent) /// Called when the mouse is either clicked or double-clicked. /// /// - /// If is , will be invoked on every mouse event where + /// If is , will be invoked on every mouse event + /// where /// the mouse button is pressed. /// /// @@ -507,7 +510,8 @@ protected bool RaiseMouseClickEvent (MouseEventArgs args) /// Called when the mouse is either clicked or double-clicked. /// /// - /// If is , will be called on every mouse event where + /// If is , will be called on every mouse event + /// where /// the mouse button is pressed. /// /// @@ -521,14 +525,16 @@ protected bool RaiseMouseClickEvent (MouseEventArgs args) /// Raised when the mouse is either clicked or double-clicked. /// /// - /// If is , will be raised on every mouse event where + /// If is , will be raised on every mouse event + /// where /// the mouse button is pressed. /// /// public event EventHandler? MouseClick; /// - /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the click event (typically + /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the click event + /// (typically /// when or are set). /// /// @@ -562,10 +568,8 @@ internal bool WhenGrabbedHandleClicked (MouseEventArgs mouseEvent) return false; } - #endregion Mouse Clicked Events - #region Mouse Wheel Events /// Raises the / event. @@ -601,7 +605,8 @@ protected bool RaiseMouseWheelEvent (MouseEventArgs args) } /// - /// Called when a mouse wheel event occurs. Check to see which wheel was moved was clicked. + /// Called when a mouse wheel event occurs. Check to see which wheel was moved was + /// clicked. /// /// /// @@ -828,8 +833,5 @@ internal bool SetPressedHighlight (HighlightStyle newHighlightStyle) return viewsUnderMouse; } - private void DisposeMouse () - { - - } + private void DisposeMouse () { } } From 9563ec973485dced1746f291826d97655a58f328 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 9 Dec 2024 22:29:09 -0800 Subject: [PATCH 37/40] Key -> nullable enable --- Terminal.Gui/Input/Keyboard/Key.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Terminal.Gui/Input/Keyboard/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs index 3fece5c1a8..cf524b6e5b 100644 --- a/Terminal.Gui/Input/Keyboard/Key.cs +++ b/Terminal.Gui/Input/Keyboard/Key.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +#nullable enable using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -70,7 +70,6 @@ namespace Terminal.Gui; /// /// /// -[DefaultValue(KeyCode.Null)] public class Key : EventArgs, IEquatable { /// Constructs a new @@ -213,7 +212,7 @@ public KeyCode KeyCode #if DEBUG if (GetIsKeyCodeAtoZ (value) && (value & KeyCode.Space) != 0) { - throw new ArgumentException ($"Invalid KeyCode: {value} is invalid.", nameof (value)); + throw new ArgumentException (@$"Invalid KeyCode: {value} is invalid.", nameof (value)); } #endif @@ -395,7 +394,7 @@ public static Rune ToRune (KeyCode key) public static implicit operator string (Key key) { return key.ToString (); } /// - public override bool Equals (object obj) + public override bool Equals (object? obj) { if (obj is Key other) { @@ -404,7 +403,7 @@ public override bool Equals (object obj) return false; } - bool IEquatable.Equals (Key other) { return Equals (other); } + bool IEquatable.Equals (Key? other) { return Equals (other); } /// public override int GetHashCode () { return _keyCode.GetHashCode (); } @@ -473,7 +472,7 @@ private static string GetKeyString (KeyCode key) return ((Rune)(uint)key).ToString (); } - string keyName = Enum.GetName (typeof (KeyCode), key); + string? keyName = Enum.GetName (typeof (KeyCode), key); return !string.IsNullOrEmpty (keyName) ? keyName : ((Rune)(uint)key).ToString (); } @@ -576,7 +575,7 @@ private static string TrimEndSeparator (string input, Rune separator) /// The parsed value. /// A boolean value indicating whether parsing was successful. /// - public static bool TryParse (string text, [NotNullWhen (true)] out Key key) + public static bool TryParse (string text, out Key key) { if (string.IsNullOrEmpty (text)) { @@ -601,7 +600,7 @@ public static bool TryParse (string text, [NotNullWhen (true)] out Key key) return true; } - key = null; + key = null!; Rune separator = Separator; From 97d4c0a0d8d0bb0db6a3760531277a5d72caea3c Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 9 Dec 2024 22:52:45 -0800 Subject: [PATCH 38/40] Refactored RadioGroup to just use Commands --- Terminal.Gui/Views/RadioGroup.cs | 343 ++++++++++++---------------- UICatalog/Scenarios/MessageBoxes.cs | 4 +- 2 files changed, 154 insertions(+), 193 deletions(-) diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 623ccc4b83..9a1e8af8e4 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -16,162 +16,22 @@ public RadioGroup () Width = Dim.Auto (DimAutoStyle.Content); Height = Dim.Auto (DimAutoStyle.Content); - // Select (Space key or mouse click) - The default implementation sets focus. RadioGroup does not. - AddCommand ( - Command.Select, - (ctx) => - { - bool cursorChanged = false; - if (SelectedItem == Cursor) - { - cursorChanged = MoveDownRight (); - if (!cursorChanged) - { - cursorChanged = MoveHome (); - } - } - - bool selectedItemChanged = false; - if (SelectedItem != Cursor) - { - selectedItemChanged = ChangeSelectedItem (Cursor); - } - - if (cursorChanged || selectedItemChanged) - { - if (RaiseSelecting (ctx) == true) - { - return true; - } - } + AddCommand (Command.Select, HandleSelectCommand); - return cursorChanged || selectedItemChanged; - }); - - // Accept (Enter key) - Raise Accept event - DO NOT advance state - AddCommand (Command.Accept, RaiseAccepting); + // Accept (Enter key or Doubleclick) - Raise Accept event - DO NOT advance state + AddCommand (Command.Accept, HandleAcceptCommand); // Hotkey - ctx may indicate a radio item hotkey was pressed. Behavior depends on HasFocus // If HasFocus and it's this.HotKey invoke Select command - DO NOT raise Accept // If it's a radio item HotKey select that item and raise Selected event - DO NOT raise Accept // If nothing is selected, select first and raise Selected event - DO NOT raise Accept - AddCommand (Command.HotKey, - ctx => - { - // If the command did not come from a keyboard event, ignore it - if (ctx is not CommandContext keyCommandContext) - { - return false; - } - - var item = keyCommandContext.Binding.Data as int?; - - if (HasFocus) - { - if (keyCommandContext is { Binding : { } } && (item is null || 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); - } - } - - if (item is { } && item < _radioLabels.Count) - { - if (item.Value == SelectedItem) - { - return true; - } - - // If a RadioItem.HotKey is pressed we always set the selected item - never SetFocus - bool selectedItemChanged = ChangeSelectedItem (item.Value); - - if (selectedItemChanged) - { - // Doesn't matter if it's handled - RaiseSelecting (ctx); - return true; - } - - - return false; - } - - if (SelectedItem == -1 && ChangeSelectedItem (0)) - { - if (RaiseSelecting (ctx) == true) - { - return true; - } - return false; - } - - if (RaiseHandlingHotKey () == true) - { - return true; - }; - - // Default Command.Hotkey sets focus - SetFocus (); - - return true; - }); - - AddCommand ( - Command.Up, - () => - { - if (!HasFocus) - { - return false; - } - - return MoveUpLeft (); - } - ); + AddCommand (Command.HotKey, HandleHotKeyCommand); - AddCommand ( - Command.Down, - () => - { - if (!HasFocus) - { - return false; - } - - return MoveDownRight (); - } - ); - - AddCommand ( - Command.Start, - () => - { - if (!HasFocus) - { - return false; - } - - MoveHome (); - - return true; - } - ); - - AddCommand ( - Command.End, - () => - { - if (!HasFocus) - { - return false; - } - - MoveEnd (); - - return true; - } - ); + AddCommand (Command.Up, () => HasFocus && MoveUpLeft ()); + AddCommand (Command.Down, () => HasFocus && MoveDownRight ()); + AddCommand (Command.Start, () => HasFocus && MoveHome ()); + AddCommand (Command.End, () => HasFocus && MoveEnd ()); // ReSharper disable once UseObjectOrCollectionInitializer _orientationHelper = new (this); @@ -181,11 +41,147 @@ public RadioGroup () SetupKeyBindings (); + // By default, single click is already bound to Command.Select + MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept); + SubviewLayout += RadioGroup_LayoutStarted; HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed; + } + + private bool? HandleHotKeyCommand (ICommandContext? ctx) + { + // If the command did not come from a keyboard event, ignore it + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + var item = keyCommandContext.Binding.Data as int?; + + if (HasFocus) + { + if ((item is null || 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); + } + } + + if (item is { } && item < _radioLabels.Count) + { + if (item.Value == SelectedItem) + { + return true; + } + + // If a RadioItem.HotKey is pressed we always set the selected item - never SetFocus + bool selectedItemChanged = ChangeSelectedItem (item.Value); + + if (selectedItemChanged) + { + // Doesn't matter if it's handled + RaiseSelecting (ctx); + + return true; + } + + return false; + } + + if (SelectedItem == -1 && ChangeSelectedItem (0)) + { + if (RaiseSelecting (ctx) == true) + { + return true; + } + + return false; + } + + if (RaiseHandlingHotKey () == true) + { + return true; + } + + ; + + // Default Command.Hotkey sets focus + SetFocus (); + + return true; + } + + private bool? HandleAcceptCommand (ICommandContext? ctx) + { + if (!DoubleClickAccepts + && ctx is CommandContext mouseCommandContext + && mouseCommandContext.Binding.MouseEventArgs!.Flags.HasFlag (MouseFlags.Button1DoubleClicked)) + { + return false; + } + + return RaiseAccepting (ctx); + } + + private bool? HandleSelectCommand (ICommandContext? ctx) + { + if (ctx is CommandContext mouseCommandContext + && mouseCommandContext.Binding.MouseEventArgs!.Flags.HasFlag (MouseFlags.Button1Clicked)) + { + int viewportX = mouseCommandContext.Binding.MouseEventArgs.Position.X; + int viewportY = mouseCommandContext.Binding.MouseEventArgs.Position.Y; + + int pos = Orientation == Orientation.Horizontal ? viewportX : viewportY; + + int rCount = Orientation == Orientation.Horizontal + ? _horizontal!.Last ().pos + _horizontal!.Last ().length + : _radioLabels.Count; + + if (pos < rCount) + { + int c = Orientation == Orientation.Horizontal + ? _horizontal!.FindIndex (x => x.pos <= viewportX && x.pos + x.length - 2 >= viewportX) + : viewportY; + + if (c > -1) + { + // Just like the user pressing the items' hotkey + return InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], target: this, data: c)) == true; + } + } + + return false; + } + + bool cursorChanged = false; + + if (SelectedItem == Cursor) + { + cursorChanged = MoveDownRight (); + + if (!cursorChanged) + { + cursorChanged = MoveHome (); + } + } + + bool selectedItemChanged = false; + + if (SelectedItem != Cursor) + { + selectedItemChanged = ChangeSelectedItem (Cursor); + } + + if (cursorChanged || selectedItemChanged) + { + if (RaiseSelecting (ctx) == true) + { + return true; + } + } - MouseClick += RadioGroup_MouseClick; + return cursorChanged || selectedItemChanged; } // TODO: Fix InvertColorsOnPress - only highlight the selected item @@ -226,48 +222,6 @@ private void SetupKeyBindings () /// public bool DoubleClickAccepts { get; set; } = true; - private void RadioGroup_MouseClick (object? sender, MouseEventArgs e) - { - if (e.Flags.HasFlag (MouseFlags.Button1Clicked)) - { - int viewportX = e.Position.X; - int viewportY = e.Position.Y; - - int pos = Orientation == Orientation.Horizontal ? viewportX : viewportY; - - int rCount = Orientation == Orientation.Horizontal - ? _horizontal!.Last ().pos + _horizontal!.Last ().length - : _radioLabels.Count; - - if (pos < rCount) - { - int c = Orientation == Orientation.Horizontal - ? _horizontal!.FindIndex (x => x.pos <= viewportX && x.pos + x.length - 2 >= viewportX) - : viewportY; - - if (c > -1) - { - // Just like the user pressing the items' hotkey - e.Handled = InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], target: this, data: c)) == true; - } - } - - return; - } - - if (DoubleClickAccepts && e.Flags.HasFlag (MouseFlags.Button1DoubleClicked)) - { - // NOTE: Drivers ALWAYS generate a Button1Clicked event before Button1DoubleClicked - // NOTE: So, we've already selected an item. - - // Just like the user pressing `Enter` - InvokeCommand (Command.Accept); - } - - // HACK: Always eat so Select is not invoked by base - e.Handled = true; - } - private List<(int pos, int length)>? _horizontal; private int _horizontalSpace = 2; @@ -538,7 +492,12 @@ private bool MoveDownRight () return false; } - private void MoveEnd () { Cursor = Math.Max (_radioLabels.Count - 1, 0); } + private bool MoveEnd () + { + Cursor = Math.Max (_radioLabels.Count - 1, 0); + + return true; + } private bool MoveHome () { diff --git a/UICatalog/Scenarios/MessageBoxes.cs b/UICatalog/Scenarios/MessageBoxes.cs index 0e6db7c16c..934a700709 100644 --- a/UICatalog/Scenarios/MessageBoxes.cs +++ b/UICatalog/Scenarios/MessageBoxes.cs @@ -185,7 +185,9 @@ public override void Main () var styleRadioGroup = new RadioGroup { - X = Pos.Right (label) + 1, Y = Pos.Top (label), RadioLabels = new [] { "_Query", "_Error" } + X = Pos.Right (label) + 1, + Y = Pos.Top (label), + RadioLabels = ["_Query", "_Error"], }; frame.Add (styleRadioGroup); From ea74179833217d0a5cc8714b721639e6b6671663 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 9 Dec 2024 22:56:19 -0800 Subject: [PATCH 39/40] Refactored RadioGroup to just use Commands --- Terminal.Gui/Views/RadioGroup.cs | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 9a1e8af8e4..d72bebad79 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -36,8 +36,6 @@ public RadioGroup () // ReSharper disable once UseObjectOrCollectionInitializer _orientationHelper = new (this); _orientationHelper.Orientation = Orientation.Vertical; - _orientationHelper.OrientationChanging += (sender, e) => OrientationChanging?.Invoke (this, e); - _orientationHelper.OrientationChanged += (sender, e) => OrientationChanged?.Invoke (this, e); SetupKeyBindings (); @@ -353,11 +351,7 @@ protected override bool OnDrawingContent () if (j == hotPos && i == Cursor) { - SetAttribute ( - HasFocus - ? ColorScheme!.HotFocus - : GetHotNormalColor () - ); + SetAttribute (HasFocus ? GetHotFocusColor() : GetHotNormalColor ()); } else if (j == hotPos && i != Cursor) { @@ -375,11 +369,7 @@ protected override bool OnDrawingContent () if (i == Cursor) { - SetAttribute ( - HasFocus - ? ColorScheme!.HotFocus - : GetHotNormalColor () - ); + SetAttribute (HasFocus ? GetHotFocusColor() : GetHotNormalColor ()); } else if (i != Cursor) { From 63b3ebf2f43946ea114495706d9b31086358b88a Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 10 Dec 2024 06:51:53 -0800 Subject: [PATCH 40/40] Fixed RadioGroup unit test that was previosly bogus --- UnitTests/Views/RadioGroupTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/UnitTests/Views/RadioGroupTests.cs b/UnitTests/Views/RadioGroupTests.cs index 43f5756278..4276ca71f6 100644 --- a/UnitTests/Views/RadioGroupTests.cs +++ b/UnitTests/Views/RadioGroupTests.cs @@ -668,7 +668,7 @@ public void Mouse_Click () [Fact] [SetupFakeDriver] - public void Mouse_DoubleClick () + public void Mouse_DoubleClick_Accepts () { var radioGroup = new RadioGroup { @@ -705,12 +705,13 @@ public void Mouse_DoubleClick () // NOTE: We need to do the same Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked })); - Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked })); + Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 0), Flags = MouseFlags.Button1DoubleClicked })); Assert.Equal (0, radioGroup.SelectedItem); Assert.Equal (0, selectedItemChanged); Assert.Equal (0, selectingCount); Assert.Equal (1, acceptedCount); + // single click twice Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked })); Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked })); Assert.Equal (1, radioGroup.SelectedItem); @@ -719,7 +720,7 @@ public void Mouse_DoubleClick () Assert.Equal (1, acceptedCount); Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked })); - Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked })); + Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked })); Assert.Equal (1, radioGroup.SelectedItem); Assert.Equal (1, selectedItemChanged); Assert.Equal (1, selectingCount); @@ -766,7 +767,7 @@ public void Mouse_DoubleClick () radioGroup.DoubleClickAccepts = false; Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1Clicked })); - Assert.True (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked })); + Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked })); } #endregion Mouse Tests