From 8da833a4c6363efb5e50b3bf2037fb054f976d93 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 2 Aug 2024 13:57:23 -0600 Subject: [PATCH] Added Next/PrevTabKeys. Refactored ApplicationNavigation in prep for further work --- .../Application/Application.Initialization.cs | 2 + .../Application/Application.Keyboard.cs | 110 ++++++++---- .../Application/Application.Navigation.cs | 150 +---------------- Terminal.Gui/Application/Application.cs | 2 + .../Application/ApplicationNavigation.cs | 159 ++++++++++++++++++ ...Overlapped.cs => ApplicationOverlapped.cs} | 0 Terminal.Gui/Resources/config.json | 2 + UnitTests/Application/ApplicationTests.cs | 5 + 8 files changed, 252 insertions(+), 178 deletions(-) create mode 100644 Terminal.Gui/Application/ApplicationNavigation.cs rename Terminal.Gui/Application/{Application.Overlapped.cs => ApplicationOverlapped.cs} (100%) diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index a2aacaab5..d9b4529d0 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -74,6 +74,8 @@ public static partial class Application // Initialization (Init/Shutdown) ResetState (); } + Navigation = new (); + // For UnitTests if (driver is { }) { diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index c7a5f8b50..0981bbd2d 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -1,11 +1,64 @@ #nullable enable using System.Text.Json.Serialization; -using static System.Formats.Asn1.AsnWriter; namespace Terminal.Gui; public static partial class Application // Keyboard handling { + private static Key _nextTabKey = Key.Empty; // Defined in config.json + + /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + [JsonConverter (typeof (KeyJsonConverter))] + public static Key NextTabKey + { + get => _nextTabKey; + set + { + if (_nextTabKey != value) + { + Key oldKey = _nextTabKey; + _nextTabKey = value; + + if (_nextTabKey == Key.Empty) + { + KeyBindings.Remove (_nextTabKey); + } + else + { + KeyBindings.ReplaceKey (oldKey, _nextTabKey); + } + } + } + } + + private static Key _prevTabKey = Key.Empty; // Defined in config.json + + /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + [JsonConverter (typeof (KeyJsonConverter))] + public static Key PrevTabKey + { + get => _prevTabKey; + set + { + if (_prevTabKey != value) + { + Key oldKey = _prevTabKey; + _prevTabKey = value; + + if (_prevTabKey == Key.Empty) + { + KeyBindings.Remove (_prevTabKey); + } + else + { + KeyBindings.ReplaceKey (oldKey, _prevTabKey); + } + } + } + } + private static Key _nextTabGroupKey = Key.Empty; // Defined in config.json /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. @@ -74,6 +127,7 @@ public static partial class Application // Keyboard handling { Key oldKey = _quitKey; _quitKey = value; + if (_quitKey == Key.Empty) { KeyBindings.Remove (_quitKey); @@ -139,7 +193,7 @@ public static partial class Application // Keyboard handling } else { - if (Application.Current.NewKeyDownEvent (keyEvent)) + if (Current.NewKeyDownEvent (keyEvent)) { return true; } @@ -147,7 +201,7 @@ public static partial class Application // Keyboard handling // Invoke any Application-scoped KeyBindings. // The first view that handles the key will stop the loop. - foreach (var binding in KeyBindings.Bindings.Where (b => b.Key == keyEvent.KeyCode)) + foreach (KeyValuePair binding in KeyBindings.Bindings.Where (b => b.Key == keyEvent.KeyCode)) { if (binding.Value.BoundView is { }) { @@ -193,7 +247,6 @@ public static partial class Application // Keyboard handling } } - return false; } @@ -252,13 +305,13 @@ public static partial class Application // Keyboard handling public static KeyBindings KeyBindings { get; internal set; } = new (); /// - /// Commands for Application. + /// Commands for Application. /// private static Dictionary> CommandImplementations { get; set; } /// /// - /// Sets the function that will be invoked for a . + /// Sets the function that will be invoked for a . /// /// /// If AddCommand has already been called for will @@ -266,28 +319,23 @@ public static partial class Application // Keyboard handling /// /// /// - /// - /// 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 (); } - static Application () - { - AddApplicationKeyBindings(); - } + static Application () { AddApplicationKeyBindings (); } internal static void AddApplicationKeyBindings () { - CommandImplementations = new Dictionary> (); + CommandImplementations = new (); + // Things this view knows how to do AddCommand ( - Command.QuitToplevel, // TODO: IRunnable: Rename to Command.Quit to make more generic. + Command.QuitToplevel, // TODO: IRunnable: Rename to Command.Quit to make more generic. () => { if (ApplicationOverlapped.OverlappedTop is { }) @@ -296,7 +344,7 @@ public static partial class Application // Keyboard handling } else { - Application.RequestStop (); + RequestStop (); } return true; @@ -363,24 +411,24 @@ public static partial class Application // Keyboard handling } ); - KeyBindings.Clear (); - KeyBindings.Add (Application.QuitKey, KeyBindingScope.Application, Command.QuitToplevel); + KeyBindings.Add (QuitKey, KeyBindingScope.Application, Command.QuitToplevel); KeyBindings.Add (Key.CursorRight, KeyBindingScope.Application, Command.NextView); KeyBindings.Add (Key.CursorDown, KeyBindingScope.Application, Command.NextView); KeyBindings.Add (Key.CursorLeft, KeyBindingScope.Application, Command.PreviousView); KeyBindings.Add (Key.CursorUp, KeyBindingScope.Application, Command.PreviousView); - KeyBindings.Add (Key.Tab, KeyBindingScope.Application, Command.NextView); - KeyBindings.Add (Key.Tab.WithShift, KeyBindingScope.Application, Command.PreviousView); + KeyBindings.Add (NextTabKey, KeyBindingScope.Application, Command.NextView); + KeyBindings.Add (PrevTabKey, KeyBindingScope.Application, Command.PreviousView); - KeyBindings.Add (Application.NextTabGroupKey, KeyBindingScope.Application, Command.NextViewOrTop); // Needed on Unix - KeyBindings.Add (Application.PrevTabGroupKey, KeyBindingScope.Application, Command.PreviousViewOrTop); // Needed on Unix + KeyBindings.Add (NextTabGroupKey, KeyBindingScope.Application, Command.NextViewOrTop); // Needed on Unix + KeyBindings.Add (PrevTabGroupKey, KeyBindingScope.Application, Command.PreviousViewOrTop); // Needed on Unix // TODO: Refresh Key should be configurable KeyBindings.Add (Key.F5, KeyBindingScope.Application, Command.Refresh); + // TODO: Suspend Key should be configurable if (Environment.OSVersion.Platform == PlatformID.Unix) { KeyBindings.Add (Key.Z.WithCtrl, KeyBindingScope.Application, Command.Suspend); @@ -431,10 +479,10 @@ public static partial class Application // Keyboard handling /// The view that is bound to the key. internal static void RemoveKeyBindings (View view) { - var list = KeyBindings.Bindings - .Where (kv => kv.Value.Scope != KeyBindingScope.Application) - .Select (kv => kv.Value) - .Distinct () - .ToList (); + List list = KeyBindings.Bindings + .Where (kv => kv.Value.Scope != KeyBindingScope.Application) + .Select (kv => kv.Value) + .Distinct () + .ToList (); } } diff --git a/Terminal.Gui/Application/Application.Navigation.cs b/Terminal.Gui/Application/Application.Navigation.cs index bab8f9e77..440cd4b42 100644 --- a/Terminal.Gui/Application/Application.Navigation.cs +++ b/Terminal.Gui/Application/Application.Navigation.cs @@ -1,154 +1,10 @@ #nullable enable -using System.Diagnostics; -using System.Reflection.PortableExecutable; -using System.Security.Cryptography; - namespace Terminal.Gui; -/// -/// Helper class for navigation. -/// -internal static class ApplicationNavigation +public static partial class Application // Navigation stuff { /// - /// Gets the deepest focused subview of the specified . + /// Gets the instance for the current . /// - /// - /// - internal static View? GetDeepestFocusedSubview (View? view) - { - if (view is null) - { - return null; - } - - foreach (View v in view.Subviews) - { - if (v.HasFocus) - { - return GetDeepestFocusedSubview (v); - } - } - - return view; - } - - /// - /// Moves the focus to the next focusable view. - /// Honors and will only move to the next subview - /// if the current and next subviews are not overlapped. - /// - internal static void MoveNextView () - { - View? old = GetDeepestFocusedSubview (Application.Current!.Focused); - - if (!Application.Current.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)) - { - Application.Current.AdvanceFocus (NavigationDirection.Forward, null); - } - - if (old != Application.Current.Focused && old != Application.Current.Focused?.Focused) - { - old?.SetNeedsDisplay (); - Application.Current.Focused?.SetNeedsDisplay (); - } - else - { - ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward); - } - } - - /// - /// Moves the focus to the next subview or the next subview that has set. - /// - internal static void MoveNextViewOrTop () - { - if (ApplicationOverlapped.OverlappedTop is null) - { - Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; - - if (!Application.Current.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup)) - { - Application.Current.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); - - if (Application.Current.Focused is null) - { - Application.Current.RestoreFocus (); - } - } - - if (top != Application.Current.Focused && top != Application.Current.Focused?.Focused) - { - top?.SetNeedsDisplay (); - Application.Current.Focused?.SetNeedsDisplay (); - } - else - { - ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward); - } - - - - //top!.AdvanceFocus (NavigationDirection.Forward); - - //if (top.Focused is null) - //{ - // top.AdvanceFocus (NavigationDirection.Forward); - //} - - //top.SetNeedsDisplay (); - ApplicationOverlapped.BringOverlappedTopToFront (); - } - else - { - ApplicationOverlapped.OverlappedMoveNext (); - } - } - - // TODO: These methods should return bool to indicate if the focus was moved or not. - - /// - /// Moves the focus to the next view. Honors and will only move to the next subview - /// if the current and next subviews are not overlapped. - /// - internal static void MovePreviousView () - { - View? old = GetDeepestFocusedSubview (Application.Current!.Focused); - - if (!Application.Current.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop)) - { - Application.Current.AdvanceFocus (NavigationDirection.Backward, null); - } - - if (old != Application.Current.Focused && old != Application.Current.Focused?.Focused) - { - old?.SetNeedsDisplay (); - Application.Current.Focused?.SetNeedsDisplay (); - } - else - { - ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes?.Reverse (), NavigationDirection.Backward); - } - } - - internal static void MovePreviousViewOrTop () - { - if (ApplicationOverlapped.OverlappedTop is null) - { - Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; - top!.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup); - - if (top.Focused is null) - { - top.AdvanceFocus (NavigationDirection.Backward, null); - } - - top.SetNeedsDisplay (); - ApplicationOverlapped.BringOverlappedTopToFront (); - } - else - { - ApplicationOverlapped.OverlappedMovePrevious (); - } - } + public static ApplicationNavigation? Navigation { get; internal set; } } diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index b2d2f9c59..e5332ee7f 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -148,6 +148,8 @@ public static partial class Application KeyDown = null; KeyUp = null; SizeChanging = null; + + Navigation = null; AddApplicationKeyBindings (); Colors.Reset (); diff --git a/Terminal.Gui/Application/ApplicationNavigation.cs b/Terminal.Gui/Application/ApplicationNavigation.cs new file mode 100644 index 000000000..8794dc2f2 --- /dev/null +++ b/Terminal.Gui/Application/ApplicationNavigation.cs @@ -0,0 +1,159 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// Helper class for navigation. Held by +/// +public class ApplicationNavigation +{ + /// + /// Initializes a new instance of the class. + /// + public ApplicationNavigation () + { + // TODO: Move navigation key bindings here from AddApplicationKeyBindings + } + + /// + /// Gets the deepest focused subview of the specified . + /// + /// + /// + internal static View? GetDeepestFocusedSubview (View? view) + { + if (view is null) + { + return null; + } + + foreach (View v in view.Subviews) + { + if (v.HasFocus) + { + return GetDeepestFocusedSubview (v); + } + } + + return view; + } + + /// + /// Moves the focus to the next focusable view. + /// Honors and will only move to the next subview + /// if the current and next subviews are not overlapped. + /// + internal static void MoveNextView () + { + View? old = GetDeepestFocusedSubview (Application.Current!.Focused); + + if (!Application.Current.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop)) + { + Application.Current.AdvanceFocus (NavigationDirection.Forward, null); + } + + if (old != Application.Current.Focused && old != Application.Current.Focused?.Focused) + { + old?.SetNeedsDisplay (); + Application.Current.Focused?.SetNeedsDisplay (); + } + else + { + ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward); + } + } + + /// + /// Moves the focus to the next subview or the next subview that has + /// set. + /// + internal static void MoveNextViewOrTop () + { + if (ApplicationOverlapped.OverlappedTop is null) + { + Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; + + if (!Application.Current.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup)) + { + Application.Current.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + + if (Application.Current.Focused is null) + { + Application.Current.RestoreFocus (); + } + } + + if (top != Application.Current.Focused && top != Application.Current.Focused?.Focused) + { + top?.SetNeedsDisplay (); + Application.Current.Focused?.SetNeedsDisplay (); + } + else + { + ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes, NavigationDirection.Forward); + } + + //top!.AdvanceFocus (NavigationDirection.Forward); + + //if (top.Focused is null) + //{ + // top.AdvanceFocus (NavigationDirection.Forward); + //} + + //top.SetNeedsDisplay (); + ApplicationOverlapped.BringOverlappedTopToFront (); + } + else + { + ApplicationOverlapped.OverlappedMoveNext (); + } + } + + // TODO: These methods should return bool to indicate if the focus was moved or not. + + /// + /// Moves the focus to the next view. Honors and will only move to the next + /// subview + /// if the current and next subviews are not overlapped. + /// + internal static void MovePreviousView () + { + View? old = GetDeepestFocusedSubview (Application.Current!.Focused); + + if (!Application.Current.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop)) + { + Application.Current.AdvanceFocus (NavigationDirection.Backward, null); + } + + if (old != Application.Current.Focused && old != Application.Current.Focused?.Focused) + { + old?.SetNeedsDisplay (); + Application.Current.Focused?.SetNeedsDisplay (); + } + else + { + ApplicationOverlapped.SetFocusToNextViewWithWrap (Application.Current.SuperView?.TabIndexes?.Reverse (), NavigationDirection.Backward); + } + } + + internal static void MovePreviousViewOrTop () + { + if (ApplicationOverlapped.OverlappedTop is null) + { + Toplevel? top = Application.Current!.Modal ? Application.Current : Application.Top; + top!.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup); + + if (top.Focused is null) + { + top.AdvanceFocus (NavigationDirection.Backward, null); + } + + top.SetNeedsDisplay (); + ApplicationOverlapped.BringOverlappedTopToFront (); + } + else + { + ApplicationOverlapped.OverlappedMovePrevious (); + } + } +} diff --git a/Terminal.Gui/Application/Application.Overlapped.cs b/Terminal.Gui/Application/ApplicationOverlapped.cs similarity index 100% rename from Terminal.Gui/Application/Application.Overlapped.cs rename to Terminal.Gui/Application/ApplicationOverlapped.cs diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index 688537779..a80d8334e 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -17,6 +17,8 @@ // to throw exceptions. "ConfigurationManager.ThrowOnJsonErrors": false, + "Application.NextTabKey": "Tab", + "Application.PrevTabKey": "Shift+Tab", "Application.NextTabGroupKey": "F6", "Application.PrevTabGroupKey": "Shift+F6", "Application.QuitKey": "Esc", diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 268eebcd6..ce03ac11f 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -201,6 +201,9 @@ public class ApplicationTests // Keyboard Assert.Empty (Application.GetViewKeyBindings ()); + // Navigation + Assert.Null (Application.Navigation); + // Events - Can't check //Assert.Null (Application.NotifyNewRunState); //Assert.Null (Application.NotifyNewRunState); @@ -241,6 +244,8 @@ public class ApplicationTests //Application.WantContinuousButtonPressedView = new View (); + Application.Navigation = new (); + Application.ResetState (); CheckReset ();