Added Next/PrevTabKeys.

Refactored ApplicationNavigation in prep for further work
This commit is contained in:
Tig
2024-08-02 13:57:23 -06:00
parent 79e50b4d8f
commit 8da833a4c6
8 changed files with 252 additions and 178 deletions

View File

@@ -74,6 +74,8 @@ public static partial class Application // Initialization (Init/Shutdown)
ResetState ();
}
Navigation = new ();
// For UnitTests
if (driver is { })
{

View File

@@ -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
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
[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
/// <summary>Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key.</summary>
[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
/// <summary>Alternative key to navigate forwards through views. Ctrl+Tab is the primary key.</summary>
@@ -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<Key, KeyBinding> 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 ();
/// <summary>
/// Commands for Application.
/// Commands for Application.
/// </summary>
private static Dictionary<Command, Func<CommandContext, bool?>> CommandImplementations { get; set; }
/// <summary>
/// <para>
/// Sets the function that will be invoked for a <see cref="Command"/>.
/// Sets the function that will be invoked for a <see cref="Command"/>.
/// </para>
/// <para>
/// If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
@@ -266,28 +319,23 @@ public static partial class Application // Keyboard handling
/// </para>
/// </summary>
/// <remarks>
/// <para>
/// This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
/// </para>
/// <para>
/// This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
/// </para>
/// </remarks>
/// <param name="command">The command.</param>
/// <param name="f">The function.</param>
private static void AddCommand (Command command, Func<bool?> f)
{
CommandImplementations [command] = ctx => f ();
}
private static void AddCommand (Command command, Func<bool?> f) { CommandImplementations [command] = ctx => f (); }
static Application ()
{
AddApplicationKeyBindings();
}
static Application () { AddApplicationKeyBindings (); }
internal static void AddApplicationKeyBindings ()
{
CommandImplementations = new Dictionary<Command, Func<CommandContext, bool?>> ();
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
/// <param name="view">The view that is bound to the key.</param>
internal static void RemoveKeyBindings (View view)
{
var list = KeyBindings.Bindings
.Where (kv => kv.Value.Scope != KeyBindingScope.Application)
.Select (kv => kv.Value)
.Distinct ()
.ToList ();
List<KeyBinding> list = KeyBindings.Bindings
.Where (kv => kv.Value.Scope != KeyBindingScope.Application)
.Select (kv => kv.Value)
.Distinct ()
.ToList ();
}
}

View File

@@ -1,154 +1,10 @@
#nullable enable
using System.Diagnostics;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;
namespace Terminal.Gui;
/// <summary>
/// Helper class for <see cref="Application"/> navigation.
/// </summary>
internal static class ApplicationNavigation
public static partial class Application // Navigation stuff
{
/// <summary>
/// Gets the deepest focused subview of the specified <paramref name="view"/>.
/// Gets the <see cref="ApplicationNavigation"/> instance for the current <see cref="Application"/>.
/// </summary>
/// <param name="view"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Moves the focus to the next focusable view.
/// Honors <see cref="ViewArrangement.Overlapped"/> and will only move to the next subview
/// if the current and next subviews are not overlapped.
/// </summary>
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);
}
}
/// <summary>
/// Moves the focus to the next <see cref="Toplevel"/> subview or the next subview that has <see cref="ApplicationOverlapped.OverlappedTop"/> set.
/// </summary>
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.
/// <summary>
/// Moves the focus to the next view. Honors <see cref="ViewArrangement.Overlapped"/> and will only move to the next subview
/// if the current and next subviews are not overlapped.
/// </summary>
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; }
}

View File

@@ -148,6 +148,8 @@ public static partial class Application
KeyDown = null;
KeyUp = null;
SizeChanging = null;
Navigation = null;
AddApplicationKeyBindings ();
Colors.Reset ();

View File

@@ -0,0 +1,159 @@
#nullable enable
namespace Terminal.Gui;
/// <summary>
/// Helper class for <see cref="Application"/> navigation. Held by <see cref="Application.Navigation"/>
/// </summary>
public class ApplicationNavigation
{
/// <summary>
/// Initializes a new instance of the <see cref="ApplicationNavigation"/> class.
/// </summary>
public ApplicationNavigation ()
{
// TODO: Move navigation key bindings here from AddApplicationKeyBindings
}
/// <summary>
/// Gets the deepest focused subview of the specified <paramref name="view"/>.
/// </summary>
/// <param name="view"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Moves the focus to the next focusable view.
/// Honors <see cref="ViewArrangement.Overlapped"/> and will only move to the next subview
/// if the current and next subviews are not overlapped.
/// </summary>
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);
}
}
/// <summary>
/// Moves the focus to the next <see cref="Toplevel"/> subview or the next subview that has
/// <see cref="ApplicationOverlapped.OverlappedTop"/> set.
/// </summary>
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.
/// <summary>
/// Moves the focus to the next view. Honors <see cref="ViewArrangement.Overlapped"/> and will only move to the next
/// subview
/// if the current and next subviews are not overlapped.
/// </summary>
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 ();
}
}
}

View File

@@ -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",

View File

@@ -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 ();