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 7205841300..625b5b15f2 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.Bindings.Where (b => b.Key == key.KeyCode)) + // foreach (KeyValuePair binding in KeyBindings.GetBindings (key)) + if (KeyBindings.TryGet (key, out KeyBinding binding)) { - if (binding.Value.BoundView is { }) + if (binding.Target is { }) { - if (!binding.Value.BoundView.Enabled) + if (!binding.Target.Enabled) { return false; } - bool? handled = binding.Value.BoundView?.InvokeCommands (binding.Value.Commands, binding.Key, binding.Value); + bool? handled = binding.Target?.InvokeCommands (binding.Commands, binding); if (handled != null && (bool)handled) { @@ -63,16 +64,17 @@ public static bool RaiseKeyDownEvent (Key key) } else { - if (!KeyBindings.TryGet (key, KeyBindingScope.Application, out KeyBinding appBinding)) + // BUGBUG: this seems unneeded. + if (!KeyBindings.TryGet (key, out KeyBinding keybinding)) { - continue; + 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; @@ -81,18 +83,18 @@ 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)) + 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, key, appBinding); // Create the context here + CommandContext context = new (command, binding); // Create the context here return implementation (context); } @@ -116,7 +118,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 . /// @@ -155,14 +158,14 @@ public static bool RaiseKeyUpEvent (Key key) #region Application-scoped KeyBindings - static Application () { AddApplicationKeyBindings (); } + static Application () { AddKeyBindings (); } /// Gets the Application-scoped key bindings. - public static KeyBindings KeyBindings { get; internal set; } = new (); + public static KeyBindings KeyBindings { get; internal set; } = new (null); - internal static void AddApplicationKeyBindings () + internal static void AddKeyBindings () { - CommandImplementations = new (); + _commandImplementations.Clear (); // Things this view knows how to do AddCommand ( @@ -231,83 +234,40 @@ 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, KeyBindingScope.Application, 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); + // Need to clear after setting the above to ensure actually clear + // because set_QuitKey etc.. may call Add + KeyBindings.Clear (); - KeyBindings.Add (NextTabGroupKey, KeyBindingScope.Application, Command.NextTabGroup); - KeyBindings.Add (PrevTabGroupKey, KeyBindingScope.Application, Command.PreviousTabGroup); + 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 (ArrangeKey, KeyBindingScope.Application, 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, 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); - } - } - - /// - /// Gets the list of Views that have key bindings. - /// - /// - /// 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.Scope != KeyBindingScope.Application) - .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 KeyBinding binding)) - { - KeyBindings.Remove (oldKey); - KeyBindings.Add (newKey, binding); - } - else - { - KeyBindings.Add (newKey, binding); - } + KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend); } } - #endregion Application-scoped KeyBindings /// @@ -321,16 +281,15 @@ 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. /// 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.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/Application/Application.Navigation.cs b/Terminal.Gui/Application/Application.Navigation.cs index a4f9dcc545..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) { - ReplaceKey (_nextTabGroupKey, value); + KeyBindings.Replace (_nextTabGroupKey, value); _nextTabGroupKey = value; } } @@ -37,7 +37,7 @@ public static Key NextTabKey { if (_nextTabKey != value) { - ReplaceKey (_nextTabKey, value); + KeyBindings.Replace (_nextTabKey, value); _nextTabKey = value; } } @@ -66,7 +66,7 @@ public static Key PrevTabGroupKey { if (_prevTabGroupKey != value) { - ReplaceKey (_prevTabGroupKey, value); + KeyBindings.Replace (_prevTabGroupKey, value); _prevTabGroupKey = value; } } @@ -78,10 +78,10 @@ public static Key PrevTabKey { get => _prevTabKey; set - { + { if (_prevTabKey != value) { - 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 64f7960079..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) { - ReplaceKey (_quitKey, value); + KeyBindings.Replace (_quitKey, value); _quitKey = value; } } @@ -37,7 +37,7 @@ public static Key ArrangeKey { if (_arrangeKey != value) { - ReplaceKey (_arrangeKey, value); + KeyBindings.Replace (_arrangeKey, value); _arrangeKey = value; } } 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/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/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/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/Command.cs b/Terminal.Gui/Input/Command.cs index e0af3c2af1..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 mouse events. +/// 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/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs index 095a3976aa..e4fdfba680 100644 --- a/Terminal.Gui/Input/CommandContext.cs +++ b/Terminal.Gui/Input/CommandContext.cs @@ -3,52 +3,28 @@ 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 +public record struct CommandContext : ICommandContext { /// - /// Initializes a new instance of with the specified , + /// Initializes a new instance with the specified , /// /// - /// - /// - /// - public CommandContext (Command command, Key? key, KeyBinding? keyBinding = null, object? data = null) + /// + public CommandContext (Command command, TBinding? binding) { Command = command; - Key = key; - KeyBinding = keyBinding; - Data = data; + Binding = binding; } - /// - /// The that is being invoked. - /// + /// public Command Command { get; set; } /// - /// The that is being invoked. This is the key that was pressed to invoke the . - /// - public Key? Key { get; set; } - - /// - /// The KeyBinding that was used to invoke the , if any. - /// - public KeyBinding? KeyBinding { get; set; } - - /// - /// Arbitrary data. + /// The keyboard or mouse minding that was used to invoke the , if any. /// - public object? Data { get; set; } -} + public TBinding? Binding { get; set; } +} \ No newline at end of file diff --git a/Terminal.Gui/Input/CommandEventArgs.cs b/Terminal.Gui/Input/CommandEventArgs.cs index f12d21be89..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 CommandContext 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 new file mode 100644 index 0000000000..644029ca28 --- /dev/null +++ b/Terminal.Gui/Input/ICommandContext.cs @@ -0,0 +1,18 @@ +#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 +{ + /// + /// The that is being invoked. + /// + public Command Command { get; set; } +} diff --git a/Terminal.Gui/Input/IInputBinding.cs b/Terminal.Gui/Input/IInputBinding.cs new file mode 100644 index 0000000000..2ce2bec8bc --- /dev/null +++ b/Terminal.Gui/Input/IInputBinding.cs @@ -0,0 +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..3812cdfd49 --- /dev/null +++ b/Terminal.Gui/Input/InputBindings.cs @@ -0,0 +1,233 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Abstract 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 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/KeyBindingScope.cs b/Terminal.Gui/Input/KeyBindingScope.cs deleted file mode 100644 index 4c14fecd57..0000000000 --- a/Terminal.Gui/Input/KeyBindingScope.cs +++ /dev/null @@ -1,58 +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, - - /// - /// 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, - - /// - /// 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 -} diff --git a/Terminal.Gui/Input/KeyBindings.cs b/Terminal.Gui/Input/KeyBindings.cs deleted file mode 100644 index 71777d804d..0000000000 --- a/Terminal.Gui/Input/KeyBindings.cs +++ /dev/null @@ -1,447 +0,0 @@ -#nullable enable - -using static System.Formats.Asn1.AsnWriter; - -namespace Terminal.Gui; - -/// -/// Provides a collection of objects bound to a . -/// -/// -/// -/// -public class KeyBindings -{ - /// - /// Initializes a new instance. This constructor is used when the are not bound to a - /// . This is used for Application.KeyBindings and unit tests. - /// - public KeyBindings () { } - - /// Initializes a new instance bound to . - public KeyBindings (View? boundView) { BoundView = boundView; } - - /// Adds a to the collection. - /// - /// - /// Optional View for bindings. - public void Add (Key key, KeyBinding binding, View? boundViewForAppScope = null) - { - if (BoundView is { } && binding.Scope.FastHasFlags (KeyBindingScope.Application)) - { - throw new InvalidOperationException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add"); - } - - if (BoundView is { } && boundViewForAppScope is null) - { - boundViewForAppScope = BoundView; - } - - if (TryGet (key, out KeyBinding _)) - { - throw new InvalidOperationException (@$"A key binding for {key} exists ({binding})."); - - //Bindings [key] = binding; - } - - if (BoundView is { }) - { - 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 - // 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 . - /// - /// 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 . - /// - /// If the key is already bound to a different array of s it will be rebound - /// . - /// - /// - /// - /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch - /// focus to another view and perform multiple commands there). - /// - /// The key to check. - /// The scope for the command. - /// - /// The command to invoked on the when is pressed. When - /// multiple commands are provided,they will be applied in sequence. The bound strike will be - /// consumed if any took effect. - /// - public void Add (Key key, KeyBindingScope scope, params Command [] commands) - { - if (BoundView is 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)); - } - - 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), 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); - } - - /// - /// - /// Adds a new key combination that will trigger the commands in (if supported by the - /// View - see ). - /// - /// - /// This is a helper function for . If used for a View ( - /// is set), the scope will be set to . - /// Otherwise, it will be set to . - /// - /// - /// If the key is already bound to a different array of s it will be rebound - /// . - /// - /// - /// - /// Commands are only ever applied to the current (i.e. this feature cannot be used to switch - /// focus to another view and perform multiple commands there). - /// - /// The key to check. - /// - /// The command to invoked on the when is pressed. When - /// multiple commands are provided,they will be applied in sequence. The bound strike will be - /// consumed if any took effect. - /// - public void Add (Key key, params Command [] commands) - { - if (BoundView is null) - { - throw new ArgumentException (@"Application scoped KeyBindings must provide a boundViewForAppScope to Add."); - } - - Add (key, BoundView is { } ? KeyBindingScope.Focused : KeyBindingScope.Application, null, commands); - } - - // 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 keys that are bound. - /// - /// - public IEnumerable GetBoundKeys () - { - return Bindings.Keys; - } - - /// - /// The view that the are bound to. - /// - /// - /// If the KeyBindings object is being used for Application.KeyBindings. - /// - internal View? BoundView { get; } - - /// Removes all objects from the collection. - public void Clear () { Bindings.Clear (); } - - /// - /// Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to - /// the same command sets and this method will clear all of them. - /// - /// - public void Clear (params Command [] command) - { - KeyValuePair [] kvps = Bindings - .Where (kvp => kvp.Value.Commands.SequenceEqual (command)) - .ToArray (); - - foreach (KeyValuePair kvp in kvps) - { - Remove (kvp.Key); - } - } - - /// Gets the for the specified . - /// - /// - public KeyBinding Get (Key key) - { - if (TryGet (key, out KeyBinding binding)) - { - return binding; - } - - throw new InvalidOperationException ($"Key {key} is not bound."); - } - - /// Gets the for the specified . - /// - /// - /// - public KeyBinding Get (Key key, KeyBindingScope scope) - { - if (TryGet (key, scope, out KeyBinding binding)) - { - return binding; - } - - throw new InvalidOperationException ($"Key {key}/{scope} is not bound."); - } - - /// Gets the array of s bound to if it exists. - /// The key to check. - /// - /// The array of s if is bound. An empty array - /// if not. - /// - public Command [] GetCommands (Key key) - { - if (TryGet (key, out KeyBinding bindings)) - { - return bindings.Commands; - } - - return Array.Empty (); - } - - /// Gets the 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. - /// - /// Optional View for bindings. - public void Remove (Key key, View? boundViewForAppScope = null) - { - 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 [] commands) - { - if (TryGet (key, out KeyBinding binding)) - { - binding.Commands = commands; - } - else - { - Add (key, commands); - } - } - - /// 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 KeyBinding _)) - { - throw new InvalidOperationException ($"Key {oldKey} is not bound."); - } - - if (!newKey.IsValid) - { - throw new InvalidOperationException ($"Key {newKey} is is not valid."); - } - - KeyBinding value = Bindings [oldKey]; - Remove (oldKey); - Add (newKey, value); - } - - /// 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) - { - //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); - - if (key.IsValid) - { - return Bindings.TryGetValue (key, out binding); - } - - return false; - } - - /// Gets the commands bound with the specified Key that are scoped to a particular scope. - /// - /// The key to check. - /// the scope to filter on - /// - /// When this method returns, contains the commands bound with the specified Key, if the Key is - /// found; otherwise, null. This parameter is passed uninitialized. - /// - /// if the Key is bound; otherwise . - public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding) - { - 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)) - { - return true; - } - } - else - { - binding = new (Array.Empty (), KeyBindingScope.Disabled, null); - } - - return false; - } -} diff --git a/Terminal.Gui/Input/Key.cs b/Terminal.Gui/Input/Keyboard/Key.cs similarity index 99% rename from Terminal.Gui/Input/Key.cs rename to Terminal.Gui/Input/Keyboard/Key.cs index 84f42ee3fd..cf524b6e5b 100644 --- a/Terminal.Gui/Input/Key.cs +++ b/Terminal.Gui/Input/Keyboard/Key.cs @@ -1,3 +1,4 @@ +#nullable enable using System.Diagnostics.CodeAnalysis; using System.Globalization; @@ -211,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 @@ -393,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) { @@ -402,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 (); } @@ -471,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 (); } @@ -574,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)) { @@ -599,7 +600,7 @@ public static bool TryParse (string text, [NotNullWhen (true)] out Key key) return true; } - key = null; + key = null!; Rune separator = Separator; diff --git a/Terminal.Gui/Input/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs similarity index 53% rename from Terminal.Gui/Input/KeyBinding.cs rename to Terminal.Gui/Input/Keyboard/KeyBinding.cs index 0073a483ae..eb87b33813 100644 --- a/Terminal.Gui/Input/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -6,48 +6,46 @@ namespace Terminal.Gui; /// -/// Provides a collection of objects that are scoped to . +/// Provides a collection of objects stored in . /// /// /// /// -public record struct KeyBinding +public record struct KeyBinding : IInputBinding { /// 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; - 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) + /// The view the key binding is bound to. + /// Arbitrary data that can be associated with this key binding. + public KeyBinding (Command [] commands, View? target, object? data = null) { Commands = commands; - Scope = scope; - BoundView = boundView; - Context = context; + Target = target; + Data = data; } /// 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 . + /// + 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. /// - 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 new file mode 100644 index 0000000000..7b7590729d --- /dev/null +++ b/Terminal.Gui/Input/Keyboard/KeyBindings.cs @@ -0,0 +1,56 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Provides a collection of s bound to s. +/// +/// +/// +/// +public class KeyBindings : InputBindings +{ + /// Initializes a new instance bound to . + 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 + /// 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); + } + + /// + /// The view that the are bound to. + /// + /// + /// If the KeyBindings object is being used for Application.KeyBindings. + /// + public View? Target { get; init; } +} 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..11689719a7 --- /dev/null +++ b/Terminal.Gui/Input/Mouse/MouseBinding.cs @@ -0,0 +1,32 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// Provides a collection of bound to s. +/// +/// +/// +public record struct MouseBinding : IInputBinding +{ + /// Initializes a new instance. + /// The commands this mouse binding will invoke. + /// The mouse flags that trigger this binding. + public MouseBinding (Command [] commands, MouseFlags mouseFlags) + { + Commands = commands; + + MouseEventArgs = new MouseEventArgs() + { + Flags = mouseFlags + }; + } + + /// The commands this 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 new file mode 100644 index 0000000000..afcab50dae --- /dev/null +++ b/Terminal.Gui/Input/Mouse/MouseBindings.cs @@ -0,0 +1,21 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Provides a collection of objects bound to a combination of . +/// +/// +/// +public class MouseBindings : InputBindings +{ + /// + /// Initializes a new instance. + /// + public MouseBindings () : base ( + (commands, flags) => new (commands, flags), + EqualityComparer.Default) + { } + + /// + public override bool IsValid (MouseFlags eventArgs) { return eventArgs != MouseFlags.None; } +} 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 85% rename from Terminal.Gui/Input/MouseFlagsChangedEventArgs.cs rename to Terminal.Gui/Input/Mouse/MouseFlagsChangedEventArgs.cs index b71ad51df0..0af4c996a0 100644 --- a/Terminal.Gui/Input/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 { 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/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/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 1ef0dbb279..6b33782265 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 readonly Dictionary _commandImplementations = new (); + #region Default Implementation /// @@ -30,7 +32,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 +56,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 +67,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 (CommandContext ctx) + protected bool? RaiseAccepting (ICommandContext? ctx) { CommandEventArgs args = new () { Context = ctx }; @@ -93,14 +95,17 @@ 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], null, this)); if (handled == true) { return true; } } - return SuperView?.InvokeCommand (Command.Accept, ctx: new (Command.Accept, null, null, this)) == true; + if (SuperView is { }) + { + return SuperView?.InvokeCommand (Command.Accept, new ([Command.Accept], null, this)) is true; + } } return Accepting is null ? null : args.Cancel; @@ -135,14 +140,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 (CommandContext ctx) + protected bool? RaiseSelecting (ICommandContext? ctx) { CommandEventArgs args = new () { Context = ctx }; @@ -179,13 +184,13 @@ 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 () { - 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. @@ -219,13 +224,13 @@ 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 (CommandContext ctx); + public delegate bool? CommandImplementation (ICommandContext? ctx); /// /// @@ -239,12 +244,12 @@ private void SetupCommands () /// /// /// - /// This version of AddCommand is for commands that require . + /// This version of AddCommand is for commands that require . /// /// /// The command. /// The delegate. - protected void AddCommand (Command command, CommandImplementation impl) { CommandImplementations [command] = impl; } + protected void AddCommand (Command command, CommandImplementation impl) { _commandImplementations [command] = impl; } /// /// @@ -258,45 +263,44 @@ 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 /// /// /// /// 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. /// /// 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, Key? key = null, KeyBinding? keyBinding = null) + public bool? InvokeCommands (Command [] commands, TBindingType binding) { bool? toReturn = null; foreach (Command command in commands) { - 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 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 +315,44 @@ private void SetupCommands () return toReturn; } - /// Invokes the specified command. + /// + /// 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. + /// 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, Key? key = null, KeyBinding? keyBinding = null) + public bool? InvokeCommand (Command command, TBindingType binding) { - if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation)) + if (_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { - var context = new CommandContext (command, key, keyBinding); // Create the context here - return implementation (context); + return implementation (new CommandContext () + { + Command = command, + Binding = binding, + }); } return null; } /// - /// Invokes the specified command. + /// Invokes the specified command without context. /// /// 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. + /// 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, CommandContext ctx) + public bool? InvokeCommand (Command command) { - if (CommandImplementations.TryGetValue (command, out CommandImplementation? implementation)) + if (_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { - return implementation (ctx); + return implementation (null); } return null; diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 3028402a9f..cc21f6203c 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -1,6 +1,4 @@ #nullable enable -using System.Diagnostics; - namespace Terminal.Gui; public partial class View // Keyboard APIs @@ -14,6 +12,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; @@ -152,50 +152,55 @@ 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], + Key = newKey, + 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); } } @@ -255,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. @@ -291,7 +297,14 @@ 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 (InvokeCommandsBoundToHotKey (key, ref handled)) { return true; } @@ -344,10 +357,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; } @@ -373,10 +382,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. /// @@ -393,10 +398,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. /// @@ -499,10 +500,11 @@ 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!; - private Dictionary CommandImplementations { get; } = new (); + /// 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!; /// /// INTERNAL API: Invokes any commands bound to on this view, adornments, and subviews. @@ -516,17 +518,15 @@ 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) { - 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 = InvokeCommands (key, scope); + bool? handled = DoInvokeCommands (key); if (handled is true) { @@ -535,22 +535,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, ref handled)) { return true; } - if (Border is { } && InvokeCommandsBoundToKeyOnAdornment (Border, key, scope, ref handled)) + if (Padding is { } && InvokeCommandsBoundToKeyOnAdornment (Padding, key, ref handled)) { return true; } - if (InvokeCommandsBoundToKeyOnSubviews (key, scope, ref handled)) + if (Border is { } && InvokeCommandsBoundToKeyOnAdornment (Border, key, ref handled)) { return true; } @@ -558,9 +553,9 @@ 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); + bool? adornmentHandled = adornment.InvokeCommands (key); if (adornmentHandled is true) { @@ -574,7 +569,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 { }) { @@ -590,76 +585,35 @@ private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Ke return false; } - private bool InvokeCommandsBoundToKeyOnSubviews (Key key, KeyBindingScope scope, 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 hotKey, ref bool? handled) { - // Now, process any key bindings in the subviews that are tagged to KeyBindingScope.HotKey. - foreach (View subview in Subviews) + // Process this View + if (HotKeyBindings.TryGet (hotKey, out KeyBinding binding)) { - if (subview == Focused) - { - 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); - - if (recurse || (handled is { } && (bool)handled)) + if (InvokeCommands (binding.Commands, binding) is true) { return true; } } - 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; - + // Now, process any HotKey bindings in the subviews foreach (View subview in Subviews) { - if (subview.KeyBindings.TryGet (key, KeyBindingScope.HotKey, out _)) + if (subview == Focused) { - boundView = subview; - - return true; + continue; } - if (subview.IsHotKeyBound (key, out boundView)) + bool recurse = subview.InvokeCommandsBoundToHotKey (hotKey, ref handled); + + if (recurse || (handled is { } && (bool)handled)) { return true; } @@ -673,7 +627,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 @@ -681,31 +634,36 @@ 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? DoInvokeCommands (Key key) { - if (!KeyBindings.TryGet (key, scope, out KeyBinding binding)) + 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."); - //} + return InvokeCommands (binding.Commands, binding); + } - // 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) + /// + /// Invokes the Commands bound to . + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// 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 + /// should continue. + /// if at least one command was invoked and handled (or cancelled); input processing should + /// stop. + /// + protected bool? InvokeCommandsBoundToHotKey (Key hotKey) + { + if (!HotKeyBindings.TryGet (hotKey, out KeyBinding binding)) { - Debug.WriteLine ($"WARNING: InvokeKeyBindings ({key}) - A subview or peer has bound this Key and will not see it: {previouslyBoundView}."); + return null; } -#endif - return InvokeCommands (binding.Commands, key, 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 4dbb65e38c..41f68a3202 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -5,13 +5,53 @@ 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 (); + + // 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); + } + + /// + /// 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; 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. @@ -126,7 +166,8 @@ public partial class View // Mouse APIs 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. /// /// @@ -204,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. /// /// @@ -219,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. /// /// /// @@ -269,14 +313,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; } @@ -287,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; } @@ -305,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. /// @@ -320,6 +365,98 @@ 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. @@ -328,7 +465,8 @@ protected virtual bool OnMouseEvent (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. /// /// @@ -358,9 +496,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, ctx: new (Command.Select, key: null, data: args)) == true; + args.Handled = InvokeCommandsBoundToMouse (args) == true; return args.Handled; } @@ -373,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. /// /// @@ -387,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). /// /// @@ -428,93 +568,58 @@ 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). - /// + #endregion Mouse Clicked Events + + #region Mouse Wheel Events + + /// Raises the / event. /// - /// Marked internal just to support unit tests /// - /// /// , if the event was handled, otherwise. - internal bool WhenGrabbedHandleReleased (MouseEventArgs mouseEvent) + protected bool RaiseMouseWheelEvent (MouseEventArgs args) { - mouseEvent.Handled = false; + // Pre-conditions + if (!Enabled) + { + // QUESTION: Is this right? Should a disabled view eat mouse? + return args.Handled = false; + } - if (mouseEvent.IsReleased) + // Cancellable event + + if (OnMouseWheel (args) || args.Handled) { - if (Application.MouseGrabView == this) - { - SetPressedHighlight (HighlightStyle.None); - } + return args.Handled; + } - return mouseEvent.Handled = true; + MouseWheel?.Invoke (this, args); + + if (args.Handled) + { + return true; } - return false; + args.Handled = InvokeCommandsBoundToMouse (args) == true; + + return args.Handled; } /// - /// INTERNAL For cases where the view is grabbed and the mouse is clicked, this method handles the released event (typically - /// when or are set). + /// Called when a mouse wheel event occurs. Check to see which wheel was moved was + /// clicked. /// /// - /// - /// Marked internal just to support unit tests - /// /// - /// + /// /// , if the event was handled, otherwise. - private bool WhenGrabbedHandlePressed (MouseEventArgs mouseEvent) - { - mouseEvent.Handled = false; + protected virtual bool OnMouseWheel (MouseEventArgs args) { return 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; - } + /// Raised when a mouse wheel event occurs. + /// + /// + public event EventHandler? MouseWheel; - #endregion Mouse Click Events + #endregion Mouse Wheel Events #region Highlight Handling @@ -541,6 +646,13 @@ private bool RaiseHighlight (CancelEventArgs args) Highlight?.Invoke (this, args); + //if (args.Cancel) + //{ + // return true; + //} + + //args.Cancel = InvokeCommandsBoundToMouse (args) == true; + return args.Cancel; } @@ -666,15 +778,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; } @@ -720,4 +832,6 @@ 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/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index eaf2d3956e..46a1314e3e 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -70,16 +70,16 @@ public Button () HighlightStyle = DefaultHighlightStyle; } - private bool? HandleHotKeyCommand (CommandContext ctx) + private bool? HandleHotKeyCommand (ICommandContext commandContext) { 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) { @@ -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 (Command.HotKey, null, data: this)) == 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/UICatalog/Scenarios/CharacterMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs similarity index 72% rename from UICatalog/Scenarios/CharacterMap/CharMap.cs rename to Terminal.Gui/Views/CharMap/CharMap.cs index 8b7d283ad9..b49c8c4e6f 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. @@ -23,106 +20,36 @@ public class CharMap : View, IDesignable private ContextMenu _contextMenu = new (); + /// /// Initializes a new instance. /// + [RequiresUnreferencedCode ("AOT")] + [RequiresDynamicCode ("AOT")] public CharMap () { base.ColorScheme = Colors.ColorSchemes ["Dialog"]; CanFocus = true; CursorVisibility = CursorVisibility.Default; - AddCommand ( - Command.Up, - () => - { - SelectedCodePoint -= 16; + 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.Down, - () => - { - SelectedCodePoint += 16; + AddCommand (Command.ScrollDown, () => ScrollVertical (1)); + AddCommand (Command.ScrollUp, () => ScrollVertical (-1)); + AddCommand (Command.ScrollRight, () => ScrollHorizontal (1)); + AddCommand (Command.ScrollLeft, () => ScrollHorizontal (-1)); - return true; - } - ); - - AddCommand ( - Command.Left, - () => - { - SelectedCodePoint--; - - return true; - } - ); - - AddCommand ( - Command.Right, - () => - { - SelectedCodePoint++; - - 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 +59,15 @@ 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.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); + MouseBindings.Add (MouseFlags.WheeledRight, Command.ScrollRight); SetContentSize (new (COLUMN_WIDTH * 16 + RowLabelWidth, MAX_CODE_POINT / 16 * _rowHeight + HEADER_HEIGHT)); @@ -169,6 +103,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 +149,7 @@ private Point GetCursor (int codePoint) return new (x, y); } + /// public override Point? PositionCursor () { Point cursor = GetCursor (SelectedCodePoint); @@ -297,6 +244,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 +413,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) + { + position = position with { Y = GetCursor (SelectedCodePoint).Y }; + } + + if (position.X < RowLabelWidth || position.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1) { - ScrollVertical (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,76 +505,83 @@ 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 () { + if (!Application.Initialized) + { + // Some unit tests invoke Accept without Init + return; + } UcdApiClient? client = new (); var decResponse = string.Empty; var getCodePointError = string.Empty; 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 +608,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 +643,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 +714,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 +738,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/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index 04156637e1..6b7c7ad190 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -24,17 +24,26 @@ 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); + MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept); + TitleChanged += Checkbox_TitleChanged; HighlightStyle = DefaultHighlightStyle; } - private bool? AdvanceAndSelect (CommandContext ctx) + private bool? AdvanceAndSelect (ICommandContext? commandContext) { bool? cancelled = AdvanceCheckState (); @@ -43,12 +52,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 39a02f351e..1c6808bf79 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,40 +43,39 @@ 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; } } /// Moves the selected item index to the next row. /// - private bool MoveDown (CommandContext ctx) + private bool MoveDown (ICommandContext? commandContext) { - if (RaiseSelecting (ctx) == true) + if (RaiseSelecting (commandContext) == true) { return true; } - if (Cursor.Y < _rows - 1) + if (Cursor.Y < ROWS - 1) { - SelectedColor += _cols; + SelectedColor += COLS; } return true; @@ -83,9 +83,9 @@ 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 (RaiseSelecting (ctx) == true) + if (RaiseSelecting (commandContext) == true) { return true; } @@ -100,13 +100,13 @@ 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 (RaiseSelecting (ctx) == true) + if (RaiseSelecting (commandContext) == true) { return true; } - if (Cursor.X < _cols - 1) + if (Cursor.X < COLS - 1) { SelectedColor++; } @@ -116,15 +116,15 @@ 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 (RaiseSelecting (ctx) == true) + if (RaiseSelecting (commandContext) == true) { return true; } if (Cursor.Y > 0) { - SelectedColor -= _cols; + SelectedColor -= COLS; } return true; @@ -133,14 +133,14 @@ private bool MoveUp (CommandContext ctx) /// 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++) { 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) { @@ -188,10 +188,11 @@ private void AddCommands () AddCommand (Command.Select, (ctx) => { - bool set = false; - if (ctx.Data is MouseEventArgs me) + var set = false; + + if (ctx is CommandContext { Binding.MouseEventArgs: { } } mouseCommandContext) { - Cursor = new (me.Position.X / _boxWidth, me.Position.Y / _boxHeight); + Cursor = new (mouseCommandContext.Binding.MouseEventArgs.Position.X / _boxWidth, mouseCommandContext.Binding.MouseEventArgs.Position.Y / _boxHeight); set = true; } return RaiseAccepting (ctx) == true || set; @@ -209,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++; } } @@ -280,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)); } } 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/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index abfee0fde5..7b30e09cfb 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; } @@ -395,7 +399,7 @@ public void SetSource (ObservableCollection source) } } - private bool ActivateSelected (CommandContext ctx) + private bool ActivateSelected (ICommandContext commandContext) { if (HasItems ()) { @@ -404,7 +408,7 @@ private bool ActivateSelected (CommandContext ctx) return false; } - return RaiseAccepting (ctx) == true; + return RaiseAccepting (commandContext) == true; } return false; diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs index de2f862150..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, ctx: new (Command.HotKey, key: null, data: this)) == true; + e.Handled = InvokeCommand (Command.HotKey, new ([Command.HotKey], this, this)) == true; } 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; } /// diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 07eca7983c..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, ctx: new (Command.HotKey, key: null, data: this)) == true; + e.Handled = InvokeCommand (Command.HotKey, new ([Command.HotKey], this, data: this)) == true; } } @@ -60,7 +60,7 @@ public override Rune HotKeySpecifier set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value; } - private bool? InvokeHotKeyOnNext (CommandContext context) + private bool? InvokeHotKeyOnNext (ICommandContext commandContext) { if (RaiseHandlingHotKey () == true) { @@ -78,7 +78,8 @@ 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) == true; } return false; diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 8b19c91475..772a9775cc 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -141,7 +141,15 @@ public ListView () return !SetFocus (); }); - AddCommand (Command.SelectAll, (ctx) => MarkAll ((bool)ctx.KeyBinding?.Context!)); + AddCommand (Command.SelectAll, (ctx) => + { + if (ctx is not CommandContext keyCommandContext) + { + return false; + } + + return keyCommandContext.Binding.Data is { } && MarkAll ((bool)keyCommandContext.Binding.Data); + }); // Default keybindings for all ListViews KeyBindings.Add (Key.CursorUp, Command.Up); @@ -163,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)); } /// @@ -805,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/Menu/ContextMenu.cs b/Terminal.Gui/Views/Menu/ContextMenu.cs index a963036dcc..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!); } } } @@ -250,6 +250,7 @@ public void Show (MenuBarItem? menuItems) _menuBar._isContextMenuLoading = true; _menuBar.MenuAllClosed += MenuBar_MenuAllClosed; + _menuBar.BeginInit (); _menuBar.EndInit (); IsShow = true; diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index ac3d617977..369bf88189 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; @@ -93,10 +130,11 @@ 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!); - menuItem._menuBar.KeyBindings.Add (menuItem.ShortcutKey!, keyBinding); + menuItem._menuBar.HotKeyBindings.Remove (menuItem.ShortcutKey!); + menuItem._menuBar.HotKeyBindings.Add (menuItem.ShortcutKey!, keyBinding); } } } @@ -155,196 +193,302 @@ _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.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)!)); + 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 + bool? handled = null; + return _host.InvokeCommandsBoundToHotKey (keyEvent, ref handled) == 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]; - - if (item is null) - { - return true; - } + disabled = true; + } - // ReSharper disable once ConditionIsAlwaysTrueOrFalse - bool disabled = item is null || !item.IsEnabled (); + if (disabled) + { + return me.Handled = true; + } - if (!disabled && (_host.UseSubMenusSingleFrame || !CheckSubMenu ())) - { - SetNeedsDraw (); - SetParentSetNeedsDisplay (); + _currentChild = me.Position.Y; + RunSelected (); - return true; - } + return me.Handled = true; + } - if (!disabled) - { - _host.OnMenuOpened (); - } + 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; + } + + { + 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], this, data: menuItem); + + if (menuItem.HotKey != Key.Empty) + { + HotKeyBindings.Remove (menuItem.HotKey!); + HotKeyBindings.Add (menuItem.HotKey!, keyBinding); + HotKeyBindings.Remove (menuItem.HotKey!.WithAlt); + HotKeyBindings.Add (menuItem.HotKey.WithAlt, keyBinding); + } } } @@ -378,279 +522,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 (); - } - - if (index == _currentChild) - { - return GetFocusColor (); - } - - return !item.IsEnabled () ? ColorScheme!.Disabled : GetNormalColor (); + Application.UngrabMouse (); + _host.CloseAllMenus (); } - // By doing this we draw last, over everything else. - private void Top_DrawComplete (object? sender, DrawEventArgs e) + private void Current_TerminalResized (object? sender, SizeChangedEventArgs e) { - if (!Visible) + if (_host.IsMenuOpen) { - return; + _host.CloseAllMenus (); } + } - if (_barItems!.Children is null) + /// Called when a key bound to Command.ToggleExpandCollapse is pressed. This means a hot key was pressed. + /// + private bool ExpandCollapse (MenuItem? menuItem) + { + if (!IsInitialized || !Visible) { - return; + return true; } - 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 (); - - SetAttribute (GetNormalColor ()); - - for (int i = Viewport.Y; i < _barItems!.Children.Length; i++) + for (var c = 0; c < _barItems!.Children!.Length; c++) { - if (i < 0) + if (_barItems.Children [c] == menuItem) { - continue; - } + _currentChild = c; - if (ViewportToScreen (Viewport).Y + i >= Driver.Rows) - { break; } + } - MenuItem? item = _barItems.Children [i]; - - SetAttribute ( - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - item is null ? GetNormalColor () : - i == _currentChild ? GetFocusColor () : GetNormalColor () - ); + if (menuItem is { }) + { + var m = menuItem as MenuBarItem; - 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) + if (m?.Children?.Length > 0) { - Move (0, i); - } + MenuItem? item = _barItems.Children [_currentChild]; - SetAttribute (DetermineColorSchemeFor (item, i)); + if (item is null) + { + return true; + } - 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; - } + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + bool disabled = item is null || !item.IsEnabled (); - 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 +749,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 +808,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 21c1a5b9f4..749ec1a5a2 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.KeyBinding?.Context)); + 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.KeyBinding?.Context as MenuItem)?.Action!); + var res = Run ((keyCommandContext.Binding.Data as MenuItem)?.Action!); CloseAllMenus (); return res; @@ -154,13 +162,13 @@ 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); + 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); @@ -196,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); @@ -1302,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..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); @@ -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..7a222ebca1 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; } } @@ -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], KeyBindingScope.HotKey, this); + 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); } } @@ -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); } } } @@ -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/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index f36e41ace1..cd14dfc4fa 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.KeyBinding?.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 ac2867a3af..d72bebad79 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -16,170 +16,170 @@ 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 => - { - var item = ctx.KeyBinding?.Context as int?; - - if (HasFocus) - { - if (ctx is { KeyBinding: { } } && (ctx.KeyBinding.Value.BoundView != this || HotKey == ctx.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); - } - } - - 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; - } + AddCommand (Command.HotKey, HandleHotKeyCommand); - return MoveUpLeft (); - } - ); + AddCommand (Command.Up, () => HasFocus && MoveUpLeft ()); + AddCommand (Command.Down, () => HasFocus && MoveDownRight ()); + AddCommand (Command.Start, () => HasFocus && MoveHome ()); + AddCommand (Command.End, () => HasFocus && MoveEnd ()); - AddCommand ( - Command.Down, - () => - { - if (!HasFocus) - { - return false; - } + // ReSharper disable once UseObjectOrCollectionInitializer + _orientationHelper = new (this); + _orientationHelper.Orientation = Orientation.Vertical; - return MoveDownRight (); - } - ); + SetupKeyBindings (); - AddCommand ( - Command.Start, - () => - { - if (!HasFocus) - { - return false; - } + // By default, single click is already bound to Command.Select + MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept); - MoveHome (); + SubviewLayout += RadioGroup_LayoutStarted; - return true; - } - ); + HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed; + } - AddCommand ( - Command.End, - () => - { - if (!HasFocus) - { - return false; - } + 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; + } - MoveEnd (); + var item = keyCommandContext.Binding.Data as int?; - return true; - } - ); + 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); + } + } - // 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); + if (item is { } && item < _radioLabels.Count) + { + if (item.Value == SelectedItem) + { + return true; + } - SetupKeyBindings (); + // If a RadioItem.HotKey is pressed we always set the selected item - never SetFocus + bool selectedItemChanged = ChangeSelectedItem (item.Value); - SubviewLayout += RadioGroup_LayoutStarted; + if (selectedItemChanged) + { + // Doesn't matter if it's handled + RaiseSelecting (ctx); - HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed; + 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); + } - MouseClick += RadioGroup_MouseClick; + if (cursorChanged || selectedItemChanged) + { + if (RaiseSelecting (ctx) == true) + { + return true; + } + } + + return cursorChanged || selectedItemChanged; } // TODO: Fix InvertColorsOnPress - only highlight the selected item @@ -220,48 +220,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, null, new KeyBinding ([Command.HotKey], KeyBindingScope.HotKey, boundView: this, context: 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; @@ -393,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) { @@ -415,11 +369,7 @@ protected override bool OnDrawingContent () if (i == Cursor) { - SetAttribute ( - HasFocus - ? ColorScheme!.HotFocus - : GetHotNormalColor () - ); + SetAttribute (HasFocus ? GetHotFocusColor() : GetHotNormalColor ()); } else if (i != Cursor) { @@ -532,7 +482,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/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs index 899ee56e08..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 (Command.Select, null, 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 f10bb423b7..9cb02ca3cf 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. /// @@ -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.GetFirstFromCommands (command)!, commandText, null, helpText) @@ -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 @@ -300,17 +300,19 @@ private void AddCommands () AddCommand (Command.Select, DispatchCommand); } - private bool? DispatchCommand (CommandContext ctx) + private bool? DispatchCommand (ICommandContext? commandContext) { - if (ctx.Data != this) + CommandContext? keyCommandContext = commandContext is CommandContext ? (CommandContext)commandContext : default; + + 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.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; } @@ -320,16 +322,16 @@ 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; + // return false; } if (Action is { }) @@ -342,7 +344,7 @@ private void AddCommands () if (_targetView is { }) { - _targetView.InvokeCommand (Command); + _targetView.InvokeCommand (Command, commandContext); } return cancel; @@ -493,10 +495,11 @@ 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) || + e.Context is CommandContext) { // Forward command to ourselves - InvokeCommand (Command.Select, new (Command.Select, null, null, this)); + InvokeCommand (Command.Select, new ([Command.Select], null, this)); } // BUGBUG: This prevents NumericUpDown on statusbar in HexEditor from working @@ -612,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 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); + HotKeyBindings.Remove (Key); } - _keyBindingScope = value; + _bindKeyToApplication = value; UpdateKeyBindings (Key.Empty); } @@ -700,25 +702,25 @@ 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 { if (oldKey != Key.Empty) { - KeyBindings.Remove (oldKey); + HotKeyBindings.Remove (oldKey); } - KeyBindings.Remove (Key); - KeyBindings.Add (Key, KeyBindingScope | KeyBindingScope.HotKey, Command.HotKey); + HotKeyBindings.Remove (Key); + HotKeyBindings.Add (Key, Command.HotKey); } } } @@ -766,7 +768,7 @@ internal void SetColors (bool highlight = false) if (_nonFocusColorScheme is { }) { base.ColorScheme = _nonFocusColorScheme; - //_nonFocusColorScheme = null; + //_nonFocusColorScheme = null; } else { diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index ff37d513c0..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 (); } @@ -1785,11 +1785,11 @@ internal bool Select () return SetFocusedOption (); } - internal bool Accept (CommandContext ctx) + internal bool Accept (ICommandContext commandContext) { SetFocusedOption (); - return RaiseAccepting (ctx) == true; + return RaiseAccepting (commandContext) == true; } internal bool MovePlus () 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 df2d9ddf41..5bb386ca72 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); } @@ -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 829af98ebf..0dbd1f960f 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 (); } @@ -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 (col.Value, row.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 @@ -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 () { @@ -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 ); @@ -6143,7 +6143,7 @@ private void ProcessPaste () Paste (); } - private bool ProcessEnterKey (CommandContext ctx) + private bool ProcessEnterKey (ICommandContext? commandContext) { ResetColumnTrack (); @@ -6156,7 +6156,7 @@ private bool ProcessEnterKey (CommandContext ctx) { // 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 728a411bdb..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 (); } @@ -462,10 +462,10 @@ 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) { // 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/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/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/Editor.cs b/UICatalog/Scenarios/Editor.cs index e0abc19b82..438d9ab508 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); @@ -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/Editors/EventLog.cs b/UICatalog/Scenarios/Editors/EventLog.cs index e9ce2efbaa..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,28 +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) => - { - Log ($"HandlingHotKey: {args.Context.Command} {args.Context.Data}"); - }; - _viewToLog.Selecting += (s, args) => - { - Log ($"Selecting: {args.Context.Command} {args.Context.Data}"); - }; - _viewToLog.Accepting += (s, args) => - { - Log ($"Accepting: {args.Context.Command} {args.Context.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}"); }; } } } @@ -111,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/KeyBindings.cs b/UICatalog/Scenarios/KeyBindings.cs index 2b5de89ac5..2e6165c27b 100644 --- a/UICatalog/Scenarios/KeyBindings.cs +++ b/UICatalog/Scenarios/KeyBindings.cs @@ -80,10 +80,10 @@ 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.BoundView?.GetType ().Name} - {binding.Commands [0]}"); + appBindings.Add ($"{key} -> {binding.Target?.GetType ().Name} - {binding.Commands [0]}"); } ObservableCollection hotkeyBindings = new (); @@ -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 (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.Where (b => b.Value.Scope == KeyBindingScope.Focused)!) + _focusedBindings.Clear (); + foreach (var binding in focused?.KeyBindings!.GetBindings ()) { _focusedBindings.Add ($"{binding.Key} -> {binding.Value.Commands [0]}"); } @@ -165,28 +165,32 @@ public KeyBindingsDemo () AddCommand (Command.Save, ctx => { - MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); return true; }); AddCommand (Command.New, ctx => { - MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}", buttons: "Ok"); return true; }); AddCommand (Command.HotKey, ctx => { - MessageBox.Query ($"{ctx.KeyBinding?.Scope}", $"Key: {ctx.Key}\nCommand: {ctx.Command}", buttons: "Ok"); + MessageBox.Query ($"{ctx.Command}", $"Ctx: {ctx}\nCommand: {ctx.Command}", buttons: "Ok"); SetFocus (); 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); 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}", $"Key: {keyCommandContext.Binding.Key}\nCommand: {ctx.Command}", buttons: "Ok"); Application.RequestStop (); return true; }); 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); 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/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"), 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/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..e3528e3764 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); @@ -358,7 +355,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 (); @@ -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.True (Application.KeyBindings.TryGet (Key.Q.WithCtrl, out _)); Application.Shutdown (); Locations = ConfigLocations.Default; diff --git a/UnitTests/Application/KeyboardTests.cs b/UnitTests/Application/KeyboardTests.cs index 99ea9df29e..970d6a5b87 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.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 (); @@ -189,6 +189,7 @@ public void KeyBinding_OnKeyDown () keyWasHandled = false; Application.RaiseKeyDownEvent (Key.H); Assert.False (keyWasHandled); + Assert.True (view.HotKeyCommand); keyWasHandled = false; Assert.False (view.HasFocus); @@ -204,7 +205,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 +231,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/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/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 50% rename from UnitTests/Input/KeyBindingTests.cs rename to UnitTests/Input/Keyboard/KeyBindingsTests.cs index edade42391..05147e6152 100644 --- a/UnitTests/Input/KeyBindingTests.cs +++ b/UnitTests/Input/Keyboard/KeyBindingsTests.cs @@ -1,41 +1,52 @@ 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 () + public void Add_Adds () { - var keyBindings = new KeyBindings (new View ()); - List commands = new (); - Assert.Throws (() => keyBindings.Add (Key.Empty, KeyBindingScope.HotKey, Command.Accept)); + 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_BoundView_Null_Non_AppScope_Throws () + public void Add_Invalid_Key_Throws () { - var keyBindings = new KeyBindings (); + 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] - public void Add_Multiple_Adds () + public void Add_Multiple_Commands_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); @@ -44,20 +55,20 @@ 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 ())); } [Fact] - public void Add_Single_Adds () + public void Add_Single_Command_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); } @@ -65,40 +76,39 @@ public void Add_Single_Adds () // Add should not allow duplicates [Fact] - public void Add_With_Bound_View_Throws_If_App_Scope () + public void Add_Throws_If_Exists () { 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 () - { - 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, 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); Assert.Contains (Command.HotKey, resultCommands); @@ -108,8 +118,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); @@ -120,25 +130,25 @@ public void Clear_Clears () [Fact] public void Defaults () { - var keyBindings = new KeyBindings (); - Assert.Empty (keyBindings.Bindings); - Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept)); - Assert.Null (keyBindings.BoundView); + var keyBindings = new KeyBindings (new ()); + Assert.Empty (keyBindings.GetBindings ()); + Assert.Null (keyBindings.GetFirstFromCommands (Command.Accept)); + Assert.NotNull (keyBindings.Target); } [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); } @@ -146,8 +156,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); } @@ -155,10 +165,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); @@ -170,9 +180,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); @@ -181,27 +191,27 @@ 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); + Key key = keyBindings.GetFirstFromCommands (commands1); Assert.Equal (Key.A, key); - key = keyBindings.GetKeyFromCommands (commands2); + key = keyBindings.GetFirstFromCommands (commands2); Assert.Equal (Key.B, key); } [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); + Key key = keyBindings.GetFirstFromCommands (Command.Right); Assert.Equal (Key.A, key); } @@ -209,16 +219,16 @@ public void GetKeyFromCommands_OneCommand () [Fact] public void GetKeyFromCommands_Unknown_Returns_Key_Empty () { - var keyBindings = new KeyBindings (); - Assert.Null (keyBindings.GetKeyFromCommands (Command.Accept)); + var keyBindings = new KeyBindings (new ()); + Assert.Null (keyBindings.GetFirstFromCommands (Command.Accept)); } [Fact] public void GetKeyFromCommands_WithCommands_ReturnsKey () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); - Key resultKey = keyBindings.GetKeyFromCommands (Command.HotKey); + var keyBindings = new KeyBindings (new ()); + keyBindings.Add (Key.A, Command.HotKey); + Key resultKey = keyBindings.GetFirstFromCommands (Command.HotKey); Assert.Equal (Key.A, resultKey); } @@ -226,24 +236,24 @@ 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); + 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)); } @@ -252,120 +262,62 @@ 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); + keyBindings.Replace (keyBindings.GetFirstFromCommands (Command.Accept), Key.C); Assert.Empty (keyBindings.GetCommands (Key.A)); Assert.Contains (Command.Accept, keyBindings.GetCommands (Key.C)); } [Fact] - public void ReplaceKey_Throws_If_DoesNotContain_Old () + public void ReplaceKey_Adds_If_DoesNotContain_Old () { - var keyBindings = new KeyBindings (); - Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.B)); + var keyBindings = new KeyBindings (new ()); + keyBindings.Replace (Key.A, Key.B); + Assert.True (keyBindings.TryGet (Key.B, out _)); } [Fact] public void ReplaceKey_Throws_If_New_Is_Empty () { - var keyBindings = new KeyBindings (); - keyBindings.Add (Key.A, KeyBindingScope.Application, Command.HotKey); - Assert.Throws (() => keyBindings.ReplaceKey (Key.A, Key.Empty)); - } - - // Add with scope does the right things - [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 ()); - Command [] commands = { Command.Right, Command.Left }; - - var key = new Key (Key.A); - keyBindings.Add (Key.A, scope, commands); - KeyBinding binding = keyBindings.Get (key); - Assert.Contains (Command.Right, binding.Commands); - Assert.Contains (Command.Left, binding.Commands); - - binding = keyBindings.Get (key, scope); - 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); + var keyBindings = new KeyBindings (new ()); + keyBindings.Add (Key.A, Command.HotKey); + Assert.Throws (() => keyBindings.Replace (Key.A, Key.Empty)); } - [Theory] - [InlineData (KeyBindingScope.Focused)] - [InlineData (KeyBindingScope.HotKey)] - [InlineData (KeyBindingScope.Application)] - public void Scope_Get_Filters (KeyBindingScope scope) + [Fact] + public void Get_Gets () { - var keyBindings = new KeyBindings (scope.FastHasFlags (KeyBindingScope.Application) ? null : new ()); - Command [] commands = { Command.Right, Command.Left }; + 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); - Assert.Contains (Command.Right, binding.Commands); - Assert.Contains (Command.Left, binding.Commands); - } - - [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 ()); - Command [] commands = { Command.Right, Command.Left }; - - var key = new Key (Key.A); - keyBindings.Add (key, scope, 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); + binding = keyBindings.Get (key); Assert.Contains (Command.Right, binding.Commands); Assert.Contains (Command.Left, binding.Commands); - - // negative test - success = keyBindings.TryGet (key, 0, 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 () { - 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);; - - result = keyBindings.Bindings.TryGetValue (key, out KeyBinding _); - Assert.True (result); + Assert.True (result); ; } [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); } @@ -373,10 +325,24 @@ 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); } + + [Fact] + public void ReplaceCommands_Replaces () + { + var keyBindings = new KeyBindings (new ()); + keyBindings.Add (Key.A, 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/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..3cc86d259f --- /dev/null +++ b/UnitTests/Input/Mouse/MouseBindingsTests.cs @@ -0,0 +1,344 @@ +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)); + } + + [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.GetFirstFromCommands (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.GetFirstFromCommands (commands1); + Assert.Equal (MouseFlags.Button1Clicked, mouseFlags); + + mouseFlags = mouseBindings.GetFirstFromCommands (commands2); + Assert.Equal (MouseFlags.Button2Clicked, mouseFlags); + } + + [Fact] + public void GetMouseFlagsFromCommands_OneCommand () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.Right); + + MouseFlags mouseFlags = mouseBindings.GetFirstFromCommands (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.GetFirstFromCommands (Command.Accept)); + } + + [Fact] + public void GetMouseFlagsFromCommands_WithCommands_ReturnsKey () + { + var mouseBindings = new MouseBindings (); + mouseBindings.Add (MouseFlags.Button1Clicked, Command.HotKey); + MouseFlags mouseFlags = mouseBindings.GetFirstFromCommands (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.Replace (MouseFlags.Button1Clicked, MouseFlags.Button1DoubleClicked); + Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button1Clicked)); + Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button1DoubleClicked)); + + mouseBindings.Replace (MouseFlags.Button2Clicked, MouseFlags.Button2DoubleClicked); + Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button2Clicked)); + Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button2DoubleClicked)); + + mouseBindings.Replace (MouseFlags.Button3Clicked, MouseFlags.Button3DoubleClicked); + Assert.Empty (mouseBindings.GetCommands (MouseFlags.Button3Clicked)); + Assert.Contains (Command.HotKey, mouseBindings.GetCommands (MouseFlags.Button3DoubleClicked)); + + mouseBindings.Replace (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.Replace (mouseBindings.GetFirstFromCommands (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.Replace (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.Replace (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/View/Keyboard/HotKeyTests.cs b/UnitTests/View/Keyboard/HotKeyTests.cs index 4c1e7812cd..1127ca0ff4 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,42 @@ 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); + view.HotKeyBindings.TryGet (Key.A, out var binding); + Assert.Equal (Key.A, binding.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"); + view.HotKeyBindings.TryGet (Key.A, out var binding); + Assert.Equal ("data", binding.Data); + } + [Fact] public void Defaults () { @@ -72,6 +98,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.GetBindings ()); } [Theory] @@ -94,7 +125,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 (); @@ -108,7 +139,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; }; @@ -167,16 +198,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 +215,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 +263,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 +271,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 +285,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 +397,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 94% rename from UnitTests/View/Keyboard/ViewKeyBindingTests.cs rename to UnitTests/View/Keyboard/KeyBindingsTests.cs index b1483c4dad..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; @@ -139,7 +140,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; } 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; } - } } 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); } } 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/ContextMenuTests.cs b/UnitTests/Views/ContextMenuTests.cs index 2d4da3ffbd..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!.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.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.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.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.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (menuBar.KeyBindings.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.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.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.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.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.KeyBindings.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.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.True (cm.MenuBar!.KeyBindings.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.KeyBindings.Bindings.ContainsKey (Key.N.WithCtrl)); - Assert.False (cm.MenuBar.KeyBindings.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!.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.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].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.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.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.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.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.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].KeyBindings.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.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.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!.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.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].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.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].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.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.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.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 534ac5085e..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.KeyBindings.Bindings); - Assert.DoesNotContain (Key.I, menuBar.KeyBindings.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.KeyBindings.Bindings); - Assert.DoesNotContain (Key.I, menuBar.KeyBindings.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.KeyBindings.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.KeyBindings.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.KeyBindings.Bindings); - Assert.Contains (Key.B.WithCtrl, menuBar.KeyBindings.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/RadioGroupTests.cs b/UnitTests/Views/RadioGroupTests.cs index 8c11b5b6e3..4276ca71f6 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) => { @@ -664,7 +668,7 @@ public void Mouse_Click () [Fact] [SetupFakeDriver] - public void Mouse_DoubleClick () + public void Mouse_DoubleClick_Accepts () { var radioGroup = new RadioGroup { @@ -701,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); @@ -715,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); @@ -762,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 diff --git a/UnitTests/Views/ShortcutTests.cs b/UnitTests/Views/ShortcutTests.cs index 691d862888..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,47 +281,47 @@ public void Key_Changing_Removes_Previous_Binding () var shortcut = new Shortcut (); shortcut.Key = Key.A; - Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys); + Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _)); shortcut.Key = Key.B; - Assert.DoesNotContain (Key.A, shortcut.KeyBindings.Bindings.Keys); - Assert.Contains (Key.B, shortcut.KeyBindings.Bindings.Keys); + Assert.False (shortcut.HotKeyBindings.TryGet (Key.A, out _)); + Assert.True (shortcut.HotKeyBindings.TryGet (Key.B, out _)); } // Test Key gets bound correctly [Fact] - public void KeyBindingScope_Defaults_To_HotKey () + public void BindKeyToApplication_Defaults_To_HotKey () { var shortcut = new Shortcut (); - Assert.Equal (KeyBindingScope.HotKey, shortcut.KeyBindingScope); + Assert.False (shortcut.BindKeyToApplication); } [Fact] - public void KeyBindingScope_Can_Be_Set () + public void BindKeyToApplication_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] - 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.True (shortcut.HotKeyBindings.TryGet (Key.A, out _)); - shortcut.KeyBindingScope = KeyBindingScope.Application; - Assert.DoesNotContain (Key.A, shortcut.KeyBindings.Bindings.Keys); - Assert.Contains (Key.A, Application.KeyBindings.Bindings.Keys); + shortcut.BindKeyToApplication = true; + Assert.False (shortcut.HotKeyBindings.TryGet (Key.A, out _)); + Assert.True (Application.KeyBindings.TryGet (Key.A, out _)); - shortcut.KeyBindingScope = KeyBindingScope.HotKey; - Assert.Contains (Key.A, shortcut.KeyBindings.Bindings.Keys); - Assert.DoesNotContain (Key.A, Application.KeyBindings.Bindings.Keys); + shortcut.BindKeyToApplication = false; + Assert.True (shortcut.HotKeyBindings.TryGet (Key.A, out _)); + Assert.False (Application.KeyBindings.TryGet (Key.A, out _)); } [Theory] @@ -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 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); 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)); 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 ddace3282c..bd03900cec 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,19 +50,29 @@ 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"** +### 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). @@ -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. @@ -116,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.